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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -79,7 +81,7 @@ jobs:
- 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 }}
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,4 @@ MigrationBackup/
.ionide/

# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "cedar-integration-tests"]
path = cedar-integration-tests
url = https://github.com/cedar-policy/cedar-integration-tests
1 change: 1 addition & 0 deletions cedar-integration-tests
2 changes: 1 addition & 1 deletion examples/BasicExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
27 changes: 27 additions & 0 deletions src/CedarDotNet/CedarUtilities.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using CedarDotNet.Interop;
using CedarDotNet.Models;
using System.Runtime.InteropServices;
using System.Text.Json;

namespace CedarDotNet;

Expand Down Expand Up @@ -28,4 +31,28 @@ public static string PolicyFormatJsonToText(
=> FfiUtilities.CallUnaryStr(
policyJson,
CedarFfi.PolicyFormatJsonToText);

/// <summary>
/// Loads a policy set from the given text.
/// </summary>
/// <param name="text">The policies text.</param>
/// <returns>The policy collection.</returns>
public static IReadOnlyCollection<string> 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);
}
}
}
3 changes: 3 additions & 0 deletions src/CedarDotNet/Interop/CedarFfi.Utilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
1 change: 1 addition & 0 deletions src/CedarDotNet/Models/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public sealed record class Entity
/// The tags.
/// </summary>
[JsonPropertyName("tags")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary<string, Value> Tags { get; init; } = FrozenDictionary<string, Value>.Empty;

}
4 changes: 2 additions & 2 deletions src/CedarDotNet/Values/ValueJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
};
}
Expand Down Expand Up @@ -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()}");
Expand Down
23 changes: 22 additions & 1 deletion src/CedarDotNetFfi/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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::<Vec<_>>();

let result_json_str = serde_json::to_string(&arr).unwrap();

CString::new(result_json_str.to_string()).unwrap().into_raw()
}
2 changes: 1 addition & 1 deletion tests/CedarDotNet.UnitTests/CedarDotNet.UnitTests.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using CedarDotNet.Models;
using System.Text.Json.Serialization;

namespace CedarDotNet.UnitTests.IntegrationTests.Dtos;

[JsonSerializable(typeof(TestScenarioDto))]
[JsonSerializable(typeof(IReadOnlyCollection<Entity>))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,UseStringEnumConverter = true)]
internal sealed partial class TestJsonSerializedContext
: JsonSerializerContext;
Original file line number Diff line number Diff line change
@@ -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<string, Value> Context { get; init; }

[JsonPropertyName("decision")]
public required Decision Decision { get; init; }

[JsonPropertyName("reason")]
public required IReadOnlyCollection<string> Reason { get; init; }

[JsonPropertyName("errors")]
public required IReadOnlyCollection<string> Errors { get; init; }
}
Original file line number Diff line number Diff line change
@@ -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<TestRequestDto> Requests { get; init; }
}
Original file line number Diff line number Diff line change
@@ -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<object[]>
{
private static readonly string BaseDirectory = "../../../../../cedar-integration-tests";

public IEnumerator<object[]> 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();
}
34 changes: 34 additions & 0 deletions tests/CedarDotNet.UnitTests/IntegrationTests/IntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -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<AuthorizationAnswerSuccess>(result);

var actualDecision = ((AuthorizationAnswerSuccess)result).Response.Decision;

Assert.Equal(request.Decision, actualDecision);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Entity> Entities { get; init; }
public required Schema Schema { get; init; }
public required bool ShouldValidate { get; init; }
public required IReadOnlyCollection<TestRequestDto> Requests { get; init; }
}