forked from gongxuegit/tiku-backend.net
feat(security): add distributed authorization foundation
This commit is contained in:
87
Tiku.Infrastructure/Security/CapabilityAccessEvaluator.cs
Normal file
87
Tiku.Infrastructure/Security/CapabilityAccessEvaluator.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Platform;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Security;
|
||||
|
||||
internal sealed class CapabilityAccessEvaluator(TikuDbContext dbContext) : ICapabilityAccessEvaluator
|
||||
{
|
||||
public async Task<bool> IsAllowedAsync(
|
||||
Guid tenantId,
|
||||
string moduleCode,
|
||||
CapabilityOperation operation,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalized = moduleCode.Trim().ToLowerInvariant();
|
||||
var moduleExists = await dbContext.ProductModules.AsNoTracking()
|
||||
.AnyAsync(item => item.Code == normalized && item.Status == ProductModuleStatus.Active, cancellationToken);
|
||||
if (!moduleExists)
|
||||
{
|
||||
// Compatibility while the fixed module catalog is introduced module-by-module.
|
||||
return true;
|
||||
}
|
||||
|
||||
var tenantActive = await dbContext.Tenants.AsNoTracking()
|
||||
.AnyAsync(item => item.Id == tenantId && item.Status == TenantStatus.Active, cancellationToken);
|
||||
if (!tenantActive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var overrideMode = await dbContext.TenantModuleOverrides.AsNoTracking()
|
||||
.Where(item => item.TenantId == tenantId && item.ModuleCode == normalized &&
|
||||
(item.ExpiresAt == null || item.ExpiresAt > now))
|
||||
.Select(item => (TenantModuleOverrideMode?)item.Mode)
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
if (overrideMode == TenantModuleOverrideMode.Disabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var subscription = await dbContext.TenantSubscriptions.AsNoTracking()
|
||||
.Where(item => item.TenantId == tenantId)
|
||||
.OrderByDescending(item => item.UpdatedAt)
|
||||
.Select(item => new { item.PlanCode, item.Status, item.StartsAt, item.ExpiresAt })
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (subscription is null || subscription.StartsAt > now || subscription.ExpiresAt <= now)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var entitled = overrideMode == TenantModuleOverrideMode.Enabled ||
|
||||
await dbContext.PlanModuleEntitlements.AsNoTracking().AnyAsync(
|
||||
item => item.PlanCode == subscription.PlanCode && item.ModuleCode == normalized && item.Enabled,
|
||||
cancellationToken);
|
||||
if (!entitled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return operation == CapabilityOperation.Read ||
|
||||
subscription.Status is TenantSubscriptionStatus.Trial or TenantSubscriptionStatus.Active;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlySet<string>> GetEnabledModulesAsync(
|
||||
Guid tenantId,
|
||||
CapabilityOperation operation = CapabilityOperation.Read,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var modules = await dbContext.ProductModules.AsNoTracking()
|
||||
.Where(item => item.Status == ProductModuleStatus.Active)
|
||||
.Select(item => item.Code)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var enabled = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var module in modules)
|
||||
{
|
||||
if (await IsAllowedAsync(tenantId, module, operation, cancellationToken))
|
||||
{
|
||||
enabled.Add(module);
|
||||
}
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user