297 lines
12 KiB
C#
297 lines
12 KiB
C#
using System.Security.Claims;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
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<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,
|
|
IFeatureAccessService featureAccessService,
|
|
IHttpContextAccessor httpContextAccessor) :
|
|
AuthorizationHandler<TenantPermissionRequirement>
|
|
{
|
|
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) =>
|
|
method is not null &&
|
|
(HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsOptions(method));
|
|
}
|
|
|
|
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 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.AddHttpContextAccessor();
|
|
services.TryAddScoped<IFeatureAccessService, CompatibilityFeatureAccessService>();
|
|
services.AddScoped<IAuthorizationHandler, CurrentAccessAuthorizationHandler>();
|
|
services.AddScoped<IAuthorizationHandler, CurrentPlatformAccessAuthorizationHandler>();
|
|
services.AddScoped<IAuthorizationHandler, TenantPermissionAuthorizationHandler>();
|
|
services.AddScoped<IAuthorizationHandler, PlatformPermissionAuthorizationHandler>();
|
|
services.AddScoped<IAuthorizationHandler, AllDataScopeAuthorizationHandler>();
|
|
services.AddScoped<IAuthorizationHandler, TenantResourceAccessAuthorizationHandler>();
|
|
|
|
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<FeatureAccessDecision> EvaluateAsync(Guid tenantId, string featureCode, FeatureAccessOperation operation, CancellationToken cancellationToken = default) =>
|
|
Task.FromResult(new FeatureAccessDecision(true, null, featureCode, operation));
|
|
|
|
public Task<IReadOnlySet<string>> GetEnabledFeaturesAsync(Guid tenantId, FeatureAccessOperation operation = FeatureAccessOperation.Read, CancellationToken cancellationToken = default) =>
|
|
Task.FromResult<IReadOnlySet<string>>(new HashSet<string>(SaasFeatureCatalog.All, StringComparer.Ordinal));
|
|
|
|
public Task<IReadOnlySet<string>> FilterPermissionCodesAsync(Guid tenantId, IEnumerable<string> permissionCodes, FeatureAccessOperation operation = FeatureAccessOperation.Read, CancellationToken cancellationToken = default) =>
|
|
Task.FromResult<IReadOnlySet<string>>(permissionCodes.ToHashSet(StringComparer.Ordinal));
|
|
|
|
public Task<IReadOnlyCollection<FeatureQuotaSnapshot>> GetQuotaSummaryAsync(Guid tenantId, CancellationToken cancellationToken = default) =>
|
|
Task.FromResult<IReadOnlyCollection<FeatureQuotaSnapshot>>([]);
|
|
|
|
public Task<bool> TryConsumeQuotaAsync(Guid tenantId, string metricCode, long amount, CancellationToken cancellationToken = default) => Task.FromResult(true);
|
|
|
|
public Task ReleaseQuotaAsync(Guid tenantId, string metricCode, long amount, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
|
}
|