25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

195 satır
8.0KB

  1. using System.IdentityModel.Tokens.Jwt;
  2. using System.Net;
  3. using System.Net.Http.Headers;
  4. using System.Security.Claims;
  5. using System.Text;
  6. using Campaign_Tracker.Server.Authentication;
  7. using Campaign_Tracker.Server.Authorization;
  8. using Microsoft.AspNetCore.Mvc.Testing;
  9. using Microsoft.Extensions.DependencyInjection;
  10. using Microsoft.IdentityModel.Tokens;
  11. namespace Campaign_Tracker.Server.Tests;
  12. public sealed class ApplicationAuthorizationTests : IClassFixture<WebApplicationFactory<Program>>
  13. {
  14. private const string Issuer = "http://kci-app01.ntp.kentcommunications.com:8180/realms/KCI";
  15. private const string ClientId = "canopy-web";
  16. private const string SigningKey = "test-signing-key-with-at-least-32-characters";
  17. private readonly WebApplicationFactory<Program> _factory;
  18. public ApplicationAuthorizationTests(WebApplicationFactory<Program> factory)
  19. {
  20. Environment.SetEnvironmentVariable("Keycloak__Authority", Issuer);
  21. Environment.SetEnvironmentVariable("Keycloak__ValidIssuer", Issuer);
  22. Environment.SetEnvironmentVariable("Keycloak__PublicAuthority", Issuer);
  23. Environment.SetEnvironmentVariable("Keycloak__ClientId", ClientId);
  24. Environment.SetEnvironmentVariable("Keycloak__DisableHttpsMetadata", "true");
  25. Environment.SetEnvironmentVariable("Keycloak__TestSigningKey", SigningKey);
  26. _factory = factory;
  27. }
  28. [Fact]
  29. public async Task ClientServices_CanAccessMunicipalityAndCycleRoutes_ButNotAdminOrProduction()
  30. {
  31. using var client = CreateClientWithRole("ClientServices");
  32. Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/api/municipalities/profile")).StatusCode);
  33. Assert.Equal(HttpStatusCode.OK, (await client.PostAsync("/api/election-cycles", null)).StatusCode);
  34. Assert.Equal(HttpStatusCode.Forbidden, (await client.GetAsync("/api/admin/settings")).StatusCode);
  35. Assert.Equal(HttpStatusCode.Forbidden, (await client.GetAsync("/api/production/work-queue")).StatusCode);
  36. }
  37. [Fact]
  38. public async Task Admin_CanAccessAllApplicationRoutes()
  39. {
  40. using var client = CreateClientWithRole("Admin");
  41. Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/api/municipalities/profile")).StatusCode);
  42. Assert.Equal(HttpStatusCode.OK, (await client.PostAsync("/api/election-cycles", null)).StatusCode);
  43. Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/api/admin/settings")).StatusCode);
  44. Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/api/production/work-queue")).StatusCode);
  45. }
  46. [Fact]
  47. public async Task KeycloakRealmAccessRole_IsMappedToApplicationPolicy()
  48. {
  49. var client = _factory.CreateClient();
  50. client.DefaultRequestHeaders.Authorization =
  51. new AuthenticationHeaderValue("Bearer", CreateTokenWithRealmAccessRole("daniel@example.test", "ClientServices"));
  52. var response = await client.GetAsync("/api/municipalities/profile");
  53. Assert.Equal(HttpStatusCode.OK, response.StatusCode);
  54. }
  55. [Fact]
  56. public async Task UnrecognizedRole_ReceivesForbidden_AndAuditCapturesActor()
  57. {
  58. using var client = CreateClientWithRole("SeasonalViewer", "unknown@example.test");
  59. var response = await client.GetAsync("/api/municipalities/profile");
  60. var auditStore = _factory.Services.GetRequiredService<IAuthenticationAuditStore>();
  61. Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
  62. Assert.Contains(auditStore.Events, audit =>
  63. audit.EventType == AuthenticationAuditEventType.AuthorizationDenied &&
  64. audit.Subject == "unknown@example.test" &&
  65. audit.Resource == "/api/municipalities/profile");
  66. }
  67. [Fact]
  68. public async Task PrivilegedOperation_AuditsAllowedAuthorizationResultActorAndResource()
  69. {
  70. using var client = CreateClientWithRole("Admin", "admin@example.test");
  71. var response = await client.PostAsync("/api/admin/privileged-operation", null);
  72. var auditStore = _factory.Services.GetRequiredService<IAuthenticationAuditStore>();
  73. Assert.Equal(HttpStatusCode.OK, response.StatusCode);
  74. Assert.Contains(auditStore.Events, audit =>
  75. audit.EventType == AuthenticationAuditEventType.AuthorizationAllowed &&
  76. audit.Subject == "admin@example.test" &&
  77. audit.Resource == "/api/admin/privileged-operation");
  78. }
  79. [Fact]
  80. public async Task AllowedPermissionCheck_AuditsAuthorizationAllowedCentrally()
  81. {
  82. using var client = CreateClientWithRole("ClientServices", "client@example.test");
  83. var response = await client.GetAsync("/api/municipalities/profile");
  84. var auditStore = _factory.Services.GetRequiredService<IAuthenticationAuditStore>();
  85. Assert.Equal(HttpStatusCode.OK, response.StatusCode);
  86. Assert.Contains(auditStore.Events, audit =>
  87. audit.EventType == AuthenticationAuditEventType.AuthorizationAllowed &&
  88. audit.Subject == "client@example.test" &&
  89. audit.Resource == "/api/municipalities/profile");
  90. }
  91. [Fact]
  92. public async Task AnonymousPermissionCheck_AuditsAuthorizationDenied()
  93. {
  94. using var client = _factory.CreateClient();
  95. var response = await client.GetAsync("/api/municipalities/profile");
  96. var auditStore = _factory.Services.GetRequiredService<IAuthenticationAuditStore>();
  97. Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
  98. Assert.Contains(auditStore.Events, audit =>
  99. audit.EventType == AuthenticationAuditEventType.AuthorizationDenied &&
  100. audit.Subject == "anonymous" &&
  101. audit.Resource == "/api/municipalities/profile");
  102. }
  103. [Fact]
  104. public void RoleExtraction_WhenKeycloakJsonClaimIsMalformed_IgnoresClaim()
  105. {
  106. var roles = ApplicationRole.ExtractKeycloakRoles(
  107. [new Claim("realm_access", "{not-json")],
  108. ClientId);
  109. Assert.Empty(roles);
  110. }
  111. [Fact]
  112. public void WorkspaceResolver_WhenUserHasMultipleRoles_UsesStablePriority()
  113. {
  114. var path = RoleWorkspaceResolver.ResolveWorkspacePath(
  115. [ApplicationRole.Production, ApplicationRole.Admin]);
  116. Assert.Equal("/workspace/admin", path);
  117. }
  118. private HttpClient CreateClientWithRole(string role, string subject = "daniel@example.test")
  119. {
  120. var client = _factory.CreateClient();
  121. client.DefaultRequestHeaders.Authorization =
  122. new AuthenticationHeaderValue("Bearer", CreateToken(subject, role));
  123. return client;
  124. }
  125. private static string CreateToken(string subject, string role)
  126. {
  127. var credentials = new SigningCredentials(
  128. new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SigningKey)),
  129. SecurityAlgorithms.HmacSha256);
  130. var token = new JwtSecurityToken(
  131. issuer: Issuer,
  132. audience: ClientId,
  133. claims:
  134. [
  135. new Claim(JwtRegisteredClaimNames.Sub, subject),
  136. new Claim(ClaimTypes.Name, subject),
  137. new Claim(ClaimTypes.Role, role),
  138. ],
  139. expires: DateTime.UtcNow.AddMinutes(10),
  140. signingCredentials: credentials);
  141. return new JwtSecurityTokenHandler().WriteToken(token);
  142. }
  143. private static string CreateTokenWithRealmAccessRole(string subject, string role)
  144. {
  145. var credentials = new SigningCredentials(
  146. new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SigningKey)),
  147. SecurityAlgorithms.HmacSha256);
  148. var token = new JwtSecurityToken(
  149. issuer: Issuer,
  150. audience: ClientId,
  151. claims:
  152. [
  153. new Claim(JwtRegisteredClaimNames.Sub, subject),
  154. new Claim(ClaimTypes.Name, subject),
  155. new Claim("realm_access", $$"""{"roles":["{{role}}"]}""", JsonClaimValueTypes.Json),
  156. ],
  157. expires: DateTime.UtcNow.AddMinutes(10),
  158. signingCredentials: credentials);
  159. return new JwtSecurityTokenHandler().WriteToken(token);
  160. }
  161. }

Powered by TurnKey Linux.