forked from gongxuegit/tiku-backend.net
feat: harden SaaS authentication and authorization
This commit is contained in:
288
Tiku.Api/Security/AccessAuthorizationRequirements.cs
Normal file
288
Tiku.Api/Security/AccessAuthorizationRequirements.cs
Normal file
@@ -0,0 +1,288 @@
|
||||
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<CurrentTenantMemberRequirement>
|
||||
{
|
||||
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<TenantPermissionRequirement>
|
||||
{
|
||||
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<CurrentPlatformAccessRequirement>
|
||||
{
|
||||
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<TenantResourceAccessRequirement, TenantResourceAuthorizationResource>
|
||||
{
|
||||
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<PlatformPermissionRequirement>
|
||||
{
|
||||
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<MfaRequirement>
|
||||
{
|
||||
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<AllDataScopeRequirement>
|
||||
{
|
||||
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<IAuthorizationHandler, CurrentAccessAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, CurrentPlatformAccessAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, TenantPermissionAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, PlatformPermissionAuthorizationHandler>();
|
||||
services.AddSingleton<IAuthorizationHandler, MfaAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, AllDataScopeAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, TenantResourceAccessAuthorizationHandler>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user