using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; using Tiku.Application.Security; namespace Tiku.Api.Security; public sealed record CurrentTenantMemberRequirement : IAuthorizationRequirement; public sealed record CurrentPlatformAccessRequirement : IAuthorizationRequirement; public sealed record TenantPermissionRequirement : IAuthorizationRequirement { public TenantPermissionRequirement(string permissionCode) { BackendPermissions.EnsureTenant(permissionCode); PermissionCode = permissionCode; } public string PermissionCode { get; } } public sealed record PlatformPermissionRequirement : IAuthorizationRequirement { public PlatformPermissionRequirement(string permissionCode) { BackendPermissions.EnsurePlatform(permissionCode); PermissionCode = permissionCode; } public string PermissionCode { get; } } public sealed record MfaRequirement : IAuthorizationRequirement; public sealed record AllDataScopeRequirement : IAuthorizationRequirement; public sealed record TenantResourceAccessRequirement : IAuthorizationRequirement; public sealed record TenantResourceAuthorizationResource( Guid TenantId, Guid? OwnerUserId = null, Guid? RegionId = null, Guid? ClassId = null); internal sealed class CurrentAccessAuthorizationHandler(ICurrentAccessContext accessContext) : AuthorizationHandler { protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, CurrentTenantMemberRequirement requirement) { if (!IsTenantRealm(context.User)) { return; } var access = await accessContext.GetAsync(); if (access.IsCurrentTenantMember && access.TenantId is { } tenantId && FindTenantId(context.User) == tenantId) { context.Succeed(requirement); } } internal static bool IsTenantRealm(ClaimsPrincipal principal) => string.Equals(principal.FindFirst(TikuClaimTypes.Realm)?.Value, "tenant", StringComparison.Ordinal); internal static bool IsPlatformRealm(ClaimsPrincipal principal) => string.Equals(principal.FindFirst(TikuClaimTypes.Realm)?.Value, "platform", StringComparison.Ordinal) && principal.FindFirst(TikuClaimTypes.TenantId) is null; private static Guid? FindTenantId(ClaimsPrincipal principal) => Guid.TryParse(principal.FindFirst(TikuClaimTypes.TenantId)?.Value, out var tenantId) ? tenantId : null; } internal sealed class TenantPermissionAuthorizationHandler(ICurrentAccessContext accessContext) : AuthorizationHandler { protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, TenantPermissionRequirement requirement) { if (!CurrentAccessAuthorizationHandler.IsTenantRealm(context.User)) { return; } var access = await accessContext.GetAsync(); if (access.HasTenantPermission(requirement.PermissionCode)) { context.Succeed(requirement); } } } internal sealed class CurrentPlatformAccessAuthorizationHandler(ICurrentAccessContext accessContext) : AuthorizationHandler { protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, CurrentPlatformAccessRequirement requirement) { if (!CurrentAccessAuthorizationHandler.IsPlatformRealm(context.User)) { return; } var access = await accessContext.GetAsync(); if (access.IsUserActive && access.PlatformPermissions.Count > 0) { context.Succeed(requirement); } } } internal sealed class TenantResourceAccessAuthorizationHandler(ICurrentAccessContext accessContext) : AuthorizationHandler { protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, TenantResourceAccessRequirement requirement, TenantResourceAuthorizationResource resource) { if (!CurrentAccessAuthorizationHandler.IsTenantRealm(context.User)) { return; } var access = await accessContext.GetAsync(); if (access.UserId is { } userId && access.IsCurrentTenantMember && access.TenantId == resource.TenantId && access.DataScope.AllowsResource(userId, resource.OwnerUserId, resource.RegionId, resource.ClassId)) { context.Succeed(requirement); } } } internal sealed class PlatformPermissionAuthorizationHandler(ICurrentAccessContext accessContext) : AuthorizationHandler { protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, PlatformPermissionRequirement requirement) { if (!CurrentAccessAuthorizationHandler.IsPlatformRealm(context.User)) { return; } var access = await accessContext.GetAsync(); if (access.HasPlatformPermission(requirement.PermissionCode)) { context.Succeed(requirement); } } } internal sealed class MfaAuthorizationHandler : AuthorizationHandler { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MfaRequirement requirement) { if (context.User.FindAll(TikuClaimTypes.Mfa).Any(claim => string.Equals(claim.Value, "mfa", StringComparison.OrdinalIgnoreCase) || string.Equals(claim.Value, "totp", StringComparison.OrdinalIgnoreCase) || string.Equals(claim.Value, bool.TrueString, StringComparison.OrdinalIgnoreCase))) { context.Succeed(requirement); } return Task.CompletedTask; } } internal sealed class AllDataScopeAuthorizationHandler(ICurrentAccessContext accessContext) : AuthorizationHandler { protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, AllDataScopeRequirement requirement) { var access = await accessContext.GetAsync(); if (access.IsCurrentTenantMember && access.DataScope.Mode == DataScopeMode.All) { context.Succeed(requirement); } } } public static class AccessAuthorizationServiceCollectionExtensions { public static IServiceCollection AddTikuRbacAuthorization(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddAuthorization(options => { options.AddPolicy( TikuPolicies.CurrentTenantMember, policy => policy .RequireAuthenticatedUser() .AddRequirements(new CurrentTenantMemberRequirement())); options.AddPolicy( TikuPolicies.Mfa, policy => policy .RequireAuthenticatedUser() .AddRequirements(new MfaRequirement())); options.AddPolicy( TikuPolicies.TenantBackofficeBootstrap, policy => policy .RequireAuthenticatedUser() .AddRequirements( new CurrentTenantMemberRequirement(), new MfaRequirement())); options.AddPolicy( TikuPolicies.PlatformBackofficeBootstrap, policy => policy .RequireAuthenticatedUser() .AddRequirements( new CurrentPlatformAccessRequirement(), new MfaRequirement())); // Temporary compatibility for controllers that have not yet been // split into their module-specific permission policy. This must // remain database-backed; an authenticated-only alias would reopen // every legacy tenant administration endpoint to ordinary users. options.AddPolicy( TikuPolicies.TenantAdmin, policy => policy .RequireAuthenticatedUser() .AddRequirements( new CurrentTenantMemberRequirement(), new TenantPermissionRequirement(BackendPermissions.TenantRoleManage), new MfaRequirement())); options.AddPolicy( TikuPolicies.TenantContentManageAllScope, policy => policy .RequireAuthenticatedUser() .AddRequirements( new CurrentTenantMemberRequirement(), new TenantPermissionRequirement(BackendPermissions.TenantContentManage), new AllDataScopeRequirement(), new MfaRequirement())); options.AddPolicy( TikuPolicies.TenantCommerceOperateAllScope, policy => policy .RequireAuthenticatedUser() .AddRequirements( new CurrentTenantMemberRequirement(), new TenantPermissionRequirement(BackendPermissions.TenantCommerceOperate), new AllDataScopeRequirement(), new MfaRequirement())); foreach (var permissionCode in BackendPermissions.Tenant) { options.AddPolicy( permissionCode, policy => policy .RequireAuthenticatedUser() .AddRequirements( new CurrentTenantMemberRequirement(), new TenantPermissionRequirement(permissionCode), new MfaRequirement())); } foreach (var permissionCode in BackendPermissions.Platform) { options.AddPolicy( permissionCode, policy => policy .RequireAuthenticatedUser() .AddRequirements( new PlatformPermissionRequirement(permissionCode), new MfaRequirement())); } }); return services; } }