Define strict base install schema catalog
This commit is contained in:
parent
3be4e77c66
commit
16885258f0
6 changed files with 163 additions and 10 deletions
|
|
@ -126,7 +126,7 @@
|
||||||
|
|
||||||
- [ ] preflight ОС, архитектуры, SSH, firewall, APT и capabilities;
|
- [ ] preflight ОС, архитектуры, SSH, firewall, APT и capabilities;
|
||||||
- [ ] Desktop wizard timezone/locale/packages/swap/unattended upgrades;
|
- [ ] Desktop wizard timezone/locale/packages/swap/unattended upgrades;
|
||||||
- [ ] versioned package allowlist;
|
- [x] versioned package allowlist (catalog v1 с фиксированными package groups);
|
||||||
- [ ] root-only backups и symlink protection;
|
- [ ] root-only backups и symlink protection;
|
||||||
- [ ] user lifecycle без sudo по умолчанию;
|
- [ ] user lifecycle без sudo по умолчанию;
|
||||||
- [ ] SSH public keys, fingerprints и permissions;
|
- [ ] SSH public keys, fingerprints и permissions;
|
||||||
|
|
|
||||||
|
|
@ -464,6 +464,8 @@ agents.MapPost("/provisioning/jobs/{id}/preflight-facts", async (
|
||||||
var control = app.MapGroup("/api/v1/control").RequireAuthorization("Operator");
|
var control = app.MapGroup("/api/v1/control").RequireAuthorization("Operator");
|
||||||
control.MapGet("/agents", async (ControlStore controlStore, CancellationToken cancellationToken) =>
|
control.MapGet("/agents", async (ControlStore controlStore, CancellationToken cancellationToken) =>
|
||||||
Results.Ok((await controlStore.ListAgentsAsync(cancellationToken)).ToArray()));
|
Results.Ok((await controlStore.ListAgentsAsync(cancellationToken)).ToArray()));
|
||||||
|
control.MapGet("/provisioning/catalogs/system-base-install/1", () =>
|
||||||
|
Results.Ok(SystemBaseInstallCatalogDefinition.Create()));
|
||||||
control.MapGet("/agents/{nodeId}/facts/preflight", async (
|
control.MapGet("/agents/{nodeId}/facts/preflight", async (
|
||||||
string nodeId,
|
string nodeId,
|
||||||
ControlStore controlStore,
|
ControlStore controlStore,
|
||||||
|
|
@ -945,14 +947,35 @@ internal static class ProvisioningJobValidator
|
||||||
private const int MaximumParametersBytes = 16 * 1024;
|
private const int MaximumParametersBytes = 16 * 1024;
|
||||||
|
|
||||||
public static bool IsValid(ProvisioningJobCreateRequest request)
|
public static bool IsValid(ProvisioningJobCreateRequest request)
|
||||||
=> request.SchemaVersion == 1
|
{
|
||||||
&& request.ActionType is "preflight" or "system.base-install"
|
if (request.SchemaVersion != 1
|
||||||
&& request.Parameters.ValueKind == JsonValueKind.Object
|
|| request.Parameters.ValueKind != JsonValueKind.Object
|
||||||
&& !request.Parameters.EnumerateObject().Any()
|
|| request.Parameters.GetRawText().Length > MaximumParametersBytes
|
||||||
&& request.Parameters.GetRawText().Length <= MaximumParametersBytes
|
|| request.TtlMinutes is < 5 or > 1440
|
||||||
&& request.TtlMinutes is >= 5 and <= 1440
|
|| request.AuditReason is not { Length: >= 1 and <= 256 }
|
||||||
&& request.AuditReason.Length is >= 1 and <= 256
|
|| !IdempotencyKeyValidator.IsValid(request.IdempotencyKey))
|
||||||
&& IdempotencyKeyValidator.IsValid(request.IdempotencyKey);
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (request.ActionType == "preflight")
|
||||||
|
{
|
||||||
|
return !request.Parameters.EnumerateObject().Any();
|
||||||
|
}
|
||||||
|
if (request.ActionType != "system.base-install")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parameters = JsonSerializer.Deserialize(
|
||||||
|
request.Parameters, SmmJsonContext.Default.SystemBaseInstallParameters);
|
||||||
|
return parameters is not null && IsValid(parameters);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static bool IsValid(ProvisioningJobCommandRequest request)
|
public static bool IsValid(ProvisioningJobCommandRequest request)
|
||||||
=> request.Reason.Length is >= 1 and <= 256
|
=> request.Reason.Length is >= 1 and <= 256
|
||||||
|
|
@ -989,6 +1012,22 @@ internal static class ProvisioningJobValidator
|
||||||
public static bool RequiresConfirmation(string actionType)
|
public static bool RequiresConfirmation(string actionType)
|
||||||
=> actionType == "system.base-install";
|
=> actionType == "system.base-install";
|
||||||
|
|
||||||
|
private static bool IsValid(SystemBaseInstallParameters parameters)
|
||||||
|
=> IsSafeTimezone(parameters.Timezone)
|
||||||
|
&& IsSafeLocale(parameters.Locale)
|
||||||
|
&& (!parameters.AptUpgrade || parameters.AptUpdate)
|
||||||
|
&& parameters.PackageCatalogVersion == SystemBaseInstallCatalogDefinition.Version
|
||||||
|
&& parameters.PackageGroupIds is { Length: <= 4 }
|
||||||
|
&& parameters.PackageGroupIds.Distinct(StringComparer.Ordinal).Count()
|
||||||
|
== parameters.PackageGroupIds.Length
|
||||||
|
&& parameters.PackageGroupIds.All(SystemBaseInstallCatalogDefinition.ContainsGroup)
|
||||||
|
&& parameters.SwapMode is "disabled" or "automatic" or "explicit"
|
||||||
|
&& (parameters.SwapMode == "explicit"
|
||||||
|
? parameters.SwapSizeMiB is >= 128 and <= 1_048_576
|
||||||
|
: parameters.SwapSizeMiB is null)
|
||||||
|
&& parameters.VmSwappiness is >= 0 and <= 200
|
||||||
|
&& parameters.RebootPolicy == "never";
|
||||||
|
|
||||||
private static bool IsSafeCode(string value, int maximumLength)
|
private static bool IsSafeCode(string value, int maximumLength)
|
||||||
=> value.Length is >= 1 && value.Length <= maximumLength
|
=> value.Length is >= 1 && value.Length <= maximumLength
|
||||||
&& value.All(character => character is >= 'a' and <= 'z'
|
&& value.All(character => character is >= 'a' and <= 'z'
|
||||||
|
|
@ -1000,6 +1039,18 @@ internal static class ProvisioningJobValidator
|
||||||
&& value.Length is >= 1 && value.Length <= maximumLength
|
&& value.Length is >= 1 && value.Length <= maximumLength
|
||||||
&& value.All(character => char.IsAsciiLetterOrDigit(character)
|
&& value.All(character => char.IsAsciiLetterOrDigit(character)
|
||||||
|| character is '_' or '-' or '.');
|
|| character is '_' or '-' or '.');
|
||||||
|
|
||||||
|
private static bool IsSafeTimezone(string? value)
|
||||||
|
=> value is { Length: >= 1 and <= 64 }
|
||||||
|
&& value[0] is not '/' and not '.'
|
||||||
|
&& !value.Contains("..", StringComparison.Ordinal)
|
||||||
|
&& value.All(character => char.IsAsciiLetterOrDigit(character)
|
||||||
|
|| character is '/' or '_' or '-' or '+');
|
||||||
|
|
||||||
|
private static bool IsSafeLocale(string? value)
|
||||||
|
=> value is { Length: >= 1 and <= 32 }
|
||||||
|
&& value.All(character => char.IsAsciiLetterOrDigit(character)
|
||||||
|
|| character is '_' or '-' or '.' or '@');
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class PreflightDesiredStateValidator
|
internal static class PreflightDesiredStateValidator
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,28 @@ public sealed record PreflightDriftAssessment(
|
||||||
NodePreflightDesiredState? Desired,
|
NodePreflightDesiredState? Desired,
|
||||||
NodePreflightFacts? Facts);
|
NodePreflightFacts? Facts);
|
||||||
|
|
||||||
|
[JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Disallow)]
|
||||||
|
public sealed record SystemBaseInstallParameters(
|
||||||
|
string Timezone,
|
||||||
|
string Locale,
|
||||||
|
bool AptUpdate,
|
||||||
|
bool AptUpgrade,
|
||||||
|
int PackageCatalogVersion,
|
||||||
|
string[] PackageGroupIds,
|
||||||
|
string SwapMode,
|
||||||
|
int? SwapSizeMiB,
|
||||||
|
int VmSwappiness,
|
||||||
|
bool EnableUnattendedUpgrades,
|
||||||
|
string RebootPolicy);
|
||||||
|
|
||||||
|
public sealed record SystemPackageGroup(
|
||||||
|
string Id,
|
||||||
|
string[] Packages);
|
||||||
|
|
||||||
|
public sealed record SystemBaseInstallCatalog(
|
||||||
|
int Version,
|
||||||
|
SystemPackageGroup[] Groups);
|
||||||
|
|
||||||
public static class PreflightDriftStatuses
|
public static class PreflightDriftStatuses
|
||||||
{
|
{
|
||||||
public const string NotConfigured = "NotConfigured";
|
public const string NotConfigured = "NotConfigured";
|
||||||
|
|
@ -306,3 +328,20 @@ public static class ProvisioningActionCatalog
|
||||||
public const string PreflightModuleHash =
|
public const string PreflightModuleHash =
|
||||||
"2dc48fb4528a291221954fc2dd3478d431b66fe34228f29684ce1648dbe2f32b";
|
"2dc48fb4528a291221954fc2dd3478d431b66fe34228f29684ce1648dbe2f32b";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class SystemBaseInstallCatalogDefinition
|
||||||
|
{
|
||||||
|
public const int Version = 1;
|
||||||
|
|
||||||
|
public static SystemBaseInstallCatalog Create()
|
||||||
|
=> new(Version,
|
||||||
|
[
|
||||||
|
new("core", ["ca-certificates", "curl", "jq"]),
|
||||||
|
new("development", ["build-essential", "git"]),
|
||||||
|
new("diagnostics", ["htop", "iotop"]),
|
||||||
|
new("container-host", ["dbus-user-session", "uidmap"])
|
||||||
|
]);
|
||||||
|
|
||||||
|
public static bool ContainsGroup(string id)
|
||||||
|
=> id is "core" or "development" or "diagnostics" or "container-host";
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace ServerMonitorManager.Core;
|
namespace ServerMonitorManager.Core;
|
||||||
|
|
||||||
|
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
|
||||||
[JsonSerializable(typeof(EnrollmentRequest))]
|
[JsonSerializable(typeof(EnrollmentRequest))]
|
||||||
[JsonSerializable(typeof(EnrollmentResponse))]
|
[JsonSerializable(typeof(EnrollmentResponse))]
|
||||||
[JsonSerializable(typeof(AgentHeartbeat))]
|
[JsonSerializable(typeof(AgentHeartbeat))]
|
||||||
|
|
@ -37,6 +38,10 @@ namespace ServerMonitorManager.Core;
|
||||||
[JsonSerializable(typeof(PreflightDesiredStateUpdateRequest))]
|
[JsonSerializable(typeof(PreflightDesiredStateUpdateRequest))]
|
||||||
[JsonSerializable(typeof(NodePreflightDesiredState))]
|
[JsonSerializable(typeof(NodePreflightDesiredState))]
|
||||||
[JsonSerializable(typeof(PreflightDriftAssessment))]
|
[JsonSerializable(typeof(PreflightDriftAssessment))]
|
||||||
|
[JsonSerializable(typeof(SystemBaseInstallParameters))]
|
||||||
|
[JsonSerializable(typeof(SystemPackageGroup))]
|
||||||
|
[JsonSerializable(typeof(SystemPackageGroup[]))]
|
||||||
|
[JsonSerializable(typeof(SystemBaseInstallCatalog))]
|
||||||
[JsonSerializable(typeof(ProvisioningJob))]
|
[JsonSerializable(typeof(ProvisioningJob))]
|
||||||
[JsonSerializable(typeof(ProvisioningJob[]))]
|
[JsonSerializable(typeof(ProvisioningJob[]))]
|
||||||
[JsonSerializable(typeof(ProvisioningEvent))]
|
[JsonSerializable(typeof(ProvisioningEvent))]
|
||||||
|
|
|
||||||
|
|
@ -96,13 +96,32 @@ public sealed class ControlApiTests : IAsyncDisposable
|
||||||
using var client = _factory.CreateClient();
|
using var client = _factory.CreateClient();
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Identity", "windows-pc");
|
client.DefaultRequestHeaders.Add("X-Test-Identity", "windows-pc");
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Role", "Operator");
|
client.DefaultRequestHeaders.Add("X-Test-Role", "Operator");
|
||||||
|
var catalog = await client.GetFromJsonAsync<ServerMonitorManager.Core.SystemBaseInstallCatalog>(
|
||||||
|
"/api/v1/control/provisioning/catalogs/system-base-install/1",
|
||||||
|
cancellationToken);
|
||||||
|
Assert.Equal(1, catalog!.Version);
|
||||||
|
Assert.Contains(catalog.Groups, group => group.Id == "development"
|
||||||
|
&& group.Packages.Contains("git"));
|
||||||
using var response = await client.PostAsJsonAsync(
|
using var response = await client.PostAsJsonAsync(
|
||||||
"/api/v1/control/agents/home/provisioning/jobs",
|
"/api/v1/control/agents/home/provisioning/jobs",
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
actionType = "system.base-install",
|
actionType = "system.base-install",
|
||||||
schemaVersion = 1,
|
schemaVersion = 1,
|
||||||
parameters = new { },
|
parameters = new
|
||||||
|
{
|
||||||
|
timezone = "UTC",
|
||||||
|
locale = "en_US.UTF-8",
|
||||||
|
aptUpdate = true,
|
||||||
|
aptUpgrade = false,
|
||||||
|
packageCatalogVersion = 1,
|
||||||
|
packageGroupIds = new[] { "core", "development" },
|
||||||
|
swapMode = "automatic",
|
||||||
|
swapSizeMiB = (int?)null,
|
||||||
|
vmSwappiness = 60,
|
||||||
|
enableUnattendedUpgrades = true,
|
||||||
|
rebootPolicy = "never"
|
||||||
|
},
|
||||||
ttlMinutes = 60,
|
ttlMinutes = 60,
|
||||||
auditReason = "API integration test",
|
auditReason = "API integration test",
|
||||||
idempotencyKey = Guid.NewGuid().ToString()
|
idempotencyKey = Guid.NewGuid().ToString()
|
||||||
|
|
@ -114,6 +133,31 @@ public sealed class ControlApiTests : IAsyncDisposable
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
Assert.NotNull(job);
|
Assert.NotNull(job);
|
||||||
Assert.Equal(ServerMonitorManager.Core.ProvisioningJobStates.AwaitingConfirmation, job.State);
|
Assert.Equal(ServerMonitorManager.Core.ProvisioningJobStates.AwaitingConfirmation, job.State);
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, (await client.PostAsJsonAsync(
|
||||||
|
"/api/v1/control/agents/home/provisioning/jobs",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
actionType = "system.base-install",
|
||||||
|
schemaVersion = 1,
|
||||||
|
parameters = new
|
||||||
|
{
|
||||||
|
timezone = "UTC",
|
||||||
|
locale = "en_US.UTF-8",
|
||||||
|
aptUpdate = true,
|
||||||
|
aptUpgrade = false,
|
||||||
|
packageCatalogVersion = 1,
|
||||||
|
packageGroupIds = new[] { "curl" },
|
||||||
|
swapMode = "disabled",
|
||||||
|
swapSizeMiB = (int?)null,
|
||||||
|
vmSwappiness = 60,
|
||||||
|
enableUnattendedUpgrades = false,
|
||||||
|
rebootPolicy = "never"
|
||||||
|
},
|
||||||
|
ttlMinutes = 60,
|
||||||
|
auditReason = "Reject arbitrary package input",
|
||||||
|
idempotencyKey = Guid.NewGuid().ToString()
|
||||||
|
},
|
||||||
|
cancellationToken)).StatusCode);
|
||||||
Assert.Equal(HttpStatusCode.OK, (await client.GetAsync(
|
Assert.Equal(HttpStatusCode.OK, (await client.GetAsync(
|
||||||
$"/api/v1/control/provisioning/jobs/{job.Id}", cancellationToken)).StatusCode);
|
$"/api/v1/control/provisioning/jobs/{job.Id}", cancellationToken)).StatusCode);
|
||||||
using var eventsResponse = await client.GetAsync(
|
using var eventsResponse = await client.GetAsync(
|
||||||
|
|
|
||||||
|
|
@ -53,4 +53,18 @@ public sealed class ProvisioningHelperTests
|
||||||
Assert.Throws<JsonException>(() =>
|
Assert.Throws<JsonException>(() =>
|
||||||
JsonSerializer.Deserialize(json, SmmJsonContext.Default.ProvisioningHelperRequest));
|
JsonSerializer.Deserialize(json, SmmJsonContext.Default.ProvisioningHelperRequest));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BaseInstallSchemaRejectsCommandText()
|
||||||
|
{
|
||||||
|
const string json = """
|
||||||
|
{"timezone":"UTC","locale":"en_US.UTF-8","aptUpdate":true,"aptUpgrade":false,
|
||||||
|
"packageCatalogVersion":1,"packageGroupIds":["core"],"swapMode":"disabled",
|
||||||
|
"swapSizeMiB":null,"vmSwappiness":60,"enableUnattendedUpgrades":true,
|
||||||
|
"rebootPolicy":"never","command":"id"}
|
||||||
|
""";
|
||||||
|
|
||||||
|
Assert.Throws<JsonException>(() =>
|
||||||
|
JsonSerializer.Deserialize(json, SmmJsonContext.Default.SystemBaseInstallParameters));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue