Implemented all 3 critical fixes identified in Day 6 Architecture Gap Analysis:
**Fix 1: UpdateUserRole Feature (RESTful PUT endpoint)**
- Created UpdateUserRoleCommand and UpdateUserRoleCommandHandler
- Added PUT /api/tenants/{tenantId}/users/{userId}/role endpoint
- Implements self-demotion prevention (cannot demote self from TenantOwner)
- Implements last owner protection (cannot remove last TenantOwner)
- Returns UserWithRoleDto with updated role information
- Follows RESTful best practices (PUT for updates)
**Fix 2: Last TenantOwner Deletion Prevention (Security)**
- Verified CountByTenantAndRoleAsync repository method exists
- Verified IsLastTenantOwnerAsync validation in RemoveUserFromTenantCommandHandler
- UpdateUserRoleCommandHandler now prevents:
* Self-demotion from TenantOwner role
* Removing the last TenantOwner from tenant
- SECURITY: Prevents tenant from becoming ownerless (critical vulnerability fix)
**Fix 3: Database-Backed Rate Limiting (Security & Reliability)**
- Created EmailRateLimit entity with proper domain logic
- Added EmailRateLimitConfiguration for EF Core
- Implemented DatabaseEmailRateLimiter service (replaces MemoryRateLimitService)
- Updated DependencyInjection to use database-backed implementation
- Created database migration: AddEmailRateLimitsTable
- Added composite unique index on (email, tenant_id, operation_type)
- SECURITY: Rate limit state persists across server restarts (prevents email bombing)
- Implements cleanup logic for expired rate limit records
**Testing:**
- Added 9 comprehensive integration tests in Day8GapFixesTests.cs
- Fix 1: 3 tests (valid update, self-demote prevention, idempotency)
- Fix 2: 3 tests (remove last owner fails, update last owner fails, remove 2nd-to-last succeeds)
- Fix 3: 3 tests (persists across requests, expiry after window, prevents bulk emails)
- 6 tests passing, 3 skipped (long-running/environment-specific tests)
**Files Changed:**
- 6 new files created
- 6 existing files modified
- 1 database migration added
- All existing tests still pass (no regressions)
**Verification:**
- Build succeeds with no errors
- All critical business logic tests pass
- Database migration generated successfully
- Security vulnerabilities addressed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
69 lines
3.1 KiB
C#
69 lines
3.1 KiB
C#
using ColaFlow.Modules.Identity.Application.Services;
|
|
using ColaFlow.Modules.Identity.Domain.Repositories;
|
|
using ColaFlow.Modules.Identity.Infrastructure.Persistence;
|
|
using ColaFlow.Modules.Identity.Infrastructure.Persistence.Repositories;
|
|
using ColaFlow.Modules.Identity.Infrastructure.Services;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
|
|
namespace ColaFlow.Modules.Identity.Infrastructure;
|
|
|
|
public static class DependencyInjection
|
|
{
|
|
public static IServiceCollection AddIdentityInfrastructure(
|
|
this IServiceCollection services,
|
|
IConfiguration configuration,
|
|
IHostEnvironment? environment = null)
|
|
{
|
|
// Only register PostgreSQL DbContext in non-Testing environments
|
|
// In Testing environment, WebApplicationFactory will register InMemory provider
|
|
if (environment == null || environment.EnvironmentName != "Testing")
|
|
{
|
|
// DbContext (using connection string)
|
|
services.AddDbContext<IdentityDbContext>(options =>
|
|
options.UseNpgsql(
|
|
configuration.GetConnectionString("DefaultConnection"),
|
|
b => b.MigrationsAssembly(typeof(IdentityDbContext).Assembly.FullName)));
|
|
}
|
|
|
|
// Tenant Context (Scoped - one instance per request)
|
|
services.AddScoped<ITenantContext, TenantContext>();
|
|
services.AddHttpContextAccessor(); // Required for HttpContext access
|
|
|
|
// Repositories
|
|
services.AddScoped<ITenantRepository, TenantRepository>();
|
|
services.AddScoped<IUserRepository, UserRepository>();
|
|
services.AddScoped<IRefreshTokenRepository, RefreshTokenRepository>();
|
|
services.AddScoped<IUserTenantRoleRepository, UserTenantRoleRepository>();
|
|
services.AddScoped<IEmailVerificationTokenRepository, EmailVerificationTokenRepository>();
|
|
services.AddScoped<IPasswordResetTokenRepository, PasswordResetTokenRepository>();
|
|
services.AddScoped<IInvitationRepository, InvitationRepository>();
|
|
|
|
// Application Services
|
|
services.AddScoped<IJwtService, JwtService>();
|
|
services.AddScoped<IPasswordHasher, PasswordHasher>();
|
|
services.AddScoped<IRefreshTokenService, RefreshTokenService>();
|
|
services.AddScoped<ISecurityTokenService, SecurityTokenService>();
|
|
|
|
// Database-backed rate limiting (replaces in-memory implementation)
|
|
// Persists rate limit state to survive server restarts and prevent email bombing attacks
|
|
services.AddScoped<IRateLimitService, DatabaseEmailRateLimiter>();
|
|
|
|
// Email Services
|
|
var emailProvider = configuration["Email:Provider"] ?? "Mock";
|
|
if (emailProvider.Equals("Mock", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
services.AddSingleton<IEmailService, MockEmailService>();
|
|
}
|
|
else
|
|
{
|
|
services.AddScoped<IEmailService, SmtpEmailService>();
|
|
}
|
|
services.AddScoped<IEmailTemplateService, EmailTemplateService>();
|
|
|
|
return services;
|
|
}
|
|
}
|