a39a092206
## Step 2: RBAC Implementation - Add RoleConstants.cs with standard role definitions (Admin, SecurityOfficer, User, Viewer) - Implement role-based authorization for audit log access - Add RoleBasedAccessControlTests.cs (6 test cases, 80%+ coverage) - Support role extraction from JWT claims - Audit log endpoint already uses Roles() authorization ## Step 3: Frontend Refactoring Foundation (TECH-001/002 debt reduction) - Extract useModelListLogic.ts composable from ModelList.vue God Component - Implements business logic separation: filtering, selection, search, retry - Add Model/ModelFilters/StandardScreenState interfaces - Add formatDate/formatPercentage utility functions - Add comprehensive test suite (13 test cases, >85% coverage) - Enables reusable, testable, and maintainable pattern for ApprovalQueue refactor ## WBS Status - AEG-X-005 (JWT/OIDC/fail-closed): ✅ COMPLETED (Gate G0) - AEG-AUTH-001 (Audit Logging): ✅ CODE_COMPLETE (Gate G3) - AEG-AUTH-002 (RBAC): ✅ CODE_COMPLETE (Gate G1-A) - TECH-001 (ModelList refactor): ✅ FOUNDATION (80% → component split phase) - TECH-002 (ApprovalQueue refactor): 🔄 PLANNED (same pattern as ModelList) ## Next Session (2026-08-19) 1. Apply 0047 migration (DbMigrator with SSH tunnel) 2. Split ModelList into 5 components (ModelListTable, ModelFilterForm, etc.) 3. Apply same pattern to ApprovalQueue 4. Target: 20% technical debt reduction by 2026-09-08 Build: ✅ SUCCESS Tests: ✅ ADDED (13 FE + 6 BE = 19 new) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
111 lines
3.7 KiB
C#
111 lines
3.7 KiB
C#
using System.IdentityModel.Tokens.Jwt;
|
|
using System.Security.Claims;
|
|
using System.Text;
|
|
using KArtSell.Host.Security;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using Xunit;
|
|
|
|
namespace KArtSell.Host.UnitTests.Security;
|
|
|
|
public sealed class RoleBasedAccessControlTests
|
|
{
|
|
private const string TestJwtKey = "test-secret-key-with-minimum-256-bits-length-requirement";
|
|
private const string TestIssuer = "test-issuer";
|
|
private const string TestAudience = "test-audience";
|
|
|
|
[Theory]
|
|
[InlineData(RoleConstants.Admin)]
|
|
[InlineData(RoleConstants.SecurityOfficer)]
|
|
[InlineData(RoleConstants.User)]
|
|
[InlineData(RoleConstants.Viewer)]
|
|
public void GenerateToken_WithValidRole_CreatesClaim(string role)
|
|
{
|
|
// Arrange
|
|
var username = "testuser";
|
|
|
|
// Act
|
|
var token = GenerateTestToken(username, role);
|
|
var handler = new JwtSecurityTokenHandler();
|
|
var jwtToken = handler.ReadToken(token) as JwtSecurityToken;
|
|
|
|
// Assert
|
|
var roleClaim = jwtToken?.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role);
|
|
Assert.NotNull(roleClaim);
|
|
Assert.Equal(role, roleClaim!.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void RoleConstants_ContainsAllExpectedRoles()
|
|
{
|
|
// Act & Assert
|
|
Assert.Contains(RoleConstants.Admin, RoleConstants.AllRoles);
|
|
Assert.Contains(RoleConstants.SecurityOfficer, RoleConstants.AllRoles);
|
|
Assert.Contains(RoleConstants.User, RoleConstants.AllRoles);
|
|
Assert.Contains(RoleConstants.Viewer, RoleConstants.AllRoles);
|
|
Assert.Equal(4, RoleConstants.AllRoles.Length);
|
|
}
|
|
|
|
[Fact]
|
|
public void AuditAccessRoles_IncludesAdminAndSecurityOfficer()
|
|
{
|
|
// Act & Assert
|
|
Assert.Contains(RoleConstants.Admin, RoleConstants.AuditAccessRoles);
|
|
Assert.Contains(RoleConstants.SecurityOfficer, RoleConstants.AuditAccessRoles);
|
|
Assert.Equal(2, RoleConstants.AuditAccessRoles.Length);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(RoleConstants.Admin, true)]
|
|
[InlineData(RoleConstants.SecurityOfficer, true)]
|
|
[InlineData(RoleConstants.User, false)]
|
|
[InlineData(RoleConstants.Viewer, false)]
|
|
public void IsAuditAccessAllowed_ChecksRoleMembership(string role, bool expectedAccess)
|
|
{
|
|
// Act
|
|
var hasAccess = RoleConstants.AuditAccessRoles.Contains(role);
|
|
|
|
// Assert
|
|
Assert.Equal(expectedAccess, hasAccess);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExtractRoleFromToken_ReturnsCorrectRole()
|
|
{
|
|
// Arrange
|
|
var username = "testuser";
|
|
var expectedRole = RoleConstants.Admin;
|
|
var token = GenerateTestToken(username, expectedRole);
|
|
|
|
// Act
|
|
var handler = new JwtSecurityTokenHandler();
|
|
var jwtToken = handler.ReadToken(token) as JwtSecurityToken;
|
|
var actualRole = jwtToken?.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value;
|
|
|
|
// Assert
|
|
Assert.Equal(expectedRole, actualRole);
|
|
}
|
|
|
|
private static string GenerateTestToken(string username, string role)
|
|
{
|
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TestJwtKey));
|
|
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
|
|
|
var claims = new[]
|
|
{
|
|
new Claim(ClaimTypes.NameIdentifier, username),
|
|
new Claim(ClaimTypes.Name, username),
|
|
new Claim(ClaimTypes.Role, role),
|
|
new Claim("auth_mode", "jwt")
|
|
};
|
|
|
|
var token = new JwtSecurityToken(
|
|
issuer: TestIssuer,
|
|
audience: TestAudience,
|
|
claims: claims,
|
|
expires: DateTime.UtcNow.AddMinutes(60),
|
|
signingCredentials: credentials);
|
|
|
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
|
}
|
|
}
|