fix: secure bootstrap administrator creation
This commit is contained in:
parent
09ced1cd32
commit
3f6082a9b1
@ -18,6 +18,7 @@ using IM.Domain.Common;
|
|||||||
using IM.Infrastructure;
|
using IM.Infrastructure;
|
||||||
using Workflow.Domain.Common;
|
using Workflow.Domain.Common;
|
||||||
using BuildFlow.Platform.Application;
|
using BuildFlow.Platform.Application;
|
||||||
|
using BuildFlow.Platform.Infrastructure.Configuration;
|
||||||
using Scripting.Application;
|
using Scripting.Application;
|
||||||
using Scripting.Infrastructure;
|
using Scripting.Infrastructure;
|
||||||
using Monitoring.Application;
|
using Monitoring.Application;
|
||||||
@ -122,6 +123,10 @@ public class BuildFlowHostModule : AbpModule
|
|||||||
// 头像 URL 解析:platform 模块查询用户信息时,把持久化的 objectKey 换算成预签名 URL
|
// 头像 URL 解析:platform 模块查询用户信息时,把持久化的 objectKey 换算成预签名 URL
|
||||||
services.AddScoped<BuildFlow.Platform.Domain.Services.IAvatarUrlResolver, InProcessAvatarUrlResolver>();
|
services.AddScoped<BuildFlow.Platform.Domain.Services.IAvatarUrlResolver, InProcessAvatarUrlResolver>();
|
||||||
|
|
||||||
|
// 首次部署管理员默认关闭,密码只能从外部配置源注入。
|
||||||
|
services.Configure<BootstrapAdminOptions>(
|
||||||
|
config.GetSection(BootstrapAdminOptions.SectionName));
|
||||||
|
|
||||||
// JWT 认证 + CORS:使用共享内核扩展方法(消除重复的认证/跨域配置)
|
// JWT 认证 + CORS:使用共享内核扩展方法(消除重复的认证/跨域配置)
|
||||||
services.AddSharedJwtAuthentication(config);
|
services.AddSharedJwtAuthentication(config);
|
||||||
services.AddSharedCors();
|
services.AddSharedCors();
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
using BuildFlow.Infrastructure.Persistence;
|
using BuildFlow.Infrastructure.Persistence;
|
||||||
using BuildFlow.Platform.Infrastructure.Persistence;
|
using BuildFlow.Platform.Infrastructure.Persistence;
|
||||||
|
using BuildFlow.Platform.Infrastructure.Configuration;
|
||||||
using File.Infrastructure.Persistence;
|
using File.Infrastructure.Persistence;
|
||||||
using IM.Infrastructure.Mongo;
|
using IM.Infrastructure.Mongo;
|
||||||
using IM.Infrastructure.Persistence;
|
using IM.Infrastructure.Persistence;
|
||||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using MongoDB.Entities;
|
using MongoDB.Entities;
|
||||||
using Monitoring.Infrastructure.Persistence;
|
using Monitoring.Infrastructure.Persistence;
|
||||||
using Scripting.Infrastructure.Persistence;
|
using Scripting.Infrastructure.Persistence;
|
||||||
@ -71,13 +73,29 @@ public static class DatabaseInitializer
|
|||||||
logger,
|
logger,
|
||||||
"IM MongoDB 建索引");
|
"IM MongoDB 建索引");
|
||||||
|
|
||||||
// 3. 种子数据:平台(权限/角色/用户/菜单)+ workflow(组件注册)
|
// 3. 平台种子和首次管理员初始化。管理员配置失败不阻断独立的 Workflow 种子。
|
||||||
await SafeRunAsync(
|
await SafeRunAsync(
|
||||||
async () =>
|
async () =>
|
||||||
{
|
{
|
||||||
var platformDb = sp.GetRequiredService<PlatformDbContext>();
|
var platformDb = sp.GetRequiredService<PlatformDbContext>();
|
||||||
await PlatformSeedData.SeedAsync(platformDb);
|
await PlatformSeedData.SeedAsync(platformDb);
|
||||||
|
|
||||||
|
var bootstrapOptions =
|
||||||
|
sp.GetRequiredService<IOptions<BootstrapAdminOptions>>().Value;
|
||||||
|
var adminCreated = await BootstrapAdminSeeder.SeedAsync(
|
||||||
|
platformDb,
|
||||||
|
bootstrapOptions);
|
||||||
|
logger.LogInformation(adminCreated
|
||||||
|
? "Bootstrap admin 已创建"
|
||||||
|
: "Bootstrap admin 已跳过");
|
||||||
|
},
|
||||||
|
logger,
|
||||||
|
"Platform Seed");
|
||||||
|
|
||||||
|
// 4. Workflow 组件注册与历史图数据回填。
|
||||||
|
await SafeRunAsync(
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
var wfDb = sp.GetRequiredService<WorkflowDbContext>();
|
var wfDb = sp.GetRequiredService<WorkflowDbContext>();
|
||||||
await WorkflowSeedData.SeedAsync(wfDb);
|
await WorkflowSeedData.SeedAsync(wfDb);
|
||||||
|
|
||||||
@ -87,7 +105,7 @@ public static class DatabaseInitializer
|
|||||||
logger.LogInformation("Workflow graph migration: {Defs} definitions, {Snaps} snapshots, {Insts} instances backfilled", defs, snaps, insts);
|
logger.LogInformation("Workflow graph migration: {Defs} definitions, {Snaps} snapshots, {Insts} instances backfilled", defs, snaps, insts);
|
||||||
},
|
},
|
||||||
logger,
|
logger,
|
||||||
"Seed");
|
"Workflow Seed");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using BuildFlow.Platform.Application.Auth.DTOs;
|
using BuildFlow.Platform.Application.Auth.DTOs;
|
||||||
|
using BuildFlow.Platform.Domain.Services;
|
||||||
|
|
||||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||||
|
|
||||||
@ -18,7 +19,10 @@ public class RegisterRequestValidator : Validator<RegisterRequest>
|
|||||||
.EmailAddress().WithMessage("邮箱格式不正确");
|
.EmailAddress().WithMessage("邮箱格式不正确");
|
||||||
|
|
||||||
RuleFor(x => x.Password)
|
RuleFor(x => x.Password)
|
||||||
.NotEmpty().WithMessage("密码不能为空")
|
.Custom((password, context) =>
|
||||||
.Length(6, 100).WithMessage("密码长度 6-100 个字符");
|
{
|
||||||
|
var error = PasswordPolicy.GetValidationError(password);
|
||||||
|
if (error is not null) context.AddFailure(error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"MongoDb": "mongodb://appuser:YOUR_MONGO_PASSWORD@YOUR_HOST:17017/im_system?authSource=app",
|
"MongoDb": "mongodb://appuser:YOUR_MONGO_PASSWORD@YOUR_HOST:17017/im_system?authSource=app",
|
||||||
"Redis": "YOUR_HOST:16379,password=YOUR_REDIS_PASSWORD,abortConnect=false"
|
"Redis": "YOUR_HOST:16379,password=YOUR_REDIS_PASSWORD,abortConnect=false"
|
||||||
},
|
},
|
||||||
|
"Platform": {
|
||||||
|
"BootstrapAdmin": {
|
||||||
|
"Enabled": false,
|
||||||
|
"Username": "admin",
|
||||||
|
"Email": "admin@your-company.example",
|
||||||
|
"Password": "YOUR_STRONG_BOOTSTRAP_PASSWORD"
|
||||||
|
}
|
||||||
|
},
|
||||||
"RabbitMq": {
|
"RabbitMq": {
|
||||||
"Host": "YOUR_HOST",
|
"Host": "YOUR_HOST",
|
||||||
"Username": "rag",
|
"Username": "rag",
|
||||||
|
|||||||
@ -34,6 +34,14 @@
|
|||||||
"Platform": "platform",
|
"Platform": "platform",
|
||||||
"Monitoring": "monitoring"
|
"Monitoring": "monitoring"
|
||||||
},
|
},
|
||||||
|
"Platform": {
|
||||||
|
"BootstrapAdmin": {
|
||||||
|
"Enabled": false,
|
||||||
|
"Username": "admin",
|
||||||
|
"Email": "admin@rag.local",
|
||||||
|
"Password": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"RabbitMq": {
|
"RabbitMq": {
|
||||||
"Host": "{INFRA_HOST}",
|
"Host": "{INFRA_HOST}",
|
||||||
"Port": 25672,
|
"Port": 25672,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using BuildFlow.Platform.Application.Auth.Commands;
|
using BuildFlow.Platform.Application.Auth.Commands;
|
||||||
|
using BuildFlow.Platform.Domain.Services;
|
||||||
|
|
||||||
namespace BuildFlow.Platform.Application.Auth.Validators;
|
namespace BuildFlow.Platform.Application.Auth.Validators;
|
||||||
|
|
||||||
@ -16,11 +17,11 @@ public class RegisterCommandValidator : AbstractValidator<RegisterCommand>
|
|||||||
RuleFor(x => x.Email).NotEmpty().WithMessage("邮箱不能为空")
|
RuleFor(x => x.Email).NotEmpty().WithMessage("邮箱不能为空")
|
||||||
.EmailAddress().WithMessage("邮箱格式不正确");
|
.EmailAddress().WithMessage("邮箱格式不正确");
|
||||||
|
|
||||||
RuleFor(x => x.Password).NotEmpty().WithMessage("密码不能为空")
|
RuleFor(x => x.Password)
|
||||||
.MinimumLength(8).WithMessage("密码至少 8 个字符")
|
.Custom((password, context) =>
|
||||||
.MaximumLength(100).WithMessage("密码最多 100 个字符")
|
{
|
||||||
.Matches("[A-Z]").WithMessage("密码必须包含至少一个大写字母")
|
var error = PasswordPolicy.GetValidationError(password);
|
||||||
.Matches("[a-z]").WithMessage("密码必须包含至少一个小写字母")
|
if (error is not null) context.AddFailure(error);
|
||||||
.Matches("[0-9]").WithMessage("密码必须包含至少一个数字");
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
27
src/modules/platform/Domain/Services/PasswordPolicy.cs
Normal file
27
src/modules/platform/Domain/Services/PasswordPolicy.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
namespace BuildFlow.Platform.Domain.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 平台账号统一密码强度规则,供注册和首次部署管理员初始化共同使用。
|
||||||
|
/// </summary>
|
||||||
|
public static class PasswordPolicy
|
||||||
|
{
|
||||||
|
/// <summary>密码最小长度。</summary>
|
||||||
|
public const int MinimumLength = 8;
|
||||||
|
|
||||||
|
/// <summary>密码最大长度。</summary>
|
||||||
|
public const int MaximumLength = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回首个密码强度错误;返回 <see langword="null"/> 表示密码符合要求。
|
||||||
|
/// </summary>
|
||||||
|
public static string? GetValidationError(string? password)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(password)) return "密码不能为空";
|
||||||
|
if (password.Length < MinimumLength) return "密码至少 8 个字符";
|
||||||
|
if (password.Length > MaximumLength) return "密码最多 100 个字符";
|
||||||
|
if (!password.Any(c => c is >= 'A' and <= 'Z')) return "密码必须包含至少一个大写字母";
|
||||||
|
if (!password.Any(c => c is >= 'a' and <= 'z')) return "密码必须包含至少一个小写字母";
|
||||||
|
if (!password.Any(c => c is >= '0' and <= '9')) return "密码必须包含至少一个数字";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
namespace BuildFlow.Platform.Infrastructure.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首次部署管理员初始化配置。密码必须由环境变量、Secrets 或 Consul 注入。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BootstrapAdminOptions
|
||||||
|
{
|
||||||
|
/// <summary>配置节名称。</summary>
|
||||||
|
public const string SectionName = "Platform:BootstrapAdmin";
|
||||||
|
|
||||||
|
/// <summary>是否启用首次部署管理员初始化。</summary>
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>首次创建的管理员用户名。</summary>
|
||||||
|
public string Username { get; set; } = "admin";
|
||||||
|
|
||||||
|
/// <summary>首次创建的管理员邮箱。</summary>
|
||||||
|
public string Email { get; set; } = "admin@rag.local";
|
||||||
|
|
||||||
|
/// <summary>由外部安全配置源提供的管理员明文初始密码。</summary>
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
using BuildFlow.Platform.Domain.Entities.User;
|
||||||
|
using BuildFlow.Platform.Domain.Entities.UserRole;
|
||||||
|
using BuildFlow.Platform.Domain.Services;
|
||||||
|
using BuildFlow.Platform.Infrastructure.Configuration;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace BuildFlow.Platform.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按部署配置幂等初始化最高权限管理员,已存在管理员时绝不修改凭据。
|
||||||
|
/// </summary>
|
||||||
|
public static class BootstrapAdminSeeder
|
||||||
|
{
|
||||||
|
/// <summary>平台最高权限 Admin 角色的固定标识。</summary>
|
||||||
|
public static readonly Guid AdminRoleId =
|
||||||
|
Guid.Parse("20000000-0000-0000-0000-000000000002");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行管理员初始化;返回 true 表示本次创建,false 表示功能关闭或已有管理员。
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<bool> SeedAsync(
|
||||||
|
PlatformDbContext db,
|
||||||
|
BootstrapAdminOptions options,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!options.Enabled) return false;
|
||||||
|
|
||||||
|
var existingAdminId = await db.UserRoles
|
||||||
|
.Where(x => x.RoleId == AdminRoleId)
|
||||||
|
.Select(x => x.UserId)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
if (existingAdminId != Guid.Empty) return false;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(options.Username))
|
||||||
|
throw new InvalidOperationException("BootstrapAdmin 用户名不能为空");
|
||||||
|
if (string.IsNullOrWhiteSpace(options.Email))
|
||||||
|
throw new InvalidOperationException("BootstrapAdmin 邮箱不能为空");
|
||||||
|
|
||||||
|
var passwordError = PasswordPolicy.GetValidationError(options.Password);
|
||||||
|
if (passwordError is not null)
|
||||||
|
throw new InvalidOperationException($"BootstrapAdmin 配置无效:{passwordError}");
|
||||||
|
|
||||||
|
if (!await db.Roles.AnyAsync(x => x.Id == AdminRoleId, ct))
|
||||||
|
throw new InvalidOperationException("BootstrapAdmin 初始化失败:Admin 角色不存在");
|
||||||
|
|
||||||
|
var conflictingUser = await db.Users.FirstOrDefaultAsync(
|
||||||
|
x => x.Username == options.Username || x.Email == options.Email,
|
||||||
|
ct);
|
||||||
|
if (conflictingUser is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"BootstrapAdmin 用户名或邮箱已被非管理员账号占用");
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = new UserEntity
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Username = options.Username,
|
||||||
|
Email = options.Email,
|
||||||
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword(options.Password),
|
||||||
|
};
|
||||||
|
await db.Users.AddAsync(user, ct);
|
||||||
|
await db.UserRoles.AddAsync(new UserRoleEntity
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
RoleId = AdminRoleId,
|
||||||
|
}, ct);
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -51,35 +51,6 @@ public static class SeedData
|
|||||||
// 方法已实现:先把 SuperAdmin 用户迁移到 Admin,再清理关联并软删除该角色。幂等。
|
// 方法已实现:先把 SuperAdmin 用户迁移到 Admin,再清理关联并软删除该角色。幂等。
|
||||||
await PurgeDeprecatedSuperAdminRoleAsync(db);
|
await PurgeDeprecatedSuperAdminRoleAsync(db);
|
||||||
|
|
||||||
// 管理员用户
|
|
||||||
var adminUserId = Guid.Parse("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
|
||||||
var existingAdmin = await db.Users.FirstOrDefaultAsync(u => u.Id == adminUserId);
|
|
||||||
if (existingAdmin == null)
|
|
||||||
{
|
|
||||||
existingAdmin = await db.Users.FirstOrDefaultAsync(u => u.Username == "admin");
|
|
||||||
if (existingAdmin == null)
|
|
||||||
{
|
|
||||||
existingAdmin = new UserEntity
|
|
||||||
{
|
|
||||||
Id = adminUserId,
|
|
||||||
Username = "admin",
|
|
||||||
Email = "admin@rag.local",
|
|
||||||
PasswordHash = BCrypt.Net.BCrypt.HashPassword("admin123")
|
|
||||||
};
|
|
||||||
await db.Users.AddAsync(existingAdmin);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保管理员有 Admin 角色(最高权限)
|
|
||||||
var adminRoleForUser = Guid.Parse("20000000-0000-0000-0000-000000000002");
|
|
||||||
var hasRole = await db.UserRoles.AnyAsync(ur => ur.UserId == existingAdmin.Id && ur.RoleId == adminRoleForUser);
|
|
||||||
if (!hasRole)
|
|
||||||
{
|
|
||||||
await db.UserRoles.AddAsync(new UserRoleEntity { UserId = existingAdmin.Id, RoleId = adminRoleForUser });
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
// IM 演示用户:ID 与 im-system SeedData 保持一致,供通讯录和 Auth gRPC 查询使用
|
// IM 演示用户:ID 与 im-system SeedData 保持一致,供通讯录和 Auth gRPC 查询使用
|
||||||
await SeedImDemoUsersAsync(db);
|
await SeedImDemoUsersAsync(db);
|
||||||
}
|
}
|
||||||
|
|||||||
104
tests/BuildFlow.Platform.Tests/Auth/BootstrapAdminSeederTests.cs
Normal file
104
tests/BuildFlow.Platform.Tests/Auth/BootstrapAdminSeederTests.cs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
using BuildFlow.Platform.Domain.Entities.Role;
|
||||||
|
using BuildFlow.Platform.Infrastructure.Configuration;
|
||||||
|
using BuildFlow.Platform.Infrastructure.Persistence;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BuildFlow.Platform.Tests.Auth;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首次部署管理员初始化安全边界测试。
|
||||||
|
/// </summary>
|
||||||
|
public class BootstrapAdminSeederTests
|
||||||
|
{
|
||||||
|
private static BootstrapAdminOptions ValidOptions(string password = "BootstrapAdmin1") => new()
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Username = "bootstrap-admin",
|
||||||
|
Email = "bootstrap-admin@test.local",
|
||||||
|
Password = password,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static async Task SeedAdminRoleAsync(PlatformDbContext db)
|
||||||
|
{
|
||||||
|
db.Roles.Add(PlatformTestDb.WithAudit(new RoleEntity
|
||||||
|
{
|
||||||
|
Id = BootstrapAdminSeeder.AdminRoleId,
|
||||||
|
Name = "Admin",
|
||||||
|
Description = "管理员",
|
||||||
|
}));
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DisabledShouldNotCreateAUser()
|
||||||
|
{
|
||||||
|
await using var db = PlatformTestDb.CreateDbContext();
|
||||||
|
|
||||||
|
var created = await BootstrapAdminSeeder.SeedAsync(
|
||||||
|
db,
|
||||||
|
new BootstrapAdminOptions());
|
||||||
|
|
||||||
|
created.Should().BeFalse();
|
||||||
|
db.Users.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EnabledWithoutPasswordShouldFailClearly()
|
||||||
|
{
|
||||||
|
await using var db = PlatformTestDb.CreateDbContext();
|
||||||
|
var options = ValidOptions("");
|
||||||
|
|
||||||
|
var act = () => BootstrapAdminSeeder.SeedAsync(db, options);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||||
|
.WithMessage("*密码不能为空*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WeakPasswordShouldFailClearly()
|
||||||
|
{
|
||||||
|
await using var db = PlatformTestDb.CreateDbContext();
|
||||||
|
|
||||||
|
var act = () => BootstrapAdminSeeder.SeedAsync(db, ValidOptions("weakpass"));
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||||
|
.WithMessage("*大写字母*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FirstRunShouldCreateAnAdminWithTheConfiguredPassword()
|
||||||
|
{
|
||||||
|
await using var db = PlatformTestDb.CreateDbContext();
|
||||||
|
await SeedAdminRoleAsync(db);
|
||||||
|
var options = ValidOptions();
|
||||||
|
|
||||||
|
var created = await BootstrapAdminSeeder.SeedAsync(db, options);
|
||||||
|
|
||||||
|
created.Should().BeTrue();
|
||||||
|
var user = await db.Users.SingleAsync();
|
||||||
|
user.Username.Should().Be(options.Username);
|
||||||
|
BCrypt.Net.BCrypt.Verify(options.Password, user.PasswordHash).Should().BeTrue();
|
||||||
|
(await db.UserRoles.CountAsync(x =>
|
||||||
|
x.UserId == user.Id && x.RoleId == BootstrapAdminSeeder.AdminRoleId))
|
||||||
|
.Should().Be(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RepeatedRunShouldNotResetTheExistingAdminPassword()
|
||||||
|
{
|
||||||
|
await using var db = PlatformTestDb.CreateDbContext();
|
||||||
|
await SeedAdminRoleAsync(db);
|
||||||
|
await BootstrapAdminSeeder.SeedAsync(db, ValidOptions("BootstrapAdmin1"));
|
||||||
|
var originalHash = await db.Users.Select(x => x.PasswordHash).SingleAsync();
|
||||||
|
|
||||||
|
var created = await BootstrapAdminSeeder.SeedAsync(
|
||||||
|
db,
|
||||||
|
ValidOptions("DifferentAdmin2"));
|
||||||
|
|
||||||
|
created.Should().BeFalse();
|
||||||
|
(await db.Users.Select(x => x.PasswordHash).SingleAsync()).Should().Be(originalHash);
|
||||||
|
BCrypt.Net.BCrypt.Verify("BootstrapAdmin1", originalHash).Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
27
tests/BuildFlow.Platform.Tests/Auth/PasswordPolicyTests.cs
Normal file
27
tests/BuildFlow.Platform.Tests/Auth/PasswordPolicyTests.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using BuildFlow.Platform.Domain.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BuildFlow.Platform.Tests.Auth;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 平台统一密码强度策略测试。
|
||||||
|
/// </summary>
|
||||||
|
public class PasswordPolicyTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData("short1A")]
|
||||||
|
[InlineData("alllowercase1")]
|
||||||
|
[InlineData("ALLUPPERCASE1")]
|
||||||
|
[InlineData("NoDigitsHere")]
|
||||||
|
public void InvalidPasswordsShouldReturnAnError(string password)
|
||||||
|
{
|
||||||
|
Assert.NotNull(PasswordPolicy.GetValidationError(password));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StrongPasswordShouldPass()
|
||||||
|
{
|
||||||
|
Assert.Null(PasswordPolicy.GetValidationError("BootstrapAdmin1"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user