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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace CodeBeam.UltimateAuth.Core.Contracts;

/// <summary>
/// Represents a trusted, server-resolved role assigned to the authenticated actor during access evaluation.
/// </summary>
public sealed record AccessActorRole(string Id, string Name);
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public sealed class AccessContext
public bool IsAuthenticated { get; init; }
public bool IsSystemActor { get; init; }
public SessionChainId? ActorChainId { get; }
public IReadOnlyList<AccessActorRole> ActorRoles { get; }

// Target
public string? Resource { get; init; }
Expand Down Expand Up @@ -45,6 +46,7 @@ internal AccessContext(
bool isAuthenticated,
bool isSystemActor,
SessionChainId? actorChainId,
IReadOnlyList<AccessActorRole> actorRoles,
string? resource,
UserKey? targetUserKey,
TenantKey resourceTenant,
Expand All @@ -56,6 +58,7 @@ internal AccessContext(
IsAuthenticated = isAuthenticated;
IsSystemActor = isSystemActor;
ActorChainId = actorChainId;
ActorRoles = Array.AsReadOnly(actorRoles.ToArray());

Resource = resource;
TargetUserKey = targetUserKey;
Expand All @@ -78,6 +81,7 @@ public AccessContext WithAttribute(string key, object value)
IsAuthenticated,
IsSystemActor,
ActorChainId,
ActorRoles,
Resource,
TargetUserKey,
ResourceTenant,
Expand Down
1 change: 1 addition & 0 deletions src/CodeBeam.UltimateAuth.Core/Defaults/UAuthConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public static class Claims
{
public const string Tenant = "uauth:tenant";
public const string Permission = "uauth:permission";
public const string ActorRole = "uauth:actor_role";
}

public static class HttpItems
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using CodeBeam.UltimateAuth.Authorization;
using CodeBeam.UltimateAuth.Authorization.Contracts;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.MultiTenancy;

namespace CodeBeam.UltimateAuth.Server.Auth;

internal sealed class AccessActorRoleResolver(IUserRoleStoreFactory userRoleFactory, IRoleStoreFactory roleFactory)
{
public async Task<ResolvedActorRoles> ResolveAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default)
{
var userRoleStore = userRoleFactory.Create(tenant);
var assignments = await userRoleStore.GetAssignmentsAsync(userKey, ct);
var roleIds = assignments.Select(x => x.RoleId).Distinct().ToArray();

if (roleIds.Length == 0)
return new ResolvedActorRoles([], roleIds);

var roleStore = roleFactory.Create(tenant);

var roles = await roleStore.GetByIdsAsync(roleIds, ct);

var activeRoles = roles
.Where(x => !x.IsDeleted && x.Tenant == tenant)
.ToArray();

var actorRoles = activeRoles
.Select(x => new AccessActorRole(x.Id.ToString(), x.Name))
.ToArray();

var activeRoleIds = activeRoles
.Select(x => x.Id)
.ToArray();

return new ResolvedActorRoles(actorRoles, activeRoleIds);
}
}

internal sealed record ResolvedActorRoles(
IReadOnlyList<AccessActorRole> ActorRoles,
IReadOnlyCollection<RoleId> RoleIds);
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ namespace CodeBeam.UltimateAuth.Server.Auth;

internal sealed class AccessContextFactory : IAccessContextFactory
{
private readonly IUserRoleStoreFactory _userRoleFactory;
private readonly AccessActorRoleResolver _roleResolver;
private readonly IUserIdConverterResolver _converterResolver;

public AccessContextFactory(IUserRoleStoreFactory userRoleFactory, IUserIdConverterResolver converterResolver)
public AccessContextFactory(AccessActorRoleResolver roleResolver, IUserIdConverterResolver converterResolver)
{
_userRoleFactory = userRoleFactory;
_roleResolver = roleResolver;
_converterResolver = converterResolver;
}

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

IReadOnlyList<AccessActorRole> actorRoles = Array.Empty<AccessActorRole>();

var attrs = attributes != null
? new Dictionary<string, object>(attributes)
: new Dictionary<string, object>();

attrs.Remove("roles");

if (authFlow.IsAuthenticated && authFlow.UserKey is not null)
{
var userRoleStore = _userRoleFactory.Create(authFlow.Tenant);
var assignments = await userRoleStore.GetAssignmentsAsync(authFlow.UserKey.Value, ct);
var roleIds = assignments.Select(x => x.RoleId).ToArray();

attrs["roles"] = roleIds;
var resolved = await _roleResolver.ResolveAsync(authFlow.Tenant, authFlow.UserKey.Value, ct);
actorRoles = resolved.ActorRoles;
attrs["roles"] = resolved.RoleIds.ToArray();
}

UserKey? targetUserKey = null;
Expand All @@ -71,6 +73,7 @@ private async Task<AccessContext> CreateInternalAsync(AuthFlowContext authFlow,
isAuthenticated: authFlow.IsAuthenticated,
isSystemActor: authFlow.Tenant.IsSystem,
actorChainId: authFlow.Session?.ChainId,
actorRoles: actorRoles,
resource: resource,
targetUserKey: targetUserKey,
resourceTenant: resourceTenant,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,102 @@
using CodeBeam.UltimateAuth.Core;
using CodeBeam.UltimateAuth.Authorization.Contracts;
using CodeBeam.UltimateAuth.Core;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Defaults;
using CodeBeam.UltimateAuth.Core.Runtime;
using CodeBeam.UltimateAuth.Server.Auth;
using CodeBeam.UltimateAuth.Server.Extensions;
using CodeBeam.UltimateAuth.Server.Runtime;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.ObjectModel;
using System.Text.Json;

namespace CodeBeam.UltimateAuth.Server.Authorization;

internal static class ResourceAccessContextBuilder
{
public static AccessContext Create(HttpContext http, string action)
public static async Task<AccessContext> CreateAsync(HttpContext http, string action, CancellationToken ct = default)
{
var user = http.RequestServices.GetRequiredService<ICurrentUser>();

var tenant = http.GetTenant();

IReadOnlyList<AccessActorRole> actorRoles = [];
IReadOnlyDictionary<string, object> attributes = EmptyAttributes.Instance;

if (user.IsAuthenticated)
{
RoleId[] roleIds;

var runtime = http.RequestServices.GetRequiredService<IUAuthRuntimeMarker>();

if (runtime is ResourceRuntimeMarker)
{
// Remote session validation has already produced
// the authenticated principal.
var identity = http.User.Identities.SingleOrDefault(x => x.IsAuthenticated && x.AuthenticationType == UAuthConstants.SchemeDefaults.GlobalScheme);

if (identity is null)
throw new InvalidOperationException("Trusted UltimateAuth identity is missing.");

var roles = new List<AccessActorRole>();
var ids = new List<RoleId>();

foreach (var claim in identity.FindAll(UAuthConstants.Claims.ActorRole))
{
AccessActorRole? role;

try
{
role = JsonSerializer.Deserialize<AccessActorRole>(claim.Value);
}
catch (JsonException)
{
throw new InvalidOperationException("Invalid actor role claim.");
}

if (role is null ||
string.IsNullOrWhiteSpace(role.Name) ||
!RoleId.TryParse(role.Id, null, out var roleId))
{
throw new InvalidOperationException("Invalid actor role claim.");
}

roles.Add(role);
ids.Add(roleId);
}

actorRoles = roles;
roleIds = ids.ToArray();
}
else
{
var resolver = http.RequestServices.GetRequiredService<AccessActorRoleResolver>();
var resolved = await resolver.ResolveAsync(tenant, user.UserKey, ct);

actorRoles = resolved.ActorRoles;
roleIds = resolved.RoleIds.ToArray();
}

attributes = new ReadOnlyDictionary<string, object>(
new Dictionary<string, object>
{
["roles"] = roleIds
});
}

return new AccessContext(
actorUserKey: user.IsAuthenticated ? user.UserKey : null,
actorTenant: http.GetTenant(),
actorTenant: tenant,
isAuthenticated: user.IsAuthenticated,
isSystemActor: false,
actorChainId: null,

actorRoles: actorRoles,
resource: ResolveResource(action),
targetUserKey: null,
resourceTenant: http.GetTenant(),

resourceTenant: tenant,
action: action,
attributes: EmptyAttributes.Instance
);
attributes: attributes);
}

private static string ResolveResource(string action)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public UAuthAuthorizationHandler(
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, UAuthActionRequirement requirement)
{
var http = _httpContextAccessor.HttpContext!;
var accessContext = ResourceAccessContextBuilder.Create(http, requirement.Action);
var accessContext = await ResourceAccessContextBuilder.CreateAsync(http, requirement.Action, http.RequestAborted);

try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol
services.TryAddSingleton<IEffectiveAuthModeResolver, EffectiveAuthModeResolver>();
services.TryAddSingleton<IPrimaryTokenResolver, PrimaryTokenResolver>();
services.TryAddSingleton<IAuthRedirectResolver, AuthRedirectResolver>();
services.TryAddScoped<AccessActorRoleResolver>();

services.TryAddScoped<ISessionTouchService, SessionTouchService>();
services.TryAddScoped<ISessionQueryService, UAuthSessionQueryService>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using CodeBeam.UltimateAuth.Core;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Defaults;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
using System.Security.Claims;
using System.Text.Json;

namespace CodeBeam.UltimateAuth.Authorization;

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

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

foreach (var role in roles)
foreach (var role in roles.Where(x => !x.IsDeleted && x.Tenant == tenant))
{
builder.Add(ClaimTypes.Role, role.Name);
builder.Add(UAuthConstants.Claims.ActorRole, JsonSerializer.Serialize(new AccessActorRole(role.Id.ToString(), role.Name)));
}

foreach (var perm in perms)
builder.Add(UAuthConstants.Claims.Permission, perm.Value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static AccessContext WithAction(string action)
isAuthenticated: false,
isSystemActor: false,
actorChainId: null,
actorRoles: Array.Empty<AccessActorRole>(),
resource: "test",
targetUserKey: null,
resourceTenant: TenantKey.Single,
Expand All @@ -37,6 +38,7 @@ public static AccessContext ForUser(
isAuthenticated: true,
isSystemActor: false,
actorChainId: actorChainId,
actorRoles: Array.Empty<AccessActorRole>(),
resource: resource,
targetUserKey: userKey,
resourceTenant: t,
Expand All @@ -61,6 +63,7 @@ public static AccessContext ForTargetUser(
isAuthenticated: true,
isSystemActor: false,
actorChainId: actorChainId,
actorRoles: Array.Empty<AccessActorRole>(),
resource: resource,
targetUserKey: targetUserKey,
resourceTenant: t,
Expand All @@ -83,6 +86,7 @@ public static AccessContext ForUserCreation(
isAuthenticated: true,
isSystemActor: false,
actorChainId: actorChainId,
actorRoles: Array.Empty<AccessActorRole>(),
resource: "users",
targetUserKey: null,
resourceTenant: t,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static AccessContext WithAction(string action)
isAuthenticated: false,
isSystemActor: false,
actorChainId: null,
actorRoles: Array.Empty<AccessActorRole>(),
resource: "test",
targetUserKey: null,
resourceTenant: TenantKey.Single,
Expand All @@ -37,6 +38,7 @@ public static AccessContext ForUser(
isAuthenticated: true,
isSystemActor: false,
actorChainId: actorChainId,
actorRoles: Array.Empty<AccessActorRole>(),
resource: resource,
targetUserKey: userKey,
resourceTenant: t,
Expand All @@ -61,6 +63,7 @@ public static AccessContext ForTargetUser(
isAuthenticated: true,
isSystemActor: false,
actorChainId: actorChainId,
actorRoles: Array.Empty<AccessActorRole>(),
resource: resource,
targetUserKey: targetUserKey,
resourceTenant: t,
Expand All @@ -83,6 +86,7 @@ public static AccessContext ForUserCreation(
isAuthenticated: true,
isSystemActor: false,
actorChainId: actorChainId,
actorRoles: Array.Empty<AccessActorRole>(),
resource: "users",
targetUserKey: null,
resourceTenant: t,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ await profileFactory
isAuthenticated: true,
isSystemActor: false,
actorChainId: null,
actorRoles: Array.Empty<AccessActorRole>(),
resource: "users",
targetUserKey: actor,
resourceTenant: TenantKeys.Single,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static AccessContext WithAction(string action)
isAuthenticated: false,
isSystemActor: false,
actorChainId: null,
actorRoles: Array.Empty<AccessActorRole>(),
resource: "test",
targetUserKey: null,
resourceTenant: TenantKey.Single,
Expand All @@ -37,6 +38,7 @@ public static AccessContext ForUser(
isAuthenticated: true,
isSystemActor: false,
actorChainId: actorChainId,
actorRoles: Array.Empty<AccessActorRole>(),
resource: resource,
targetUserKey: userKey,
resourceTenant: t,
Expand All @@ -61,6 +63,7 @@ public static AccessContext ForTargetUser(
isAuthenticated: true,
isSystemActor: false,
actorChainId: actorChainId,
actorRoles: Array.Empty<AccessActorRole>(),
resource: resource,
targetUserKey: targetUserKey,
resourceTenant: t,
Expand Down
Loading
Loading