In progress
This commit is contained in:
@@ -314,61 +314,118 @@ public class RoleManagementTests : IClassFixture<DatabaseFixture>
|
||||
|
||||
#endregion
|
||||
|
||||
#region Category 5: Cross-Tenant Protection Tests (2 tests)
|
||||
#region Category 5: Cross-Tenant Protection Tests (5 tests)
|
||||
|
||||
[Fact]
|
||||
public async Task AssignRole_CrossTenant_ShouldFail()
|
||||
public async Task ListUsers_WithCrossTenantAccess_ShouldReturn403Forbidden()
|
||||
{
|
||||
// Arrange - Create two separate tenants
|
||||
var (ownerAToken, tenantAId) = await RegisterTenantAndGetTokenAsync();
|
||||
var (_, tenantBId, userBId) = await RegisterTenantAndGetDetailedTokenAsync();
|
||||
var (ownerBToken, tenantBId) = await RegisterTenantAndGetTokenAsync();
|
||||
|
||||
// Act - Owner of Tenant A tries to assign role in Tenant B
|
||||
// This should fail because JWT tenant_id claim doesn't match tenantBId
|
||||
// Act - Tenant A owner tries to list Tenant B users
|
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ownerAToken);
|
||||
var response = await _client.GetAsync($"/api/tenants/{tenantBId}/users");
|
||||
|
||||
// Assert - Should return 403 Forbidden
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Forbidden,
|
||||
"Users should not be able to access other tenants' user lists");
|
||||
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
errorContent.Should().Contain("your own tenant",
|
||||
"Error message should explain tenant isolation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AssignRole_WithCrossTenantAccess_ShouldReturn403Forbidden()
|
||||
{
|
||||
// Arrange - Create two separate tenants
|
||||
var (ownerAToken, tenantAId) = await RegisterTenantAndGetTokenAsync();
|
||||
var (ownerBToken, tenantBId, userBId) = await RegisterTenantAndGetDetailedTokenAsync();
|
||||
|
||||
// Act - Tenant A owner tries to assign role in Tenant B
|
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ownerAToken);
|
||||
var response = await _client.PostAsJsonAsync(
|
||||
$"/api/tenants/{tenantBId}/users/{userBId}/role",
|
||||
new { Role = "TenantMember" });
|
||||
|
||||
// Assert - Should fail (cross-tenant access blocked by authorization policy)
|
||||
// Could be 403 Forbidden or 400 Bad Request depending on implementation
|
||||
response.StatusCode.Should().BeOneOf(HttpStatusCode.Forbidden, HttpStatusCode.BadRequest, HttpStatusCode.Unauthorized);
|
||||
// Assert - Should return 403 Forbidden
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Forbidden,
|
||||
"Users should not be able to assign roles in other tenants");
|
||||
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
errorContent.Should().Contain("your own tenant",
|
||||
"Error message should explain tenant isolation");
|
||||
}
|
||||
|
||||
[Fact(Skip = "Cross-tenant protection not yet implemented - security gap identified")]
|
||||
public async Task ListUsers_CrossTenant_ShouldFail()
|
||||
[Fact]
|
||||
public async Task RemoveUser_WithCrossTenantAccess_ShouldReturn403Forbidden()
|
||||
{
|
||||
// SECURITY GAP IDENTIFIED: Cross-tenant validation is not implemented
|
||||
// Currently, a user from Tenant A CAN list users from Tenant B
|
||||
// This is a security issue that needs to be fixed in Day 7+
|
||||
|
||||
// TODO: Implement cross-tenant protection in authorization policies:
|
||||
// 1. Add RequireTenantMatch policy that validates route {tenantId} matches JWT tenant_id claim
|
||||
// 2. Apply this policy to all tenant-scoped endpoints
|
||||
// 3. Return 403 Forbidden when tenant mismatch is detected
|
||||
|
||||
// Current behavior (INSECURE):
|
||||
// - User A can access /api/tenants/B/users and get 200 OK
|
||||
// - No validation that route tenantId matches user's JWT tenant_id
|
||||
|
||||
// Expected behavior (SECURE):
|
||||
// - User A accessing /api/tenants/B/users should get 403 Forbidden
|
||||
// - Only users belonging to Tenant B should access Tenant B resources
|
||||
|
||||
// Arrange - Create two separate tenants
|
||||
var (ownerAToken, tenantAId) = await RegisterTenantAndGetTokenAsync();
|
||||
var (_, tenantBId, _) = await RegisterTenantAndGetDetailedTokenAsync();
|
||||
var (ownerBToken, tenantBId, userBId) = await RegisterTenantAndGetDetailedTokenAsync();
|
||||
|
||||
// Act - Owner of Tenant A tries to list users in Tenant B
|
||||
// Act - Tenant A owner tries to remove user from Tenant B
|
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ownerAToken);
|
||||
var response = await _client.GetAsync($"/api/tenants/{tenantBId}/users");
|
||||
var response = await _client.DeleteAsync($"/api/tenants/{tenantBId}/users/{userBId}");
|
||||
|
||||
// Assert - Currently returns 200 OK (BUG), should return 403 Forbidden
|
||||
// Uncomment this once cross-tenant protection is implemented:
|
||||
// response.StatusCode.Should().Be(HttpStatusCode.Forbidden,
|
||||
// "Users should not be able to access other tenants' resources");
|
||||
// Assert - Should return 403 Forbidden
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Forbidden,
|
||||
"Users should not be able to remove users from other tenants");
|
||||
|
||||
await Task.CompletedTask;
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
errorContent.Should().Contain("your own tenant",
|
||||
"Error message should explain tenant isolation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListUsers_WithSameTenantAccess_ShouldReturn200OK()
|
||||
{
|
||||
// Arrange - Register tenant
|
||||
var (ownerToken, tenantId) = await RegisterTenantAndGetTokenAsync();
|
||||
|
||||
// Act - Tenant owner accesses their own tenant's users
|
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ownerToken);
|
||||
var response = await _client.GetAsync($"/api/tenants/{tenantId}/users");
|
||||
|
||||
// Assert - Should return 200 OK (regression test - ensure same-tenant access still works)
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK,
|
||||
"Users should be able to access their own tenant's resources");
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<PagedResultDto<UserWithRoleDto>>();
|
||||
result.Should().NotBeNull();
|
||||
result!.Items.Should().HaveCountGreaterThan(0, "Owner should be listed in their own tenant");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CrossTenantProtection_WithMultipleEndpoints_ShouldBeConsistent()
|
||||
{
|
||||
// Arrange - Create two separate tenants
|
||||
var (ownerAToken, tenantAId, userAId) = await RegisterTenantAndGetDetailedTokenAsync();
|
||||
var (ownerBToken, tenantBId, userBId) = await RegisterTenantAndGetDetailedTokenAsync();
|
||||
|
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ownerAToken);
|
||||
|
||||
// Act & Assert - Test all three endpoints consistently block cross-tenant access
|
||||
var listUsersResponse = await _client.GetAsync($"/api/tenants/{tenantBId}/users");
|
||||
listUsersResponse.StatusCode.Should().Be(HttpStatusCode.Forbidden,
|
||||
"ListUsers should block cross-tenant access");
|
||||
|
||||
var assignRoleResponse = await _client.PostAsJsonAsync(
|
||||
$"/api/tenants/{tenantBId}/users/{userBId}/role",
|
||||
new { Role = "TenantMember" });
|
||||
assignRoleResponse.StatusCode.Should().Be(HttpStatusCode.Forbidden,
|
||||
"AssignRole should block cross-tenant access");
|
||||
|
||||
var removeUserResponse = await _client.DeleteAsync($"/api/tenants/{tenantBId}/users/{userBId}");
|
||||
removeUserResponse.StatusCode.Should().Be(HttpStatusCode.Forbidden,
|
||||
"RemoveUser should block cross-tenant access");
|
||||
|
||||
// Verify same-tenant access still works for Tenant A
|
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ownerAToken);
|
||||
var sameTenantResponse = await _client.GetAsync($"/api/tenants/{tenantAId}/users");
|
||||
sameTenantResponse.StatusCode.Should().Be(HttpStatusCode.OK,
|
||||
"Same-tenant access should still work");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
Reference in New Issue
Block a user