forked from xiongyuxing/tiku-backend.net
feat: harden SaaS authentication and authorization
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Backoffice;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Tenancy;
|
||||
@@ -14,19 +15,23 @@ internal sealed class BackofficeService(
|
||||
{
|
||||
private static readonly BuiltinPermission[] BuiltinPermissions =
|
||||
[
|
||||
new("tenant:dashboard:view", "租户总览", BackendPermissionArea.Tenant, "tenant_dashboard"),
|
||||
new("tenant:staff:manage", "租户员工管理", BackendPermissionArea.Tenant, "tenant_staff"),
|
||||
new("tenant:role:manage", "租户角色权限管理", BackendPermissionArea.Tenant, "tenant_staff"),
|
||||
new("tenant:student:manage", "学生与班级管理", BackendPermissionArea.Tenant, "tenant_student"),
|
||||
new("tenant:content:manage", "租户内容管理", BackendPermissionArea.Tenant, "tenant_content"),
|
||||
new("tenant:provider:manage", "租户外部服务配置", BackendPermissionArea.Tenant, "tenant_provider"),
|
||||
new("tenant:commerce:operate", "租户交易运营", BackendPermissionArea.Tenant, "tenant_commerce"),
|
||||
new("platform:dashboard:view", "平台总览", BackendPermissionArea.Platform, "platform_dashboard"),
|
||||
new("platform:tenant:manage", "平台租户管理", BackendPermissionArea.Platform, "platform_tenant"),
|
||||
new("platform:staff:manage", "平台员工管理", BackendPermissionArea.Platform, "platform_staff"),
|
||||
new("platform:role:manage", "平台角色权限管理", BackendPermissionArea.Platform, "platform_staff"),
|
||||
new("platform:question-bank:manage", "平台公共题库运营", BackendPermissionArea.Platform, "platform_content"),
|
||||
new("platform:audit:view", "平台审计查询", BackendPermissionArea.Platform, "platform_audit"),
|
||||
new(BackendPermissions.TenantDashboardView, "租户总览", BackendPermissionArea.Tenant, "tenant_dashboard"),
|
||||
new(BackendPermissions.TenantStaffManage, "租户员工管理", BackendPermissionArea.Tenant, "tenant_staff"),
|
||||
new(BackendPermissions.TenantRoleManage, "租户角色权限管理", BackendPermissionArea.Tenant, "tenant_staff"),
|
||||
new(BackendPermissions.TenantStudentManage, "学生与班级管理", BackendPermissionArea.Tenant, "tenant_student"),
|
||||
new(BackendPermissions.TenantContentManage, "租户内容管理", BackendPermissionArea.Tenant, "tenant_content"),
|
||||
new(BackendPermissions.TenantSettingsManage, "租户设置管理", BackendPermissionArea.Tenant, "tenant_settings"),
|
||||
new(BackendPermissions.TenantProviderManage, "租户外部服务配置", BackendPermissionArea.Tenant, "tenant_provider"),
|
||||
new(BackendPermissions.TenantCommerceOperate, "租户交易运营", BackendPermissionArea.Tenant, "tenant_commerce"),
|
||||
new(BackendPermissions.TenantCrmManage, "租户客户管理", BackendPermissionArea.Tenant, "tenant_crm"),
|
||||
new(BackendPermissions.TenantCommissionManage, "租户佣金管理", BackendPermissionArea.Tenant, "tenant_commission"),
|
||||
new(BackendPermissions.TenantJobManage, "租户任务管理", BackendPermissionArea.Tenant, "tenant_job"),
|
||||
new(BackendPermissions.PlatformDashboardView, "平台总览", BackendPermissionArea.Platform, "platform_dashboard"),
|
||||
new(BackendPermissions.PlatformTenantManage, "平台租户管理", BackendPermissionArea.Platform, "platform_tenant"),
|
||||
new(BackendPermissions.PlatformStaffManage, "平台员工管理", BackendPermissionArea.Platform, "platform_staff"),
|
||||
new(BackendPermissions.PlatformRoleManage, "平台角色权限管理", BackendPermissionArea.Platform, "platform_staff"),
|
||||
new(BackendPermissions.PlatformQuestionBankManage, "平台公共题库运营", BackendPermissionArea.Platform, "platform_content"),
|
||||
new(BackendPermissions.PlatformAuditView, "平台审计查询", BackendPermissionArea.Platform, "platform_audit"),
|
||||
new("commerce:refund:approve", "退款审核", BackendPermissionArea.Both, "commerce"),
|
||||
new("commerce:reconciliation:manage", "对账管理", BackendPermissionArea.Both, "commerce"),
|
||||
new("commerce:adjustment:manage", "调账管理", BackendPermissionArea.Both, "commerce")
|
||||
@@ -47,6 +52,43 @@ internal sealed class BackofficeService(
|
||||
new("platform.audit", null, "平台审计", BackendPermissionArea.Platform, "/platform/audit", "platform:audit:view", 50)
|
||||
];
|
||||
|
||||
public async Task<BackofficeUiBootstrap> GetTenantUiBootstrapAsync(
|
||||
CurrentAccessSnapshot access,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!access.IsUserActive || !access.IsCurrentTenantMember ||
|
||||
access.UserId is null || access.TenantId is null)
|
||||
{
|
||||
throw new BackofficeException("Tenant backoffice access is denied.", "tenant_access_denied");
|
||||
}
|
||||
|
||||
await EnsureCatalogAsync(cancellationToken);
|
||||
var permissionCodes = access.TenantPermissions.Order(StringComparer.Ordinal).ToArray();
|
||||
var menus = await LoadEffectiveMenusAsync(
|
||||
BackendPermissionArea.Tenant,
|
||||
permissionCodes,
|
||||
cancellationToken);
|
||||
return new BackofficeUiBootstrap(permissionCodes, menus);
|
||||
}
|
||||
|
||||
public async Task<BackofficeUiBootstrap> GetPlatformUiBootstrapAsync(
|
||||
CurrentAccessSnapshot access,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!access.IsUserActive || access.UserId is null || access.PlatformPermissions.Count == 0)
|
||||
{
|
||||
throw new BackofficeException("Platform backoffice access is denied.", "platform_access_denied");
|
||||
}
|
||||
|
||||
await EnsureCatalogAsync(cancellationToken);
|
||||
var permissionCodes = access.PlatformPermissions.Order(StringComparer.Ordinal).ToArray();
|
||||
var menus = await LoadEffectiveMenusAsync(
|
||||
BackendPermissionArea.Platform,
|
||||
permissionCodes,
|
||||
cancellationToken);
|
||||
return new BackofficeUiBootstrap(permissionCodes, menus);
|
||||
}
|
||||
|
||||
public async Task<BackofficeBootstrap> GetTenantBootstrapAsync(
|
||||
BackofficeActor actor,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -193,6 +235,21 @@ internal sealed class BackofficeService(
|
||||
{
|
||||
var tenantId = RequireTenantAdmin(actor);
|
||||
var roleIds = command.RoleIds.Distinct().ToArray();
|
||||
var ownerRoleId = await dbContext.TenantBackendRoles
|
||||
.Where(item => item.TenantId == tenantId && item.Code == "tenant_owner" && item.IsSystem)
|
||||
.Select(item => (Guid?)item.Id)
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
var isActiveOwner = await dbContext.TenantMemberships.AnyAsync(
|
||||
item => item.TenantId == tenantId &&
|
||||
item.UserId == command.UserId &&
|
||||
item.Role == TenantRole.TenantOwner &&
|
||||
item.Status == MembershipStatus.Active,
|
||||
cancellationToken);
|
||||
if (isActiveOwner && ownerRoleId.HasValue && !roleIds.Contains(ownerRoleId.Value))
|
||||
{
|
||||
throw new BackofficeException("Tenant owner system role cannot be removed.", "system_role_locked");
|
||||
}
|
||||
|
||||
var count = await dbContext.TenantBackendRoles.CountAsync(
|
||||
item => item.TenantId == tenantId && roleIds.Contains(item.Id) && item.Status == BackendRoleStatus.Active,
|
||||
cancellationToken);
|
||||
@@ -329,6 +386,21 @@ internal sealed class BackofficeService(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<BackofficeMenuItem[]> LoadEffectiveMenusAsync(
|
||||
BackendPermissionArea area,
|
||||
IReadOnlyCollection<string> permissionCodes,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var codes = permissionCodes.ToArray();
|
||||
var menus = await dbContext.BackendMenus.AsNoTracking()
|
||||
.Where(item => item.IsActive && item.Area == area &&
|
||||
(item.PermissionCode == null || codes.Contains(item.PermissionCode)))
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.ThenBy(item => item.Code)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
return menus.Select(ToMenuItem).ToArray();
|
||||
}
|
||||
|
||||
private async Task ValidateMenuCodesAsync(string[] codes, BackendPermissionArea area, CancellationToken cancellationToken)
|
||||
{
|
||||
var count = await dbContext.BackendMenus.CountAsync(
|
||||
|
||||
Reference in New Issue
Block a user