Skip to content

Commit f3b76ed

Browse files
authored
Expose trusted actor roles in AccessContext for policy evaluation (#108)
* Expose trusted actor roles in AccessContext for policy evaluation * Fix PR * Some Improvements * Add and Fix Tests
1 parent bac7b82 commit f3b76ed

22 files changed

Lines changed: 602 additions & 83 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace CodeBeam.UltimateAuth.Core.Contracts;
2+
3+
/// <summary>
4+
/// Represents a trusted, server-resolved role assigned to the authenticated actor during access evaluation.
5+
/// </summary>
6+
public sealed record AccessActorRole(string Id, string Name);

‎src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public sealed class AccessContext
1313
public bool IsAuthenticated { get; init; }
1414
public bool IsSystemActor { get; init; }
1515
public SessionChainId? ActorChainId { get; }
16+
public IReadOnlyList<AccessActorRole> ActorRoles { get; }
1617

1718
// Target
1819
public string? Resource { get; init; }
@@ -45,6 +46,7 @@ internal AccessContext(
4546
bool isAuthenticated,
4647
bool isSystemActor,
4748
SessionChainId? actorChainId,
49+
IReadOnlyList<AccessActorRole> actorRoles,
4850
string? resource,
4951
UserKey? targetUserKey,
5052
TenantKey resourceTenant,
@@ -56,6 +58,7 @@ internal AccessContext(
5658
IsAuthenticated = isAuthenticated;
5759
IsSystemActor = isSystemActor;
5860
ActorChainId = actorChainId;
61+
ActorRoles = Array.AsReadOnly(actorRoles.ToArray());
5962

6063
Resource = resource;
6164
TargetUserKey = targetUserKey;
@@ -78,6 +81,7 @@ public AccessContext WithAttribute(string key, object value)
7881
IsAuthenticated,
7982
IsSystemActor,
8083
ActorChainId,
84+
ActorRoles,
8185
Resource,
8286
TargetUserKey,
8387
ResourceTenant,

‎src/CodeBeam.UltimateAuth.Core/Defaults/UAuthConstants.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public static class Claims
1616
{
1717
public const string Tenant = "uauth:tenant";
1818
public const string Permission = "uauth:permission";
19+
public const string ActorRole = "uauth:actor_role";
1920
}
2021

2122
public static class HttpItems
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using CodeBeam.UltimateAuth.Authorization;
2+
using CodeBeam.UltimateAuth.Authorization.Contracts;
3+
using CodeBeam.UltimateAuth.Core.Contracts;
4+
using CodeBeam.UltimateAuth.Core.Domain;
5+
using CodeBeam.UltimateAuth.Core.MultiTenancy;
6+
7+
namespace CodeBeam.UltimateAuth.Server.Auth;
8+
9+
internal sealed class AccessActorRoleResolver(IUserRoleStoreFactory userRoleFactory, IRoleStoreFactory roleFactory)
10+
{
11+
public async Task<ResolvedActorRoles> ResolveAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default)
12+
{
13+
var userRoleStore = userRoleFactory.Create(tenant);
14+
var assignments = await userRoleStore.GetAssignmentsAsync(userKey, ct);
15+
var roleIds = assignments.Select(x => x.RoleId).Distinct().ToArray();
16+
17+
if (roleIds.Length == 0)
18+
return new ResolvedActorRoles([], roleIds);
19+
20+
var roleStore = roleFactory.Create(tenant);
21+
22+
var roles = await roleStore.GetByIdsAsync(roleIds, ct);
23+
24+
var activeRoles = roles
25+
.Where(x => !x.IsDeleted && x.Tenant == tenant)
26+
.ToArray();
27+
28+
var actorRoles = activeRoles
29+
.Select(x => new AccessActorRole(x.Id.ToString(), x.Name))
30+
.ToArray();
31+
32+
var activeRoleIds = activeRoles
33+
.Select(x => x.Id)
34+
.ToArray();
35+
36+
return new ResolvedActorRoles(actorRoles, activeRoleIds);
37+
}
38+
}
39+
40+
internal sealed record ResolvedActorRoles(
41+
IReadOnlyList<AccessActorRole> ActorRoles,
42+
IReadOnlyCollection<RoleId> RoleIds);

‎src/CodeBeam.UltimateAuth.Server/Auth/Context/AccessContextFactory.cs‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ namespace CodeBeam.UltimateAuth.Server.Auth;
99

1010
internal sealed class AccessContextFactory : IAccessContextFactory
1111
{
12-
private readonly IUserRoleStoreFactory _userRoleFactory;
12+
private readonly AccessActorRoleResolver _roleResolver;
1313
private readonly IUserIdConverterResolver _converterResolver;
1414

15-
public AccessContextFactory(IUserRoleStoreFactory userRoleFactory, IUserIdConverterResolver converterResolver)
15+
public AccessContextFactory(AccessActorRoleResolver roleResolver, IUserIdConverterResolver converterResolver)
1616
{
17-
_userRoleFactory = userRoleFactory;
17+
_roleResolver = roleResolver;
1818
_converterResolver = converterResolver;
1919
}
2020

@@ -39,17 +39,19 @@ private async Task<AccessContext> CreateInternalAsync(AuthFlowContext authFlow,
3939
if (string.IsNullOrWhiteSpace(resource))
4040
throw new ArgumentException("Resource is required.", nameof(resource));
4141

42+
IReadOnlyList<AccessActorRole> actorRoles = Array.Empty<AccessActorRole>();
43+
4244
var attrs = attributes != null
4345
? new Dictionary<string, object>(attributes)
4446
: new Dictionary<string, object>();
4547

48+
attrs.Remove("roles");
49+
4650
if (authFlow.IsAuthenticated && authFlow.UserKey is not null)
4751
{
48-
var userRoleStore = _userRoleFactory.Create(authFlow.Tenant);
49-
var assignments = await userRoleStore.GetAssignmentsAsync(authFlow.UserKey.Value, ct);
50-
var roleIds = assignments.Select(x => x.RoleId).ToArray();
51-
52-
attrs["roles"] = roleIds;
52+
var resolved = await _roleResolver.ResolveAsync(authFlow.Tenant, authFlow.UserKey.Value, ct);
53+
actorRoles = resolved.ActorRoles;
54+
attrs["roles"] = resolved.RoleIds.ToArray();
5355
}
5456

5557
UserKey? targetUserKey = null;
@@ -71,6 +73,7 @@ private async Task<AccessContext> CreateInternalAsync(AuthFlowContext authFlow,
7173
isAuthenticated: authFlow.IsAuthenticated,
7274
isSystemActor: authFlow.Tenant.IsSystem,
7375
actorChainId: authFlow.Session?.ChainId,
76+
actorRoles: actorRoles,
7477
resource: resource,
7578
targetUserKey: targetUserKey,
7679
resourceTenant: resourceTenant,

‎src/CodeBeam.UltimateAuth.Server/Authorization/AspNetCore/ResourceAccessContextBuilder.cs‎

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,102 @@
1-
using CodeBeam.UltimateAuth.Core;
1+
using CodeBeam.UltimateAuth.Authorization.Contracts;
2+
using CodeBeam.UltimateAuth.Core;
23
using CodeBeam.UltimateAuth.Core.Contracts;
4+
using CodeBeam.UltimateAuth.Core.Defaults;
5+
using CodeBeam.UltimateAuth.Core.Runtime;
6+
using CodeBeam.UltimateAuth.Server.Auth;
37
using CodeBeam.UltimateAuth.Server.Extensions;
8+
using CodeBeam.UltimateAuth.Server.Runtime;
49
using Microsoft.AspNetCore.Http;
510
using Microsoft.Extensions.DependencyInjection;
11+
using System.Collections.ObjectModel;
12+
using System.Text.Json;
613

714
namespace CodeBeam.UltimateAuth.Server.Authorization;
815

916
internal static class ResourceAccessContextBuilder
1017
{
11-
public static AccessContext Create(HttpContext http, string action)
18+
public static async Task<AccessContext> CreateAsync(HttpContext http, string action, CancellationToken ct = default)
1219
{
1320
var user = http.RequestServices.GetRequiredService<ICurrentUser>();
1421

22+
var tenant = http.GetTenant();
23+
24+
IReadOnlyList<AccessActorRole> actorRoles = [];
25+
IReadOnlyDictionary<string, object> attributes = EmptyAttributes.Instance;
26+
27+
if (user.IsAuthenticated)
28+
{
29+
RoleId[] roleIds;
30+
31+
var runtime = http.RequestServices.GetRequiredService<IUAuthRuntimeMarker>();
32+
33+
if (runtime is ResourceRuntimeMarker)
34+
{
35+
// Remote session validation has already produced
36+
// the authenticated principal.
37+
var identity = http.User.Identities.SingleOrDefault(x => x.IsAuthenticated && x.AuthenticationType == UAuthConstants.SchemeDefaults.GlobalScheme);
38+
39+
if (identity is null)
40+
throw new InvalidOperationException("Trusted UltimateAuth identity is missing.");
41+
42+
var roles = new List<AccessActorRole>();
43+
var ids = new List<RoleId>();
44+
45+
foreach (var claim in identity.FindAll(UAuthConstants.Claims.ActorRole))
46+
{
47+
AccessActorRole? role;
48+
49+
try
50+
{
51+
role = JsonSerializer.Deserialize<AccessActorRole>(claim.Value);
52+
}
53+
catch (JsonException)
54+
{
55+
throw new InvalidOperationException("Invalid actor role claim.");
56+
}
57+
58+
if (role is null ||
59+
string.IsNullOrWhiteSpace(role.Name) ||
60+
!RoleId.TryParse(role.Id, null, out var roleId))
61+
{
62+
throw new InvalidOperationException("Invalid actor role claim.");
63+
}
64+
65+
roles.Add(role);
66+
ids.Add(roleId);
67+
}
68+
69+
actorRoles = roles;
70+
roleIds = ids.ToArray();
71+
}
72+
else
73+
{
74+
var resolver = http.RequestServices.GetRequiredService<AccessActorRoleResolver>();
75+
var resolved = await resolver.ResolveAsync(tenant, user.UserKey, ct);
76+
77+
actorRoles = resolved.ActorRoles;
78+
roleIds = resolved.RoleIds.ToArray();
79+
}
80+
81+
attributes = new ReadOnlyDictionary<string, object>(
82+
new Dictionary<string, object>
83+
{
84+
["roles"] = roleIds
85+
});
86+
}
87+
1588
return new AccessContext(
1689
actorUserKey: user.IsAuthenticated ? user.UserKey : null,
17-
actorTenant: http.GetTenant(),
90+
actorTenant: tenant,
1891
isAuthenticated: user.IsAuthenticated,
1992
isSystemActor: false,
2093
actorChainId: null,
21-
94+
actorRoles: actorRoles,
2295
resource: ResolveResource(action),
2396
targetUserKey: null,
24-
resourceTenant: http.GetTenant(),
25-
97+
resourceTenant: tenant,
2698
action: action,
27-
attributes: EmptyAttributes.Instance
28-
);
99+
attributes: attributes);
29100
}
30101

31102
private static string ResolveResource(string action)

‎src/CodeBeam.UltimateAuth.Server/Authorization/AspNetCore/UAuthAuthorizationHandler.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public UAuthAuthorizationHandler(
2121
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, UAuthActionRequirement requirement)
2222
{
2323
var http = _httpContextAccessor.HttpContext!;
24-
var accessContext = ResourceAccessContextBuilder.Create(http, requirement.Action);
24+
var accessContext = await ResourceAccessContextBuilder.CreateAsync(http, requirement.Action, http.RequestAborted);
2525

2626
try
2727
{

‎src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol
237237
services.TryAddSingleton<IEffectiveAuthModeResolver, EffectiveAuthModeResolver>();
238238
services.TryAddSingleton<IPrimaryTokenResolver, PrimaryTokenResolver>();
239239
services.TryAddSingleton<IAuthRedirectResolver, AuthRedirectResolver>();
240+
services.TryAddScoped<AccessActorRoleResolver>();
240241

241242
services.TryAddScoped<ISessionTouchService, SessionTouchService>();
242243
services.TryAddScoped<ISessionQueryService, UAuthSessionQueryService>();

‎src/authorization/CodeBeam.UltimateAuth.Authorization/Infrastructure/AuthorizationClaimsProvider.cs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using CodeBeam.UltimateAuth.Core;
2+
using CodeBeam.UltimateAuth.Core.Contracts;
23
using CodeBeam.UltimateAuth.Core.Defaults;
34
using CodeBeam.UltimateAuth.Core.Domain;
45
using CodeBeam.UltimateAuth.Core.MultiTenancy;
56
using System.Security.Claims;
7+
using System.Text.Json;
68

79
namespace CodeBeam.UltimateAuth.Authorization;
810

@@ -32,8 +34,11 @@ public async Task<ClaimsSnapshot> GetClaimsAsync(TenantKey tenant, UserKey userK
3234

3335
builder.Add(UAuthConstants.Claims.Tenant, tenant.Value);
3436

35-
foreach (var role in roles)
37+
foreach (var role in roles.Where(x => !x.IsDeleted && x.Tenant == tenant))
38+
{
3639
builder.Add(ClaimTypes.Role, role.Name);
40+
builder.Add(UAuthConstants.Claims.ActorRole, JsonSerializer.Serialize(new AccessActorRole(role.Id.ToString(), role.Name)));
41+
}
3742

3843
foreach (var perm in perms)
3944
builder.Add(UAuthConstants.Claims.Permission, perm.Value);

‎tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/Infrastructure/TestAccessContext.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public static AccessContext WithAction(string action)
1414
isAuthenticated: false,
1515
isSystemActor: false,
1616
actorChainId: null,
17+
actorRoles: Array.Empty<AccessActorRole>(),
1718
resource: "test",
1819
targetUserKey: null,
1920
resourceTenant: TenantKey.Single,
@@ -37,6 +38,7 @@ public static AccessContext ForUser(
3738
isAuthenticated: true,
3839
isSystemActor: false,
3940
actorChainId: actorChainId,
41+
actorRoles: Array.Empty<AccessActorRole>(),
4042
resource: resource,
4143
targetUserKey: userKey,
4244
resourceTenant: t,
@@ -61,6 +63,7 @@ public static AccessContext ForTargetUser(
6163
isAuthenticated: true,
6264
isSystemActor: false,
6365
actorChainId: actorChainId,
66+
actorRoles: Array.Empty<AccessActorRole>(),
6467
resource: resource,
6568
targetUserKey: targetUserKey,
6669
resourceTenant: t,
@@ -83,6 +86,7 @@ public static AccessContext ForUserCreation(
8386
isAuthenticated: true,
8487
isSystemActor: false,
8588
actorChainId: actorChainId,
89+
actorRoles: Array.Empty<AccessActorRole>(),
8690
resource: "users",
8791
targetUserKey: null,
8892
resourceTenant: t,

0 commit comments

Comments
 (0)