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
Expand Up @@ -11,25 +11,25 @@ public bool AppliesTo(AccessContext context)
"authorization.roles.",
StringComparison.Ordinal);

public AccessDecision Decide(AccessContext context)
public ValueTask<AccessDecision> DecideAsync(AccessContext context, CancellationToken cancellationToken = default)
{
// Trusted internal operations.
if (context.IsSystemActor)
return AccessDecision.Allow();
return ValueTask.FromResult(AccessDecision.Allow());

if (!context.IsAuthenticated ||
context.ActorUserKey is null)
{
return AccessDecision.Deny("lab_authentication_required");
return ValueTask.FromResult(AccessDecision.Deny("lab_authentication_required"));
}

// Ladmin has unrestricted application-level access.
if (rootResolver.IsLadmin(context))
return AccessDecision.Allow();
return ValueTask.FromResult(AccessDecision.Allow());

// Role reading is available to authenticated users.
if (IsReadAction(context.Action))
return AccessDecision.Allow();
return ValueTask.FromResult(AccessDecision.Allow());

// TODO (v0.2.0):
// Support sandbox-scoped role assignment and removal.
Expand All @@ -43,7 +43,7 @@ public AccessDecision Decide(AccessContext context)
// operation metadata supplied by UltimateAuth.
// - Enforce all checks through IAccessAuthority.

return AccessDecision.Deny("lab_role_management_restricted");
return ValueTask.FromResult(AccessDecision.Deny("lab_role_management_restricted"));
}

private static bool IsReadAction(string action)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ public sealed class SandboxUAuthAccessPolicy(IDbContextFactory<LabDbContext> dbF
public bool AppliesTo(AccessContext context)
=> IsProtectedAction(context.Action);

public AccessDecision Decide(AccessContext context)
public async ValueTask<AccessDecision> DecideAsync(
AccessContext context,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

var action = context.Action;

// Authentication recovery and registration require
Expand All @@ -41,9 +45,10 @@ public AccessDecision Decide(AccessContext context)
if (rootResolver.IsLadmin(context))
return AccessDecision.Allow();

using var db = dbFactory.CreateDbContext();
await using var db = await dbFactory.CreateDbContextAsync(
cancellationToken);

var actor = db.SandboxMembers
var actor = await db.SandboxMembers
.AsNoTracking()
.Where(x => x.UserKey == actorKey)
.Select(x => new
Expand All @@ -52,7 +57,7 @@ public AccessDecision Decide(AccessContext context)
x.Role,
x.Sandbox.Status
})
.SingleOrDefault();
.SingleOrDefaultAsync(cancellationToken);

if (actor is null)
return AccessDecision.Deny("sandbox_membership_required");
Expand Down Expand Up @@ -84,11 +89,12 @@ public AccessDecision Decide(AccessContext context)
if (context.TargetUserKey is not UserKey targetKey)
return AccessDecision.Deny("sandbox_target_required");

var belongsToSandbox = db.SandboxMembers
var belongsToSandbox = await db.SandboxMembers
.AsNoTracking()
.Any(x =>
x.UserKey == targetKey &&
x.SandboxId == actor.SandboxId);
.AnyAsync(
x => x.UserKey == targetKey &&
x.SandboxId == actor.SandboxId,
cancellationToken);

if (!belongsToSandbox)
return AccessDecision.Deny("sandbox_target_out_of_scope");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,14 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions;

public interface IAccessAuthority
{
AccessDecision Decide(AccessContext context, IEnumerable<IAccessPolicy> runtimePolicies);
[Obsolete("Synchronous authority evaluation is no longer supported. Use DecideAsync instead.")]
AccessDecision Decide(AccessContext context, IEnumerable<IAccessPolicy> runtimePolicies)
=> throw new NotSupportedException("Synchronous authority evaluation is not supported. Use DecideAsync.");

ValueTask<AccessDecision> DecideAsync(AccessContext context, IEnumerable<IAccessPolicy> runtimePolicies, CancellationToken cancellationToken = default)
{
#pragma warning disable CS0618
return ValueTask.FromResult(Decide(context, runtimePolicies));
#pragma warning restore CS0618
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,14 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions;

public interface IAccessInvariant
{
AccessDecision Decide(AccessContext context);
[Obsolete("Implement DecideAsync instead.")]
AccessDecision Decide(AccessContext context)
=> throw new NotSupportedException("Implement DecideAsync for asynchronous invariant evaluation.");

ValueTask<AccessDecision> DecideAsync(AccessContext context, CancellationToken cancellationToken = default)
{
#pragma warning disable CS0618
return ValueTask.FromResult(Decide(context));
#pragma warning restore CS0618
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,15 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions;
public interface IAccessPolicy
{
bool AppliesTo(AccessContext context);
AccessDecision Decide(AccessContext context);

[Obsolete("Implement DecideAsync instead.")]
AccessDecision Decide(AccessContext context)
=> throw new NotSupportedException("Implement DecideAsync for asynchronous policy evaluation.");

ValueTask<AccessDecision> DecideAsync(AccessContext context, CancellationToken cancellationToken = default)
{
#pragma warning disable CS0618
return ValueTask.FromResult(Decide(context));
#pragma warning restore CS0618
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public async Task ExecuteAsync(AccessContext context, IAccessCommand command, Ca
context = EnrichFromClaims(context);

var policies = _policyProvider.GetPolicies(context);
var decision = _authority.Decide(context, policies);
var decision = await _authority.DecideAsync(context, policies, ct);

if (decision.RequiresReauthentication)
throw new UAuthChallengeRequiredException();
Expand All @@ -50,7 +50,7 @@ public async Task<TResult> ExecuteAsync<TResult>(AccessContext context, IAccessC
context = EnrichFromClaims(context);

var policies = _policyProvider.GetPolicies(context);
var decision = _authority.Decide(context, policies);
var decision = await _authority.DecideAsync(context, policies, ct);

if (decision.RequiresReauthentication)
throw new UAuthChallengeRequiredException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,43 @@ public UAuthAccessAuthority(IEnumerable<IAccessInvariant> invariants, IEnumerabl
_globalPolicies = globalPolicies ?? Array.Empty<IAccessPolicy>();
}

public AccessDecision Decide(AccessContext context, IEnumerable<IAccessPolicy> runtimePolicies)
public async ValueTask<AccessDecision> DecideAsync(AccessContext context, IEnumerable<IAccessPolicy> runtimePolicies, CancellationToken ct = default)
{
foreach (var invariant in _invariants)
{
var result = invariant.Decide(context);
ct.ThrowIfCancellationRequested();

var result = await invariant.DecideAsync(context, ct);

if (!result.IsAllowed)
return result;
}

foreach (var policy in _globalPolicies)
{
ct.ThrowIfCancellationRequested();

if (!policy.AppliesTo(context))
continue;

var result = policy.Decide(context);
var result = await policy.DecideAsync(context, ct);

if (!result.IsAllowed)
return result;

// Allow here means "no objection", NOT permission
// Allow here means "no objection", NOT permission.
}

bool requiresReauth = false;

foreach (var policy in runtimePolicies)
{
ct.ThrowIfCancellationRequested();

if (!policy.AppliesTo(context))
continue;

var result = policy.Decide(context);
var result = await policy.DecideAsync(context, ct);

if (result.IsDenied)
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public async Task ExecuteAsync(AccessContext context, IAccessCommand command, Ca
context = await EnrichAsync(context, ct);

var policies = _policyProvider.GetPolicies(context);
var decision = _authority.Decide(context, policies);
var decision = await _authority.DecideAsync(context, policies, ct);

if (decision.RequiresReauthentication)
throw new UAuthChallengeRequiredException(decision.DenyReason ?? "Requires reauthentication.");
Expand All @@ -46,7 +46,7 @@ public async Task<TResult> ExecuteAsync<TResult>(AccessContext context, IAccessC
context = await EnrichAsync(context, ct);

var policies = _policyProvider.GetPolicies(context);
var decision = _authority.Decide(context, policies);
var decision = await _authority.DecideAsync(context, policies, ct);

if (decision.RequiresReauthentication)
throw new UAuthChallengeRequiredException(decision.DenyReason ?? "Requires reauthentication.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ public ConditionalAccessPolicy(Func<AccessContext, bool> condition, bool expecte

public bool AppliesTo(AccessContext context) => _condition(context) == _expected;

public AccessDecision Decide(AccessContext context) => _inner.Decide(context);
public ValueTask<AccessDecision> DecideAsync(AccessContext context, CancellationToken ct = default)
=> _inner.DecideAsync(context, ct);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ namespace CodeBeam.UltimateAuth.Policies;

internal sealed class DenyAdminSelfModificationPolicy : IAccessPolicy
{
public AccessDecision Decide(AccessContext context)
public ValueTask<AccessDecision> DecideAsync(AccessContext context, CancellationToken ct = default)
{
if (!context.IsAuthenticated)
return AccessDecision.Deny("unauthenticated");
return ValueTask.FromResult(AccessDecision.Deny("unauthenticated"));

if (context.ActorUserKey == context.TargetUserKey)
return AccessDecision.Deny("admin_cannot_modify_own_account");
return ValueTask.FromResult(AccessDecision.Deny("admin_cannot_modify_own_account"));

return AccessDecision.Allow();
return ValueTask.FromResult(AccessDecision.Allow());
}

public bool AppliesTo(AccessContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ namespace CodeBeam.UltimateAuth.Policies;

internal sealed class DenyCrossTenantPolicy : IAccessPolicy
{
public AccessDecision Decide(AccessContext context)
public ValueTask<AccessDecision> DecideAsync(AccessContext context, CancellationToken ct = default)
{
return context.IsCrossTenant
? AccessDecision.Deny("cross_tenant_access_denied")
: AccessDecision.Allow();
? ValueTask.FromResult(AccessDecision.Deny("cross_tenant_access_denied"))
: ValueTask.FromResult(AccessDecision.Allow());
}

public bool AppliesTo(AccessContext context) => true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ namespace CodeBeam.UltimateAuth.Authorization.Policies;

public sealed class MustHavePermissionPolicy : IAccessPolicy
{
public AccessDecision Decide(AccessContext context)
public ValueTask<AccessDecision> DecideAsync(AccessContext context, CancellationToken ct = default)
{
if (context.Attributes.TryGetValue(UAuthConstants.Access.Permissions, out var value) && value is CompiledPermissionSet permissions)
{
if (permissions.IsAllowed(context.Action))
return AccessDecision.Allow();
return ValueTask.FromResult(AccessDecision.Allow());
}

return AccessDecision.Deny("missing_permission");
return ValueTask.FromResult(AccessDecision.Deny("missing_permission"));
}

public bool AppliesTo(AccessContext context)
{
return context.Action.EndsWith(".admin", StringComparison.OrdinalIgnoreCase);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ public RequireActiveUserPolicy(IUserRuntimeStateProvider runtime)
_runtime = runtime;
}

public AccessDecision Decide(AccessContext context)
public async ValueTask<AccessDecision> DecideAsync(AccessContext context, CancellationToken ct = default)
{
if (context.ActorUserKey is null)
return AccessDecision.Deny("missing_actor");

var state = _runtime.GetAsync(context.ActorTenant, context.ActorUserKey!.Value).GetAwaiter().GetResult();
ct.ThrowIfCancellationRequested();

if (state == null || !state.Exists || state.IsDeleted)
var state = await _runtime.GetAsync(context.ActorTenant, context.ActorUserKey.Value, ct);

if (state is null || !state.Exists || state.IsDeleted)
return AccessDecision.Deny("user_not_found");

return state.IsActive
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ namespace CodeBeam.UltimateAuth.Policies;

internal sealed class RequireAuthenticatedPolicy : IAccessPolicy
{
public AccessDecision Decide(AccessContext context)
public ValueTask<AccessDecision> DecideAsync(AccessContext context, CancellationToken ct = default)
{
return context.IsAuthenticated
? AccessDecision.Allow()
: AccessDecision.Deny("unauthenticated");
? ValueTask.FromResult(AccessDecision.Allow())
: ValueTask.FromResult(AccessDecision.Deny("unauthenticated"));
}

public bool AppliesTo(AccessContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ namespace CodeBeam.UltimateAuth.Policies;

internal sealed class RequireSelfPolicy : IAccessPolicy
{
public AccessDecision Decide(AccessContext context)
public ValueTask<AccessDecision> DecideAsync(AccessContext context, CancellationToken ct = default)
{
if (!context.IsAuthenticated)
return AccessDecision.Deny("unauthenticated");
return ValueTask.FromResult(AccessDecision.Deny("unauthenticated"));

return context.IsSelfAction
? AccessDecision.Allow()
: AccessDecision.Deny("not_self");
? ValueTask.FromResult(AccessDecision.Allow())
: ValueTask.FromResult(AccessDecision.Deny("not_self"));
}

public bool AppliesTo(AccessContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ namespace CodeBeam.UltimateAuth.Policies;

internal sealed class RequireSystemPolicy : IAccessPolicy
{
public AccessDecision Decide(AccessContext context)
public ValueTask<AccessDecision> DecideAsync(AccessContext context, CancellationToken ct = default)
=> context.IsSystemActor
? AccessDecision.Allow()
: AccessDecision.Deny("system_actor_required");
? ValueTask.FromResult(AccessDecision.Allow())
: ValueTask.FromResult(AccessDecision.Deny("system_actor_required"));

public bool AppliesTo(AccessContext context) => context.Action.EndsWith(".system", StringComparison.Ordinal);
}
Loading
Loading