Fix RLS permissions and JWT validation for admin club creation #5

Merged
MasterMito merged 18 commits from fix/rls-permission-test-failure into epic/admin_rework_second_try 2026-03-20 11:42:05 +01:00
6 changed files with 77 additions and 2 deletions
Showing only changes of commit 04641319ce - Show all commits
@@ -54,6 +54,14 @@ public class ClubRoleClaimsTransformation : IClaimsTransformation
return Task.FromResult(principal);
}
// --- NEW: Skip DB role lookup if user is a global admin ---
var realmAccess = principal.FindFirst("realm_access")?.Value;
if (!string.IsNullOrEmpty(realmAccess) && realmAccess.Contains("\"admin\"", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(principal);
}
// ---------------------------------------------------------
// Look up the user's role in the database for the requested tenant
_httpContextAccessor.HttpContext!.Items["TenantId"] = tenantId;
var memberRole = GetMemberRole(userIdClaim, tenantId);
+1 -1
View File
@@ -90,8 +90,8 @@ if (app.Environment.IsDevelopment())
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMiddleware<TenantValidationMiddleware>();
app.UseAuthorization();
app.UseMiddleware<TenantValidationMiddleware>();
app.UseMiddleware<MemberSyncMiddleware>();
app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
@@ -0,0 +1,57 @@
using System.Net;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Text.Json;
using WorkClub.Domain.Enums;
using WorkClub.Application.Clubs.DTOs;
using WorkClub.Tests.Integration.Infrastructure;
using Xunit;
namespace WorkClub.Tests.Integration.Clubs;
public class AdminClubEndpointsTests : IntegrationTestBase
{
public AdminClubEndpointsTests(CustomWebApplicationFactory<Program> factory) : base(factory)
{
}
[Fact]
public async Task CreateClub_WithAdminRole_ReturnsCreated()
{
AuthenticateAsAdmin();
var request = new CreateClubRequest("New Admin Club", SportType.Tennis, "Desc");
var response = await Client.PostAsJsonAsync("/api/admin/clubs", request);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
[Fact]
public async Task CreateClub_WithoutAdminRole_ReturnsForbidden()
{
AuthenticateAsNonAdmin();
var request = new CreateClubRequest("New Club", SportType.Tennis, "Desc");
var response = await Client.PostAsJsonAsync("/api/admin/clubs", request);
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
private void AuthenticateAsAdmin()
{
Client.DefaultRequestHeaders.Remove("X-Test-Email");
Client.DefaultRequestHeaders.Add("X-Test-Email", "admin@workclub.com");
Client.DefaultRequestHeaders.Remove("X-Test-Realm-Access");
Client.DefaultRequestHeaders.Add("X-Test-Realm-Access", "{\"roles\":[\"admin\"]}");
}
private void AuthenticateAsNonAdmin()
{
Client.DefaultRequestHeaders.Remove("X-Test-Email");
Client.DefaultRequestHeaders.Add("X-Test-Email", "user@workclub.com");
Client.DefaultRequestHeaders.Remove("X-Test-Realm-Access");
Client.DefaultRequestHeaders.Add("X-Test-Realm-Access", "{\"roles\":[\"user\"]}");
}
}
@@ -67,6 +67,8 @@ public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProg
GRANT CONNECT ON DATABASE workclub_test TO rls_test_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO rls_test_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO rls_test_user;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_admin') THEN
CREATE ROLE app_admin;
END IF;
END $$;
";
@@ -30,9 +30,10 @@ public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions
var emailClaim = Context.Request.Headers["X-Test-Email"].ToString();
var userIdClaim = Context.Request.Headers["X-Test-UserId"].ToString();
var clubRolesJson = Context.Request.Headers["X-Test-ClubRoles"].ToString();
var realmAccessClaim = Context.Request.Headers["X-Test-Realm-Access"].ToString();
// If no test auth headers are present, return NoResult (unauthenticated)
if (string.IsNullOrEmpty(emailClaim) && string.IsNullOrEmpty(userIdClaim) && string.IsNullOrEmpty(clubsClaim))
if (string.IsNullOrEmpty(emailClaim) && string.IsNullOrEmpty(userIdClaim) && string.IsNullOrEmpty(clubsClaim) && string.IsNullOrEmpty(realmAccessClaim))
{
return Task.FromResult(AuthenticateResult.NoResult());
}
@@ -46,6 +47,11 @@ public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions
new Claim(ClaimTypes.Email, resolvedEmail),
new Claim("preferred_username", resolvedEmail),
};
if (!string.IsNullOrEmpty(realmAccessClaim))
{
claims.Add(new Claim("realm_access", realmAccessClaim, ClaimValueTypes.String));
}
if (!string.IsNullOrEmpty(clubsClaim))
{
+2
View File
@@ -71,6 +71,8 @@ services:
- "5001:8080"
extra_hosts:
- "localhost:host-gateway"
- "127.0.0.1:host-gateway"
- "keycloak:host-gateway"
working_dir: /app
volumes:
- ./backend:/app:cached