From 0ae7725d25e4f4e38652f5ef0460e2ddc81dfa5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Thu, 3 Sep 2026 12:09:10 -0400 Subject: [PATCH] fix(policy): validate standalone constraints Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../PolicyTests.cs | 71 +++++++++++++++++++ .../PolicySerializer.cs | 49 +++++++------ 2 files changed, 99 insertions(+), 21 deletions(-) diff --git a/policies/dotnet/Devolutions.Now.Policy.Model.Tests/PolicyTests.cs b/policies/dotnet/Devolutions.Now.Policy.Model.Tests/PolicyTests.cs index edcd046..0d236fa 100644 --- a/policies/dotnet/Devolutions.Now.Policy.Model.Tests/PolicyTests.cs +++ b/policies/dotnet/Devolutions.Now.Policy.Model.Tests/PolicyTests.cs @@ -22,6 +22,14 @@ public class PolicyTests public static IEnumerable PolicySamples() => Directory.GetFiles(SamplesDir, "*.policy.json").Select(f => new object[] { f }); + public static TheoryData ConstraintTextCollections() => new() + { + { nameof(PolicyConstraints.AllowedInstallLocationPatterns), 256 }, + { nameof(PolicyConstraints.AllowedCustomParameters), 512 }, + { nameof(PolicyConstraints.AllowedCustomParameterPatterns), 512 }, + { nameof(PolicyConstraints.DeniedCustomParameters), 512 }, + }; + [Fact] public void Tests_run_with_reflection_json_disabled() { @@ -322,6 +330,45 @@ public void Policy_text_lists_count_unicode_scalars_at_length_boundaries(string Assert.Throws(() => PolicyDocument.ParseJson(document.ToJsonString())); } + [Theory] + [MemberData(nameof(ConstraintTextCollections))] + public void Direct_policy_constraints_reject_invalid_bounded_strings(string collectionName, int maximum) + { + foreach (var value in new[] { "", new string('x', maximum + 1) }) + { + var constraints = CreateConstraints(collectionName, value); + var json = new JsonObject { [collectionName] = new JsonArray(value) }.ToJsonString(); + + Assert.Throws(() => PolicySerializer.Serialize(constraints)); + Assert.Throws(() => PolicySerializer.DeserializeStrict(json)); + + foreach (var options in new[] { PolicySerializer.Options, PolicySerializer.StrictOptions }) + { + Assert.Throws(() => JsonSerializer.Serialize(constraints, options)); + Assert.Throws( + () => JsonSerializer.Deserialize(json, options)); + } + } + } + + [Theory] + [MemberData(nameof(ConstraintTextCollections))] + public void Direct_policy_constraints_accept_valid_boundary_strings(string collectionName, int maximum) + { + var value = string.Concat(Enumerable.Repeat("\U0001F600", maximum)); + var constraints = CreateConstraints(collectionName, value); + var json = new JsonObject { [collectionName] = new JsonArray(value) }.ToJsonString(); + + Assert.NotNull(PolicySerializer.Serialize(constraints)); + Assert.NotNull(PolicySerializer.DeserializeStrict(json)); + + foreach (var options in new[] { PolicySerializer.Options, PolicySerializer.StrictOptions }) + { + Assert.NotNull(JsonSerializer.Serialize(constraints, options)); + Assert.NotNull(JsonSerializer.Deserialize(json, options)); + } + } + [Fact] public void Draft_rejects_server_managed_metadata() { @@ -339,6 +386,30 @@ private static PolicyDocument ParsePolicy(string path) private static JsonNode ParseJsonString(string value) => JsonNode.Parse($"\"{value}\"")!; + private static PolicyConstraints CreateConstraints(string collectionName, string value) + { + var constraints = new PolicyConstraints(); + switch (collectionName) + { + case nameof(PolicyConstraints.AllowedInstallLocationPatterns): + constraints.AllowedInstallLocationPatterns = [value]; + break; + case nameof(PolicyConstraints.AllowedCustomParameters): + constraints.AllowedCustomParameters = [value]; + break; + case nameof(PolicyConstraints.AllowedCustomParameterPatterns): + constraints.AllowedCustomParameterPatterns = [value]; + break; + case nameof(PolicyConstraints.DeniedCustomParameters): + constraints.DeniedCustomParameters = [value]; + break; + default: + throw new ArgumentOutOfRangeException(nameof(collectionName), collectionName, null); + } + + return constraints; + } + private static string ResolvePolicyCrateRoot([CallerFilePath] string thisFile = "") { var testsDir = Path.GetDirectoryName(thisFile)!; diff --git a/policies/dotnet/Devolutions.Now.Policy.Model/PolicySerializer.cs b/policies/dotnet/Devolutions.Now.Policy.Model/PolicySerializer.cs index 905c015..1392084 100644 --- a/policies/dotnet/Devolutions.Now.Policy.Model/PolicySerializer.cs +++ b/policies/dotnet/Devolutions.Now.Policy.Model/PolicySerializer.cs @@ -64,6 +64,9 @@ private static void ValidateSemanticValue(object? value) case PolicyMatch match: ValidateRequiredCollectionElements(match, "$"); break; + case PolicyConstraints constraints: + ValidateRequiredCollectionElements(constraints, "$"); + break; } } @@ -96,30 +99,34 @@ private static void ValidateRequiredCollectionElements(PolicyRule rule, string p if (rule.Constraints is { } constraints) { - var constraintsPath = $"{path}.Constraints"; - RejectBoundedStrings( - constraints.AllowedInstallLocationPatterns, - 1, - 256, - $"{constraintsPath}.AllowedInstallLocationPatterns"); - RejectBoundedStrings( - constraints.AllowedCustomParameters, - 1, - 512, - $"{constraintsPath}.AllowedCustomParameters"); - RejectBoundedStrings( - constraints.AllowedCustomParameterPatterns, - 1, - 512, - $"{constraintsPath}.AllowedCustomParameterPatterns"); - RejectBoundedStrings( - constraints.DeniedCustomParameters, - 1, - 512, - $"{constraintsPath}.DeniedCustomParameters"); + ValidateRequiredCollectionElements(constraints, $"{path}.Constraints"); } } + private static void ValidateRequiredCollectionElements(PolicyConstraints constraints, string path) + { + RejectBoundedStrings( + constraints.AllowedInstallLocationPatterns, + 1, + 256, + $"{path}.AllowedInstallLocationPatterns"); + RejectBoundedStrings( + constraints.AllowedCustomParameters, + 1, + 512, + $"{path}.AllowedCustomParameters"); + RejectBoundedStrings( + constraints.AllowedCustomParameterPatterns, + 1, + 512, + $"{path}.AllowedCustomParameterPatterns"); + RejectBoundedStrings( + constraints.DeniedCustomParameters, + 1, + 512, + $"{path}.DeniedCustomParameters"); + } + private static void ValidateRequiredCollectionElements(PolicyMatch match, string path) { RejectBoundedStrings(match.Sources, 1, 256, $"{path}.Sources");