diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/LabApplicationPolicy.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/LabApplicationPolicy.cs index 29b9edcc..e337d5c9 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/LabApplicationPolicy.cs +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/LabApplicationPolicy.cs @@ -11,25 +11,25 @@ public bool AppliesTo(AccessContext context) "authorization.roles.", StringComparison.Ordinal); - public AccessDecision Decide(AccessContext context) + public ValueTask 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. @@ -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) diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs index b698bea8..b2f6c8a6 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Policy/SandboxUAuthAccessPolicy.cs @@ -14,8 +14,12 @@ public sealed class SandboxUAuthAccessPolicy(IDbContextFactory dbF public bool AppliesTo(AccessContext context) => IsProtectedAction(context.Action); - public AccessDecision Decide(AccessContext context) + public async ValueTask DecideAsync( + AccessContext context, + CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + var action = context.Action; // Authentication recovery and registration require @@ -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 @@ -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"); @@ -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"); diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessAuthority.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessAuthority.cs index a5b3f69c..6c596943 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessAuthority.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessAuthority.cs @@ -4,5 +4,14 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions; public interface IAccessAuthority { - AccessDecision Decide(AccessContext context, IEnumerable runtimePolicies); + [Obsolete("Synchronous authority evaluation is no longer supported. Use DecideAsync instead.")] + AccessDecision Decide(AccessContext context, IEnumerable runtimePolicies) + => throw new NotSupportedException("Synchronous authority evaluation is not supported. Use DecideAsync."); + + ValueTask DecideAsync(AccessContext context, IEnumerable runtimePolicies, CancellationToken cancellationToken = default) + { +#pragma warning disable CS0618 + return ValueTask.FromResult(Decide(context, runtimePolicies)); +#pragma warning restore CS0618 + } } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessInvariant.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessInvariant.cs index c043d44d..0246791c 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessInvariant.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessInvariant.cs @@ -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 DecideAsync(AccessContext context, CancellationToken cancellationToken = default) + { +#pragma warning disable CS0618 + return ValueTask.FromResult(Decide(context)); +#pragma warning restore CS0618 + } } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessPolicy.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessPolicy.cs index 49a1efad..86d477db 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessPolicy.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessPolicy.cs @@ -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 DecideAsync(AccessContext context, CancellationToken cancellationToken = default) + { +#pragma warning disable CS0618 + return ValueTask.FromResult(Decide(context)); +#pragma warning restore CS0618 + } } diff --git a/src/CodeBeam.UltimateAuth.Server/Authorization/AspNetCore/UAuthResourceAccessOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Authorization/AspNetCore/UAuthResourceAccessOrchestrator.cs index b7451e38..53cfeaec 100644 --- a/src/CodeBeam.UltimateAuth.Server/Authorization/AspNetCore/UAuthResourceAccessOrchestrator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Authorization/AspNetCore/UAuthResourceAccessOrchestrator.cs @@ -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(); @@ -50,7 +50,7 @@ public async Task ExecuteAsync(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(); diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessAuthority.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessAuthority.cs index c8a3b6ff..31223b52 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessAuthority.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessAuthority.cs @@ -14,36 +14,43 @@ public UAuthAccessAuthority(IEnumerable invariants, IEnumerabl _globalPolicies = globalPolicies ?? Array.Empty(); } - public AccessDecision Decide(AccessContext context, IEnumerable runtimePolicies) + public async ValueTask DecideAsync(AccessContext context, IEnumerable 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; diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessOrchestrator.cs index d96af659..4eb6ef3c 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessOrchestrator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessOrchestrator.cs @@ -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."); @@ -46,7 +46,7 @@ public async Task ExecuteAsync(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."); diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/ConditionalAccessPolicy.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/ConditionalAccessPolicy.cs index eb7e33bc..bd4100aa 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/ConditionalAccessPolicy.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/ConditionalAccessPolicy.cs @@ -18,5 +18,6 @@ public ConditionalAccessPolicy(Func condition, bool expecte public bool AppliesTo(AccessContext context) => _condition(context) == _expected; - public AccessDecision Decide(AccessContext context) => _inner.Decide(context); + public ValueTask DecideAsync(AccessContext context, CancellationToken ct = default) + => _inner.DecideAsync(context, ct); } diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/DenyAdminSelfModificationPolicy.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/DenyAdminSelfModificationPolicy.cs index d2f682ee..6a6731c8 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/DenyAdminSelfModificationPolicy.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/DenyAdminSelfModificationPolicy.cs @@ -5,15 +5,15 @@ namespace CodeBeam.UltimateAuth.Policies; internal sealed class DenyAdminSelfModificationPolicy : IAccessPolicy { - public AccessDecision Decide(AccessContext context) + public ValueTask 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) diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/DenyCrossTenantPolicy.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/DenyCrossTenantPolicy.cs index b3a987b8..c6194f6e 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/DenyCrossTenantPolicy.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/DenyCrossTenantPolicy.cs @@ -5,11 +5,11 @@ namespace CodeBeam.UltimateAuth.Policies; internal sealed class DenyCrossTenantPolicy : IAccessPolicy { - public AccessDecision Decide(AccessContext context) + public ValueTask 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; diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/MustHavePermissionPolicy.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/MustHavePermissionPolicy.cs index 0e5814ec..db8ced27 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/MustHavePermissionPolicy.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/MustHavePermissionPolicy.cs @@ -7,19 +7,19 @@ namespace CodeBeam.UltimateAuth.Authorization.Policies; public sealed class MustHavePermissionPolicy : IAccessPolicy { - public AccessDecision Decide(AccessContext context) + public ValueTask 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); } -} \ No newline at end of file +} diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireActiveUserPolicy.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireActiveUserPolicy.cs index ba1cbd35..865f7240 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireActiveUserPolicy.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireActiveUserPolicy.cs @@ -13,14 +13,16 @@ public RequireActiveUserPolicy(IUserRuntimeStateProvider runtime) _runtime = runtime; } - public AccessDecision Decide(AccessContext context) + public async ValueTask 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 diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireAuthenticatedPolicy.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireAuthenticatedPolicy.cs index fb26bb6a..19f79aea 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireAuthenticatedPolicy.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireAuthenticatedPolicy.cs @@ -5,11 +5,11 @@ namespace CodeBeam.UltimateAuth.Policies; internal sealed class RequireAuthenticatedPolicy : IAccessPolicy { - public AccessDecision Decide(AccessContext context) + public ValueTask 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) diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireSelfPolicy.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireSelfPolicy.cs index 595fde6b..f38fa487 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireSelfPolicy.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireSelfPolicy.cs @@ -5,14 +5,14 @@ namespace CodeBeam.UltimateAuth.Policies; internal sealed class RequireSelfPolicy : IAccessPolicy { - public AccessDecision Decide(AccessContext context) + public ValueTask 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) diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireSystemPolicy.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireSystemPolicy.cs index 8f2740ab..7305b84b 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireSystemPolicy.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireSystemPolicy.cs @@ -5,10 +5,10 @@ namespace CodeBeam.UltimateAuth.Policies; internal sealed class RequireSystemPolicy : IAccessPolicy { - public AccessDecision Decide(AccessContext context) + public ValueTask 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); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiAuthorizationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiAuthorizationTests.cs index 520ef517..afc84bd6 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiAuthorizationTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiAuthorizationTests.cs @@ -105,9 +105,10 @@ await CreateHostAsync( .Be(HttpStatusCode.OK); authority.Verify( - x => x.Decide( + x => x.DecideAsync( It.IsAny(), - It.IsAny>()), + It.IsAny>(), + It.IsAny()), Times.Once); } @@ -145,12 +146,14 @@ public async Task ProductsReadPolicy_ShouldBuildExpectedAccessContext() var authority = new Mock(); authority - .Setup(x => x.Decide( + .Setup(x => x.DecideAsync( It.IsAny(), - It.IsAny>())) - .Callback>( - (context, _) => captured = context) - .Returns(CreateAllowedDecision()); + It.IsAny>(), + It.IsAny())) + .Callback, CancellationToken>( + (context, _, _) => captured = context) + .Returns(new ValueTask( + CreateAllowedDecision())); var policyProvider = CreatePolicyProvider(); @@ -201,12 +204,14 @@ public async Task PolicyAuthorization_ShouldEnrichAccessContextWithPermissionsFr var authority = new Mock(); authority - .Setup(x => x.Decide( - It.IsAny(), - It.IsAny>())) - .Callback>( - (context, _) => captured = context) - .Returns(CreateAllowedDecision()); + .Setup(x => x.DecideAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, CancellationToken>( + (context, _, _) => captured = context) + .Returns(new ValueTask( + CreateAllowedDecision())); var policyProvider = CreatePolicyProvider(); @@ -288,12 +293,14 @@ public async Task ProductsUpdatePolicy_ShouldPreserveFullActionAndResolveProduct var authority = new Mock(); authority - .Setup(x => x.Decide( + .Setup(x => x.DecideAsync( It.IsAny(), - It.IsAny>())) - .Callback>( - (context, _) => captured = context) - .Returns(CreateAllowedDecision()); + It.IsAny>(), + It.IsAny())) + .Callback, CancellationToken>( + (context, _, _) => captured = context) + .Returns(new ValueTask( + CreateAllowedDecision())); var policyProvider = CreatePolicyProvider(); @@ -332,10 +339,12 @@ public async Task PolicyEndpoint_WhenReauthenticationRequired_ShouldReturn401Pro var authority = new Mock(); authority - .Setup(x => x.Decide( + .Setup(x => x.DecideAsync( It.IsAny(), - It.IsAny>())) - .Returns(AccessDecision.ReauthenticationRequired()); + It.IsAny>(), + It.IsAny())) + .Returns(new ValueTask( + AccessDecision.ReauthenticationRequired())); var policyProvider = CreatePolicyProvider(); @@ -363,9 +372,10 @@ public async Task PolicyEndpoint_WhenReauthenticationRequired_ShouldReturn401Pro problem.Type.Should().EndWith("/challenge_required"); authority.Verify( - x => x.Decide( + x => x.DecideAsync( It.IsAny(), - It.IsAny>()), + It.IsAny>(), + It.IsAny()), Times.Once); } @@ -404,10 +414,12 @@ private static Mock CreateAllowAuthority() var authority = new Mock(); authority - .Setup(x => x.Decide( + .Setup(x => x.DecideAsync( It.IsAny(), - It.IsAny>())) - .Returns(CreateAllowedDecision()); + It.IsAny>(), + It.IsAny())) + .Returns(new ValueTask( + CreateAllowedDecision())); return authority; } @@ -417,10 +429,12 @@ private static Mock CreateDenyAuthority() var authority = new Mock(); authority - .Setup(x => x.Decide( + .Setup(x => x.DecideAsync( It.IsAny(), - It.IsAny>())) - .Returns(CreateDeniedDecision()); + It.IsAny>(), + It.IsAny())) + .Returns(new ValueTask( + CreateDeniedDecision())); return authority; } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/AccessPoliciesTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/AccessPoliciesTests.cs index 1dc64189..85d71f3e 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/AccessPoliciesTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/AccessPoliciesTests.cs @@ -37,20 +37,20 @@ public void AppliesTo_AnonymousAction_ShouldReturnFalse() } [Fact] - public void Decide_UnauthenticatedActor_ShouldDeny() + public async Task Decide_UnauthenticatedActor_ShouldDeny() { var sut = new RequireAuthenticatedPolicy(); var context = TestAccessContext.WithAction("users.get.self"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("unauthenticated"); } [Fact] - public void Decide_AuthenticatedActor_ShouldAllow() + public async Task Decide_AuthenticatedActor_ShouldAllow() { var sut = new RequireAuthenticatedPolicy(); @@ -58,7 +58,7 @@ public void Decide_AuthenticatedActor_ShouldAllow() UserKey.New(), "users.get.self"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeTrue(); } @@ -78,21 +78,19 @@ public void AppliesTo_ShouldAlwaysReturnTrue() } [Fact] - public void Decide_SameTenant_ShouldAllow() + public async Task Decide_SameTenant_ShouldAllow() { var sut = new DenyCrossTenantPolicy(); - var context = TestAccessContext.ForUser( - UserKey.New(), - "users.get.self"); + var context = TestAccessContext.ForUser(UserKey.New(), "users.get.self"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeTrue(); } [Fact] - public void Decide_CrossTenant_ShouldDeny() + public async Task Decide_CrossTenant_ShouldDeny() { var sut = new DenyCrossTenantPolicy(); @@ -111,7 +109,7 @@ public void Decide_CrossTenant_ShouldDeny() action: "users.get.admin", attributes: EmptyAttributes.Instance); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("cross_tenant_access_denied"); @@ -146,21 +144,20 @@ public void AppliesTo_NonSelfAction_ShouldReturnFalse(string action) } [Fact] - public void Decide_UnauthenticatedActor_ShouldDeny() + public async Task Decide_UnauthenticatedActor_ShouldDeny() { var sut = new RequireSelfPolicy(); - var context = - TestAccessContext.WithAction("users.update.self"); + var context = TestAccessContext.WithAction("users.update.self"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("unauthenticated"); } [Fact] - public void Decide_WhenActorIsTarget_ShouldAllow() + public async Task Decide_WhenActorIsTarget_ShouldAllow() { var sut = new RequireSelfPolicy(); @@ -171,14 +168,14 @@ public void Decide_WhenActorIsTarget_ShouldAllow() userKey, "users.update.self"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeTrue(); result.DenyReason.Should().BeNull(); } [Fact] - public void Decide_WhenActorIsNotTarget_ShouldDeny() + public async Task Decide_WhenActorIsNotTarget_ShouldDeny() { var sut = new RequireSelfPolicy(); @@ -191,7 +188,7 @@ public void Decide_WhenActorIsNotTarget_ShouldDeny() target, "users.update.self"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("not_self"); @@ -219,28 +216,27 @@ public void AppliesTo_NonSystemAction_ShouldReturnFalse(string action) { var sut = new RequireSystemPolicy(); - var context = - TestAccessContext.WithAction(action); + var context = TestAccessContext.WithAction(action); sut.AppliesTo(context).Should().BeFalse(); } [Fact] - public void Decide_NormalActor_ShouldDeny() + public async Task Decide_NormalActor_ShouldDeny() { var sut = new RequireSystemPolicy(); var context = TestAccessContext.WithAction("users.repair.system"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("system_actor_required"); } [Fact] - public void Decide_SystemActor_ShouldAllow() + public async Task Decide_SystemActor_ShouldAllow() { var sut = new RequireSystemPolicy(); @@ -256,7 +252,7 @@ public void Decide_SystemActor_ShouldAllow() action: "users.repair.system", attributes: EmptyAttributes.Instance); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeTrue(); result.DenyReason.Should().BeNull(); @@ -336,21 +332,21 @@ public void AppliesTo_AdminReadAction_ShouldReturnFalse(string action) } [Fact] - public void Decide_UnauthenticatedActor_ShouldDeny() + public async Task Decide_UnauthenticatedActor_ShouldDeny() { var sut = new DenyAdminSelfModificationPolicy(); var context = TestAccessContext.WithAction("users.update.admin"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("unauthenticated"); } [Fact] - public void Decide_AdminModifyingOwnAccount_ShouldDeny() + public async Task Decide_AdminModifyingOwnAccount_ShouldDeny() { var sut = new DenyAdminSelfModificationPolicy(); @@ -362,7 +358,7 @@ public void Decide_AdminModifyingOwnAccount_ShouldDeny() userKey, "users.update.admin"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should() @@ -370,7 +366,7 @@ public void Decide_AdminModifyingOwnAccount_ShouldDeny() } [Fact] - public void Decide_AdminDeletingOwnAccount_ShouldDeny() + public async Task Decide_AdminDeletingOwnAccount_ShouldDeny() { var sut = new DenyAdminSelfModificationPolicy(); @@ -382,7 +378,7 @@ public void Decide_AdminDeletingOwnAccount_ShouldDeny() userKey, "users.delete.admin"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should() @@ -390,7 +386,7 @@ public void Decide_AdminDeletingOwnAccount_ShouldDeny() } [Fact] - public void Decide_AdminModifyingDifferentUser_ShouldAllow() + public async Task Decide_AdminModifyingDifferentUser_ShouldAllow() { var sut = new DenyAdminSelfModificationPolicy(); @@ -403,7 +399,7 @@ public void Decide_AdminModifyingDifferentUser_ShouldAllow() target, "users.update.admin"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeTrue(); result.DenyReason.Should().BeNull(); @@ -501,20 +497,16 @@ public void AppliesTo_ShouldPassContextToCondition() } [Fact] - public void Decide_ShouldDelegateToInnerPolicy() + public async Task Decide_ShouldDelegateToInnerPolicy() { - var inner = new TestPolicy( - AccessDecision.Deny("inner_denied")); + var inner = new TestPolicy(AccessDecision.Deny("inner_denied")); var sut = new ConditionalAccessPolicy( - _ => true, - expected: true, - inner); + _ => true, expected: true, inner); - var context = - TestAccessContext.WithAction("users.get.self"); + var context = TestAccessContext.WithAction("users.get.self"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("inner_denied"); @@ -524,7 +516,7 @@ public void Decide_ShouldDelegateToInnerPolicy() } [Fact] - public void Decide_ShouldReturnInnerAllowDecision() + public async Task Decide_ShouldReturnInnerAllowDecision() { var inner = new TestPolicy( AccessDecision.Allow()); @@ -537,7 +529,7 @@ public void Decide_ShouldReturnInnerAllowDecision() var context = TestAccessContext.WithAction("users.get.self"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeTrue(); inner.DecideCallCount.Should().Be(1); @@ -551,8 +543,7 @@ private sealed class TestPolicy : IAccessPolicy public AccessContext? LastContext { get; private set; } - public TestPolicy() - : this(AccessDecision.Allow()) + public TestPolicy() : this(AccessDecision.Allow()) { } @@ -566,12 +557,12 @@ public bool AppliesTo(AccessContext context) return true; } - public AccessDecision Decide(AccessContext context) + public ValueTask DecideAsync(AccessContext context, CancellationToken ct = default) { DecideCallCount++; LastContext = context; - return _decision; + return ValueTask.FromResult(_decision); } } } @@ -616,32 +607,27 @@ public void AppliesTo_NonAdminAction_ShouldReturnFalse(string action) } [Fact] - public void Decide_WhenPermissionsAttributeIsMissing_ShouldDeny() + public async Task Decide_WhenPermissionsAttributeIsMissing_ShouldDeny() { var sut = new MustHavePermissionPolicy(); var context = TestAccessContext.WithAction("users.update.admin"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("missing_permission"); } [Fact] - public void Decide_WhenPermissionsAttributeHasWrongType_ShouldDeny() + public async Task Decide_WhenPermissionsAttributeHasWrongType_ShouldDeny() { var sut = new MustHavePermissionPolicy(); - var context = - TestAccessContext - .WithAction("users.update.admin") - .WithAttribute( - UAuthConstants.Access.Permissions, - "invalid-permissions"); + var context = TestAccessContext.WithAction("users.update.admin") .WithAttribute(UAuthConstants.Access.Permissions, "invalid-permissions"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("missing_permission"); @@ -649,7 +635,7 @@ public void Decide_WhenPermissionsAttributeHasWrongType_ShouldDeny() } [Fact] - public void Decide_WhenPermissionAllowsAction_ShouldAllow() + public async Task Decide_WhenPermissionAllowsAction_ShouldAllow() { var sut = new MustHavePermissionPolicy(); @@ -663,13 +649,13 @@ public void Decide_WhenPermissionAllowsAction_ShouldAllow() UAuthConstants.Access.Permissions, permissions); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeTrue(); } [Fact] - public void Decide_WhenPermissionDoesNotAllowAction_ShouldDeny() + public async Task Decide_WhenPermissionDoesNotAllowAction_ShouldDeny() { var sut = new MustHavePermissionPolicy(); @@ -683,14 +669,14 @@ public void Decide_WhenPermissionDoesNotAllowAction_ShouldDeny() UAuthConstants.Access.Permissions, permissions); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("missing_permission"); } [Fact] - public void Decide_WhenPermissionAllowsExactAction_ShouldAllow() + public async Task Decide_WhenPermissionAllowsExactAction_ShouldAllow() { var sut = new MustHavePermissionPolicy(); @@ -704,7 +690,7 @@ public void Decide_WhenPermissionAllowsExactAction_ShouldAllow() UAuthConstants.Access.Permissions, permissions); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeTrue(); result.DenyReason.Should().BeNull(); @@ -780,7 +766,7 @@ public void AppliesTo_SystemActor_ShouldReturnFalse() } [Fact] - public void Decide_WhenActorIsMissing_ShouldDeny() + public async Task Decide_WhenActorIsMissing_ShouldDeny() { var runtime = new TestUserRuntimeStateProvider(); var sut = new RequireActiveUserPolicy(runtime); @@ -788,7 +774,7 @@ public void Decide_WhenActorIsMissing_ShouldDeny() var context = TestAccessContext.WithAction("users.get.self"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("missing_actor"); @@ -797,7 +783,7 @@ public void Decide_WhenActorIsMissing_ShouldDeny() } [Fact] - public void Decide_WhenRuntimeStateIsMissing_ShouldDeny() + public async Task Decide_WhenRuntimeStateIsMissing_ShouldDeny() { var runtime = new TestUserRuntimeStateProvider { @@ -806,18 +792,16 @@ public void Decide_WhenRuntimeStateIsMissing_ShouldDeny() var sut = new RequireActiveUserPolicy(runtime); - var context = TestAccessContext.ForUser( - UserKey.New(), - "users.get.self"); + var context = TestAccessContext.ForUser(UserKey.New(), "users.get.self"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("user_not_found"); } [Fact] - public void Decide_WhenUserDoesNotExist_ShouldDeny() + public async Task Decide_WhenUserDoesNotExist_ShouldDeny() { var runtime = new TestUserRuntimeStateProvider { @@ -829,18 +813,16 @@ public void Decide_WhenUserDoesNotExist_ShouldDeny() var sut = new RequireActiveUserPolicy(runtime); - var context = TestAccessContext.ForUser( - UserKey.New(), - "users.get.self"); + var context = TestAccessContext.ForUser(UserKey.New(), "users.get.self"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("user_not_found"); } [Fact] - public void Decide_WhenUserIsDeleted_ShouldDeny() + public async Task Decide_WhenUserIsDeleted_ShouldDeny() { var runtime = new TestUserRuntimeStateProvider { @@ -856,14 +838,14 @@ public void Decide_WhenUserIsDeleted_ShouldDeny() UserKey.New(), "users.get.self"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("user_not_found"); } [Fact] - public void Decide_WhenUserIsInactive_ShouldDeny() + public async Task Decide_WhenUserIsInactive_ShouldDeny() { var runtime = new TestUserRuntimeStateProvider { @@ -879,14 +861,14 @@ public void Decide_WhenUserIsInactive_ShouldDeny() UserKey.New(), "users.get.self"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("user_not_active"); } [Fact] - public void Decide_WhenUserIsActive_ShouldAllow() + public async Task Decide_WhenUserIsActive_ShouldAllow() { var runtime = new TestUserRuntimeStateProvider { @@ -902,14 +884,14 @@ public void Decide_WhenUserIsActive_ShouldAllow() UserKey.New(), "users.get.self"); - var result = sut.Decide(context); + var result = await sut.DecideAsync(context); result.IsAllowed.Should().BeTrue(); result.DenyReason.Should().BeNull(); } [Fact] - public void Decide_ShouldQueryRuntimeUsingActorTenantAndUserKey() + public async Task Decide_ShouldQueryRuntimeUsingActorTenantAndUserKey() { var userKey = UserKey.New(); var tenant = TenantKey.FromExternal("tenant-a"); @@ -926,7 +908,7 @@ public void Decide_ShouldQueryRuntimeUsingActorTenantAndUserKey() var sut = new RequireActiveUserPolicy(runtime); var context = TestAccessContext.ForUser(userKey, "users.get.self", tenant); - sut.Decide(context); + var result = await sut.DecideAsync(context); runtime.LastTenant.Should().Be(tenant); runtime.LastUserKey.Should().Be(userKey); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/PolicyTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/PolicyTests.cs index 18088136..df02fa11 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/PolicyTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/PolicyTests.cs @@ -301,7 +301,8 @@ public TestPolicy(string? name = null, bool applies = true) public bool AppliesTo(AccessContext context) => _applies; - public AccessDecision Decide(AccessContext context) => AccessDecision.Allow(); + public ValueTask DecideAsync(AccessContext context, CancellationToken ct = default) + => ValueTask.FromResult(AccessDecision.Allow()); } private sealed class TestDependency @@ -320,8 +321,8 @@ public DependencyPolicy(TestDependency dependency) public bool AppliesTo(AccessContext context) => true; - public AccessDecision Decide(AccessContext context) - => AccessDecision.Allow(); + public ValueTask DecideAsync(AccessContext context, CancellationToken ct = default) + => ValueTask.FromResult(AccessDecision.Allow()); } private static AccessContext CreateContext(string action) diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessPipelineTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessPipelineTests.cs index d809cda5..b7ffd6e5 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessPipelineTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessPipelineTests.cs @@ -174,7 +174,7 @@ await pipeline.Orchestrator.ExecuteAsync( executed.Should().BeTrue(); skipped.Verify( - p => p.Decide(It.IsAny()), + p => p.DecideAsync(It.IsAny()), Times.Never); } @@ -301,7 +301,7 @@ await act.Should() } [Fact] - public void Runtime_reauthentication_should_be_reported_by_authority() + public async Task Runtime_reauthentication_should_be_reported_by_authority() { var first = Policy( _ => true, @@ -315,7 +315,7 @@ public void Runtime_reauthentication_should_be_reported_by_authority() Array.Empty(), Array.Empty()); - var decision = authority.Decide( + var decision = await authority.DecideAsync( TestAccessContext.WithAction("users.update"), new[] { first.Object, second.Object }); @@ -324,7 +324,7 @@ public void Runtime_reauthentication_should_be_reported_by_authority() } [Fact] - public void Runtime_denial_should_take_priority_over_earlier_reauthentication() + public async Task Runtime_denial_should_take_priority_over_earlier_reauthentication() { var reauth = Policy( _ => true, @@ -338,7 +338,7 @@ public void Runtime_denial_should_take_priority_over_earlier_reauthentication() Array.Empty(), Array.Empty()); - var decision = authority.Decide( + var decision = await authority.DecideAsync( TestAccessContext.WithAction("users.update"), new[] { reauth.Object, deny.Object }); @@ -606,14 +606,14 @@ public async Task Generic_command_exception_should_propagate() } [Fact] - public void Authority_should_allow_when_no_policies_exist() + public async Task Authority_should_allow_when_no_policies_exist() { // Documents current default-allow behavior. var authority = new UAuthAccessAuthority( Array.Empty(), Array.Empty()); - var decision = authority.Decide( + var decision = await authority.DecideAsync( TestAccessContext.WithAction("users.delete"), Array.Empty()); @@ -621,13 +621,13 @@ public void Authority_should_allow_when_no_policies_exist() } [Fact] - public void Authority_should_treat_null_global_collections_as_empty() + public async Task Authority_should_treat_null_global_collections_as_empty() { var authority = new UAuthAccessAuthority( null!, null!); - var decision = authority.Decide( + var decision = await authority.DecideAsync( TestAccessContext.WithAction("users.read"), Array.Empty()); @@ -681,8 +681,11 @@ private static Mock Invariant( { var mock = new Mock(); - mock.Setup(i => i.Decide(It.IsAny())) - .Returns((AccessContext context) => decide(context)); + mock.Setup(p => p.DecideAsync( + It.IsAny(), + It.IsAny())) + .Returns((AccessContext context, CancellationToken _) => + ValueTask.FromResult(decide(context))); return mock; } @@ -696,8 +699,11 @@ private static Mock Policy( mock.Setup(p => p.AppliesTo(It.IsAny())) .Returns((AccessContext context) => appliesTo(context)); - mock.Setup(p => p.Decide(It.IsAny())) - .Returns((AccessContext context) => decide(context)); + mock.Setup(p => p.DecideAsync( + It.IsAny(), + It.IsAny())) + .Returns((AccessContext context, CancellationToken _) => + ValueTask.FromResult(decide(context))); return mock; } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthResourceAccessOrchestratorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthResourceAccessOrchestratorTests.cs index efb80fc9..55bd2e99 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthResourceAccessOrchestratorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthResourceAccessOrchestratorTests.cs @@ -46,11 +46,7 @@ public async Task ExecuteAsync_WhenAllowed_ShouldExecuteCommandExactlyOnce() .Setup(x => x.GetPolicies(It.IsAny())) .Returns(policies); - _authority - .Setup(x => x.Decide( - It.IsAny(), - policies)) - .Returns(AccessDecision.Allow()); + SetupAuthority(AccessDecision.Allow()); var executions = 0; @@ -75,11 +71,7 @@ public async Task ExecuteAsync_WhenDenied_ShouldThrowAuthorizationException() .Setup(x => x.GetPolicies(It.IsAny())) .Returns(policies); - _authority - .Setup(x => x.Decide( - It.IsAny(), - policies)) - .Returns(AccessDecision.Deny("permission_required")); + SetupAuthority(AccessDecision.Deny("permission_required")); var command = new AccessCommand(_ => Task.CompletedTask); @@ -100,11 +92,7 @@ public async Task ExecuteAsync_WhenDenied_ShouldNeverExecuteCommand() .Setup(x => x.GetPolicies(It.IsAny())) .Returns(policies); - _authority - .Setup(x => x.Decide( - It.IsAny(), - policies)) - .Returns(AccessDecision.Deny("denied")); + SetupAuthority(AccessDecision.Deny("denied")); var executed = false; @@ -132,11 +120,7 @@ public async Task ExecuteAsync_WhenDeniedWithoutReason_ShouldUseDefaultReason() .Setup(x => x.GetPolicies(It.IsAny())) .Returns(policies); - _authority - .Setup(x => x.Decide( - It.IsAny(), - policies)) - .Returns(AccessDecision.Deny(null)); + SetupAuthority(AccessDecision.Deny(null)); var command = new AccessCommand(_ => Task.CompletedTask); @@ -157,11 +141,7 @@ public async Task ExecuteAsync_WhenReauthenticationRequired_ShouldNotExecuteComm .Setup(x => x.GetPolicies(It.IsAny())) .Returns(policies); - _authority - .Setup(x => x.Decide( - It.IsAny(), - policies)) - .Returns(AccessDecision.ReauthenticationRequired()); + SetupAuthority(AccessDecision.ReauthenticationRequired()); var executed = false; @@ -194,11 +174,7 @@ public async Task ExecuteAsync_ShouldPassEnrichedContextToPolicyProvider() .Callback(x => captured = x) .Returns(Array.Empty()); - _authority - .Setup(x => x.Decide( - It.IsAny(), - It.IsAny>())) - .Returns(AccessDecision.Allow()); + SetupAuthority(AccessDecision.Allow()); await _sut.ExecuteAsync( original, @@ -226,11 +202,7 @@ public async Task ExecuteAsync_ShouldCompileWildcardPermissionClaims() .Callback(x => captured = x) .Returns(Array.Empty()); - _authority - .Setup(x => x.Decide( - It.IsAny(), - It.IsAny>())) - .Returns(AccessDecision.Allow()); + SetupAuthority(AccessDecision.Allow()); await _sut.ExecuteAsync( CreateContext(), @@ -256,11 +228,7 @@ public async Task ExecuteAsync_WithNoPermissionClaims_ShouldProvideEmptyPermissi .Callback(x => captured = x) .Returns(Array.Empty()); - _authority - .Setup(x => x.Decide( - It.IsAny(), - It.IsAny>())) - .Returns(AccessDecision.Allow()); + SetupAuthority(AccessDecision.Allow()); await _sut.ExecuteAsync( CreateContext(), @@ -287,12 +255,14 @@ public async Task ExecuteAsync_ShouldPassEnrichedContextToAuthority() .Returns(policies); _authority - .Setup(x => x.Decide( + .Setup(x => x.DecideAsync( It.IsAny(), - policies)) - .Callback>( - (context, _) => authorityContext = context) - .Returns(AccessDecision.Allow()); + It.IsAny>(), + It.IsAny())) + .Callback, CancellationToken>( + (context, _, _) => authorityContext = context) + .Returns(new ValueTask( + AccessDecision.Allow())); await _sut.ExecuteAsync( CreateContext(), @@ -315,11 +285,7 @@ public async Task ExecuteAsyncOfT_WhenAllowed_ShouldReturnCommandResult() .Setup(x => x.GetPolicies(It.IsAny())) .Returns(policies); - _authority - .Setup(x => x.Decide( - It.IsAny(), - policies)) - .Returns(AccessDecision.Allow()); + SetupAuthority(AccessDecision.Allow()); var command = new AccessCommand( _ => Task.FromResult("result")); @@ -339,11 +305,7 @@ public async Task ExecuteAsyncOfT_WhenDenied_ShouldNeverExecuteCommand() .Setup(x => x.GetPolicies(It.IsAny())) .Returns(policies); - _authority - .Setup(x => x.Decide( - It.IsAny(), - policies)) - .Returns(AccessDecision.Deny("forbidden")); + SetupAuthority(AccessDecision.Deny("forbidden")); var executed = false; @@ -383,9 +345,10 @@ await act.Should() Times.Never); _authority.Verify( - x => x.Decide( + x => x.DecideAsync( It.IsAny(), - It.IsAny>()), + It.IsAny>(), + It.IsAny()), Times.Never); } @@ -398,11 +361,7 @@ public async Task ExecuteAsyncOfT_WhenReauthenticationRequired_ShouldNotExecuteC .Setup(x => x.GetPolicies(It.IsAny())) .Returns(policies); - _authority - .Setup(x => x.Decide( - It.IsAny(), - policies)) - .Returns(AccessDecision.ReauthenticationRequired()); + SetupAuthority(AccessDecision.ReauthenticationRequired()); var executed = false; @@ -424,6 +383,74 @@ public async Task ExecuteAsyncOfT_WhenReauthenticationRequired_ShouldNotExecuteC executed.Should().BeFalse(); } + [Fact] + public async Task ExecuteAsync_ShouldForwardCancellationTokenToAuthority() + { + using var cts = new CancellationTokenSource(); + + CancellationToken receivedToken = default; + + _policyProvider + .Setup(x => x.GetPolicies(It.IsAny())) + .Returns(Array.Empty()); + + _authority + .Setup(x => x.DecideAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Callback, CancellationToken>( + (_, _, ct) => receivedToken = ct) + .Returns(new ValueTask( + AccessDecision.Allow())); + + await _sut.ExecuteAsync( + CreateContext(), + new AccessCommand(_ => Task.CompletedTask), + cts.Token); + + receivedToken.Should().Be(cts.Token); + } + + [Fact] + public async Task ExecuteAsync_ShouldAwaitAuthorityBeforeExecutingCommand() + { + var completion = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + + _policyProvider + .Setup(x => x.GetPolicies(It.IsAny())) + .Returns(Array.Empty()); + + _authority + .Setup(x => x.DecideAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(() => new ValueTask( + completion.Task)); + + var executed = false; + + var command = new AccessCommand(_ => + { + executed = true; + return Task.CompletedTask; + }); + + var executionTask = _sut.ExecuteAsync( + CreateContext(), + command); + + executed.Should().BeFalse(); + + completion.SetResult(AccessDecision.Allow()); + + await executionTask; + + executed.Should().BeTrue(); + } + private static ClaimsPrincipal CreatePrincipal( params (string Type, string Value)[] claims) @@ -449,6 +476,16 @@ private static AccessContext CreateContext() attributes: EmptyAttributes.Instance); } + private void SetupAuthority(AccessDecision decision) + { + _authority + .Setup(x => x.DecideAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(new ValueTask(decision)); + } + private static CompiledPermissionSet GetCompiledPermissions( AccessContext context) {