diff --git a/policies/dotnet/Devolutions.Now.Policy.Api/BrokerSerializer.cs b/policies/dotnet/Devolutions.Now.Policy.Api/BrokerSerializer.cs index 4911ce9..f4e9723 100644 --- a/policies/dotnet/Devolutions.Now.Policy.Api/BrokerSerializer.cs +++ b/policies/dotnet/Devolutions.Now.Policy.Api/BrokerSerializer.cs @@ -7,13 +7,6 @@ namespace Devolutions.Now.Policy.Api; -/// Canonical schema URI used in the $schema field of policy documents. -public static class SchemaUris -{ - public const string Policy = "https://devolutions.net/schemas/now-policy.schema.1.0.json"; - public const string PolicyDraft = "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json"; -} - /// Shared for broker documents. public static class BrokerSerializer { diff --git a/policies/dotnet/Devolutions.Now.Policy.Api/PolicyManagementModels.cs b/policies/dotnet/Devolutions.Now.Policy.Api/PolicyManagementModels.cs index 578061e..6875840 100644 --- a/policies/dotnet/Devolutions.Now.Policy.Api/PolicyManagementModels.cs +++ b/policies/dotnet/Devolutions.Now.Policy.Api/PolicyManagementModels.cs @@ -135,9 +135,8 @@ public enum PolicyFindingCode InvalidWildcardPattern, ContradictoryConstraints, InvalidValidityInterval, - UnsupportedSchema, UnsupportedPolicyType, - UnsupportedPolicyVersion, + UnsupportedPolicyFormatVersion, AuditModeEnabled, DefaultAllow, SensitiveOptionAllowed, diff --git a/policies/dotnet/Devolutions.Now.Policy.Api/README.md b/policies/dotnet/Devolutions.Now.Policy.Api/README.md index 287e0e5..9dc5edd 100644 --- a/policies/dotnet/Devolutions.Now.Policy.Api/README.md +++ b/policies/dotnet/Devolutions.Now.Policy.Api/README.md @@ -21,6 +21,8 @@ The DTOs are used to: - share the same JSON wire shape as the Rust source-of-truth model; - provide compatibility conversions between package broker API enums and the `Devolutions.Now.Policy.Model` policy enums. +Embedded policy documents and response policy projections use the software-managed `PolicyFormatVersion` field. New values are stamped as `1.0.0`; supported numeric versions in the 1.x line are accepted and preserved. + Architecture ------------ diff --git a/policies/dotnet/Devolutions.Now.Policy.Api/ResponseModels.cs b/policies/dotnet/Devolutions.Now.Policy.Api/ResponseModels.cs index 3d59925..6ec6e5f 100644 --- a/policies/dotnet/Devolutions.Now.Policy.Api/ResponseModels.cs +++ b/policies/dotnet/Devolutions.Now.Policy.Api/ResponseModels.cs @@ -167,8 +167,9 @@ public sealed class ResponsePolicyInfo [JsonPropertyName("Revision")] public int Revision { get; set; } - [JsonPropertyName("PolicyVersion")] - public string PolicyVersion { get; set; } = "1.0.0"; + [JsonPropertyName("PolicyFormatVersion")] + [JsonRequired] + public PolicyFormatVersion PolicyFormatVersion { get; init; } = PolicyFormatVersion.Current; } public sealed class OperationDiagnostics diff --git a/policies/dotnet/Devolutions.Now.Policy.Client.Tests/BrokerClientTests.cs b/policies/dotnet/Devolutions.Now.Policy.Client.Tests/BrokerClientTests.cs index 00b57e6..3eb6b8c 100644 --- a/policies/dotnet/Devolutions.Now.Policy.Client.Tests/BrokerClientTests.cs +++ b/policies/dotnet/Devolutions.Now.Policy.Client.Tests/BrokerClientTests.cs @@ -31,7 +31,7 @@ public async Task Evaluate_populates_client_context_and_missing_metadata_before_ var transport = new FakeBrokerTransport( CapabilitiesResponse, """ - {"ResponseKind":"EvaluationResponse","ResponseVersion":"1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"unused","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"WouldExecute":true,"Policy":{"Id":"mock.policy","Revision":1,"PolicyVersion":"1.0.0"}} + {"ResponseKind":"EvaluationResponse","ResponseVersion":"1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"unused","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"WouldExecute":true,"Policy":{"Id":"mock.policy","Revision":1,"PolicyFormatVersion":"1.0.0"}} """); var client = new BrokerClient(new BrokerClientOptions { @@ -77,7 +77,7 @@ public async Task Execute_normalizes_explicit_request_id_and_preserves_created_a var transport = new FakeBrokerTransport( CapabilitiesResponse, """ - {"ResponseKind":"ExecutionResponse","ResponseVersion":"1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyVersion":"1.0.0"},"Operation":{"OperationId":"operation:123","Status":"Starting","SubmittedAt":"2026-06-29T12:00:02Z"}} + {"ResponseKind":"ExecutionResponse","ResponseVersion":"1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyFormatVersion":"1.0.0"},"Operation":{"OperationId":"operation:123","Status":"Starting","SubmittedAt":"2026-06-29T12:00:02Z"}} """); var client = CreateClient(transport); var createdAt = DateTimeOffset.Parse("2026-06-29T12:00:00Z"); @@ -104,7 +104,7 @@ public async Task ExecuteAndWait_throws_typed_error_when_policy_denies_request() var transport = new FakeBrokerTransport( CapabilitiesResponse, """ - {"ResponseKind":"ExecutionResponse","ResponseVersion":"1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Deny","RuleId":"block-rule","Reason":"blocked"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyVersion":"1.0.0"}} + {"ResponseKind":"ExecutionResponse","ResponseVersion":"1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Deny","RuleId":"block-rule","Reason":"blocked"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyFormatVersion":"1.0.0"}} """); var client = CreateClient(transport); @@ -182,7 +182,7 @@ public async Task ExecuteAndWait_treats_canceled_status_as_terminal() var transport = new FakeBrokerTransport( CapabilitiesResponse, """ - {"ResponseKind":"ExecutionResponse","ResponseVersion": "1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyVersion":"1.0.0"},"Operation":{"OperationId":"operation:123","Status":"Starting","SubmittedAt":"2026-06-29T12:00:02Z"}} + {"ResponseKind":"ExecutionResponse","ResponseVersion": "1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyFormatVersion":"1.0.0"},"Operation":{"OperationId":"operation:123","Status":"Starting","SubmittedAt":"2026-06-29T12:00:02Z"}} """, """ {"ResponseKind":"StatusResponse","ResponseVersion": "1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"OperationId":"operation:123","RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","Status":"Canceled","Message":"operation was canceled"} @@ -208,7 +208,7 @@ public async Task ExecuteAndWait_requests_broker_cancelation_when_token_is_cance var transport = new FakeBrokerTransport( CapabilitiesResponse, """ - {"ResponseKind":"ExecutionResponse","ResponseVersion": "1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyVersion":"1.0.0"},"Operation":{"OperationId":"operation:123","Status":"Starting","SubmittedAt":"2026-06-29T12:00:02Z"}} + {"ResponseKind":"ExecutionResponse","ResponseVersion": "1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","ReceivedAt":"2026-06-29T12:00:00Z","CompletedAt":"2026-06-29T12:00:01Z","Request":{},"Decision":{"Decision":"Allow","RuleId":"","Reason":"allowed"},"Policy":{"Id":"mock.policy","Revision":1,"PolicyFormatVersion":"1.0.0"},"Operation":{"OperationId":"operation:123","Status":"Starting","SubmittedAt":"2026-06-29T12:00:02Z"}} """, """ {"ResponseKind":"CancelResponse","ResponseVersion": "1.0","Server":{"ServerVersion":"mock","Transport":"HttpNamedPipe"},"OperationId":"operation:123","RequestId":"6f8f1f54-8c42-4773-932a-ff7c7c9f58f1","Status":"Canceling"} @@ -412,7 +412,7 @@ public async Task GetPolicy_preserves_structured_unsupported_error(string transp [InlineData("Server")] [InlineData("Server.ServerVersion")] [InlineData("Server.Transport")] - [InlineData("Policy.$schema")] + [InlineData("Policy.PolicyFormatVersion")] [InlineData("Policy.Metadata.Id")] [InlineData("Policy.Enforcement.DefaultDecision")] [InlineData("Policy.Rules")] diff --git a/policies/dotnet/Devolutions.Now.Policy.Client.Tests/MetaModelTests.cs b/policies/dotnet/Devolutions.Now.Policy.Client.Tests/MetaModelTests.cs index c5dbf48..158592d 100644 --- a/policies/dotnet/Devolutions.Now.Policy.Client.Tests/MetaModelTests.cs +++ b/policies/dotnet/Devolutions.Now.Policy.Client.Tests/MetaModelTests.cs @@ -169,6 +169,42 @@ public void Public_json_options_round_trip_policy_response_without_reflection() Assert.Contains(Environment.NewLine, pretty); } + [Fact] + public void Strict_policy_response_rejects_schema_member_and_unsupported_format_version() + { + var document = JsonNode.Parse( + File.ReadAllText(Path.Combine(TestData.SamplesDir, "responses", "policy.response.json")))!; + document["Policy"]!["$schema"] = "https://example.invalid/policy.schema.json"; + Assert.Throws( + () => BrokerSerializer.DeserializeStrict(document.ToJsonString())); + + document["Policy"]!.AsObject().Remove("$schema"); + document["Policy"]!["PolicyFormatVersion"] = "2.0.0"; + Assert.Throws( + () => BrokerSerializer.DeserializeStrict(document.ToJsonString())); + } + + [Theory] + [InlineData("winget-vscode-install.allowed.response.json", "EvaluationResponse")] + [InlineData("execution-winget-vscode-install.response.json", "ExecutionResponse")] + public void Strict_operation_response_requires_policy_format_version(string fixture, string responseType) + { + var document = JsonNode.Parse( + File.ReadAllText(Path.Combine(TestData.SamplesDir, "responses", fixture)))!; + document["Policy"]!.AsObject().Remove("PolicyFormatVersion"); + + if (responseType == "EvaluationResponse") + { + Assert.Throws( + () => BrokerSerializer.DeserializeStrict(document.ToJsonString())); + } + else + { + Assert.Throws( + () => BrokerSerializer.DeserializeStrict(document.ToJsonString())); + } + } + [Fact] public async Task ErrorResponse_serializes_to_schema_valid_output() { diff --git a/policies/dotnet/Devolutions.Now.Policy.Client.Tests/PolicyManagementClientTests.cs b/policies/dotnet/Devolutions.Now.Policy.Client.Tests/PolicyManagementClientTests.cs index cc8292c..ec91b92 100644 --- a/policies/dotnet/Devolutions.Now.Policy.Client.Tests/PolicyManagementClientTests.cs +++ b/policies/dotnet/Devolutions.Now.Policy.Client.Tests/PolicyManagementClientTests.cs @@ -537,10 +537,6 @@ public async Task Public_serializer_options_enforce_policy_root_semantic_invaria } var committedDto = JsonSerializer.Deserialize(committed.ToJsonString(), options)!; - committedDto.Schema = Devolutions.Now.Policy.Model.SchemaUris.PolicyDraft; - Assert.Throws(() => JsonSerializer.Serialize(committedDto, options)); - - committedDto = JsonSerializer.Deserialize(committed.ToJsonString(), options)!; committedDto.Metadata.Revision = 0; Assert.Throws(() => JsonSerializer.Serialize(committedDto, options)); @@ -553,10 +549,6 @@ public async Task Public_serializer_options_enforce_policy_root_semantic_invaria Assert.Throws(() => JsonSerializer.Serialize(committedDto, options)); var draftDto = JsonSerializer.Deserialize(draft.ToJsonString(), options)!; - draftDto.Schema = Devolutions.Now.Policy.Model.SchemaUris.Policy; - Assert.Throws(() => JsonSerializer.Serialize(draftDto, options)); - - draftDto = JsonSerializer.Deserialize(draft.ToJsonString(), options)!; draftDto.Rules[0].Match.SkipHashCheck = [false, true]; Assert.Throws(() => JsonSerializer.Serialize(draftDto, options)); } @@ -646,10 +638,6 @@ private static JsonNode MismatchedReplacementCanonicalDraft(JsonNode response) private static IEnumerable InvalidCommittedPolicies(JsonNode committed) { - var wrongSchema = committed.DeepClone(); - wrongSchema["$schema"] = Devolutions.Now.Policy.Model.SchemaUris.PolicyDraft; - yield return wrongSchema; - var zeroRevision = committed.DeepClone(); zeroRevision["Metadata"]!["Revision"] = 0; yield return zeroRevision; @@ -665,10 +653,6 @@ private static IEnumerable InvalidCommittedPolicies(JsonNode committed private static IEnumerable InvalidDraftPolicies(JsonNode draft) { - var wrongSchema = draft.DeepClone(); - wrongSchema["$schema"] = Devolutions.Now.Policy.Model.SchemaUris.Policy; - yield return wrongSchema; - var mixedBooleanMatch = draft.DeepClone(); mixedBooleanMatch["Rules"]![0]!["Match"]!["SkipHashCheck"] = new JsonArray(false, true); yield return mixedBooleanMatch; diff --git a/policies/dotnet/Devolutions.Now.Policy.Model.Tests/PolicyTests.cs b/policies/dotnet/Devolutions.Now.Policy.Model.Tests/PolicyTests.cs index 0d236fa..ce0fa99 100644 --- a/policies/dotnet/Devolutions.Now.Policy.Model.Tests/PolicyTests.cs +++ b/policies/dotnet/Devolutions.Now.Policy.Model.Tests/PolicyTests.cs @@ -84,21 +84,24 @@ public async Task Draft_conversion_uses_and_validates_against_draft_schema() var schema = await JsonSchema.FromFileAsync(PolicyDraftSchema); var json = draft.ToJson(); - Assert.Equal(SchemaUris.PolicyDraft, draft.Schema); + Assert.Equal(PolicyFormatVersions.Current, draft.PolicyFormatVersion.Value); + Assert.Null(JsonNode.Parse(json)!["$schema"]); Assert.Empty(schema.Validate(json)); - Assert.Equal(SchemaUris.Policy, draft.ToPolicyDocument(1, DateTimeOffset.UtcNow).Schema); + Assert.Equal( + PolicyFormatVersions.Current, + draft.ToPolicyDocument(1, DateTimeOffset.UtcNow).PolicyFormatVersion.Value); } [Fact] - public void Policy_and_draft_parsers_reject_the_other_document_schema() + public void Policy_and_draft_parsers_reject_schema_member() { var policy = PolicyDocument.Create("contoso.policy", "Contoso IT"); var policyJson = JsonNode.Parse(policy.ToJson())!; - policyJson["$schema"] = SchemaUris.PolicyDraft; + policyJson["$schema"] = "https://example.invalid/policy.schema.json"; Assert.Throws(() => PolicyDocument.ParseJson(policyJson.ToJsonString())); var draftJson = JsonNode.Parse(policy.ToDraft().ToJson())!; - draftJson["$schema"] = SchemaUris.Policy; + draftJson["$schema"] = "https://example.invalid/policy-draft.schema.json"; Assert.Throws( () => PolicySerializer.DeserializePolicyDraftDocumentStrict(draftJson.ToJsonString())); } @@ -161,8 +164,7 @@ public void Negative_priority_is_rejected_by_parser() } [Theory] - [InlineData("$schema")] - [InlineData("PolicyVersion")] + [InlineData("PolicyFormatVersion")] [InlineData("PolicyType")] [InlineData("Metadata")] [InlineData("Enforcement")] @@ -189,8 +191,7 @@ public void Missing_rust_required_property_is_rejected_by_parser(string property } [Theory] - [InlineData("$schema")] - [InlineData("PolicyVersion")] + [InlineData("PolicyFormatVersion")] [InlineData("PolicyType")] [InlineData("Metadata")] [InlineData("Enforcement")] @@ -238,6 +239,7 @@ public void Draft_conversion_omits_and_restores_server_metadata_without_aliasing var draft = committed.ToDraft(); var draftJson = JsonNode.Parse(draft.ToJson())!; + Assert.Null(draftJson["$schema"]); Assert.Null(draftJson["Metadata"]!["Revision"]); Assert.Null(draftJson["Metadata"]!["PublishedAt"]); @@ -246,11 +248,60 @@ public void Draft_conversion_omits_and_restores_server_metadata_without_aliasing var publishedAt = DateTimeOffset.Parse("2026-08-29T00:00:00Z"); var recommitted = draft.ToPolicyDocument(7, publishedAt); + Assert.Equal(committed.PolicyFormatVersion, recommitted.PolicyFormatVersion); + Assert.Equal(committed.Metadata.Id, recommitted.Metadata.Id); + Assert.Equal(committed.Metadata.Publisher, recommitted.Metadata.Publisher); + Assert.Equal(committed.Metadata.Description, recommitted.Metadata.Description); + Assert.Equal(committed.Metadata.SupportUrl, recommitted.Metadata.SupportUrl); + Assert.Equal(committed.Metadata.ValidFrom, recommitted.Metadata.ValidFrom); + Assert.Equal(committed.Metadata.ValidUntil, recommitted.Metadata.ValidUntil); Assert.Equal(7U, recommitted.Metadata.Revision); Assert.Equal(publishedAt, recommitted.Metadata.PublishedAt); Assert.Equal("changed", recommitted.Rules[0].Id); } + [Theory] + [InlineData("not-semver")] + [InlineData("2.0.0")] + [InlineData("1.18446744073709551616.0")] + [InlineData("1.2.3-beta")] + [InlineData("1.0.0\n")] + public void Unsupported_policy_format_versions_are_rejected(string value) + { + var document = JsonNode.Parse( + File.ReadAllText(Path.Combine(SamplesDir, "corporate-allowlist.policy.json")))!; + document["PolicyFormatVersion"] = value; + + Assert.Throws(() => PolicyDocument.ParseJson(document.ToJsonString())); + } + + [Theory] + [InlineData("1.0.0-01")] + [InlineData("1.0.0-.")] + [InlineData("1.0.0\n")] + public async Task Rust_schema_rejects_unsupported_policy_format_versions(string value) + { + var document = JsonNode.Parse( + File.ReadAllText(Path.Combine(SamplesDir, "corporate-allowlist.policy.json")))!; + document["PolicyFormatVersion"] = value; + var schema = await JsonSchema.FromFileAsync(PolicySchema); + + Assert.NotEmpty(schema.Validate(document.ToJsonString())); + } + + [Fact] + public void Compatible_policy_format_version_is_preserved_by_conversion() + { + var document = JsonNode.Parse( + File.ReadAllText(Path.Combine(SamplesDir, "corporate-allowlist.policy.json")))!; + document["PolicyFormatVersion"] = "1.2.3"; + + var committed = PolicyDocument.ParseJson(document.ToJsonString()); + var recommitted = committed.ToDraft().ToPolicyDocument(8, DateTimeOffset.UtcNow); + + Assert.Equal("1.2.3", recommitted.PolicyFormatVersion.Value); + } + [Fact] public void Draft_conversion_enforces_revision_bounds() { @@ -420,8 +471,7 @@ private static string MinimalPolicyJson(string revision, string rules) { return $$""" { - "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "test.policy", diff --git a/policies/dotnet/Devolutions.Now.Policy.Model/PolicyModels.cs b/policies/dotnet/Devolutions.Now.Policy.Model/PolicyModels.cs index f371b58..09004fb 100644 --- a/policies/dotnet/Devolutions.Now.Policy.Model/PolicyModels.cs +++ b/policies/dotnet/Devolutions.Now.Policy.Model/PolicyModels.cs @@ -3,22 +3,105 @@ namespace Devolutions.Now.Policy.Model; -public static class SchemaUris +public static class PolicyFormatVersions { - public const string Policy = "https://devolutions.net/schemas/now-policy.schema.1.0.json"; - public const string PolicyDraft = "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json"; + public const string Current = "1.0.0"; + public const ulong SupportedMajor = 1; +} + +[JsonConverter(typeof(PolicyFormatVersionJsonConverter))] +public sealed class PolicyFormatVersion : IEquatable +{ + private PolicyFormatVersion(string value) + { + Value = value; + } + + public static PolicyFormatVersion Current { get; } = new(PolicyFormatVersions.Current); + + public string Value { get; } + + public static PolicyFormatVersion Parse(string value) + { + ArgumentNullException.ThrowIfNull(value); + if (value.Length > 128) + { + throw new FormatException("PolicyFormatVersion must contain at most 128 characters."); + } + + var components = value.Split('.'); + if (components.Length != 3 + || !TryParseComponent(components[0], out var major) + || !TryParseComponent(components[1], out _) + || !TryParseComponent(components[2], out _)) + { + throw new FormatException( + "PolicyFormatVersion must contain three canonical unsigned 64-bit integer components."); + } + if (major != PolicyFormatVersions.SupportedMajor) + { + throw new NotSupportedException( + $"Policy format major version {major} is unsupported; supported major version is {PolicyFormatVersions.SupportedMajor}."); + } + + return new PolicyFormatVersion(value); + } + + private static bool TryParseComponent(string component, out ulong value) + { + value = 0; + return component.Length >= 1 + && (component.Length == 1 || component[0] != '0') + && ulong.TryParse( + component, + System.Globalization.NumberStyles.None, + System.Globalization.CultureInfo.InvariantCulture, + out value); + } + + public bool Equals(PolicyFormatVersion? other) => + other is not null && string.Equals(Value, other.Value, StringComparison.Ordinal); + + public override bool Equals(object? obj) => Equals(obj as PolicyFormatVersion); + + public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value); + + public override string ToString() => Value; +} + +internal sealed class PolicyFormatVersionJsonConverter : JsonConverter +{ + public override PolicyFormatVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException("PolicyFormatVersion must be a string."); + } + + try + { + return PolicyFormatVersion.Parse(reader.GetString()!); + } + catch (Exception exception) when (exception is FormatException or NotSupportedException) + { + throw new JsonException(exception.Message, exception); + } + } + + public override void Write(Utf8JsonWriter writer, PolicyFormatVersion value, JsonSerializerOptions options) => + writer.WriteStringValue(value.Value); } /// A policy document governing which package operations are allowed or denied. public sealed class PolicyDocument { - [JsonPropertyName("$schema")] - [JsonRequired] - public string Schema { get; set; } = SchemaUris.Policy; - - [JsonPropertyName("PolicyVersion")] + /// + /// Software-managed document-format version. Applications must not expose + /// this as publisher-authored editable metadata. + /// + [JsonPropertyName("PolicyFormatVersion")] [JsonRequired] - public string PolicyVersion { get; set; } = "1.0.0"; + public PolicyFormatVersion PolicyFormatVersion { get; init; } = PolicyFormatVersion.Current; [JsonPropertyName("PolicyType")] [JsonRequired] @@ -65,8 +148,7 @@ public PolicyDraftDocument ToDraft() { return new PolicyDraftDocument { - Schema = SchemaUris.PolicyDraft, - PolicyVersion = PolicyVersion, + PolicyFormatVersion = PolicyFormatVersion, PolicyType = PolicyType, Metadata = PolicyModelClone.ToDraftMetadata(Metadata), Enforcement = PolicyModelClone.Enforcement(Enforcement), @@ -82,13 +164,13 @@ public sealed class PolicyDraftDocument { private const uint MaxRevision = int.MaxValue; - [JsonPropertyName("$schema")] - [JsonRequired] - public string Schema { get; set; } = SchemaUris.PolicyDraft; - - [JsonPropertyName("PolicyVersion")] + /// + /// Software-managed document-format version. Applications must stamp the + /// current value for new drafts and must not expose it as authored metadata. + /// + [JsonPropertyName("PolicyFormatVersion")] [JsonRequired] - public string PolicyVersion { get; set; } = "1.0.0"; + public PolicyFormatVersion PolicyFormatVersion { get; init; } = PolicyFormatVersion.Current; [JsonPropertyName("PolicyType")] [JsonRequired] @@ -143,8 +225,7 @@ public PolicyDocument ToPolicyDocument(uint revision, DateTimeOffset publishedAt return new PolicyDocument { - Schema = SchemaUris.Policy, - PolicyVersion = PolicyVersion, + PolicyFormatVersion = PolicyFormatVersion, PolicyType = PolicyType, Metadata = PolicyModelClone.ToCommittedMetadata(Metadata, revision, publishedAt), Enforcement = PolicyModelClone.Enforcement(Enforcement), diff --git a/policies/dotnet/Devolutions.Now.Policy.Model/PolicySerializer.cs b/policies/dotnet/Devolutions.Now.Policy.Model/PolicySerializer.cs index 1392084..7ad7f7c 100644 --- a/policies/dotnet/Devolutions.Now.Policy.Model/PolicySerializer.cs +++ b/policies/dotnet/Devolutions.Now.Policy.Model/PolicySerializer.cs @@ -72,14 +72,12 @@ private static void ValidateSemanticValue(object? value) internal static void ValidateRequiredCollectionElements(PolicyDocument policy) { - ValidateSchemaUri(policy.Schema, SchemaUris.Policy, "$.$schema"); ValidatePolicyRevision(policy.Metadata.Revision); ValidateRequiredCollectionElements(policy.Rules); } internal static void ValidateRequiredCollectionElements(PolicyDraftDocument policy) { - ValidateSchemaUri(policy.Schema, SchemaUris.PolicyDraft, "$.$schema"); ValidateRequiredCollectionElements(policy.Rules); } @@ -179,14 +177,6 @@ private static void ValidatePolicyRevision(uint revision) } } - private static void ValidateSchemaUri(string actual, string expected, string path) - { - if (!string.Equals(actual, expected, StringComparison.Ordinal)) - { - throw new JsonException($"The JSON string at {path} must be '{expected}'."); - } - } - private static void RejectNullElements(IReadOnlyList values, string path) where T : class { diff --git a/policies/dotnet/Devolutions.Now.Policy.Model/README.md b/policies/dotnet/Devolutions.Now.Policy.Model/README.md index eda151c..2e552dd 100644 --- a/policies/dotnet/Devolutions.Now.Policy.Model/README.md +++ b/policies/dotnet/Devolutions.Now.Policy.Model/README.md @@ -25,6 +25,8 @@ Architecture `PolicyDocument.Create` constructs a committed policy and `PolicyDraftDocument.Create` constructs an editable draft. `PolicyDocument.ToDraft` removes server-managed `Revision` and `PublishedAt`; `PolicyDraftDocument.ToPolicyDocument` requires those values when committing. `ParseJson` is the only policy parsing entry point. +`PolicyFormatVersion` is software-managed format compatibility metadata, not a publisher release version. New documents stamp `1.0.0`; readers accept and preserve supported numeric versions in the 1.x line and reject malformed or unsupported-major values. Applications must not expose it as authored metadata. Policy documents contain no `$schema` member, and strict readers reject documents that contain one as unknown-field input. + Breaking change --------------- diff --git a/policies/rust/now-policy-api/openapi/now-policy-api.yaml b/policies/rust/now-policy-api/openapi/now-policy-api.yaml index 61cb694..cb6f555 100644 --- a/policies/rust/now-policy-api/openapi/now-policy-api.yaml +++ b/policies/rust/now-policy-api/openapi/now-policy-api.yaml @@ -1247,9 +1247,8 @@ components: - InvalidWildcardPattern - ContradictoryConstraints - InvalidValidityInterval - - UnsupportedSchema - UnsupportedPolicyType - - UnsupportedPolicyVersion + - UnsupportedPolicyFormatVersion - AuditModeEnabled - DefaultAllow - SensitiveOptionAllowed @@ -1259,6 +1258,18 @@ components: enum: - Error - Warning + PolicyFormatVersion: + description: |- + Software-managed policy document format version. + + Readers accept canonical numeric versions in the compatible 1.x line. + Applications must stamp the current value, `1.0.0`, for new documents and + must not expose this value as publisher-authored editable metadata. + Schemas describe the canonical shape; runtime readers additionally bound + each numeric component to an unsigned 64-bit integer. + type: string + maxLength: 128 + pattern: ^1\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?![\s\S]) PolicyManagementResponse: description: Response body for `GET /v1/policy/management`. type: object @@ -1839,10 +1850,10 @@ components: description: Policy document identifier. allOf: - $ref: '#/components/schemas/ResourceId' - PolicyVersion: - description: Policy syntax version. + PolicyFormatVersion: + description: Software-managed policy document format version. allOf: - - $ref: '#/components/schemas/SemanticVersion' + - $ref: '#/components/schemas/PolicyFormatVersion' Revision: description: Policy revision number. type: integer @@ -1853,7 +1864,7 @@ components: required: - Id - Revision - - PolicyVersion + - PolicyFormatVersion RuleId: description: Rule ID in responses. Includes sentinel values not valid as policy rule IDs. type: string @@ -1865,11 +1876,6 @@ components: enum: - User - Machine - SemanticVersion: - description: Semantic version string (SemVer 2.0.0). - type: string - maxLength: 128 - pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$ ServerContext: description: Server context included in responses. type: object @@ -2117,10 +2123,6 @@ components: description: A policy document governing which package operations are allowed or denied. type: object properties: - $schema: - description: Policy schema URI constant. - allOf: - - $ref: '#/components/schemas/PolicyModelPolicySchemaUri' Enforcement: description: Enforcement configuration. allOf: @@ -2129,14 +2131,17 @@ components: description: Policy metadata. allOf: - $ref: '#/components/schemas/PolicyModelPolicyMetadata' + PolicyFormatVersion: + description: |- + Software-managed policy document format version. + + Applications must not expose this field as publisher-authored editable metadata. + allOf: + - $ref: '#/components/schemas/PolicyModelPolicyFormatVersion' PolicyType: description: Must be `"PackageBrokerPolicy"`. allOf: - $ref: '#/components/schemas/PolicyModelPackageBrokerPolicy' - PolicyVersion: - description: Policy syntax version (semver). - allOf: - - $ref: '#/components/schemas/PolicyModelSemanticVersion' Rules: description: Ordered list of policy rules (may be empty; enforcement defaults apply). type: array @@ -2145,8 +2150,7 @@ components: maxItems: 1024 additionalProperties: false required: - - $schema - - PolicyVersion + - PolicyFormatVersion - PolicyType - Metadata - Enforcement @@ -2155,10 +2159,6 @@ components: description: An editable policy document without server-managed commit metadata. type: object properties: - $schema: - description: Policy schema URI constant. - allOf: - - $ref: '#/components/schemas/PolicyModelPolicyDraftSchemaUri' Enforcement: description: Enforcement configuration. allOf: @@ -2167,14 +2167,18 @@ components: description: Editable policy metadata. allOf: - $ref: '#/components/schemas/PolicyModelPolicyDraftMetadata' + PolicyFormatVersion: + description: |- + Software-managed policy document format version. + + Applications must stamp the current value and must not expose this field + as publisher-authored editable metadata. + allOf: + - $ref: '#/components/schemas/PolicyModelPolicyFormatVersion' PolicyType: description: Must be `"PackageBrokerPolicy"`. allOf: - $ref: '#/components/schemas/PolicyModelPackageBrokerPolicy' - PolicyVersion: - description: Policy syntax version (semver). - allOf: - - $ref: '#/components/schemas/PolicyModelSemanticVersion' Rules: description: Ordered list of policy rules (may be empty; enforcement defaults apply). type: array @@ -2183,8 +2187,7 @@ components: maxItems: 1024 additionalProperties: false required: - - $schema - - PolicyVersion + - PolicyFormatVersion - PolicyType - Metadata - Enforcement @@ -2228,10 +2231,6 @@ components: required: - Id - Publisher - PolicyModelPolicyDraftSchemaUri: - type: string - enum: - - https://devolutions.net/schemas/now-policy-draft.schema.1.0.json PolicyModelPolicyEnforcement: description: Enforcement configuration. type: object @@ -2252,6 +2251,18 @@ components: required: - DefaultDecision - RulePrecedence + PolicyModelPolicyFormatVersion: + description: |- + Software-managed policy document format version. + + Readers accept canonical numeric versions in the compatible 1.x line. + Applications must stamp the current value, `1.0.0`, for new documents and + must not expose this value as publisher-authored editable metadata. + Schemas describe the canonical shape; runtime readers additionally bound + each numeric component to an unsigned 64-bit integer. + type: string + maxLength: 128 + pattern: ^1\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?![\s\S]) PolicyModelPolicyMatch: description: |- Match criteria for a policy rule. All specified fields must match. @@ -2485,10 +2496,6 @@ components: - Priority - Decision - Match - PolicyModelPolicySchemaUri: - type: string - enum: - - https://devolutions.net/schemas/now-policy.schema.1.0.json PolicyModelResourceId: description: Resource identifier (policy IDs, rule IDs, request IDs, audit IDs). type: string @@ -2505,14 +2512,6 @@ components: enum: - User - Machine - PolicyModelSemanticVersion: - description: |- - Semantic version string (SemVer 2.0.0). - - Validated at deserialization time using the `semver` crate. - type: string - maxLength: 128 - pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$ PolicyModelStringPattern: description: Case-insensitive exact value or wildcard pattern. type: string diff --git a/policies/rust/now-policy-api/src/api.rs b/policies/rust/now-policy-api/src/api.rs index 8cdc4ac..7c4150e 100644 --- a/policies/rust/now-policy-api/src/api.rs +++ b/policies/rust/now-policy-api/src/api.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::enums::{Architecture, Decision, Elevation, ErrorCode, ManagerName, Operation, Scope, Transport}; use super::{ ApiVersion, CommandString, CustomParameterString, ErrorResponseKind, PackageIdentifier, PackageRequestKind, - ProcessName, ResourceId, RuleId, SemanticVersion, VersionString, + ProcessName, ResourceId, RuleId, VersionString, }; /// Canonical request sent by a package broker client to the elevated broker. @@ -254,8 +254,8 @@ pub struct ResponsePolicyInfo { #[schemars(range(min = 1, max = 2147483647))] pub revision: u32, - /// Policy syntax version. - pub policy_version: SemanticVersion, + /// Software-managed policy document format version. + pub policy_format_version: now_policy::PolicyFormatVersion, } /// Optional operation diagnostics. diff --git a/policies/rust/now-policy-api/src/management.rs b/policies/rust/now-policy-api/src/management.rs index ed0afcb..6bb7579 100644 --- a/policies/rust/now-policy-api/src/management.rs +++ b/policies/rust/now-policy-api/src/management.rs @@ -102,9 +102,8 @@ pub enum PolicyFindingCode { InvalidWildcardPattern, ContradictoryConstraints, InvalidValidityInterval, - UnsupportedSchema, UnsupportedPolicyType, - UnsupportedPolicyVersion, + UnsupportedPolicyFormatVersion, AuditModeEnabled, DefaultAllow, SensitiveOptionAllowed, @@ -975,8 +974,7 @@ mod tests { "ValidatorVersion": "validator/1", "IsValid": true, "CanonicalDraft": { - "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "test", "Publisher": "test" }, "Enforcement": { "DefaultDecision": "Deny", "RulePrecedence": "PriorityThenDeny" }, diff --git a/policies/rust/now-policy-server-template/src/server.rs b/policies/rust/now-policy-server-template/src/server.rs index 540b64c..65ac716 100644 --- a/policies/rust/now-policy-server-template/src/server.rs +++ b/policies/rust/now-policy-server-template/src/server.rs @@ -493,7 +493,6 @@ mod tests { "Operation", "ResourceId", "Scope", - "SemanticVersion", "VersionString", ] { assert!( @@ -509,6 +508,10 @@ mod tests { "component collision must not rename {name}" ); } + + assert!(schemas.contains_key("PolicyFormatVersion")); + assert!(schemas.contains_key("PolicyModelPolicyFormatVersion")); + assert!(!schemas.contains_key("PolicyFormatVersion2")); } #[test] diff --git a/policies/rust/now-policy/README.md b/policies/rust/now-policy/README.md index 484ddf0..ee3ecdb 100644 --- a/policies/rust/now-policy/README.md +++ b/policies/rust/now-policy/README.md @@ -6,4 +6,6 @@ This crate provides the JSON-only Rust policy model and JSON Schema helpers for It contains committed `PolicyDocument` and editable `PolicyDraftDocument` types, explicit conversions that add or remove server-managed metadata, and schema generation utilities. Broker request, response, server, transport, and execution types are intentionally out of scope. +`PolicyFormatVersion` is a software-managed document-format marker, not a publisher release version. Applications stamp the current `1.0.0` value when creating documents, accept and preserve supported numeric versions in the 1.x line, reject malformed or unsupported-major values, and must not expose it as publisher-authored editable metadata. Policy documents do not contain a `$schema` member; documents that contain it are rejected as unknown-field input under the strict contract. + `parse_policy_yaml` was intentionally removed as a breaking change. OpenAPI YAML generation and unrelated YAML inputs are unaffected. diff --git a/policies/rust/now-policy/assets/samples/corporate-allowlist.policy.json b/policies/rust/now-policy/assets/samples/corporate-allowlist.policy.json index 4d50bc4..e0da844 100644 --- a/policies/rust/now-policy/assets/samples/corporate-allowlist.policy.json +++ b/policies/rust/now-policy/assets/samples/corporate-allowlist.policy.json @@ -1,6 +1,5 @@ { - "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.desktop.standard-allowlist", diff --git a/policies/rust/now-policy/assets/samples/deny-risky-options.policy.json b/policies/rust/now-policy/assets/samples/deny-risky-options.policy.json index 5ddf8d1..105eebf 100644 --- a/policies/rust/now-policy/assets/samples/deny-risky-options.policy.json +++ b/policies/rust/now-policy/assets/samples/deny-risky-options.policy.json @@ -1,6 +1,5 @@ { - "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.desktop.deny-risky-options", diff --git a/policies/rust/now-policy/assets/samples/invalid/policies/invalid-failure-decision.policy.json b/policies/rust/now-policy/assets/samples/invalid/policies/invalid-failure-decision.policy.json index 2b65f4c..6a744a9 100644 --- a/policies/rust/now-policy/assets/samples/invalid/policies/invalid-failure-decision.policy.json +++ b/policies/rust/now-policy/assets/samples/invalid/policies/invalid-failure-decision.policy.json @@ -1,6 +1,5 @@ { - "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.invalid.failure-decision", diff --git a/policies/rust/now-policy/assets/samples/powershell-advanced.policy.json b/policies/rust/now-policy/assets/samples/powershell-advanced.policy.json index 2e2e3b9..f685df2 100644 --- a/policies/rust/now-policy/assets/samples/powershell-advanced.policy.json +++ b/policies/rust/now-policy/assets/samples/powershell-advanced.policy.json @@ -1,6 +1,5 @@ { - "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.powershell.advanced-scenarios", diff --git a/policies/rust/now-policy/assets/samples/powershell-current-user.policy.json b/policies/rust/now-policy/assets/samples/powershell-current-user.policy.json index ddb5ae3..ccef4c1 100644 --- a/policies/rust/now-policy/assets/samples/powershell-current-user.policy.json +++ b/policies/rust/now-policy/assets/samples/powershell-current-user.policy.json @@ -1,6 +1,5 @@ { - "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.powershell.current-user-modules", diff --git a/policies/rust/now-policy/assets/samples/scenario-coverage.policy.json b/policies/rust/now-policy/assets/samples/scenario-coverage.policy.json index 77ba46c..4ddb628 100644 --- a/policies/rust/now-policy/assets/samples/scenario-coverage.policy.json +++ b/policies/rust/now-policy/assets/samples/scenario-coverage.policy.json @@ -1,6 +1,5 @@ { - "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.desktop.scenario-coverage", diff --git a/policies/rust/now-policy/schema/devolutions.now-policy-draft.schema.json b/policies/rust/now-policy/schema/devolutions.now-policy-draft.schema.json index 66bd0ea..c011677 100644 --- a/policies/rust/now-policy/schema/devolutions.now-policy-draft.schema.json +++ b/policies/rust/now-policy/schema/devolutions.now-policy-draft.schema.json @@ -1,5 +1,4 @@ { - "$id": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json", "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "definitions": { @@ -214,12 +213,6 @@ ], "type": "object" }, - "PolicyDraftSchemaUri": { - "enum": [ - "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json" - ], - "type": "string" - }, "PolicyEnforcement": { "additionalProperties": false, "description": "Enforcement configuration.", @@ -254,6 +247,12 @@ ], "type": "object" }, + "PolicyFormatVersion": { + "description": "Software-managed policy document format version.\n\n Readers accept canonical numeric versions in the compatible 1.x line.\n Applications must stamp the current value, `1.0.0`, for new documents and\n must not expose this value as publisher-authored editable metadata.\n Schemas describe the canonical shape; runtime readers additionally bound\n each numeric component to an unsigned 64-bit integer.", + "maxLength": 128, + "pattern": "^1\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])", + "type": "string" + }, "PolicyMatch": { "additionalProperties": false, "description": "Match criteria for a policy rule. All specified fields must match.\n At least one field must be present.", @@ -515,12 +514,6 @@ ], "type": "string" }, - "SemanticVersion": { - "description": "Semantic version string (SemVer 2.0.0).\n\n Validated at deserialization time using the `semver` crate.", - "maxLength": 128, - "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$", - "type": "string" - }, "StringPattern": { "description": "Case-insensitive exact value or wildcard pattern.", "maxLength": 256, @@ -566,14 +559,6 @@ }, "description": "An editable policy document without server-managed commit metadata.", "properties": { - "$schema": { - "allOf": [ - { - "$ref": "#/definitions/PolicyDraftSchemaUri" - } - ], - "description": "Policy schema URI constant." - }, "Enforcement": { "allOf": [ { @@ -590,21 +575,21 @@ ], "description": "Editable policy metadata." }, - "PolicyType": { + "PolicyFormatVersion": { "allOf": [ { - "$ref": "#/definitions/PackageBrokerPolicy" + "$ref": "#/definitions/PolicyFormatVersion" } ], - "description": "Must be `\"PackageBrokerPolicy\"`." + "description": "Software-managed policy document format version.\n\n Applications must stamp the current value and must not expose this field\n as publisher-authored editable metadata." }, - "PolicyVersion": { + "PolicyType": { "allOf": [ { - "$ref": "#/definitions/SemanticVersion" + "$ref": "#/definitions/PackageBrokerPolicy" } ], - "description": "Policy syntax version (semver)." + "description": "Must be `\"PackageBrokerPolicy\"`." }, "Rules": { "description": "Ordered list of policy rules (may be empty; enforcement defaults apply).", @@ -616,8 +601,7 @@ } }, "required": [ - "$schema", - "PolicyVersion", + "PolicyFormatVersion", "PolicyType", "Metadata", "Enforcement", diff --git a/policies/rust/now-policy/schema/devolutions.now-policy.schema.json b/policies/rust/now-policy/schema/devolutions.now-policy.schema.json index b4a4f85..fc1a12c 100644 --- a/policies/rust/now-policy/schema/devolutions.now-policy.schema.json +++ b/policies/rust/now-policy/schema/devolutions.now-policy.schema.json @@ -1,5 +1,4 @@ { - "$id": "https://devolutions.net/schemas/now-policy.schema.1.0.json", "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "definitions": { @@ -188,6 +187,12 @@ ], "type": "object" }, + "PolicyFormatVersion": { + "description": "Software-managed policy document format version.\n\n Readers accept canonical numeric versions in the compatible 1.x line.\n Applications must stamp the current value, `1.0.0`, for new documents and\n must not expose this value as publisher-authored editable metadata.\n Schemas describe the canonical shape; runtime readers additionally bound\n each numeric component to an unsigned 64-bit integer.", + "maxLength": 128, + "pattern": "^1\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?![\\s\\S])", + "type": "string" + }, "PolicyMatch": { "additionalProperties": false, "description": "Match criteria for a policy rule. All specified fields must match.\n At least one field must be present.", @@ -502,12 +507,6 @@ ], "type": "object" }, - "PolicySchemaUri": { - "enum": [ - "https://devolutions.net/schemas/now-policy.schema.1.0.json" - ], - "type": "string" - }, "ResourceId": { "description": "Resource identifier (policy IDs, rule IDs, request IDs, audit IDs).", "maxLength": 128, @@ -529,12 +528,6 @@ ], "type": "string" }, - "SemanticVersion": { - "description": "Semantic version string (SemVer 2.0.0).\n\n Validated at deserialization time using the `semver` crate.", - "maxLength": 128, - "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$", - "type": "string" - }, "StringPattern": { "description": "Case-insensitive exact value or wildcard pattern.", "maxLength": 256, @@ -580,14 +573,6 @@ }, "description": "A policy document governing which package operations are allowed or denied.", "properties": { - "$schema": { - "allOf": [ - { - "$ref": "#/definitions/PolicySchemaUri" - } - ], - "description": "Policy schema URI constant." - }, "Enforcement": { "allOf": [ { @@ -604,21 +589,21 @@ ], "description": "Policy metadata." }, - "PolicyType": { + "PolicyFormatVersion": { "allOf": [ { - "$ref": "#/definitions/PackageBrokerPolicy" + "$ref": "#/definitions/PolicyFormatVersion" } ], - "description": "Must be `\"PackageBrokerPolicy\"`." + "description": "Software-managed policy document format version.\n\n Applications must not expose this field as publisher-authored editable metadata." }, - "PolicyVersion": { + "PolicyType": { "allOf": [ { - "$ref": "#/definitions/SemanticVersion" + "$ref": "#/definitions/PackageBrokerPolicy" } ], - "description": "Policy syntax version (semver)." + "description": "Must be `\"PackageBrokerPolicy\"`." }, "Rules": { "description": "Ordered list of policy rules (may be empty; enforcement defaults apply).", @@ -630,8 +615,7 @@ } }, "required": [ - "$schema", - "PolicyVersion", + "PolicyFormatVersion", "PolicyType", "Metadata", "Enforcement", diff --git a/policies/rust/now-policy/src/markers.rs b/policies/rust/now-policy/src/markers.rs index dcea0b5..be12479 100644 --- a/policies/rust/now-policy/src/markers.rs +++ b/policies/rust/now-policy/src/markers.rs @@ -51,21 +51,3 @@ fixed_string_marker! { /// Marker type for policy type: serializes to `"PackageBrokerPolicy"`. pub struct PackageBrokerPolicy => "PackageBrokerPolicy"; } - -/// Schema URI for package policy documents. -pub const POLICY_SCHEMA_URI: &str = "https://devolutions.net/schemas/now-policy.schema.1.0.json"; - -/// Schema URI for editable package policy draft documents. -pub const POLICY_DRAFT_SCHEMA_URI: &str = "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json"; - -fixed_string_marker! { - /// Marker type for the policy `$schema` field. - /// Serializes to the canonical policy schema URI. - pub struct PolicySchemaUri => POLICY_SCHEMA_URI; -} - -fixed_string_marker! { - /// Marker type for the policy draft `$schema` field. - /// Serializes to the canonical policy draft schema URI. - pub struct PolicyDraftSchemaUri => POLICY_DRAFT_SCHEMA_URI; -} diff --git a/policies/rust/now-policy/src/newtypes.rs b/policies/rust/now-policy/src/newtypes.rs index 9283f55..22292da 100644 --- a/policies/rust/now-policy/src/newtypes.rs +++ b/policies/rust/now-policy/src/newtypes.rs @@ -99,6 +99,110 @@ impl From<&str> for SemanticVersion { } } +/// Current policy document format version emitted for new documents. +pub const CURRENT_POLICY_FORMAT_VERSION: &str = "1.0.0"; + +/// Software-managed policy document format version. +/// +/// Readers accept canonical numeric versions in the compatible 1.x line. +/// Applications must stamp the current value, `1.0.0`, for new documents and +/// must not expose this value as publisher-authored editable metadata. +/// Schemas describe the canonical shape; runtime readers additionally bound +/// each numeric component to an unsigned 64-bit integer. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)] +pub struct PolicyFormatVersion( + #[schemars( + length(max = 128), + regex(pattern = r"^1\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?![\s\S])") + )] + String, +); + +impl PolicyFormatVersion { + /// Parse a supported policy document format version. + pub fn parse(s: &str) -> Result { + if s.len() > 128 { + return Err(ModelValidationError::Invalid { + type_name: "PolicyFormatVersion", + reason: format!("length {} exceeds maximum 128", s.len()), + }); + } + + let mut components = s.split('.'); + let (Some(major), Some(minor), Some(patch), None) = ( + components.next(), + components.next(), + components.next(), + components.next(), + ) else { + return Err(ModelValidationError::Invalid { + type_name: "PolicyFormatVersion", + reason: "must contain exactly three numeric components".to_owned(), + }); + }; + + let parse_component = |component: &str| { + if component.is_empty() + || (component.len() > 1 && component.starts_with('0')) + || !component.bytes().all(|byte| byte.is_ascii_digit()) + { + return None; + } + component.parse::().ok() + }; + + let (Some(major), Some(_minor), Some(_patch)) = + (parse_component(major), parse_component(minor), parse_component(patch)) + else { + return Err(ModelValidationError::Invalid { + type_name: "PolicyFormatVersion", + reason: "components must be canonical unsigned 64-bit integers".to_owned(), + }); + }; + + if major != 1 { + return Err(ModelValidationError::Invalid { + type_name: "PolicyFormatVersion", + reason: format!("unsupported major version {}; supported major version is 1", major), + }); + } + + Ok(Self(s.to_owned())) + } + + /// Return the current version stamped on new documents. + pub fn current() -> Self { + Self(CURRENT_POLICY_FORMAT_VERSION.to_owned()) + } +} + +impl Default for PolicyFormatVersion { + fn default() -> Self { + Self::current() + } +} + +impl<'de> Deserialize<'de> for PolicyFormatVersion { + fn deserialize>(deserializer: D) -> Result { + let value = String::deserialize(deserializer)?; + Self::parse(&value).map_err(serde::de::Error::custom) + } +} + +impl std::ops::Deref for PolicyFormatVersion { + type Target = str; + + fn deref(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for PolicyFormatVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + /// Resource identifier (policy IDs, rule IDs, request IDs, audit IDs). #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, JsonSchema)] pub struct ResourceId( diff --git a/policies/rust/now-policy/src/policy.rs b/policies/rust/now-policy/src/policy.rs index a78e437..43c4968 100644 --- a/policies/rust/now-policy/src/policy.rs +++ b/policies/rust/now-policy/src/policy.rs @@ -8,8 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::{ Architecture, CustomParameterString, Decision, Elevation, HttpUrl, ManagerName, ModelValidationError, Operation, - PackageBrokerPolicy, PolicyDraftSchemaUri, PolicySchemaUri, ResourceId, Scope, SemanticVersion, StringPattern, - VersionString, + PackageBrokerPolicy, PolicyFormatVersion, ResourceId, Scope, StringPattern, VersionString, }; const MAX_POLICY_REVISION: u32 = 2_147_483_647; @@ -20,12 +19,10 @@ const MAX_POLICY_REVISION: u32 = 2_147_483_647; #[serde(rename_all = "PascalCase")] #[serde(deny_unknown_fields)] pub struct PolicyDocument { - /// Policy schema URI constant. - #[serde(rename = "$schema")] - pub _schema: PolicySchemaUri, - - /// Policy syntax version (semver). - pub policy_version: SemanticVersion, + /// Software-managed policy document format version. + /// + /// Applications must not expose this field as publisher-authored editable metadata. + pub policy_format_version: PolicyFormatVersion, /// Must be `"PackageBrokerPolicy"`. pub policy_type: PackageBrokerPolicy, @@ -45,8 +42,7 @@ impl PolicyDocument { /// Create an editable draft, intentionally omitting server-managed commit metadata. pub fn to_draft(&self) -> PolicyDraftDocument { PolicyDraftDocument { - _schema: PolicyDraftSchemaUri, - policy_version: self.policy_version.clone(), + policy_format_version: self.policy_format_version.clone(), policy_type: self.policy_type, metadata: self.metadata.to_draft(), enforcement: self.enforcement.clone(), @@ -61,12 +57,11 @@ impl PolicyDocument { #[serde(rename_all = "PascalCase")] #[serde(deny_unknown_fields)] pub struct PolicyDraftDocument { - /// Policy draft schema URI constant. - #[serde(rename = "$schema")] - pub _schema: PolicyDraftSchemaUri, - - /// Policy syntax version (semver). - pub policy_version: SemanticVersion, + /// Software-managed policy document format version. + /// + /// Applications must stamp the current value and must not expose this field + /// as publisher-authored editable metadata. + pub policy_format_version: PolicyFormatVersion, /// Must be `"PackageBrokerPolicy"`. pub policy_type: PackageBrokerPolicy, @@ -97,8 +92,7 @@ impl PolicyDraftDocument { } Ok(PolicyDocument { - _schema: PolicySchemaUri, - policy_version: self.policy_version, + policy_format_version: self.policy_format_version, policy_type: self.policy_type, metadata: self.metadata.into_policy_metadata(revision, published_at), enforcement: self.enforcement, diff --git a/policies/rust/now-policy/src/schema.rs b/policies/rust/now-policy/src/schema.rs index 1a784cb..f6373ee 100644 --- a/policies/rust/now-policy/src/schema.rs +++ b/policies/rust/now-policy/src/schema.rs @@ -4,7 +4,7 @@ use schemars::generate::SchemaSettings; use crate::{PolicyDocument, PolicyDraftDocument}; -/// Get the generated policy schema as a JSON value. +/// Get the generated repository-local policy schema as a JSON value. pub fn policy_schema_json() -> serde_json::Value { let schema = SchemaSettings::draft07() .into_generator() @@ -12,7 +12,7 @@ pub fn policy_schema_json() -> serde_json::Value { serde_json::to_value(&schema).expect("BUG: schema serialization failed") } -/// Get the generated editable policy draft schema as a JSON value. +/// Get the generated repository-local editable policy draft schema as a JSON value. pub fn policy_draft_schema_json() -> serde_json::Value { let schema = SchemaSettings::draft07() .into_generator() diff --git a/policies/rust/now-policy/tests/policy_samples.rs b/policies/rust/now-policy/tests/policy_samples.rs index 3314757..549e8a3 100644 --- a/policies/rust/now-policy/tests/policy_samples.rs +++ b/policies/rust/now-policy/tests/policy_samples.rs @@ -5,9 +5,7 @@ use std::path::PathBuf; use chrono::{TimeZone, Utc}; -use now_policy::{ - CustomParameterString, POLICY_DRAFT_SCHEMA_URI, POLICY_SCHEMA_URI, PolicyDocument, StringPattern, VersionString, -}; +use now_policy::{CURRENT_POLICY_FORMAT_VERSION, CustomParameterString, PolicyDocument, StringPattern, VersionString}; fn samples_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/samples") @@ -42,17 +40,22 @@ fn draft_conversion_omits_and_restores_server_metadata() { let draft = committed.to_draft(); let draft_json = serde_json::to_value(&draft).unwrap(); - assert_eq!(draft_json["$schema"], POLICY_DRAFT_SCHEMA_URI); + assert!(draft_json.get("$schema").is_none()); + assert_eq!(draft_json["PolicyFormatVersion"], CURRENT_POLICY_FORMAT_VERSION); assert!(draft_json["Metadata"].get("Revision").is_none()); assert!(draft_json["Metadata"].get("PublishedAt").is_none()); let published_at = Utc.with_ymd_and_hms(2026, 8, 29, 0, 0, 0).unwrap(); let recommitted = draft.into_policy_document(7, published_at).unwrap(); - assert_eq!( - serde_json::to_value(&recommitted).unwrap()["$schema"], - POLICY_SCHEMA_URI - ); + let recommitted_json = serde_json::to_value(&recommitted).unwrap(); + assert!(recommitted_json.get("$schema").is_none()); + assert_eq!(recommitted_json["PolicyFormatVersion"], CURRENT_POLICY_FORMAT_VERSION); assert_eq!(recommitted.metadata.id.to_string(), committed.metadata.id.to_string()); + assert_eq!(recommitted.metadata.publisher, committed.metadata.publisher); + assert_eq!(recommitted.metadata.description, committed.metadata.description); + assert_eq!(recommitted.metadata.support_url, committed.metadata.support_url); + assert_eq!(recommitted.metadata.valid_from, committed.metadata.valid_from); + assert_eq!(recommitted.metadata.valid_until, committed.metadata.valid_until); assert_eq!(recommitted.metadata.revision, 7); assert_eq!(recommitted.metadata.published_at, published_at); } @@ -107,8 +110,7 @@ fn policy_text_newtypes_count_unicode_scalars_at_length_boundaries() { #[test] fn invalid_policy_unknown_field_fails_deserialization() { let value = serde_json::json!({ - "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "test", @@ -128,6 +130,68 @@ fn invalid_policy_unknown_field_fails_deserialization() { assert!(result.is_err(), "policy with unknown field should fail deserialization"); } +#[test] +fn schema_field_is_rejected() { + let path = samples_dir().join("corporate-allowlist.policy.json"); + let mut value: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); + value["$schema"] = serde_json::json!("https://example.invalid/policy.schema.json"); + + let error = serde_json::from_value::(value).unwrap_err().to_string(); + assert!(error.contains("unknown field `$schema`"), "unexpected error: {error}"); +} + +#[test] +fn unsupported_policy_format_version_is_rejected() { + let path = samples_dir().join("corporate-allowlist.policy.json"); + let mut value: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); + value["PolicyFormatVersion"] = serde_json::json!("2.0.0"); + + let error = serde_json::from_value::(value).unwrap_err().to_string(); + assert!( + error.contains("unsupported major version 2"), + "unexpected error: {error}" + ); +} + +#[test] +fn malformed_policy_format_versions_are_rejected() { + let path = samples_dir().join("corporate-allowlist.policy.json"); + let valid: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); + + for version in ["not-semver", "1.02.3", "1.2.3-beta", "1.0.0\n"] { + let mut value = valid.clone(); + value["PolicyFormatVersion"] = serde_json::json!(version); + assert!( + serde_json::from_value::(value).is_err(), + "{version:?} should be rejected" + ); + } +} + +#[test] +fn compatible_policy_format_version_is_preserved_by_conversions() { + let path = samples_dir().join("corporate-allowlist.policy.json"); + let mut value: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); + value["PolicyFormatVersion"] = serde_json::json!("1.2.3"); + + let committed = serde_json::from_value::(value).unwrap(); + let draft = committed.to_draft(); + assert_eq!(draft.policy_format_version.to_string(), "1.2.3"); + let recommitted = draft + .into_policy_document(5, Utc.with_ymd_and_hms(2026, 8, 29, 0, 0, 0).unwrap()) + .unwrap(); + assert_eq!(recommitted.policy_format_version.to_string(), "1.2.3"); +} + +#[test] +fn policy_format_version_rejects_components_outside_u64_range() { + let path = samples_dir().join("corporate-allowlist.policy.json"); + let mut value: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); + value["PolicyFormatVersion"] = serde_json::json!("1.18446744073709551616.0"); + + assert!(serde_json::from_value::(value).is_err()); +} + #[test] fn invalid_policy_fixture_fails_deserialization() { let path = samples_dir().join("invalid/policies/invalid-failure-decision.policy.json"); @@ -147,6 +211,24 @@ fn policy_schema_generates_valid_json() { ); } +#[test] +fn policy_schemas_are_repository_local_and_omit_document_schema() { + for schema in [ + now_policy::schema::policy_schema_json(), + now_policy::schema::policy_draft_schema_json(), + ] { + let properties = schema["properties"].as_object().unwrap(); + let required = schema["required"].as_array().unwrap(); + assert!(schema.get("$id").is_none()); + assert!(!properties.contains_key("$schema")); + assert!(!required.iter().any(|value| value == "$schema")); + + let version_schema = &schema["definitions"]["PolicyFormatVersion"]; + assert_eq!(version_schema["type"], "string"); + assert!(version_schema["pattern"].as_str().unwrap().starts_with("^1\\.")); + } +} + #[test] fn committed_policy_enforces_revision_bounds_during_serialization_and_deserialization() { let path = samples_dir().join("corporate-allowlist.policy.json"); diff --git a/policies/rust/now-policy/tools/generate_schema.rs b/policies/rust/now-policy/tools/generate_schema.rs index a80a80f..ebd4cf6 100644 --- a/policies/rust/now-policy/tools/generate_schema.rs +++ b/policies/rust/now-policy/tools/generate_schema.rs @@ -8,39 +8,24 @@ use std::path::Path; use now_policy::schema::{policy_draft_schema_json, policy_schema_json}; -use now_policy::{POLICY_DRAFT_SCHEMA_URI, POLICY_SCHEMA_URI}; -use serde_json::{Map, Value}; +use serde_json::Value; fn main() { let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR")); write_schema( &crate_dir.join("schema").join("devolutions.now-policy.schema.json"), policy_schema_json(), - POLICY_SCHEMA_URI, ); write_schema( &crate_dir .join("schema") .join("devolutions.now-policy-draft.schema.json"), policy_draft_schema_json(), - POLICY_DRAFT_SCHEMA_URI, ); } -fn write_schema(path: &Path, schema: Value, id: &str) { - let json = serde_json::to_string_pretty(&with_id(schema, id)).expect("BUG: schema serialization failed"); +fn write_schema(path: &Path, schema: Value) { + let json = serde_json::to_string_pretty(&schema).expect("BUG: schema serialization failed"); std::fs::write(path, &json).unwrap_or_else(|e| panic!("failed to write {}: {e}", path.display())); println!("Wrote {}", path.display()); } - -fn with_id(schema: Value, id: &str) -> Value { - let Value::Object(existing) = schema else { - panic!("BUG: schema root is not an object"); - }; - - let mut object = Map::new(); - object.insert("$id".to_owned(), Value::String(id.to_owned())); - object.extend(existing); - - Value::Object(object) -} diff --git a/policies/test-data/package-broker/invalid/responses/policy-validation.valid-with-error.response.json b/policies/test-data/package-broker/invalid/responses/policy-validation.valid-with-error.response.json index 5ffc01e..0503209 100644 --- a/policies/test-data/package-broker/invalid/responses/policy-validation.valid-with-error.response.json +++ b/policies/test-data/package-broker/invalid/responses/policy-validation.valid-with-error.response.json @@ -10,8 +10,7 @@ "ValidatorVersion": "gateway-policy-validator/1", "IsValid": true, "CanonicalDraft": { - "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.package-policy", diff --git a/policies/test-data/package-broker/requests/policy-replacement.create.request.json b/policies/test-data/package-broker/requests/policy-replacement.create.request.json index 04fd05b..c1e94e6 100644 --- a/policies/test-data/package-broker/requests/policy-replacement.create.request.json +++ b/policies/test-data/package-broker/requests/policy-replacement.create.request.json @@ -6,8 +6,7 @@ "ConflictHandling": "Reject", "WarningsAcknowledged": false, "Draft": { - "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.package-policy", "Publisher": "Contoso IT" }, "Enforcement": { "DefaultDecision": "Deny", "RulePrecedence": "PriorityThenDeny" }, diff --git a/policies/test-data/package-broker/requests/policy-replacement.overwrite.request.json b/policies/test-data/package-broker/requests/policy-replacement.overwrite.request.json index af5c350..8e08b9e 100644 --- a/policies/test-data/package-broker/requests/policy-replacement.overwrite.request.json +++ b/policies/test-data/package-broker/requests/policy-replacement.overwrite.request.json @@ -6,8 +6,7 @@ "ConflictHandling": "ConfirmOverwrite", "WarningsAcknowledged": true, "Draft": { - "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.package-policy", "Publisher": "Contoso IT" }, "Enforcement": { "DefaultDecision": "Deny", "RulePrecedence": "PriorityThenDeny" }, diff --git a/policies/test-data/package-broker/requests/policy-replacement.repair.request.json b/policies/test-data/package-broker/requests/policy-replacement.repair.request.json index f0729a3..9e7e6bb 100644 --- a/policies/test-data/package-broker/requests/policy-replacement.repair.request.json +++ b/policies/test-data/package-broker/requests/policy-replacement.repair.request.json @@ -6,8 +6,7 @@ "ConflictHandling": "Reject", "WarningsAcknowledged": false, "Draft": { - "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.package-policy", "Publisher": "Contoso IT" }, "Enforcement": { "DefaultDecision": "Deny", "RulePrecedence": "PriorityThenDeny" }, diff --git a/policies/test-data/package-broker/requests/policy-replacement.replace-identity.request.json b/policies/test-data/package-broker/requests/policy-replacement.replace-identity.request.json index 13668af..257cf42 100644 --- a/policies/test-data/package-broker/requests/policy-replacement.replace-identity.request.json +++ b/policies/test-data/package-broker/requests/policy-replacement.replace-identity.request.json @@ -6,8 +6,7 @@ "ConflictHandling": "Reject", "WarningsAcknowledged": false, "Draft": { - "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "fabrikam.package-policy", "Publisher": "Fabrikam IT" }, "Enforcement": { "DefaultDecision": "Deny", "RulePrecedence": "PriorityThenDeny" }, diff --git a/policies/test-data/package-broker/requests/policy-replacement.update.request.json b/policies/test-data/package-broker/requests/policy-replacement.update.request.json index f4b53b9..ba20df6 100644 --- a/policies/test-data/package-broker/requests/policy-replacement.update.request.json +++ b/policies/test-data/package-broker/requests/policy-replacement.update.request.json @@ -6,8 +6,7 @@ "ConflictHandling": "Reject", "WarningsAcknowledged": true, "Draft": { - "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.package-policy", "Publisher": "Contoso IT" }, "Enforcement": { "DefaultDecision": "Deny", "RulePrecedence": "PriorityThenDeny" }, diff --git a/policies/test-data/package-broker/requests/policy-validation.request.json b/policies/test-data/package-broker/requests/policy-validation.request.json index 641c8d1..2179ea6 100644 --- a/policies/test-data/package-broker/requests/policy-validation.request.json +++ b/policies/test-data/package-broker/requests/policy-validation.request.json @@ -2,8 +2,7 @@ "RequestKind": "PolicyValidationRequest", "RequestVersion": "1.0", "Draft": { - "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.package-policy", diff --git a/policies/test-data/package-broker/responses/execution-winget-vscode-install.response.json b/policies/test-data/package-broker/responses/execution-winget-vscode-install.response.json index feea983..cf08869 100644 --- a/policies/test-data/package-broker/responses/execution-winget-vscode-install.response.json +++ b/policies/test-data/package-broker/responses/execution-winget-vscode-install.response.json @@ -18,7 +18,7 @@ "Policy": { "Id": "contoso.desktop.standard-allowlist", "Revision": 4, - "PolicyVersion": "1.0.0" + "PolicyFormatVersion": "1.0.0" }, "Operation": { "OperationId": "op-winget-vscode-install-000001", diff --git a/policies/test-data/package-broker/responses/policy-management.active.response.json b/policies/test-data/package-broker/responses/policy-management.active.response.json index 5a96677..4a73225 100644 --- a/policies/test-data/package-broker/responses/policy-management.active.response.json +++ b/policies/test-data/package-broker/responses/policy-management.active.response.json @@ -13,8 +13,7 @@ "WriteCapability": "Writable", "ElevationRequired": true, "Policy": { - "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.package-policy", diff --git a/policies/test-data/package-broker/responses/policy-management.invalid.response.json b/policies/test-data/package-broker/responses/policy-management.invalid.response.json index 023e84f..dd17ffa 100644 --- a/policies/test-data/package-broker/responses/policy-management.invalid.response.json +++ b/policies/test-data/package-broker/responses/policy-management.invalid.response.json @@ -19,13 +19,13 @@ { "FindingVersion": "1.0", "Severity": "Error", - "Code": "UnsupportedPolicyVersion", - "Path": "/PolicyVersion", + "Code": "UnsupportedPolicyFormatVersion", + "Path": "/PolicyFormatVersion", "Arguments": { "actual": "2.0.0", - "supported": "1.0.0" + "supported": "1.x" }, - "Message": "The policy version is not supported." + "Message": "The policy format version is not supported." } ] } diff --git a/policies/test-data/package-broker/responses/policy-replacement.response.json b/policies/test-data/package-broker/responses/policy-replacement.response.json index e072c1e..c1d2d6d 100644 --- a/policies/test-data/package-broker/responses/policy-replacement.response.json +++ b/policies/test-data/package-broker/responses/policy-replacement.response.json @@ -6,8 +6,7 @@ "Transport": "HttpNamedPipe" }, "Policy": { - "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.package-policy", @@ -26,8 +25,7 @@ "ValidatorVersion": "gateway-policy-validator/1", "IsValid": true, "CanonicalDraft": { - "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.package-policy", @@ -50,8 +48,7 @@ "WriteCapability": "Writable", "ElevationRequired": true, "Policy": { - "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.package-policy", diff --git a/policies/test-data/package-broker/responses/policy-stale-token.error.json b/policies/test-data/package-broker/responses/policy-stale-token.error.json index 3d7cdc0..300b6e9 100644 --- a/policies/test-data/package-broker/responses/policy-stale-token.error.json +++ b/policies/test-data/package-broker/responses/policy-stale-token.error.json @@ -15,8 +15,7 @@ "WriteCapability": "Writable", "ElevationRequired": true, "Policy": { - "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.package-policy", diff --git a/policies/test-data/package-broker/responses/policy-validation.invalid.response.json b/policies/test-data/package-broker/responses/policy-validation.invalid.response.json index 3b1ec8e..2924228 100644 --- a/policies/test-data/package-broker/responses/policy-validation.invalid.response.json +++ b/policies/test-data/package-broker/responses/policy-validation.invalid.response.json @@ -22,9 +22,8 @@ { "FindingVersion": "1.0", "Severity": "Error", "Code": "InvalidWildcardPattern", "Path": "/Rules/0/Match/PackageIdentifiers/0", "RuleId": "duplicate", "Message": "The wildcard pattern is invalid." }, { "FindingVersion": "1.0", "Severity": "Error", "Code": "ContradictoryConstraints", "Path": "/Rules/0/Constraints", "RuleId": "duplicate", "Message": "The constraints contradict each other." }, { "FindingVersion": "1.0", "Severity": "Error", "Code": "InvalidValidityInterval", "Path": "/Metadata/ValidUntil", "Message": "ValidUntil must be after ValidFrom." }, - { "FindingVersion": "1.0", "Severity": "Error", "Code": "UnsupportedSchema", "Path": "/$schema", "Message": "The policy schema is not supported." }, { "FindingVersion": "1.0", "Severity": "Error", "Code": "UnsupportedPolicyType", "Path": "/PolicyType", "Message": "The policy type is not supported." }, - { "FindingVersion": "1.0", "Severity": "Error", "Code": "UnsupportedPolicyVersion", "Path": "/PolicyVersion", "Message": "The policy version is not supported." } + { "FindingVersion": "1.0", "Severity": "Error", "Code": "UnsupportedPolicyFormatVersion", "Path": "/PolicyFormatVersion", "Message": "The policy format version is not supported." } ] } } diff --git a/policies/test-data/package-broker/responses/policy-validation.valid.response.json b/policies/test-data/package-broker/responses/policy-validation.valid.response.json index ad39185..fd5703f 100644 --- a/policies/test-data/package-broker/responses/policy-validation.valid.response.json +++ b/policies/test-data/package-broker/responses/policy-validation.valid.response.json @@ -10,8 +10,7 @@ "ValidatorVersion": "gateway-policy-validator/1", "IsValid": true, "CanonicalDraft": { - "$schema": "https://devolutions.net/schemas/now-policy-draft.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.package-policy", diff --git a/policies/test-data/package-broker/responses/policy.response.json b/policies/test-data/package-broker/responses/policy.response.json index e1efb1d..a21f34f 100644 --- a/policies/test-data/package-broker/responses/policy.response.json +++ b/policies/test-data/package-broker/responses/policy.response.json @@ -6,8 +6,7 @@ "Transport": "HttpNamedPipe" }, "Policy": { - "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", - "PolicyVersion": "1.0.0", + "PolicyFormatVersion": "1.0.0", "PolicyType": "PackageBrokerPolicy", "Metadata": { "Id": "contoso.desktop.standard-allowlist", diff --git a/policies/test-data/package-broker/responses/winget-vscode-install.allowed.response.json b/policies/test-data/package-broker/responses/winget-vscode-install.allowed.response.json index ac01810..7a3f534 100644 --- a/policies/test-data/package-broker/responses/winget-vscode-install.allowed.response.json +++ b/policies/test-data/package-broker/responses/winget-vscode-install.allowed.response.json @@ -19,7 +19,7 @@ "Policy": { "Id": "contoso.desktop.standard-allowlist", "Revision": 4, - "PolicyVersion": "1.0.0" + "PolicyFormatVersion": "1.0.0" }, "Diagnostics": { "CommandPreview": [ diff --git a/policies/test-data/package-broker/responses/winget-vscode-skiphash.denied.response.json b/policies/test-data/package-broker/responses/winget-vscode-skiphash.denied.response.json index 3e17099..67dc132 100644 --- a/policies/test-data/package-broker/responses/winget-vscode-skiphash.denied.response.json +++ b/policies/test-data/package-broker/responses/winget-vscode-skiphash.denied.response.json @@ -19,7 +19,7 @@ "Policy": { "Id": "contoso.desktop.standard-allowlist", "Revision": 4, - "PolicyVersion": "1.0.0" + "PolicyFormatVersion": "1.0.0" }, "Diagnostics": {}, "Server": {