Skip to content
Merged
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
205 changes: 174 additions & 31 deletions policies/dotnet/Devolutions.Now.Policy.Api/BrokerSerializer.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,11 @@ public enum PolicyFindingCode
InvalidFieldType,
InvalidFieldValue,
DuplicateRuleId,
IneffectiveBooleanMatch,
InvalidVersionRange,
EmptyVersionRange,
InvalidWildcardPattern,
ContradictoryConstraints,
InvalidValidityInterval,
UnsupportedPolicyType,
UnsupportedPolicyFormatVersion,
AuditModeEnabled,
DefaultAllow,
Expand Down
1 change: 1 addition & 0 deletions policies/dotnet/Devolutions.Now.Policy.Api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Architecture
- `MetaModels.cs` defines health, capabilities, manager capability, and error DTOs.
- `Enums.cs` defines package broker API enums and JSON string enum converters.
- `BrokerSerializer.cs` defines source-generated serializer options for the broker wire format. Public `BrokerSerializer.Options` and `BrokerSerializer.PrettyOptions` support every broker DTO, including the embedded policy model, without reflection and reject JSON null for non-nullable contract members.
- Both strict and non-strict broker deserialization reject duplicate property names before typed deserialization, including duplicates inside opaque draft JSON and embedded policy/management response objects. Embedded policy models also reject unknown members on every broker input path so removed restrictions cannot be discarded. Escaped names are decoded and compared with ordinal, case-sensitive equality.
- `PolicyCompatibility.cs` maps compatible API enums to and from `Devolutions.Now.Policy.Model` enums.

Opaque policy store tokens and validation receipts are restricted to safe printable ASCII (`A-Z`, `a-z`, `0-9`, `.`, `_`, `~`, `:`, `-`) beginning with an ASCII alphanumeric character, so Rust and .NET enforce identical bounds.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,42 @@ public async Task GetPolicy_rejects_unmapped_property(string objectPath)
await AssertInvalidPolicyResponse(document);
}

[Fact]
public async Task GetPolicy_rejects_duplicate_nested_policy_property_before_deserialization()
{
var path = Path.Combine(TestData.SamplesDir, "responses", "policy.response.json");
var body = await File.ReadAllTextAsync(path);
body = body.Replace(
"\"PolicyFormatVersion\": \"1.0.0\",",
"\"PolicyFormatVersion\": \"1.0.0\",\n\"PolicyFormatVersi\\u006fn\": \"1.0.0\",",
StringComparison.Ordinal);
var client = CreateClient(new FakeBrokerTransport(body));

var exception = await Assert.ThrowsAsync<BrokerClientException>(() => client.GetPolicy());

Assert.Equal(BrokerClientErrorKind.InvalidResponse, exception.Kind);
Assert.Equal("/v1/policy", exception.Endpoint);
Assert.IsAssignableFrom<JsonException>(exception.InnerException);
}

[Fact]
public async Task GetPolicy_wraps_invalid_surrogate_property_names()
{
var path = Path.Combine(TestData.SamplesDir, "responses", "policy.response.json");
var body = await File.ReadAllTextAsync(path);
body = body.Replace(
"\"PolicyFormatVersion\": \"1.0.0\",",
"\"PolicyFormatVersion\": \"1.0.0\",\n\"\\uD800\": true,",
StringComparison.Ordinal);
var client = CreateClient(new FakeBrokerTransport(body));

var exception = await Assert.ThrowsAsync<BrokerClientException>(() => client.GetPolicy());

Assert.Equal(BrokerClientErrorKind.InvalidResponse, exception.Kind);
Assert.Equal("/v1/policy", exception.Endpoint);
Assert.IsAssignableFrom<JsonException>(exception.InnerException);
}

[Fact]
public async Task GetPolicy_rejects_integer_policy_enum_token()
{
Expand All @@ -347,7 +383,6 @@ public async Task GetPolicy_rejects_integer_policy_enum_token()
[Theory]
[InlineData("Server.Transport", "httpnamedpipe")]
[InlineData("Policy.Enforcement.DefaultDecision", "deny")]
[InlineData("Policy.Enforcement.RulePrecedence", "prioritythendeny")]
[InlineData("Policy.Rules.0.Decision", "deny")]
[InlineData("Policy.Rules.0.Match.Operations.0", "install")]
public async Task GetPolicy_rejects_noncanonical_enum_casing(string propertyPath, string value)
Expand All @@ -362,7 +397,7 @@ public async Task GetPolicy_rejects_noncanonical_enum_casing(string propertyPath

[Theory]
[InlineData("Policy.Rules.0")]
[InlineData("Policy.Rules.3.Match.Sources.0")]
[InlineData("Policy.Rules.3.Match.SourceNames.0")]
public async Task GetPolicy_rejects_null_collection_element(string elementPath)
{
var path = Path.Combine(TestData.SamplesDir, "responses", "policy.response.json");
Expand All @@ -373,6 +408,18 @@ public async Task GetPolicy_rejects_null_collection_element(string elementPath)
await AssertInvalidPolicyResponse(document);
}

[Fact]
public async Task GetPolicy_rejects_constraints_on_deny_rule()
{
var path = Path.Combine(TestData.SamplesDir, "responses", "policy.response.json");
var document = JsonNode.Parse(await File.ReadAllTextAsync(path))
?? throw new InvalidOperationException("policy response sample should parse");
document["Policy"]!["Rules"]![0]!["Constraints"] =
new JsonObject { ["AllowInteractive"] = false };

await AssertInvalidPolicyResponse(document);
}

[Fact]
public async Task GetPolicy_propagates_cancellation()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public void PolicyResponse_rejects_null_non_nullable_property(string propertyPat

[Theory]
[InlineData("Policy.Rules.0")]
[InlineData("Policy.Rules.3.Match.Sources.0")]
[InlineData("Policy.Rules.3.Match.SourceNames.0")]
public void Strict_policy_response_rejects_null_collection_element(string elementPath)
{
var path = Path.Combine(TestData.SamplesDir, "responses", "policy.response.json");
Expand Down Expand Up @@ -142,6 +142,8 @@ public void Public_json_options_source_generate_all_broker_dtos()
typeof(PolicyEnforcement),
typeof(PolicyRule),
typeof(PolicyMatch),
typeof(PackageIdentifierCondition),
typeof(VersionCondition),
typeof(VersionRange),
typeof(PolicyConstraints),
];
Expand Down Expand Up @@ -169,6 +171,23 @@ public void Public_json_options_round_trip_policy_response_without_reflection()
Assert.Contains(Environment.NewLine, pretty);
}

[Fact]
public void Non_strict_broker_policy_inputs_reject_removed_policy_members()
{
var document = JsonNode.Parse(
File.ReadAllText(Path.Combine(TestData.SamplesDir, "responses", "policy.response.json")))!;
document["Policy"]!["Rules"]![3]!["Match"]!["PackageNames"] =
new JsonArray("Visual Studio Code");
var json = document.ToJsonString();

Assert.Throws<JsonException>(() => BrokerSerializer.Deserialize<PolicyResponse>(json));
foreach (var options in new[] { BrokerSerializer.Options, BrokerSerializer.PrettyOptions })
{
Assert.Throws<JsonException>(
() => JsonSerializer.Deserialize<PolicyResponse>(json, options));
}
}

[Fact]
public void Strict_policy_response_rejects_schema_member_and_unsupported_format_version()
{
Expand Down
Loading