feat(security): complete capability messaging workflows

This commit is contained in:
2026-07-29 11:21:15 +08:00
parent df88fa19cb
commit 5c4de4b282
35 changed files with 19544 additions and 89 deletions

View File

@@ -324,6 +324,124 @@ internal sealed class PlatformAdminService(
}, cancellationToken);
}
public async Task<PlatformPlanModuleEntitlements> ReplacePlanModulesAsync(
PlatformAdminActor actor,
ReplacePlatformPlanModulesCommand command,
CancellationToken cancellationToken = default)
{
await AssertPlatformPermissionAsync(actor, BackendPermissions.PlatformTenantManage, cancellationToken);
return await ExecuteSystemAsync("platform plan module entitlements replace", async (provider, dbContext) =>
{
var planCode = NormalizeCode(command.PlanCode);
var plan = await dbContext.PlatformSaasPlans.SingleOrDefaultAsync(
item => item.Code == planCode,
cancellationToken) ?? throw new PlatformAdminException("SaaS plan was not found.", "plan_not_found");
var moduleCodes = command.ModuleCodes
.Select(NormalizeCode)
.Distinct(StringComparer.Ordinal)
.Order(StringComparer.Ordinal)
.ToArray();
var invalidModules = moduleCodes.Where(module => !ProductModuleCatalog.Contains(module)).ToArray();
if (invalidModules.Length > 0)
{
throw new PlatformAdminException("One or more product modules are invalid.", "product_module_invalid");
}
var existing = await dbContext.PlanModuleEntitlements
.Where(item => item.PlanCode == planCode)
.ToArrayAsync(cancellationToken);
var changedModules = existing.Select(item => item.ModuleCode)
.Concat(moduleCodes)
.Distinct(StringComparer.Ordinal)
.ToArray();
dbContext.PlanModuleEntitlements.RemoveRange(existing);
dbContext.PlanModuleEntitlements.AddRange(moduleCodes.Select(moduleCode => new PlanModuleEntitlement
{
PlanCode = planCode,
ModuleCode = moduleCode,
Enabled = true
}));
AddAudit(dbContext, actor, "platform.plan.modules.replaced", plan.Id, new
{
planCode,
previousModuleCodes = existing.Select(item => item.ModuleCode).Order(StringComparer.Ordinal),
moduleCodes
});
var tenantIds = await dbContext.TenantSubscriptions.AsNoTracking()
.Where(item => item.PlanCode == planCode)
.Select(item => item.TenantId)
.Distinct()
.ToArrayAsync(cancellationToken);
var publisher = provider.GetRequiredService<ISecurityEventPublisher>();
var version = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
foreach (var tenantId in tenantIds)
{
foreach (var moduleCode in changedModules)
{
await publisher.CapabilityChangedAsync(
tenantId, moduleCode, "plan_entitlements_changed", version,
$"plan-modules-{plan.Id:N}", cancellationToken);
}
}
await dbContext.SaveChangesAsync(cancellationToken);
return new PlatformPlanModuleEntitlements(planCode, moduleCodes);
}, cancellationToken);
}
public async Task<PlatformTenantModuleOverrideItem> UpsertTenantModuleOverrideAsync(
PlatformAdminActor actor,
UpsertPlatformTenantModuleOverrideCommand command,
CancellationToken cancellationToken = default)
{
await AssertPlatformPermissionAsync(actor, BackendPermissions.PlatformTenantManage, cancellationToken);
return await ExecuteSystemAsync("platform tenant module override upsert", async (provider, dbContext) =>
{
await RequireTenantAsync(dbContext, command.TenantId, cancellationToken);
var moduleCode = NormalizeCode(command.ModuleCode);
if (!ProductModuleCatalog.Contains(moduleCode))
{
throw new PlatformAdminException("Product module was not found.", "product_module_not_found");
}
var reason = command.Reason.Trim();
if (string.IsNullOrWhiteSpace(reason))
{
throw new PlatformAdminException("Module override reason is required.", "module_override_reason_required");
}
var item = await dbContext.TenantModuleOverrides.SingleOrDefaultAsync(
value => value.TenantId == command.TenantId && value.ModuleCode == moduleCode,
cancellationToken);
if (item is null)
{
item = new TenantModuleOverride { TenantId = command.TenantId, ModuleCode = moduleCode };
dbContext.TenantModuleOverrides.Add(item);
}
var previousMode = item.Mode;
item.Mode = command.Mode;
item.ExpiresAt = command.ExpiresAt;
item.Reason = reason;
AddAudit(dbContext, actor, "platform.tenant.module_override.updated", command.TenantId, new
{
moduleCode,
previousMode,
item.Mode,
item.ExpiresAt,
reason
});
await provider.GetRequiredService<ISecurityEventPublisher>().CapabilityChangedAsync(
command.TenantId,
moduleCode,
"tenant_module_override_changed",
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
$"tenant-module-override-{item.Id:N}",
cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return new PlatformTenantModuleOverrideItem(
item.TenantId, item.ModuleCode, item.Mode, item.ExpiresAt, item.Reason);
}, cancellationToken);
}
public async Task<PlatformDomainList> GetDomainsAsync(
PlatformAdminActor actor,
PlatformAdminQuery query,
@@ -768,7 +886,8 @@ internal sealed class PlatformAdminService(
SystemScopeCallerType.Platform,
nameof(PlatformAdminService),
reason,
Guid.NewGuid().ToString("N")),
Guid.NewGuid().ToString("N"),
IsGlobal: true),
async (provider, _) => await operation(provider, provider.GetRequiredService<TikuDbContext>()),
cancellationToken);
}