using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection.Extensions; 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 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) { return string.Equals(principal.FindFirst(TikuClaimTypes.Realm)?.Value, "tenant", StringComparison.Ordinal); } internal static bool IsPlatformRealm(ClaimsPrincipal principal) { return string.Equals(principal.FindFirst(TikuClaimTypes.Realm)?.Value, "platform", StringComparison.Ordinal) && principal.FindFirst(TikuClaimTypes.TenantId) is null; } private static Guid? FindTenantId(ClaimsPrincipal principal) { return Guid.TryParse(principal.FindFirst(TikuClaimTypes.TenantId)?.Value, out var tenantId) ? tenantId : null; } } internal sealed class TenantPermissionAuthorizationHandler( ICurrentAccessContext accessContext, IFeatureAccessService featureAccessService, IHttpContextAccessor httpContextAccessor) : AuthorizationHandler { protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, TenantPermissionRequirement requirement) { if (!CurrentAccessAuthorizationHandler.IsTenantRealm(context.User)) return; var access = await accessContext.GetAsync(); var operation = IsSafeMethod(httpContextAccessor.HttpContext?.Request.Method) ? FeatureAccessOperation.Read : FeatureAccessOperation.Write; if (access.TenantId is { } tenantId && access.HasTenantPermission(requirement.PermissionCode) && (await featureAccessService.FilterPermissionCodesAsync( tenantId, [requirement.PermissionCode], operation)).Contains(requirement.PermissionCode)) context.Succeed(requirement); } private static bool IsSafeMethod(string? method) { return method is not null && (HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsOptions(method)); } } 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 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.AddHttpContextAccessor(); services.TryAddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddAuthorization(options => { options.AddPolicy( TikuPolicies.CurrentTenantMember, policy => policy .RequireAuthenticatedUser() .AddRequirements(new CurrentTenantMemberRequirement())); options.AddPolicy( TikuPolicies.TenantBackofficeBootstrap, policy => policy .RequireAuthenticatedUser() .AddRequirements(new CurrentTenantMemberRequirement())); options.AddPolicy( TikuPolicies.PlatformBackofficeBootstrap, policy => policy .RequireAuthenticatedUser() .AddRequirements(new CurrentPlatformAccessRequirement())); // 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))); options.AddPolicy( TikuPolicies.TenantContentManageAllScope, policy => policy .RequireAuthenticatedUser() .AddRequirements( new CurrentTenantMemberRequirement(), new TenantPermissionRequirement(BackendPermissions.TenantContentManage), new AllDataScopeRequirement())); options.AddPolicy( TikuPolicies.TenantAllDataScope, policy => policy .RequireAuthenticatedUser() .AddRequirements( new CurrentTenantMemberRequirement(), new AllDataScopeRequirement())); options.AddPolicy( TikuPolicies.TenantCommerceOperateAllScope, policy => policy .RequireAuthenticatedUser() .AddRequirements( new CurrentTenantMemberRequirement(), new TenantPermissionRequirement(BackendPermissions.TenantCommerceOperate), new AllDataScopeRequirement())); foreach (var permissionCode in BackendPermissions.Tenant) options.AddPolicy( permissionCode, policy => policy .RequireAuthenticatedUser() .AddRequirements( new CurrentTenantMemberRequirement(), new TenantPermissionRequirement(permissionCode))); foreach (var permissionCode in BackendPermissions.Platform) options.AddPolicy( permissionCode, policy => policy .RequireAuthenticatedUser() .AddRequirements(new PlatformPermissionRequirement(permissionCode))); }); return services; } } internal sealed class CompatibilityFeatureAccessService : IFeatureAccessService { public Task EvaluateAsync(Guid tenantId, string featureCode, FeatureAccessOperation operation, CancellationToken cancellationToken = default) { return Task.FromResult(new FeatureAccessDecision(true, null, featureCode, operation)); } public Task> GetEnabledFeaturesAsync(Guid tenantId, FeatureAccessOperation operation = FeatureAccessOperation.Read, CancellationToken cancellationToken = default) { return Task.FromResult>( new HashSet(SaasFeatureCatalog.All, StringComparer.Ordinal)); } public Task> FilterPermissionCodesAsync(Guid tenantId, IEnumerable permissionCodes, FeatureAccessOperation operation = FeatureAccessOperation.Read, CancellationToken cancellationToken = default) { return Task.FromResult>(permissionCodes.ToHashSet(StringComparer.Ordinal)); } public Task> GetQuotaSummaryAsync(Guid tenantId, CancellationToken cancellationToken = default) { return Task.FromResult>([]); } public Task TryConsumeQuotaAsync(Guid tenantId, string metricCode, long amount, CancellationToken cancellationToken = default) { return Task.FromResult(true); } public Task ReleaseQuotaAsync(Guid tenantId, string metricCode, long amount, CancellationToken cancellationToken = default) { return Task.CompletedTask; } }