In progress
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled

This commit is contained in:
Yaojia Wang
2025-11-03 20:19:48 +01:00
parent 32a25b3b35
commit 709068f68b
4 changed files with 926 additions and 85 deletions

View File

@@ -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