Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions policies/dotnet/Devolutions.Now.Policy.Api/BrokerApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ public static class BrokerApi

public const string DefaultPipeName = "Devolutions.Now.PackageBroker.v1";

/// <summary>Maximum complete HTTP request-body size for policy validation and replacement.</summary>
public const int MaxPolicyManagementBodyBytes = 16 * 1024 * 1024;

public const string PackageRequestKind = "PackageRequest";
public const string StatusRequestKind = "StatusRequest";
public const string CancelRequestKind = "CancelRequest";
public const string PolicyValidationRequestKind = "PolicyValidationRequest";
public const string PolicyReplacementRequestKind = "PolicyReplacementRequest";

public const string HealthResponseKind = "HealthResponse";
public const string CapabilitiesResponseKind = "CapabilitiesResponse";
Expand All @@ -20,6 +25,9 @@ public static class BrokerApi
public const string StatusResponseKind = "StatusResponse";
public const string CancelResponseKind = "CancelResponse";
public const string PolicyResponseKind = "PolicyResponse";
public const string PolicyManagementResponseKind = "PolicyManagementResponse";
public const string PolicyValidationResponseKind = "PolicyValidationResponse";
public const string PolicyReplacementResponseKind = "PolicyReplacementResponse";
public const string ErrorResponseKind = "ErrorResponse";

internal static string ValidateMessageKind(string? value, string expected, string propertyName)
Expand Down
178 changes: 164 additions & 14 deletions policies/dotnet/Devolutions.Now.Policy.Api/BrokerJson.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,60 +25,187 @@ public static class BrokerJson

public static readonly JsonSerializerOptions PrettyOptions = CreateOptions(writeIndented: true);

public static string Serialize<T>(T value) =>
JsonSerializer.Serialize(value, TypeInfo<T>());
public static string Serialize<T>(T value)
{
ValidateSemanticValue(value);
return JsonSerializer.Serialize(value, TypeInfo<T>());
}

public static T? Deserialize<T>(string json) =>
JsonSerializer.Deserialize(json, TypeInfo<T>());
public static T? Deserialize<T>(string json)
{
var value = JsonSerializer.Deserialize(json, TypeInfo<T>());
ValidateSemanticValue(value);
return value;
}

public static T? DeserializeStrict<T>(string json)
{
var value = JsonSerializer.Deserialize(json, StrictTypeInfo<T>());
if (value is PolicyResponse response)
ValidateSemanticValue(value);
return value;
}

private static void ValidateSemanticValue<T>(T value)
{
switch (value)
{
PolicyJson.ValidateRequiredCollectionElements(response.Policy);
case PolicyResponse response:
PolicyJson.ValidateRequiredCollectionElements(response.Policy);
break;
case PolicyManagementResponse response:
ValidateManagement(response.Management);
break;
case PolicyValidationResponse response:
ValidateValidation(response.Validation);
break;
case PolicyReplacementResponse response:
PolicyJson.ValidateRequiredCollectionElements(response.Policy);
ValidateValidation(response.Validation);
ValidateManagement(response.Management);
break;
case ErrorResponse error:
ValidateError(error);
break;
}

return value;
}

private static JsonTypeInfo<T> TypeInfo<T>() =>
typeof(T) == typeof(PackageRequest) ? Cast<T>(BrokerJsonSerializerContext.Default.PackageRequest) :
typeof(T) == typeof(StatusRequest) ? Cast<T>(BrokerJsonSerializerContext.Default.StatusRequest) :
typeof(T) == typeof(CancelRequest) ? Cast<T>(BrokerJsonSerializerContext.Default.CancelRequest) :
typeof(T) == typeof(PolicyValidationRequest) ? Cast<T>(BrokerPolicyJsonSerializerContext.Default.PolicyValidationRequest) :
typeof(T) == typeof(PolicyReplacementRequest) ? Cast<T>(BrokerPolicyJsonSerializerContext.Default.PolicyReplacementRequest) :
typeof(T) == typeof(HealthResponse) ? Cast<T>(BrokerJsonSerializerContext.Default.HealthResponse) :
typeof(T) == typeof(CapabilitiesResponse) ? Cast<T>(BrokerJsonSerializerContext.Default.CapabilitiesResponse) :
typeof(T) == typeof(PolicyResponse) ? Cast<T>(BrokerPolicyJsonSerializerContext.Default.PolicyResponse) :
typeof(T) == typeof(PolicyManagementResponse) ? Cast<T>(BrokerPolicyJsonSerializerContext.Default.PolicyManagementResponse) :
typeof(T) == typeof(PolicyValidationResponse) ? Cast<T>(BrokerPolicyJsonSerializerContext.Default.PolicyValidationResponse) :
typeof(T) == typeof(PolicyReplacementResponse) ? Cast<T>(BrokerPolicyJsonSerializerContext.Default.PolicyReplacementResponse) :
typeof(T) == typeof(EvaluationResponse) ? Cast<T>(BrokerJsonSerializerContext.Default.EvaluationResponse) :
typeof(T) == typeof(ExecutionResponse) ? Cast<T>(BrokerJsonSerializerContext.Default.ExecutionResponse) :
typeof(T) == typeof(StatusResponse) ? Cast<T>(BrokerJsonSerializerContext.Default.StatusResponse) :
typeof(T) == typeof(CancelResponse) ? Cast<T>(BrokerJsonSerializerContext.Default.CancelResponse) :
typeof(T) == typeof(ErrorResponse) ? Cast<T>(BrokerJsonSerializerContext.Default.ErrorResponse) :
typeof(T) == typeof(ErrorResponse) ? Cast<T>(BrokerErrorJsonSerializerContext.Default.ErrorResponse) :
throw new NotSupportedException($"Broker JSON serialization for {typeof(T).FullName} is not source-generated.");

private static JsonTypeInfo<T> StrictTypeInfo<T>() =>
typeof(T) == typeof(PackageRequest) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.PackageRequest) :
typeof(T) == typeof(StatusRequest) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.StatusRequest) :
typeof(T) == typeof(CancelRequest) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.CancelRequest) :
typeof(T) == typeof(PolicyValidationRequest) ? Cast<T>(BrokerPolicyJsonStrictSerializerContext.Default.PolicyValidationRequest) :
typeof(T) == typeof(PolicyReplacementRequest) ? Cast<T>(BrokerPolicyJsonStrictSerializerContext.Default.PolicyReplacementRequest) :
typeof(T) == typeof(HealthResponse) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.HealthResponse) :
typeof(T) == typeof(CapabilitiesResponse) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.CapabilitiesResponse) :
typeof(T) == typeof(PolicyResponse) ? Cast<T>(BrokerPolicyJsonStrictSerializerContext.Default.PolicyResponse) :
typeof(T) == typeof(PolicyManagementResponse) ? Cast<T>(BrokerPolicyJsonStrictSerializerContext.Default.PolicyManagementResponse) :
typeof(T) == typeof(PolicyValidationResponse) ? Cast<T>(BrokerPolicyJsonStrictSerializerContext.Default.PolicyValidationResponse) :
typeof(T) == typeof(PolicyReplacementResponse) ? Cast<T>(BrokerPolicyJsonStrictSerializerContext.Default.PolicyReplacementResponse) :
typeof(T) == typeof(EvaluationResponse) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.EvaluationResponse) :
typeof(T) == typeof(ExecutionResponse) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.ExecutionResponse) :
typeof(T) == typeof(StatusResponse) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.StatusResponse) :
typeof(T) == typeof(CancelResponse) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.CancelResponse) :
typeof(T) == typeof(ErrorResponse) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.ErrorResponse) :
typeof(T) == typeof(ErrorResponse) ? Cast<T>(BrokerErrorJsonStrictSerializerContext.Default.ErrorResponse) :
throw new NotSupportedException($"Strict broker JSON deserialization for {typeof(T).FullName} is not source-generated.");

private static JsonTypeInfo<T> Cast<T>(JsonTypeInfo jsonTypeInfo) =>
(JsonTypeInfo<T>)jsonTypeInfo;

private static void ValidateManagement(PolicyManagementSnapshot management)
{
switch (management.State)
{
case PolicyManagementState.Active when management.Policy is null || management.InvalidDiagnostics is not null:
throw new JsonException("Active management snapshots require Policy and forbid InvalidDiagnostics.");
case PolicyManagementState.Missing when management.Policy is not null || management.InvalidDiagnostics is not null:
throw new JsonException("Missing management snapshots forbid Policy and InvalidDiagnostics.");
case PolicyManagementState.Invalid:
if (management.Policy is not null)
{
throw new JsonException("Invalid management snapshots forbid Policy.");
}
if (management.InvalidDiagnostics is null
|| management.InvalidDiagnostics.Findings.Count == 0
|| !management.InvalidDiagnostics.Findings.Any(
finding => finding.Severity == PolicyFindingSeverity.Error))
{
throw new JsonException(
"Invalid management snapshots require nonempty diagnostics with an Error finding.");
}
break;
}

switch (management.WriteCapability)
{
case PolicyWriteCapability.Writable when management.ReadOnlyReason is not null:
throw new JsonException("Writable management snapshots forbid ReadOnlyReason.");
case PolicyWriteCapability.ReadOnly or PolicyWriteCapability.Unsupported
when management.ReadOnlyReason is null:
throw new JsonException("ReadOnly and Unsupported management snapshots require ReadOnlyReason.");
}

if (management.Policy is { } policy)
{
PolicyJson.ValidateRequiredCollectionElements(policy);
}
}
Comment on lines +113 to +150

private static void ValidateError(ErrorResponse error)
{
if (error.Code == ErrorCode.StalePolicyStoreToken && error.Management is null)
{
throw new JsonException("StalePolicyStoreToken errors require Management.");
}

if (error.Management is { } management)
{
ValidateManagement(management);
}

if (error.Validation is { } validation)
{
ValidateValidation(validation);
}
}

private static void ValidateValidation(PolicyValidationResult validation)
{
var hasError = validation.Findings.Any(finding => finding.Severity == PolicyFindingSeverity.Error);
if (validation.IsValid)
{
if (validation.CanonicalDraft is null || validation.ValidationReceipt is null)
{
throw new JsonException(
"Valid policy validation results require CanonicalDraft and ValidationReceipt.");
}
if (hasError)
{
throw new JsonException("Valid policy validation results must not contain Error findings.");
}

PolicyJson.ValidateRequiredCollectionElements(validation.CanonicalDraft);
}
else
{
if (validation.CanonicalDraft is not null || validation.ValidationReceipt is not null)
{
throw new JsonException(
"Invalid policy validation results must not contain CanonicalDraft or ValidationReceipt.");
}
if (!hasError)
{
throw new JsonException(
"Invalid policy validation results require at least one Error finding.");
}
}
}

private static JsonSerializerOptions CreateOptions(bool writeIndented) =>
new(BrokerJsonSerializerContext.Default.Options)
{
TypeInfoResolver = JsonTypeInfoResolver.Combine(
BrokerJsonSerializerContext.Default,
BrokerPolicyJsonSerializerContext.Default),
BrokerPolicyJsonSerializerContext.Default,
BrokerErrorJsonSerializerContext.Default),
WriteIndented = writeIndented,
};
}
Expand All @@ -96,7 +223,6 @@ private static JsonSerializerOptions CreateOptions(bool writeIndented) =>
[JsonSerializable(typeof(ExecutionResponse))]
[JsonSerializable(typeof(StatusResponse))]
[JsonSerializable(typeof(CancelResponse))]
[JsonSerializable(typeof(ErrorResponse))]
[JsonSerializable(typeof(JsonNode))]
[JsonSerializable(typeof(JsonObject))]
[JsonSerializable(typeof(JsonArray))]
Expand All @@ -116,7 +242,6 @@ internal sealed partial class BrokerJsonSerializerContext : JsonSerializerContex
[JsonSerializable(typeof(ExecutionResponse))]
[JsonSerializable(typeof(StatusResponse))]
[JsonSerializable(typeof(CancelResponse))]
[JsonSerializable(typeof(ErrorResponse))]
[JsonSerializable(typeof(JsonNode))]
[JsonSerializable(typeof(JsonObject))]
[JsonSerializable(typeof(JsonArray))]
Expand All @@ -128,6 +253,11 @@ internal sealed partial class BrokerJsonStrictSerializerContext : JsonSerializer
RespectNullableAnnotations = true,
WriteIndented = false)]
[JsonSerializable(typeof(PolicyResponse))]
[JsonSerializable(typeof(PolicyManagementResponse))]
[JsonSerializable(typeof(PolicyValidationRequest))]
[JsonSerializable(typeof(PolicyValidationResponse))]
[JsonSerializable(typeof(PolicyReplacementRequest))]
[JsonSerializable(typeof(PolicyReplacementResponse))]
internal sealed partial class BrokerPolicyJsonSerializerContext : JsonSerializerContext;

[JsonSourceGenerationOptions(
Expand All @@ -137,4 +267,24 @@ internal sealed partial class BrokerPolicyJsonSerializerContext : JsonSerializer
WriteIndented = false,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow)]
[JsonSerializable(typeof(PolicyResponse))]
internal sealed partial class BrokerPolicyJsonStrictSerializerContext : JsonSerializerContext;
[JsonSerializable(typeof(PolicyManagementResponse))]
[JsonSerializable(typeof(PolicyValidationRequest))]
[JsonSerializable(typeof(PolicyValidationResponse))]
[JsonSerializable(typeof(PolicyReplacementRequest))]
[JsonSerializable(typeof(PolicyReplacementResponse))]
internal sealed partial class BrokerPolicyJsonStrictSerializerContext : JsonSerializerContext;

[JsonSourceGenerationOptions(
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
RespectNullableAnnotations = true,
WriteIndented = false)]
[JsonSerializable(typeof(ErrorResponse))]
internal sealed partial class BrokerErrorJsonSerializerContext : JsonSerializerContext;

[JsonSourceGenerationOptions(
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
RespectNullableAnnotations = true,
WriteIndented = false,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow)]
[JsonSerializable(typeof(ErrorResponse))]
internal sealed partial class BrokerErrorJsonStrictSerializerContext : JsonSerializerContext;
15 changes: 14 additions & 1 deletion policies/dotnet/Devolutions.Now.Policy.Api/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOpt
}

internal sealed class ExactCaseTransportConverter : ExactCaseStringEnumConverter<Transport>;
internal sealed class ExactCaseErrorCodeConverter : ExactCaseStringEnumConverter<ErrorCode>;

/// <summary>Package operation type.</summary>
[JsonConverter(typeof(JsonStringEnumConverter<Operation>))]
Expand Down Expand Up @@ -132,7 +133,7 @@ public enum HealthStatus
}

/// <summary>Structured machine-readable error code.</summary>
[JsonConverter(typeof(JsonStringEnumConverter<ErrorCode>))]
[JsonConverter(typeof(ExactCaseErrorCodeConverter))]
public enum ErrorCode
{
BadRequest,
Expand All @@ -146,6 +147,18 @@ public enum ErrorCode
BrokerPaused,
InternalError,
Timeout,
UnsupportedEndpoint,
MalformedDraft,
InvalidPolicy,
WarningConfirmationRequired,
Unauthenticated,
AdministratorRequired,
UnsafePolicyPath,
UnsupportedPolicyFormat,
StalePolicyStoreToken,
UnsupportedPolicyFilesystem,
PolicyPersistenceFailed,
PolicyActivationFailed,
}

/// <summary>Transport kind of a per-operation event channel.</summary>
Expand Down
8 changes: 8 additions & 0 deletions policies/dotnet/Devolutions.Now.Policy.Api/MetaModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ public string ResponseKind
[JsonPropertyName("Details")]
public List<ErrorDetail> Details { get; set; } = [];

/// <summary>Current authoritative policy findings for management errors.</summary>
[JsonPropertyName("Validation")]
public PolicyValidationResult? Validation { get; set; }

/// <summary>Atomic current policy state, required for stale store-token errors.</summary>
[JsonPropertyName("Management")]
public PolicyManagementSnapshot? Management { get; set; }

}

public sealed class ErrorDetail
Expand Down
Loading