feat: harden SaaS authentication and authorization
This commit is contained in:
161
Tiku.Infrastructure/Bootstrap/PlatformAdminBootstrapper.cs
Normal file
161
Tiku.Infrastructure/Bootstrap/PlatformAdminBootstrapper.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using System.Data;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Bootstrap;
|
||||
|
||||
public sealed record PlatformAdminBootstrapOptions(
|
||||
string Email,
|
||||
string TemporaryPassword,
|
||||
string? DisplayName = null);
|
||||
|
||||
public sealed record PlatformAdminBootstrapResult(Guid UserId, Guid RoleId, string Email);
|
||||
|
||||
public sealed class PlatformAdminBootstrapper(
|
||||
TikuDbContext dbContext,
|
||||
UserManager<User> userManager)
|
||||
{
|
||||
public const string SuperAdminRoleCode = "platform_super_admin";
|
||||
|
||||
public async Task<PlatformAdminBootstrapResult> BootstrapAsync(
|
||||
PlatformAdminBootstrapOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
var email = options.Email.Trim();
|
||||
if (email.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("Platform administrator email is required.", nameof(options));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.TemporaryPassword))
|
||||
{
|
||||
throw new ArgumentException("Platform administrator temporary password is required.", nameof(options));
|
||||
}
|
||||
|
||||
IDbContextTransaction? transaction = null;
|
||||
if (dbContext.Database.IsRelational())
|
||||
{
|
||||
transaction = await dbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable, cancellationToken);
|
||||
}
|
||||
|
||||
await using (transaction)
|
||||
{
|
||||
var existingAdministrator = await (
|
||||
from binding in dbContext.PlatformBackendUserRoles.AsNoTracking()
|
||||
join boundRole in dbContext.PlatformBackendRoles.AsNoTracking() on binding.RoleId equals boundRole.Id
|
||||
join boundUser in dbContext.Users.AsNoTracking() on binding.UserId equals boundUser.Id
|
||||
where boundRole.Status == BackendRoleStatus.Active && boundUser.Status == UserStatus.Active
|
||||
select boundUser.Id)
|
||||
.AnyAsync(cancellationToken);
|
||||
if (existingAdministrator)
|
||||
{
|
||||
throw new PlatformAdminBootstrapException(
|
||||
"A platform administrator already exists. Bootstrap is a one-time operation.",
|
||||
"platform_admin_already_exists");
|
||||
}
|
||||
|
||||
var normalizedEmail = userManager.NormalizeEmail(email);
|
||||
if (await dbContext.Users.AsNoTracking().AnyAsync(
|
||||
user => user.NormalizedEmail == normalizedEmail || user.NormalizedUserName == normalizedEmail,
|
||||
cancellationToken))
|
||||
{
|
||||
throw new PlatformAdminBootstrapException(
|
||||
"The bootstrap email is already assigned to a user.",
|
||||
"bootstrap_user_already_exists");
|
||||
}
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Email = email,
|
||||
UserName = email,
|
||||
Name = string.IsNullOrWhiteSpace(options.DisplayName) ? "Platform Administrator" : options.DisplayName.Trim(),
|
||||
EmailConfirmed = true,
|
||||
Status = UserStatus.Active,
|
||||
ForcePasswordChange = true,
|
||||
TwoFactorEnabled = false
|
||||
};
|
||||
var createResult = await userManager.CreateAsync(user, options.TemporaryPassword);
|
||||
if (!createResult.Succeeded)
|
||||
{
|
||||
var errors = string.Join(", ", createResult.Errors.Select(error => $"{error.Code}: {error.Description}"));
|
||||
throw new PlatformAdminBootstrapException(
|
||||
$"Platform administrator could not be created: {errors}",
|
||||
"bootstrap_user_invalid");
|
||||
}
|
||||
|
||||
var role = new PlatformBackendRole
|
||||
{
|
||||
Code = SuperAdminRoleCode,
|
||||
Name = "Platform Super Administrator",
|
||||
Description = "Built-in role with all platform permissions. Created by the one-time bootstrap command.",
|
||||
Status = BackendRoleStatus.Active,
|
||||
IsSystem = true
|
||||
};
|
||||
dbContext.PlatformBackendRoles.Add(role);
|
||||
|
||||
var platformPermissionCodes = BackendPermissions.Platform.ToArray();
|
||||
var existingPermissionCodes = await dbContext.BackendPermissions
|
||||
.Where(permission => platformPermissionCodes.Contains(permission.Code))
|
||||
.Select(permission => permission.Code)
|
||||
.ToHashSetAsync(StringComparer.Ordinal, cancellationToken);
|
||||
foreach (var permissionCode in platformPermissionCodes.Where(code => !existingPermissionCodes.Contains(code)))
|
||||
{
|
||||
dbContext.BackendPermissions.Add(new BackendPermission
|
||||
{
|
||||
Code = permissionCode,
|
||||
Name = permissionCode,
|
||||
Area = BackendPermissionArea.Platform,
|
||||
Module = "platform",
|
||||
Description = "Built-in platform permission.",
|
||||
IsSystem = true
|
||||
});
|
||||
}
|
||||
|
||||
dbContext.PlatformBackendRolePermissions.AddRange(
|
||||
platformPermissionCodes.Select(permissionCode => new PlatformBackendRolePermission
|
||||
{
|
||||
RoleId = role.Id,
|
||||
PermissionCode = permissionCode
|
||||
}));
|
||||
dbContext.PlatformBackendUserRoles.Add(new PlatformBackendUserRole
|
||||
{
|
||||
UserId = user.Id,
|
||||
RoleId = role.Id
|
||||
});
|
||||
dbContext.AuditLogs.Add(new AuditLog
|
||||
{
|
||||
ActorUserId = user.Id,
|
||||
Action = "platform.bootstrap_admin.created",
|
||||
TargetType = "users",
|
||||
TargetId = user.Id.ToString(),
|
||||
Details = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
user.Email,
|
||||
RoleCode = SuperAdminRoleCode,
|
||||
ForcePasswordChange = true,
|
||||
MfaEnrollmentRequired = true
|
||||
})
|
||||
});
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
if (transaction is not null)
|
||||
{
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return new PlatformAdminBootstrapResult(user.Id, role.Id, email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PlatformAdminBootstrapException(string message, string code) : InvalidOperationException(message)
|
||||
{
|
||||
public string Code { get; } = code;
|
||||
}
|
||||
Reference in New Issue
Block a user