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: 4 additions & 0 deletions .github/workflows/aria-auth-core-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ jobs:
- name: Run CLI tests
run: dotnet test src/aria-cli/Aria.Cli.Tests/Aria.Cli.Tests.csproj -c Release
--no-build

- name: Run Auth Core tests
run: dotnet test src/aria-auth-core/Aria.Auth.Core.Tests/Aria.Auth.Core.Tests.csproj -c Release
--no-build
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using Aria.Auth.Core.Models;
using Aria.Auth.Core.Services;
using Aria.Auth.Core.TestFixtures;
using Xunit;

namespace Aria.Auth.Core.Tests;

public sealed class AccessPolicyConformanceTests
{
[Fact]
public async Task SharedFixtures_ResolveAccessPolicyConsistentlyAcrossProviders()
{
var fixturePath = Path.Combine(AppContext.BaseDirectory, "provider-conformance-fixtures.json");
var fixtures = ProviderConformanceFixtures.Load(fixturePath);

foreach (var fixture in fixtures)
{
var identity = new ResolvedIdentity(
fixture.Expected.Provider,
fixture.Expected.ObjectId,
fixture.Expected.TenantId,
fixture.Expected.UserPrincipalName,
fixture.Expected.Groups.ToHashSet(StringComparer.OrdinalIgnoreCase),
fixture.Expected.Roles.ToHashSet(StringComparer.OrdinalIgnoreCase));

var provider = new FixtureIdentityProvider(fixture.Provider, identity);
var factory = new IdentityProviderFactory([provider]);
var service = new AccessPolicyService(factory);

var config = CreateConfigFor(fixture.Provider);
var context = await service.ResolveAsync(config);

Assert.NotNull(context.Identity);
Assert.Equal(fixture.Expected.ObjectId, context.Identity!.ObjectId);
Assert.Equal("restricted", context.SensitivityCeiling);
Assert.Contains("finance-group-rule", context.MatchedRules);
Assert.Contains("asset-reader-rule", context.MatchedRules);
Assert.Contains("Data Reader", context.PurviewRoles);
Assert.Contains("Data Curator", context.PurviewRoles);
}
}

private static AriaConfig CreateConfigFor(string provider)
{
return new AriaConfig
{
SensitivityCeiling = "public",
Auth = new AuthConfig
{
Provider = provider,
EnableExperimentalProviders = true
},
AccessRules =
[
new AccessRule
{
Name = "finance-group-rule",
AnyEntraGroups = ["finance-team"],
SensitivityCeiling = "confidential",
PurviewRoles = ["Data Reader"]
},
new AccessRule
{
Name = "asset-reader-rule",
AnyEntraRoles = ["Asset.Reader"],
SensitivityCeiling = "restricted",
PurviewRoles = ["Data Curator"]
}
]
};
}

private sealed class FixtureIdentityProvider : IIdentityProvider
{
private readonly ResolvedIdentity _identity;

public FixtureIdentityProvider(string name, ResolvedIdentity identity)
{
Name = name;
_identity = identity;
}

public string Name { get; }

public Task<ResolvedIdentity?> GetIdentityAsync(AriaConfig config)
{
return Task.FromResult<ResolvedIdentity?>(_identity);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../Aria.Auth.Core.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Include="../TestFixtures/ProviderConformanceFixtures.cs" Link="Shared/ProviderConformanceFixtures.cs" />
<Content Include="../TestFixtures/provider-conformance-fixtures.json" Link="provider-conformance-fixtures.json" CopyToOutputDirectory="PreserveNewest" />
<Content Include="../TestFixtures/provider-conformance-notes.txt" Link="provider-conformance-notes.txt" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
Comment on lines +1 to +24

</Project>
7 changes: 7 additions & 0 deletions src/aria-auth-core/Aria.Auth.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,11 @@
<None Include="CHANGELOG.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<Compile Remove="Aria.Auth.Core.Tests/**/*.cs" />
<Compile Remove="TestFixtures/**/*.cs" />
<None Include="Aria.Auth.Core.Tests/**/*.cs" />
<None Include="TestFixtures/**/*.cs" />
</ItemGroup>

</Project>
103 changes: 103 additions & 0 deletions src/aria-auth-core/TestFixtures/ProviderConformanceFixtures.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System.Text;
using System.Text.Json;

namespace Aria.Auth.Core.TestFixtures;

public sealed record ExpectedNormalizedIdentity(
string Provider,
string ObjectId,
string TenantId,
string? UserPrincipalName,
List<string> Groups,
List<string> Roles);

public sealed record ProviderFixtureCase(
string Name,
string Provider,
Dictionary<string, JsonElement> Claims,
ExpectedNormalizedIdentity Expected)
{
public string BuildJwtToken()
{
var payload = new Dictionary<string, object?>
{
["iss"] = "https://fixtures.aria.dev",
["iat"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
["exp"] = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeSeconds()
};

foreach (var (key, value) in Claims)
payload[key] = JsonToClr(value);

var header = new Dictionary<string, object?>
{
["alg"] = "RS256",
["typ"] = "JWT"
};

var headerEncoded = Base64UrlEncode(JsonSerializer.Serialize(header));
var payloadEncoded = Base64UrlEncode(JsonSerializer.Serialize(payload));
var signature = Base64UrlEncode("fixture-signature");

return $"{headerEncoded}.{payloadEncoded}.{signature}";
}

private static object? JsonToClr(JsonElement value)
{
return value.ValueKind switch
{
JsonValueKind.String => value.GetString(),
JsonValueKind.Number => value.TryGetInt64(out var i) ? i : value.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
JsonValueKind.Array => value.EnumerateArray().Select(JsonToClr).ToList(),
JsonValueKind.Object => value.EnumerateObject().ToDictionary(p => p.Name, p => JsonToClr(p.Value)),
_ => value.ToString()
};
}

private static string Base64UrlEncode(string input)
{
var bytes = Encoding.UTF8.GetBytes(input);
return Convert.ToBase64String(bytes)
.Replace('+', '-')
.Replace('/', '_')
.TrimEnd('=');
}
}

public static class ProviderConformanceFixtures
{
public static IReadOnlyList<ProviderFixtureCase> Load(string fixturePath)
{
var json = File.ReadAllText(fixturePath);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;

var fixtures = new List<ProviderFixtureCase>();
foreach (var item in root.GetProperty("fixtures").EnumerateArray())
{
var claims = new Dictionary<string, JsonElement>(StringComparer.OrdinalIgnoreCase);
foreach (var claim in item.GetProperty("claims").EnumerateObject())
claims[claim.Name] = claim.Value.Clone();

var expectedElement = item.GetProperty("expected");
var expected = new ExpectedNormalizedIdentity(
expectedElement.GetProperty("provider").GetString() ?? string.Empty,
expectedElement.GetProperty("objectId").GetString() ?? string.Empty,
expectedElement.GetProperty("tenantId").GetString() ?? string.Empty,
expectedElement.TryGetProperty("userPrincipalName", out var upn) ? upn.GetString() : null,
expectedElement.GetProperty("groups").EnumerateArray().Select(x => x.GetString() ?? string.Empty).ToList(),
expectedElement.GetProperty("roles").EnumerateArray().Select(x => x.GetString() ?? string.Empty).ToList());

fixtures.Add(new ProviderFixtureCase(
item.GetProperty("name").GetString() ?? string.Empty,
item.GetProperty("provider").GetString() ?? string.Empty,
claims,
expected));
}

return fixtures;
}
}
138 changes: 138 additions & 0 deletions src/aria-auth-core/TestFixtures/provider-conformance-fixtures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
{
"fixtures": [
{
"name": "entra-basic",
"provider": "entra",
"claims": {
"oid": "entra-user-001",
"tid": "entra-tenant-01",
"preferred_username": "entra.user@aria.dev",
"groups": ["finance-team", "readers"],
"roles": ["Asset.Reader"],
"scp": "asset.read asset.list"
},
"expected": {
"provider": "entra",
"objectId": "entra-user-001",
"tenantId": "entra-tenant-01",
"userPrincipalName": "entra.user@aria.dev",
"groups": ["finance-team", "readers"],
"roles": ["Asset.Reader", "asset.read", "asset.list"]
}
},
{
"name": "entra-missing-upn-scp-only",
"provider": "entra",
"claims": {
"oid": "entra-user-004",
"tid": "entra-tenant-02",
"groups": ["finance-team", "readers"],
"scp": "Asset.Reader asset.read asset.list"
},
"expected": {
"provider": "entra",
"objectId": "entra-user-004",
"tenantId": "entra-tenant-02",
"userPrincipalName": null,
"groups": ["finance-team", "readers"],
"roles": ["Asset.Reader", "asset.read", "asset.list"]
}
},
{
"name": "okta-basic",
"provider": "okta",
"claims": {
"uid": "okta-user-002",
"iss": "https://example.okta.com/oauth2/default",
"email": "okta.user@aria.dev",
"okta.groups": ["finance-team", "readers"],
"permissions": ["Asset.Reader"],
"scp": "asset.read asset.list"
},
"expected": {
"provider": "okta",
"objectId": "okta-user-002",
"tenantId": "https://example.okta.com",
"userPrincipalName": "okta.user@aria.dev",
"groups": ["finance-team", "readers"],
"roles": ["Asset.Reader", "asset.read", "asset.list"]
}
},
{
"name": "okta-scalar-claims-mixed-separators",
"provider": "okta",
"claims": {
"sub": "okta-user-005",
"email": "okta.scalar@aria.dev",
"groups": "finance-team, readers",
"scope": "Asset.Reader asset.read,asset.list"
},
"expected": {
"provider": "okta",
"objectId": "okta-user-005",
"tenantId": "https://example.okta.com",
"userPrincipalName": "okta.scalar@aria.dev",
"groups": ["finance-team", "readers"],
"roles": ["Asset.Reader", "asset.read", "asset.list"]
}
},
{
"name": "auth0-basic",
"provider": "auth0",
"claims": {
"sub": "auth0|user-003",
"aud": ["https://api.aria.dev", "https://example.auth0.com/userinfo"],
"email": "auth0.user@aria.dev",
"https://aria.dev/groups": ["finance-team", "readers"],
"permissions": ["Asset.Reader"],
"scope": "asset.read asset.list"
},
"expected": {
"provider": "auth0",
"objectId": "auth0|user-003",
"tenantId": "https://api.aria.dev",
"userPrincipalName": "auth0.user@aria.dev",
"groups": ["finance-team", "readers"],
"roles": ["Asset.Reader", "asset.read", "asset.list"]
}
},
{
"name": "auth0-scalar-claims-missing-upn",
"provider": "auth0",
"claims": {
"uid": "auth0-user-006",
"aud": "https://api.aria.dev",
"org_id": "finance-team",
"scope": "Asset.Reader,asset.read asset.list"
},
"expected": {
"provider": "auth0",
"objectId": "auth0-user-006",
"tenantId": "https://api.aria.dev",
"userPrincipalName": null,
"groups": ["finance-team"],
"roles": ["Asset.Reader", "asset.read", "asset.list"]
}
},
{
"name": "auth0-role-dedup-case-insensitive",
"provider": "auth0",
"claims": {
"sub": "auth0|user-007",
"aud": "https://api.aria.dev",
"email": "auth0.dedup@aria.dev",
"groups": ["finance-team", "Finance-Team"],
"permissions": ["Asset.Reader", "asset.reader"],
"scope": "asset.read asset.read"
},
"expected": {
"provider": "auth0",
"objectId": "auth0|user-007",
"tenantId": "https://api.aria.dev",
"userPrincipalName": "auth0.dedup@aria.dev",
"groups": ["finance-team"],
"roles": ["Asset.Reader", "asset.read"]
}
}
]
}
Loading
Loading