feat(backend): add PostgreSQL schema, RLS policies, and multi-tenant middleware
- Add EF Core migrations for initial schema (clubs, members, work_items, shifts, shift_signups)
- Implement RLS policies with SET LOCAL for tenant isolation
- Add Finbuckle multi-tenant middleware with ClaimStrategy + HeaderStrategy fallback
- Create TenantValidationMiddleware to enforce JWT claims match X-Tenant-Id header
- Add tenant-aware DB interceptors (SaveChangesTenantInterceptor, TenantDbConnectionInterceptor)
- Configure AppDbContext with tenant scoping and RLS support
- Add test infrastructure: CustomWebApplicationFactory, TestAuthHandler, DatabaseFixture
- Write TDD integration tests for multi-tenant isolation and RLS enforcement
- Add health check null safety for connection string
Tasks: 7 (PostgreSQL schema + migrations + RLS), 8 (Finbuckle multi-tenancy + validation), 12 (test infrastructure)
2026-03-03 14:32:21 +01:00
|
|
|
using System.IdentityModel.Tokens.Jwt;
|
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Net.Http.Headers;
|
|
|
|
|
using System.Security.Claims;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
using Microsoft.AspNetCore.Authentication;
|
|
|
|
|
using Microsoft.AspNetCore.Hosting;
|
|
|
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
|
|
|
using Microsoft.AspNetCore.TestHost;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
|
using Microsoft.IdentityModel.Tokens;
|
|
|
|
|
using Xunit;
|
|
|
|
|
|
|
|
|
|
namespace WorkClub.Tests.Integration.Middleware;
|
|
|
|
|
|
|
|
|
|
public class TenantValidationTests : IClassFixture<CustomWebApplicationFactory>
|
|
|
|
|
{
|
|
|
|
|
private readonly CustomWebApplicationFactory _factory;
|
|
|
|
|
private readonly HttpClient _client;
|
|
|
|
|
|
|
|
|
|
public TenantValidationTests(CustomWebApplicationFactory factory)
|
|
|
|
|
{
|
|
|
|
|
_factory = factory;
|
|
|
|
|
_client = factory.CreateClient();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Request_WithValidTenantId_Returns200()
|
|
|
|
|
{
|
|
|
|
|
// Arrange: Create JWT with clubs claim containing club-1
|
|
|
|
|
var clubs = new Dictionary<string, string>
|
|
|
|
|
{
|
|
|
|
|
{ "club-1", "admin" }
|
|
|
|
|
};
|
|
|
|
|
var token = CreateTestJwt(clubs);
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(backend): add PostgreSQL schema, RLS policies, and multi-tenant middleware
- Add EF Core migrations for initial schema (clubs, members, work_items, shifts, shift_signups)
- Implement RLS policies with SET LOCAL for tenant isolation
- Add Finbuckle multi-tenant middleware with ClaimStrategy + HeaderStrategy fallback
- Create TenantValidationMiddleware to enforce JWT claims match X-Tenant-Id header
- Add tenant-aware DB interceptors (SaveChangesTenantInterceptor, TenantDbConnectionInterceptor)
- Configure AppDbContext with tenant scoping and RLS support
- Add test infrastructure: CustomWebApplicationFactory, TestAuthHandler, DatabaseFixture
- Write TDD integration tests for multi-tenant isolation and RLS enforcement
- Add health check null safety for connection string
Tasks: 7 (PostgreSQL schema + migrations + RLS), 8 (Finbuckle multi-tenancy + validation), 12 (test infrastructure)
2026-03-03 14:32:21 +01:00
|
|
|
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
|
|
|
_client.DefaultRequestHeaders.Add("X-Tenant-Id", "club-1");
|
|
|
|
|
|
|
|
|
|
// Act: Make request to test endpoint
|
|
|
|
|
var response = await _client.GetAsync("/api/test");
|
|
|
|
|
|
|
|
|
|
// Assert: Request should succeed
|
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Request_WithNonMemberTenantId_Returns403()
|
|
|
|
|
{
|
|
|
|
|
// Arrange: Create JWT with clubs claim (only club-1, not club-2)
|
|
|
|
|
var clubs = new Dictionary<string, string>
|
|
|
|
|
{
|
|
|
|
|
{ "club-1", "admin" }
|
|
|
|
|
};
|
|
|
|
|
var token = CreateTestJwt(clubs);
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(backend): add PostgreSQL schema, RLS policies, and multi-tenant middleware
- Add EF Core migrations for initial schema (clubs, members, work_items, shifts, shift_signups)
- Implement RLS policies with SET LOCAL for tenant isolation
- Add Finbuckle multi-tenant middleware with ClaimStrategy + HeaderStrategy fallback
- Create TenantValidationMiddleware to enforce JWT claims match X-Tenant-Id header
- Add tenant-aware DB interceptors (SaveChangesTenantInterceptor, TenantDbConnectionInterceptor)
- Configure AppDbContext with tenant scoping and RLS support
- Add test infrastructure: CustomWebApplicationFactory, TestAuthHandler, DatabaseFixture
- Write TDD integration tests for multi-tenant isolation and RLS enforcement
- Add health check null safety for connection string
Tasks: 7 (PostgreSQL schema + migrations + RLS), 8 (Finbuckle multi-tenancy + validation), 12 (test infrastructure)
2026-03-03 14:32:21 +01:00
|
|
|
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
|
|
|
_client.DefaultRequestHeaders.Add("X-Tenant-Id", "club-2"); // User not member of club-2
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var response = await _client.GetAsync("/api/test");
|
|
|
|
|
|
|
|
|
|
// Assert: Cross-tenant access should be denied
|
|
|
|
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Request_WithoutTenantIdHeader_Returns400()
|
|
|
|
|
{
|
|
|
|
|
// Arrange: Create valid JWT but no X-Tenant-Id header
|
|
|
|
|
var clubs = new Dictionary<string, string>
|
|
|
|
|
{
|
|
|
|
|
{ "club-1", "admin" }
|
|
|
|
|
};
|
|
|
|
|
var token = CreateTestJwt(clubs);
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(backend): add PostgreSQL schema, RLS policies, and multi-tenant middleware
- Add EF Core migrations for initial schema (clubs, members, work_items, shifts, shift_signups)
- Implement RLS policies with SET LOCAL for tenant isolation
- Add Finbuckle multi-tenant middleware with ClaimStrategy + HeaderStrategy fallback
- Create TenantValidationMiddleware to enforce JWT claims match X-Tenant-Id header
- Add tenant-aware DB interceptors (SaveChangesTenantInterceptor, TenantDbConnectionInterceptor)
- Configure AppDbContext with tenant scoping and RLS support
- Add test infrastructure: CustomWebApplicationFactory, TestAuthHandler, DatabaseFixture
- Write TDD integration tests for multi-tenant isolation and RLS enforcement
- Add health check null safety for connection string
Tasks: 7 (PostgreSQL schema + migrations + RLS), 8 (Finbuckle multi-tenancy + validation), 12 (test infrastructure)
2026-03-03 14:32:21 +01:00
|
|
|
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
|
|
|
// No X-Tenant-Id header
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var response = await _client.GetAsync("/api/test");
|
|
|
|
|
|
|
|
|
|
// Assert: Missing header should return bad request
|
|
|
|
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Request_WithoutAuthentication_Returns401()
|
|
|
|
|
{
|
|
|
|
|
// Arrange: No authorization header
|
|
|
|
|
_client.DefaultRequestHeaders.Add("X-Tenant-Id", "club-1");
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var response = await _client.GetAsync("/api/test");
|
|
|
|
|
|
|
|
|
|
// Assert: Unauthenticated request should be denied
|
|
|
|
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string CreateTestJwt(Dictionary<string, string> clubs)
|
|
|
|
|
{
|
|
|
|
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("test-secret-key-for-jwt-signing-must-be-at-least-32-chars"));
|
|
|
|
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
|
|
|
|
|
|
|
|
|
var claims = new List<Claim>
|
|
|
|
|
{
|
|
|
|
|
new Claim(ClaimTypes.NameIdentifier, "test-user-id"),
|
|
|
|
|
new Claim(ClaimTypes.Name, "test@test.com"),
|
|
|
|
|
new Claim("clubs", JsonSerializer.Serialize(clubs)) // JSON object claim
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var token = new JwtSecurityToken(
|
|
|
|
|
issuer: "test-issuer",
|
|
|
|
|
audience: "test-audience",
|
|
|
|
|
claims: claims,
|
|
|
|
|
expires: DateTime.UtcNow.AddHours(1),
|
|
|
|
|
signingCredentials: creds
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Custom WebApplicationFactory for integration testing with test authentication.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
|
|
|
|
{
|
|
|
|
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
|
|
|
|
{
|
|
|
|
|
builder.ConfigureTestServices(services =>
|
|
|
|
|
{
|
|
|
|
|
services.AddAuthentication("TestScheme")
|
|
|
|
|
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("TestScheme", options => { });
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(backend): add PostgreSQL schema, RLS policies, and multi-tenant middleware
- Add EF Core migrations for initial schema (clubs, members, work_items, shifts, shift_signups)
- Implement RLS policies with SET LOCAL for tenant isolation
- Add Finbuckle multi-tenant middleware with ClaimStrategy + HeaderStrategy fallback
- Create TenantValidationMiddleware to enforce JWT claims match X-Tenant-Id header
- Add tenant-aware DB interceptors (SaveChangesTenantInterceptor, TenantDbConnectionInterceptor)
- Configure AppDbContext with tenant scoping and RLS support
- Add test infrastructure: CustomWebApplicationFactory, TestAuthHandler, DatabaseFixture
- Write TDD integration tests for multi-tenant isolation and RLS enforcement
- Add health check null safety for connection string
Tasks: 7 (PostgreSQL schema + migrations + RLS), 8 (Finbuckle multi-tenancy + validation), 12 (test infrastructure)
2026-03-03 14:32:21 +01:00
|
|
|
services.AddAuthorization();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
builder.UseEnvironment("Testing");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Test authentication handler that validates JWT tokens without Keycloak.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
|
|
|
|
{
|
|
|
|
|
public TestAuthHandler(
|
|
|
|
|
Microsoft.Extensions.Options.IOptionsMonitor<AuthenticationSchemeOptions> options,
|
|
|
|
|
Microsoft.Extensions.Logging.ILoggerFactory logger,
|
|
|
|
|
System.Text.Encodings.Web.UrlEncoder encoder)
|
|
|
|
|
: base(options, logger, encoder)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
|
|
|
|
{
|
|
|
|
|
var authHeader = Request.Headers.Authorization.ToString();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(backend): add PostgreSQL schema, RLS policies, and multi-tenant middleware
- Add EF Core migrations for initial schema (clubs, members, work_items, shifts, shift_signups)
- Implement RLS policies with SET LOCAL for tenant isolation
- Add Finbuckle multi-tenant middleware with ClaimStrategy + HeaderStrategy fallback
- Create TenantValidationMiddleware to enforce JWT claims match X-Tenant-Id header
- Add tenant-aware DB interceptors (SaveChangesTenantInterceptor, TenantDbConnectionInterceptor)
- Configure AppDbContext with tenant scoping and RLS support
- Add test infrastructure: CustomWebApplicationFactory, TestAuthHandler, DatabaseFixture
- Write TDD integration tests for multi-tenant isolation and RLS enforcement
- Add health check null safety for connection string
Tasks: 7 (PostgreSQL schema + migrations + RLS), 8 (Finbuckle multi-tenancy + validation), 12 (test infrastructure)
2026-03-03 14:32:21 +01:00
|
|
|
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(AuthenticateResult.NoResult());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var token = authHeader.Substring("Bearer ".Length).Trim();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(backend): add PostgreSQL schema, RLS policies, and multi-tenant middleware
- Add EF Core migrations for initial schema (clubs, members, work_items, shifts, shift_signups)
- Implement RLS policies with SET LOCAL for tenant isolation
- Add Finbuckle multi-tenant middleware with ClaimStrategy + HeaderStrategy fallback
- Create TenantValidationMiddleware to enforce JWT claims match X-Tenant-Id header
- Add tenant-aware DB interceptors (SaveChangesTenantInterceptor, TenantDbConnectionInterceptor)
- Configure AppDbContext with tenant scoping and RLS support
- Add test infrastructure: CustomWebApplicationFactory, TestAuthHandler, DatabaseFixture
- Write TDD integration tests for multi-tenant isolation and RLS enforcement
- Add health check null safety for connection string
Tasks: 7 (PostgreSQL schema + migrations + RLS), 8 (Finbuckle multi-tenancy + validation), 12 (test infrastructure)
2026-03-03 14:32:21 +01:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var handler = new JwtSecurityTokenHandler();
|
|
|
|
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("test-secret-key-for-jwt-signing-must-be-at-least-32-chars"));
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(backend): add PostgreSQL schema, RLS policies, and multi-tenant middleware
- Add EF Core migrations for initial schema (clubs, members, work_items, shifts, shift_signups)
- Implement RLS policies with SET LOCAL for tenant isolation
- Add Finbuckle multi-tenant middleware with ClaimStrategy + HeaderStrategy fallback
- Create TenantValidationMiddleware to enforce JWT claims match X-Tenant-Id header
- Add tenant-aware DB interceptors (SaveChangesTenantInterceptor, TenantDbConnectionInterceptor)
- Configure AppDbContext with tenant scoping and RLS support
- Add test infrastructure: CustomWebApplicationFactory, TestAuthHandler, DatabaseFixture
- Write TDD integration tests for multi-tenant isolation and RLS enforcement
- Add health check null safety for connection string
Tasks: 7 (PostgreSQL schema + migrations + RLS), 8 (Finbuckle multi-tenancy + validation), 12 (test infrastructure)
2026-03-03 14:32:21 +01:00
|
|
|
var validationParameters = new TokenValidationParameters
|
|
|
|
|
{
|
|
|
|
|
ValidateIssuer = true,
|
|
|
|
|
ValidateAudience = true,
|
|
|
|
|
ValidateLifetime = true,
|
|
|
|
|
ValidateIssuerSigningKey = true,
|
|
|
|
|
ValidIssuer = "test-issuer",
|
|
|
|
|
ValidAudience = "test-audience",
|
|
|
|
|
IssuerSigningKey = key
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var principal = handler.ValidateToken(token, validationParameters, out _);
|
|
|
|
|
var ticket = new AuthenticationTicket(principal, "TestScheme");
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(backend): add PostgreSQL schema, RLS policies, and multi-tenant middleware
- Add EF Core migrations for initial schema (clubs, members, work_items, shifts, shift_signups)
- Implement RLS policies with SET LOCAL for tenant isolation
- Add Finbuckle multi-tenant middleware with ClaimStrategy + HeaderStrategy fallback
- Create TenantValidationMiddleware to enforce JWT claims match X-Tenant-Id header
- Add tenant-aware DB interceptors (SaveChangesTenantInterceptor, TenantDbConnectionInterceptor)
- Configure AppDbContext with tenant scoping and RLS support
- Add test infrastructure: CustomWebApplicationFactory, TestAuthHandler, DatabaseFixture
- Write TDD integration tests for multi-tenant isolation and RLS enforcement
- Add health check null safety for connection string
Tasks: 7 (PostgreSQL schema + migrations + RLS), 8 (Finbuckle multi-tenancy + validation), 12 (test infrastructure)
2026-03-03 14:32:21 +01:00
|
|
|
return Task.FromResult(AuthenticateResult.Success(ticket));
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(AuthenticateResult.Fail(ex.Message));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|