From 549e8d5060737a9ac86f35193b4fa6418d675c65 Mon Sep 17 00:00:00 2001 From: Jamie Winder Date: Sun, 4 May 2025 21:04:46 +0100 Subject: [PATCH 1/3] incorporate integration tests, ipaddr fix --- .github/workflows/ci.yml | 6 ++ .gitignore | 5 +- examples/BasicExample/Program.cs | 2 +- src/CedarDotNet/CedarUtilities.cs | 27 ++++++++ src/CedarDotNet/Interop/CedarFfi.Utilities.cs | 3 + src/CedarDotNet/Models/Entity.cs | 1 + src/CedarDotNet/Values/ValueJsonConverter.cs | 4 +- src/CedarDotNetFfi/src/lib.rs | 23 ++++++- .../CedarDotNet.UnitTests.csproj | 2 +- .../Dtos/TestJsonSerializedContext.cs | 11 +++ .../IntegrationTests/Dtos/TestRequestDto.cs | 32 +++++++++ .../IntegrationTests/Dtos/TestScenarioDto.cs | 21 ++++++ .../IntegrationTests/IntegrationTestData.cs | 69 +++++++++++++++++++ .../IntegrationTests/IntegrationTests.cs | 34 +++++++++ .../IntegrationTests/Models/TestScenario.cs | 13 ++++ 15 files changed, 247 insertions(+), 6 deletions(-) create mode 100644 tests/CedarDotNet.UnitTests/IntegrationTests/Dtos/TestJsonSerializedContext.cs create mode 100644 tests/CedarDotNet.UnitTests/IntegrationTests/Dtos/TestRequestDto.cs create mode 100644 tests/CedarDotNet.UnitTests/IntegrationTests/Dtos/TestScenarioDto.cs create mode 100644 tests/CedarDotNet.UnitTests/IntegrationTests/IntegrationTestData.cs create mode 100644 tests/CedarDotNet.UnitTests/IntegrationTests/IntegrationTests.cs create mode 100644 tests/CedarDotNet.UnitTests/IntegrationTests/Models/TestScenario.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af9a906..8888d37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,12 @@ jobs: with: dotnet-version: '8.0.x' + - name: 🧪 Checkout Cedar integration test data + uses: actions/checkout@v4 + with: + repository: cedar-policy/cedar-integration-tests + path: cedar-integration-tests + - name: ✅ Run .NET tests run: dotnet test diff --git a/.gitignore b/.gitignore index 9491a2f..74d0957 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,7 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +# Custom +/cedar-integration-tests \ No newline at end of file diff --git a/examples/BasicExample/Program.cs b/examples/BasicExample/Program.cs index 6be9bb3..46d2779 100644 --- a/examples/BasicExample/Program.cs +++ b/examples/BasicExample/Program.cs @@ -169,7 +169,7 @@ static void TestIsAuthorized(AuthorizationCall call) static void TestIsAuthorizedPartial(PartialAuthorizationCall call) { - var answer = CedarFunctions.IsAuthorizedPartial(call); + var answer = CedarExperimentalFunctions.IsAuthorizedPartial(call); WriteTitle("IsAuthorizedPartial"); WriteAnswer(answer); diff --git a/src/CedarDotNet/CedarUtilities.cs b/src/CedarDotNet/CedarUtilities.cs index 09ef25d..c95e99a 100644 --- a/src/CedarDotNet/CedarUtilities.cs +++ b/src/CedarDotNet/CedarUtilities.cs @@ -1,4 +1,7 @@ using CedarDotNet.Interop; +using CedarDotNet.Models; +using System.Runtime.InteropServices; +using System.Text.Json; namespace CedarDotNet; @@ -28,4 +31,28 @@ public static string PolicyFormatJsonToText( => FfiUtilities.CallUnaryStr( policyJson, CedarFfi.PolicyFormatJsonToText); + + /// + /// Loads a policy set from the given text. + /// + /// The policies text. + /// The policy collection. + public static IReadOnlyCollection LoadPolicySet( + string text) + { + var result = CedarFfi.LoadPolicySet(text); + + try + { + var resultJson = Marshal.PtrToStringUTF8(result)!; + + return JsonSerializer.Deserialize( + json: resultJson, + jsonTypeInfo: CedarJsonSerializerContext.Default.IReadOnlyCollectionString)!; + } + finally + { + CedarFfi.FreeString(result); + } + } } diff --git a/src/CedarDotNet/Interop/CedarFfi.Utilities.cs b/src/CedarDotNet/Interop/CedarFfi.Utilities.cs index 38c2bb9..9d54302 100644 --- a/src/CedarDotNet/Interop/CedarFfi.Utilities.cs +++ b/src/CedarDotNet/Interop/CedarFfi.Utilities.cs @@ -9,4 +9,7 @@ internal static partial class CedarFfi [LibraryImport(CedarNativeLibrary.Name, EntryPoint = "policy_format_json_to_text", StringMarshalling = StringMarshalling.Utf8)] public static partial IntPtr PolicyFormatJsonToText(string call); + + [LibraryImport(CedarNativeLibrary.Name, EntryPoint = "load_policy_set", StringMarshalling = StringMarshalling.Utf8)] + public static partial IntPtr LoadPolicySet(string text); } \ No newline at end of file diff --git a/src/CedarDotNet/Models/Entity.cs b/src/CedarDotNet/Models/Entity.cs index 14e336f..68265a9 100644 --- a/src/CedarDotNet/Models/Entity.cs +++ b/src/CedarDotNet/Models/Entity.cs @@ -31,6 +31,7 @@ public sealed record class Entity /// The tags. /// [JsonPropertyName("tags")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IReadOnlyDictionary Tags { get; init; } = FrozenDictionary.Empty; } diff --git a/src/CedarDotNet/Values/ValueJsonConverter.cs b/src/CedarDotNet/Values/ValueJsonConverter.cs index c7d3c0b..2487093 100644 --- a/src/CedarDotNet/Values/ValueJsonConverter.cs +++ b/src/CedarDotNet/Values/ValueJsonConverter.cs @@ -71,7 +71,7 @@ private static Value ParseExtension(JsonElement extn) "decimal" => DecimalValue.Decode(arg), "datetime" => DateTimeValue.Decode(arg), "duration" => DurationValue.Decode(arg), - "ipaddr" => IpAddrValue.Decode(arg), + "ip" => IpAddrValue.Decode(arg), _ => throw new NotSupportedException($"Unsupported extension type: {fn}") }; } @@ -127,7 +127,7 @@ public override void Write(Utf8JsonWriter writer, Value value, JsonSerializerOpt WriteExtensionType(writer, "duration", durationValue.Encode()); break; case IpAddrValue ipAddrValue: - WriteExtensionType(writer, "ipaddr", ipAddrValue.Encode()); + WriteExtensionType(writer, "ip", ipAddrValue.Encode()); break; default: throw new NotSupportedException($"Unsupported value type: {value.GetType()}"); diff --git a/src/CedarDotNetFfi/src/lib.rs b/src/CedarDotNetFfi/src/lib.rs index f8acbf1..56e0a08 100644 --- a/src/CedarDotNetFfi/src/lib.rs +++ b/src/CedarDotNetFfi/src/lib.rs @@ -1,6 +1,11 @@ use std::ffi::{c_char, CString, CStr}; -use cedar_policy::Policy; +use std::str::FromStr; + +use cedar_policy::{ + Policy, + PolicySet +}; use cedar_policy::ffi::{ check_parse_policy_set_json_str, @@ -133,3 +138,19 @@ pub fn policy_format_json_to_text(json: *const c_char) -> *const c_char { CString::new(policy.to_string()).unwrap().into_raw() } + +#[unsafe(no_mangle)] +pub fn load_policy_set(text: *const c_char) -> *const c_char { + let text_str = unsafe { CStr::from_ptr(text).to_str().unwrap() }; + + let policy_set = PolicySet::from_str(text_str).expect("Could not load policy set"); + + let arr = policy_set + .policies() + .map(|p| p.to_string()) + .collect::>(); + + let result_json_str = serde_json::to_string(&arr).unwrap(); + + CString::new(result_json_str.to_string()).unwrap().into_raw() +} \ No newline at end of file diff --git a/tests/CedarDotNet.UnitTests/CedarDotNet.UnitTests.csproj b/tests/CedarDotNet.UnitTests/CedarDotNet.UnitTests.csproj index 68ff8e2..423ade6 100644 --- a/tests/CedarDotNet.UnitTests/CedarDotNet.UnitTests.csproj +++ b/tests/CedarDotNet.UnitTests/CedarDotNet.UnitTests.csproj @@ -1,4 +1,4 @@ - + net8.0 diff --git a/tests/CedarDotNet.UnitTests/IntegrationTests/Dtos/TestJsonSerializedContext.cs b/tests/CedarDotNet.UnitTests/IntegrationTests/Dtos/TestJsonSerializedContext.cs new file mode 100644 index 0000000..8a8e851 --- /dev/null +++ b/tests/CedarDotNet.UnitTests/IntegrationTests/Dtos/TestJsonSerializedContext.cs @@ -0,0 +1,11 @@ +using CedarDotNet.Models; +using System.Text.Json.Serialization; + +namespace CedarDotNet.UnitTests.IntegrationTests.Dtos; + +[JsonSerializable(typeof(TestScenarioDto))] +[JsonSerializable(typeof(IReadOnlyCollection))] +[JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,UseStringEnumConverter = true)] +internal sealed partial class TestJsonSerializedContext + : JsonSerializerContext; \ No newline at end of file diff --git a/tests/CedarDotNet.UnitTests/IntegrationTests/Dtos/TestRequestDto.cs b/tests/CedarDotNet.UnitTests/IntegrationTests/Dtos/TestRequestDto.cs new file mode 100644 index 0000000..3cc43f4 --- /dev/null +++ b/tests/CedarDotNet.UnitTests/IntegrationTests/Dtos/TestRequestDto.cs @@ -0,0 +1,32 @@ +using CedarDotNet.Models; +using CedarDotNet.Values; +using System.Text.Json.Serialization; + +namespace CedarDotNet.UnitTests.IntegrationTests.Dtos; + +public sealed class TestRequestDto +{ + [JsonPropertyName("description")] + public required string Description { get; init; } + + [JsonPropertyName("principal")] + public required EntityUid Principal { get; init; } + + [JsonPropertyName("action")] + public required EntityUid Action { get; init; } + + [JsonPropertyName("resource")] + public required EntityUid Resource { get; init; } + + [JsonPropertyName("context")] + public required IReadOnlyDictionary Context { get; init; } + + [JsonPropertyName("decision")] + public required Decision Decision { get; init; } + + [JsonPropertyName("reason")] + public required IReadOnlyCollection Reason { get; init; } + + [JsonPropertyName("errors")] + public required IReadOnlyCollection Errors { get; init; } +} \ No newline at end of file diff --git a/tests/CedarDotNet.UnitTests/IntegrationTests/Dtos/TestScenarioDto.cs b/tests/CedarDotNet.UnitTests/IntegrationTests/Dtos/TestScenarioDto.cs new file mode 100644 index 0000000..e528a6c --- /dev/null +++ b/tests/CedarDotNet.UnitTests/IntegrationTests/Dtos/TestScenarioDto.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace CedarDotNet.UnitTests.IntegrationTests.Dtos; + +public sealed class TestScenarioDto +{ + [JsonPropertyName("policies")] + public required string Policies { get; init; } + + [JsonPropertyName("entities")] + public required string Entities { get; init; } + + [JsonPropertyName("schema")] + public required string Schema { get; init; } + + [JsonPropertyName("shouldValidate")] + public required bool ShouldValidate { get; init; } + + [JsonPropertyName("requests")] + public required IReadOnlyCollection Requests { get; init; } +} diff --git a/tests/CedarDotNet.UnitTests/IntegrationTests/IntegrationTestData.cs b/tests/CedarDotNet.UnitTests/IntegrationTests/IntegrationTestData.cs new file mode 100644 index 0000000..7a4a9ec --- /dev/null +++ b/tests/CedarDotNet.UnitTests/IntegrationTests/IntegrationTestData.cs @@ -0,0 +1,69 @@ +using CedarDotNet.Models; +using CedarDotNet.UnitTests.IntegrationTests.Dtos; +using CedarDotNet.UnitTests.IntegrationTests.Models; +using System.Collections; +using System.Text.Json; + +namespace CedarDotNet.UnitTests.IntegrationTests; + +public sealed class IntegrationTestData + : IEnumerable +{ + private static readonly string BaseDirectory = "../../../../../cedar-integration-tests"; + + public IEnumerator GetEnumerator() + { + var testsDirectory = Path.Combine(BaseDirectory, "tests"); + + var testFiles = Directory.EnumerateFiles(testsDirectory, "*.json", SearchOption.AllDirectories); + + foreach (var testFile in testFiles) + { + yield return LoadTestFile(testFile); + } + } + + private static object[] LoadTestFile(string path) + { + var json = File.ReadAllText(path); + + var scenarioDto = JsonSerializer.Deserialize( + json: json, + jsonTypeInfo: TestJsonSerializedContext.Default.TestScenarioDto)!; + + var policiesText = File.ReadAllText(Path.Combine(BaseDirectory, scenarioDto.Policies)); + + var policies = CedarUtilities.LoadPolicySet(policiesText); + + var policySet = new PolicySet() + { + StaticPolicies = policies + .Select((x, i) => KeyValuePair.Create($"#{i + 1}", x)) + .ToDictionary() + }; + + var entitiesText = File.ReadAllText(Path.Combine(BaseDirectory, scenarioDto.Entities)); + + var entities = JsonSerializer.Deserialize( + json: entitiesText, + jsonTypeInfo: TestJsonSerializedContext.Default.IReadOnlyCollectionEntity)!; + + var schemaText = File.ReadAllText(Path.Combine(BaseDirectory, scenarioDto.Schema)); + + var schema = Schema.FromText(schemaText); + + var scenario = new TestScenario + { + Policies = policySet, + Entities = entities, + Schema = schema, + ShouldValidate = scenarioDto.ShouldValidate, + Requests = scenarioDto.Requests + }; + + return [scenario]; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); +} \ No newline at end of file diff --git a/tests/CedarDotNet.UnitTests/IntegrationTests/IntegrationTests.cs b/tests/CedarDotNet.UnitTests/IntegrationTests/IntegrationTests.cs new file mode 100644 index 0000000..4f540f2 --- /dev/null +++ b/tests/CedarDotNet.UnitTests/IntegrationTests/IntegrationTests.cs @@ -0,0 +1,34 @@ +using CedarDotNet.Models; +using CedarDotNet.UnitTests.IntegrationTests.Models; + +namespace CedarDotNet.UnitTests.IntegrationTests; + +public sealed class IntegrationTests +{ + [Theory] + [ClassData(typeof(IntegrationTestData))] + public void IsAuthorized_IntegrationTest_Succeeds( + TestScenario scenario) + { + foreach (var request in scenario.Requests) + { + var result = CedarFunctions.IsAuthorized(new() + { + Principal = request.Principal, + Action = request.Action, + Resource = request.Resource, + Context = request.Context, + Schema = scenario.Schema, + ValidateRequest = scenario.ShouldValidate, + Policies = scenario.Policies, + Entities = scenario.Entities + }); + + Assert.IsType(result); + + var actualDecision = ((AuthorizationAnswerSuccess)result).Response.Decision; + + Assert.Equal(request.Decision, actualDecision); + } + } +} diff --git a/tests/CedarDotNet.UnitTests/IntegrationTests/Models/TestScenario.cs b/tests/CedarDotNet.UnitTests/IntegrationTests/Models/TestScenario.cs new file mode 100644 index 0000000..06653f0 --- /dev/null +++ b/tests/CedarDotNet.UnitTests/IntegrationTests/Models/TestScenario.cs @@ -0,0 +1,13 @@ +using CedarDotNet.Models; +using CedarDotNet.UnitTests.IntegrationTests.Dtos; + +namespace CedarDotNet.UnitTests.IntegrationTests.Models; + +public sealed class TestScenario +{ + public required PolicySet Policies { get; init; } + public required IReadOnlyCollection Entities { get; init; } + public required Schema Schema { get; init; } + public required bool ShouldValidate { get; init; } + public required IReadOnlyCollection Requests { get; init; } +} From bd5ebfd21c7fa1bb68a783b2a26eed2b1bdf02ba Mon Sep 17 00:00:00 2001 From: Jamie Winder Date: Sun, 4 May 2025 21:12:04 +0100 Subject: [PATCH 2/3] use submodule for integration tests --- .gitmodules | 3 +++ cedar-integration-tests | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 cedar-integration-tests diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6c18a08 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "cedar-integration-tests"] + path = cedar-integration-tests + url = https://github.com/cedar-policy/cedar-integration-tests diff --git a/cedar-integration-tests b/cedar-integration-tests new file mode 160000 index 0000000..858d8bd --- /dev/null +++ b/cedar-integration-tests @@ -0,0 +1 @@ +Subproject commit 858d8bdc9ad4abd41020544f798a327bcf741b7e From e51cd78316d7a5202bb0dfad66ce6278f60db8b8 Mon Sep 17 00:00:00 2001 From: Jamie Winder Date: Sun, 4 May 2025 21:12:13 +0100 Subject: [PATCH 3/3] correct workdlow --- .github/workflows/ci.yml | 10 +++------- .gitignore | 3 --- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8888d37..6d47f48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,8 @@ jobs: steps: - name: 📥 Checkout code uses: actions/checkout@v4 + with: + submodules: true - name: 🦀 Install Rust target run: rustup target add ${{ matrix.target }} @@ -76,16 +78,10 @@ jobs: with: dotnet-version: '8.0.x' - - name: 🧪 Checkout Cedar integration test data - uses: actions/checkout@v4 - with: - repository: cedar-policy/cedar-integration-tests - path: cedar-integration-tests - - name: ✅ Run .NET tests run: dotnet test - - name: 📦 Upload native binary + - name: 📤 Upload native binary uses: actions/upload-artifact@v4 with: name: rustlib-${{ matrix.rid }} diff --git a/.gitignore b/.gitignore index 74d0957..eaa9902 100644 --- a/.gitignore +++ b/.gitignore @@ -361,6 +361,3 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd - -# Custom -/cedar-integration-tests \ No newline at end of file