feat(backend): Implement complete RBAC system (Day 5 Phase 2)

Implemented Role-Based Access Control (RBAC) with 5 tenant-level roles following Clean Architecture principles.

Changes:
- Created TenantRole enum (TenantOwner, TenantAdmin, TenantMember, TenantGuest, AIAgent)
- Created UserTenantRole entity with repository pattern
- Updated JWT service to include role claims (tenant_role, role)
- Updated RegisterTenant to auto-assign TenantOwner role
- Updated Login to query and include user role in JWT
- Updated RefreshToken to preserve role claims
- Added authorization policies in Program.cs (RequireTenantOwner, RequireTenantAdmin, etc.)
- Updated /api/auth/me endpoint to return role information
- Created EF Core migration for user_tenant_roles table
- Applied database migration successfully

Database:
- New table: identity.user_tenant_roles
- Columns: id, user_id, tenant_id, role, assigned_at, assigned_by_user_id
- Indexes: user_id, tenant_id, role, unique(user_id, tenant_id)
- Foreign keys: CASCADE on user and tenant deletion

Testing:
- Created test-rbac.ps1 PowerShell script
- All RBAC tests passing
- JWT tokens contain role claims
- Role persists across login and token refresh

Documentation:
- DAY5-PHASE2-RBAC-IMPLEMENTATION-SUMMARY.md with complete implementation details

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Yaojia Wang
2025-11-03 15:00:39 +01:00
parent 17f3d4a2b3
commit aaab26ba6c
19 changed files with 1714 additions and 16 deletions

View File

@@ -14,19 +14,22 @@ public class LoginCommandHandler : IRequestHandler<LoginCommand, LoginResponseDt
private readonly IJwtService _jwtService;
private readonly IPasswordHasher _passwordHasher;
private readonly IRefreshTokenService _refreshTokenService;
private readonly IUserTenantRoleRepository _userTenantRoleRepository;
public LoginCommandHandler(
ITenantRepository tenantRepository,
IUserRepository userRepository,
IJwtService jwtService,
IPasswordHasher passwordHasher,
IRefreshTokenService refreshTokenService)
IRefreshTokenService refreshTokenService,
IUserTenantRoleRepository userTenantRoleRepository)
{
_tenantRepository = tenantRepository;
_userRepository = userRepository;
_jwtService = jwtService;
_passwordHasher = passwordHasher;
_refreshTokenService = refreshTokenService;
_userTenantRoleRepository = userTenantRoleRepository;
}
public async Task<LoginResponseDto> Handle(LoginCommand request, CancellationToken cancellationToken)
@@ -53,21 +56,32 @@ public class LoginCommandHandler : IRequestHandler<LoginCommand, LoginResponseDt
throw new UnauthorizedAccessException("Invalid credentials");
}
// 4. Generate JWT token
var accessToken = _jwtService.GenerateToken(user, tenant);
// 4. Get user's tenant role
var userTenantRole = await _userTenantRoleRepository.GetByUserAndTenantAsync(
user.Id,
tenant.Id,
cancellationToken);
// 5. Generate refresh token
if (userTenantRole == null)
{
throw new InvalidOperationException($"User {user.Id} has no role assigned for tenant {tenant.Id}");
}
// 5. Generate JWT token with role
var accessToken = _jwtService.GenerateToken(user, tenant, userTenantRole.Role);
// 6. Generate refresh token
var refreshToken = await _refreshTokenService.GenerateRefreshTokenAsync(
user,
ipAddress: null,
userAgent: null,
cancellationToken);
// 6. Update last login time
// 7. Update last login time
user.RecordLogin();
await _userRepository.UpdateAsync(user, cancellationToken);
// 7. Return result
// 8. Return result
return new LoginResponseDto
{
User = new UserDto

View File

@@ -13,19 +13,22 @@ public class RegisterTenantCommandHandler : IRequestHandler<RegisterTenantComman
private readonly IJwtService _jwtService;
private readonly IPasswordHasher _passwordHasher;
private readonly IRefreshTokenService _refreshTokenService;
private readonly IUserTenantRoleRepository _userTenantRoleRepository;
public RegisterTenantCommandHandler(
ITenantRepository tenantRepository,
IUserRepository userRepository,
IJwtService jwtService,
IPasswordHasher passwordHasher,
IRefreshTokenService refreshTokenService)
IRefreshTokenService refreshTokenService,
IUserTenantRoleRepository userTenantRoleRepository)
{
_tenantRepository = tenantRepository;
_userRepository = userRepository;
_jwtService = jwtService;
_passwordHasher = passwordHasher;
_refreshTokenService = refreshTokenService;
_userTenantRoleRepository = userTenantRoleRepository;
}
public async Task<RegisterTenantResult> Handle(
@@ -59,10 +62,18 @@ public class RegisterTenantCommandHandler : IRequestHandler<RegisterTenantComman
await _userRepository.AddAsync(adminUser, cancellationToken);
// 4. Generate JWT token
var accessToken = _jwtService.GenerateToken(adminUser, tenant);
// 4. Assign TenantOwner role to admin user
var tenantOwnerRole = UserTenantRole.Create(
UserId.Create(adminUser.Id),
TenantId.Create(tenant.Id),
TenantRole.TenantOwner);
// 5. Generate refresh token
await _userTenantRoleRepository.AddAsync(tenantOwnerRole, cancellationToken);
// 5. Generate JWT token with role
var accessToken = _jwtService.GenerateToken(adminUser, tenant, TenantRole.TenantOwner);
// 6. Generate refresh token
var refreshToken = await _refreshTokenService.GenerateRefreshTokenAsync(
adminUser,
ipAddress: null,

View File

@@ -5,6 +5,6 @@ namespace ColaFlow.Modules.Identity.Application.Services;
public interface IJwtService
{
string GenerateToken(User user, Tenant tenant);
string GenerateToken(User user, Tenant tenant, TenantRole tenantRole);
Task<string> GenerateRefreshTokenAsync(User user, CancellationToken cancellationToken = default);
}