Resource-based authorization is for when the decision depends on the object itself (which customer the order belongs to).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | // ====================================================================== // Q: Roles, claims, policies: how do you implement authorization? // // Resource-based authorization is for when the decision depends on the object // itself (which customer the order belongs to). // Multi-tenancy: a global query filter in EF Core, so that no query "forgets" // the tenant. // Roles for coarse-grained checks ("who can approve"), policies and claims for // rules, resource-based authorization for ownership. And always an integration // test that verifies customer A cannot see customer B's data (Q13). // ====================================================================== // Policies and a secure-by-default fallback policy builder.Services.AddAuthorizationBuilder() // Secure by default: every endpoint requires login unless it has [AllowAnonymous] .SetFallbackPolicy(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()) .AddPolicy("ApproveOrders", p => p.RequireRole("PurchasingManager")) .AddPolicy("B2BCustomer", p => p.RequireClaim("customer_id")); [Authorize(Policy = "ApproveOrders")] [HttpPost("{id:int}/approve")] public async Task<IActionResult> Approve(int id, CancellationToken ct) { /* ... */ } // Resource-based authorization: the order must belong to the caller's customer public record SameCustomerRequirement : IAuthorizationRequirement; public class SameCustomerHandler : AuthorizationHandler<SameCustomerRequirement, Order> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext ctx, SameCustomerRequirement req, Order order) { if (ctx.User.GetCustomerId() == order.CustomerId) ctx.Succeed(req); return Task.CompletedTask; } } builder.Services.AddSingleton<IAuthorizationHandler, SameCustomerHandler>(); // In the endpoint var auth = await authorizationService.AuthorizeAsync(User, order, new SameCustomerRequirement()); if (!auth.Succeeded) return NotFound(); // 404, not 403 (Q1) // Multi-tenancy: EF Core global query filter per tenant public class AppDbContext(DbContextOptions<AppDbContext> o, ITenantContext tenant) : DbContext(o) { protected override void OnModelCreating(ModelBuilder mb) => mb.Entity<Order>().HasQueryFilter(x => x.TenantId == tenant.TenantId); } // tenant.TenantId comes from the token's claims, NEVER from a header or query string |