feat(security): add distributed authorization foundation

This commit is contained in:
2026-07-29 10:40:10 +08:00
parent c7f9a4e3c9
commit df88fa19cb
76 changed files with 22020 additions and 88 deletions

View File

@@ -11,7 +11,8 @@ namespace Tiku.Infrastructure.Backoffice;
internal sealed class BackofficeService(
TikuDbContext dbContext,
IOperationAuditService auditService) : IBackofficeService
IOperationAuditService auditService,
ICapabilityAccessEvaluator capabilityAccessEvaluator) : IBackofficeService
{
private static readonly BuiltinPermission[] BuiltinPermissions =
[
@@ -63,7 +64,11 @@ internal sealed class BackofficeService(
}
await EnsureCatalogAsync(cancellationToken);
var permissionCodes = access.TenantPermissions.Order(StringComparer.Ordinal).ToArray();
var permissionCodes = await FilterTenantPermissionCodesAsync(
access.TenantId.Value,
access.TenantPermissions,
CapabilityOperation.Read,
cancellationToken);
var menus = await LoadEffectiveMenusAsync(
BackendPermissionArea.Tenant,
permissionCodes,
@@ -100,10 +105,18 @@ internal sealed class BackofficeService(
.Where(item => item.Area == BackendPermissionArea.Tenant || item.Area == BackendPermissionArea.Both)
.OrderBy(item => item.Module).ThenBy(item => item.SortOrder).ThenBy(item => item.Code)
.ToArrayAsync(cancellationToken);
var enabledPermissionCodes = await FilterTenantPermissionCodesAsync(
tenantId,
permissions.Select(item => item.Code),
CapabilityOperation.Read,
cancellationToken);
permissions = permissions.Where(item => enabledPermissionCodes.Contains(item.Code, StringComparer.Ordinal)).ToArray();
var menus = await dbContext.BackendMenus.AsNoTracking()
.Where(item => item.IsActive && item.Area == BackendPermissionArea.Tenant)
.OrderBy(item => item.SortOrder).ThenBy(item => item.Code)
.ToArrayAsync(cancellationToken);
menus = menus.Where(item => item.PermissionCode is null ||
enabledPermissionCodes.Contains(item.PermissionCode, StringComparer.Ordinal)).ToArray();
return new BackofficeBootstrap(
permissions.Select(ToPermissionItem).ToArray(),
menus.Select(ToMenuItem).ToArray(),
@@ -350,6 +363,19 @@ internal sealed class BackofficeService(
var normalizedPermissions = NormalizeCodes(permissionCodes);
var normalizedMenus = NormalizeCodes(menuCodes);
await ValidatePermissionCodesAsync(normalizedPermissions, BackendPermissionArea.Tenant, cancellationToken);
foreach (var permissionCode in normalizedPermissions)
{
if (!await capabilityAccessEvaluator.IsAllowedAsync(
tenantId,
ResolveModuleCode(permissionCode),
CapabilityOperation.Write,
cancellationToken))
{
throw new BackofficeException(
"One or more permissions belong to a module unavailable to this tenant.",
"capability_not_available");
}
}
await ValidateMenuCodesAsync(normalizedMenus, BackendPermissionArea.Tenant, cancellationToken);
await dbContext.TenantBackendRolePermissions.Where(item => item.TenantId == tenantId && item.RoleId == roleId).ExecuteDeleteAsync(cancellationToken);
await dbContext.TenantBackendRoleMenus.Where(item => item.TenantId == tenantId && item.RoleId == roleId).ExecuteDeleteAsync(cancellationToken);
@@ -401,6 +427,33 @@ internal sealed class BackofficeService(
return menus.Select(ToMenuItem).ToArray();
}
private async Task<string[]> FilterTenantPermissionCodesAsync(
Guid tenantId,
IEnumerable<string> permissionCodes,
CapabilityOperation operation,
CancellationToken cancellationToken)
{
var enabled = new List<string>();
foreach (var permissionCode in permissionCodes.Distinct(StringComparer.Ordinal).Order(StringComparer.Ordinal))
{
if (await capabilityAccessEvaluator.IsAllowedAsync(
tenantId,
ResolveModuleCode(permissionCode),
operation,
cancellationToken))
{
enabled.Add(permissionCode);
}
}
return enabled.ToArray();
}
private static string ResolveModuleCode(string permissionCode)
{
var parts = permissionCode.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return parts.Length >= 2 ? parts[1].ToLowerInvariant() : permissionCode.ToLowerInvariant();
}
private async Task ValidateMenuCodesAsync(string[] codes, BackendPermissionArea area, CancellationToken cancellationToken)
{
var count = await dbContext.BackendMenus.CountAsync(