From f077fe1d8d084170e2a0623d107d4e464244f7a5 Mon Sep 17 00:00:00 2001 From: James Wampler Date: Wed, 10 Jun 2026 13:19:59 -0700 Subject: [PATCH] Fix build errors and enable TreatWarningsAsErrors across all projects Remove duplicate source files from old ApiKeys/, Authentication/, and Authorization/ folders that were already migrated to Common/Security/. Update test namespace usings to match. Pin EFC Relational 10.0.5 to resolve MSB3277 version conflict with Npgsql's transitive dependency. Add LangVersion and TreatWarningsAsErrors to AppHost and ServiceDefaults. --- src/MicCheck.AppHost/MicCheck.AppHost.csproj | 2 + .../MicCheck.ServiceDefaults.csproj | 2 + src/api/MicCheck.Api/ApiKeys/ApiKey.cs | 13 -- .../MicCheck.Api/ApiKeys/ApiKeyEndpoints.cs | 46 ------ src/api/MicCheck.Api/ApiKeys/ApiKeyHasher.cs | 22 --- .../MicCheck.Api/ApiKeys/ApiKeyResponse.cs | 9 -- src/api/MicCheck.Api/ApiKeys/ApiKeyService.cs | 50 ------ .../ApiKeys/CreateApiKeyRequest.cs | 3 - .../ApiKeys/CreateApiKeyResponse.cs | 8 - .../ApiKeyAuthenticationHandler.cs | 60 -------- .../EnvironmentKeyAuthenticationHandler.cs | 45 ------ .../Authorization/AuthEndpoints.cs | 66 -------- .../MicCheck.Api/Authorization/AuthService.cs | 145 ------------------ .../Authorization/AuthorizationPolicies.cs | 8 - .../Authorization/ITokenService.cs | 8 - .../Authorization/LoginRequest.cs | 3 - .../Authorization/LoginResponse.cs | 3 - .../Authorization/LogoutRequest.cs | 3 - .../Authorization/ProjectPermission.cs | 17 -- .../ProjectPermissionRequirement.cs | 10 -- .../ProjectPermissionRequirementHandler.cs | 54 ------- .../Authorization/RefreshRequest.cs | 3 - .../Authorization/RegisterRequest.cs | 8 - .../Authorization/TokenResponse.cs | 3 - .../Authorization/TokenService.cs | 52 ------- .../Authorization/UserProjectPermission.cs | 9 -- src/api/MicCheck.Api/MicCheck.Api.csproj | 3 +- .../ApiKeys/ApiKeyHasherTests.cs | 2 +- .../ApiKeys/ApiKeyTests.cs | 2 +- .../ApiKeyAuthenticationHandlerTests.cs | 2 +- .../Authorization/AuthEndpointsTests.cs | 2 +- ...rojectPermissionRequirementHandlerTests.cs | 2 +- .../Authorization/TokenServiceTests.cs | 2 +- 33 files changed, 12 insertions(+), 655 deletions(-) delete mode 100755 src/api/MicCheck.Api/ApiKeys/ApiKey.cs delete mode 100755 src/api/MicCheck.Api/ApiKeys/ApiKeyEndpoints.cs delete mode 100755 src/api/MicCheck.Api/ApiKeys/ApiKeyHasher.cs delete mode 100755 src/api/MicCheck.Api/ApiKeys/ApiKeyResponse.cs delete mode 100755 src/api/MicCheck.Api/ApiKeys/ApiKeyService.cs delete mode 100755 src/api/MicCheck.Api/ApiKeys/CreateApiKeyRequest.cs delete mode 100755 src/api/MicCheck.Api/ApiKeys/CreateApiKeyResponse.cs delete mode 100755 src/api/MicCheck.Api/Authentication/ApiKeyAuthenticationHandler.cs delete mode 100755 src/api/MicCheck.Api/Authentication/EnvironmentKeyAuthenticationHandler.cs delete mode 100755 src/api/MicCheck.Api/Authorization/AuthEndpoints.cs delete mode 100755 src/api/MicCheck.Api/Authorization/AuthService.cs delete mode 100755 src/api/MicCheck.Api/Authorization/AuthorizationPolicies.cs delete mode 100755 src/api/MicCheck.Api/Authorization/ITokenService.cs delete mode 100755 src/api/MicCheck.Api/Authorization/LoginRequest.cs delete mode 100755 src/api/MicCheck.Api/Authorization/LoginResponse.cs delete mode 100755 src/api/MicCheck.Api/Authorization/LogoutRequest.cs delete mode 100755 src/api/MicCheck.Api/Authorization/ProjectPermission.cs delete mode 100755 src/api/MicCheck.Api/Authorization/ProjectPermissionRequirement.cs delete mode 100755 src/api/MicCheck.Api/Authorization/ProjectPermissionRequirementHandler.cs delete mode 100755 src/api/MicCheck.Api/Authorization/RefreshRequest.cs delete mode 100755 src/api/MicCheck.Api/Authorization/RegisterRequest.cs delete mode 100755 src/api/MicCheck.Api/Authorization/TokenResponse.cs delete mode 100755 src/api/MicCheck.Api/Authorization/TokenService.cs delete mode 100755 src/api/MicCheck.Api/Authorization/UserProjectPermission.cs diff --git a/src/MicCheck.AppHost/MicCheck.AppHost.csproj b/src/MicCheck.AppHost/MicCheck.AppHost.csproj index 6262cb9..41f4d69 100755 --- a/src/MicCheck.AppHost/MicCheck.AppHost.csproj +++ b/src/MicCheck.AppHost/MicCheck.AppHost.csproj @@ -14,6 +14,8 @@ net10.0 enable enable + latest + true d10a4485-2ac0-4ba7-bda5-8eb63e417567 diff --git a/src/MicCheck.ServiceDefaults/MicCheck.ServiceDefaults.csproj b/src/MicCheck.ServiceDefaults/MicCheck.ServiceDefaults.csproj index 1efc067..bec9d25 100755 --- a/src/MicCheck.ServiceDefaults/MicCheck.ServiceDefaults.csproj +++ b/src/MicCheck.ServiceDefaults/MicCheck.ServiceDefaults.csproj @@ -4,6 +4,8 @@ net10.0 enable enable + latest + true true diff --git a/src/api/MicCheck.Api/ApiKeys/ApiKey.cs b/src/api/MicCheck.Api/ApiKeys/ApiKey.cs deleted file mode 100755 index c3319e0..0000000 --- a/src/api/MicCheck.Api/ApiKeys/ApiKey.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace MicCheck.Api.ApiKeys; - -public class ApiKey -{ - public int Id { get; init; } - public required string Key { get; set; } - public required string Prefix { get; set; } - public required string Name { get; set; } - public int OrganizationId { get; init; } - public bool IsActive { get; set; } - public DateTimeOffset? ExpiresAt { get; set; } - public DateTimeOffset CreatedAt { get; init; } -} diff --git a/src/api/MicCheck.Api/ApiKeys/ApiKeyEndpoints.cs b/src/api/MicCheck.Api/ApiKeys/ApiKeyEndpoints.cs deleted file mode 100755 index ca100f9..0000000 --- a/src/api/MicCheck.Api/ApiKeys/ApiKeyEndpoints.cs +++ /dev/null @@ -1,46 +0,0 @@ -using MicCheck.Api.Authorization; - -namespace MicCheck.Api.ApiKeys; - -public static class ApiKeyEndpoints -{ - public static void MapApiKeyEndpoints(this WebApplication app) - { - var group = app - .MapGroup("/api/v1/organisations/{organizationId:int}/api-keys") - .RequireAuthorization(AuthorizationPolicies.OrganizationAdmin) - .WithTags("ApiKeys"); - - group.MapPost("/", async ( - int organizationId, - CreateApiKeyRequest request, - ApiKeyService apiKeyService, - CancellationToken ct) => - { - var (key, rawKey) = await apiKeyService.CreateAsync( - organizationId, request.Name, request.ExpiresAt, ct); - - return Results.Ok(new CreateApiKeyResponse(key.Id, key.Name, rawKey, key.Prefix, key.ExpiresAt)); - }).WithName("CreateApiKey"); - - group.MapGet("/", async ( - int organizationId, - ApiKeyService apiKeyService, - CancellationToken ct) => - { - var keys = await apiKeyService.ListAsync(organizationId, ct); - return Results.Ok(keys.Select(k => - new ApiKeyResponse(k.Id, k.Name, k.Prefix, k.IsActive, k.ExpiresAt, k.CreatedAt))); - }).WithName("ListApiKeys"); - - group.MapDelete("/{keyId:int}", async ( - int organizationId, - int keyId, - ApiKeyService apiKeyService, - CancellationToken ct) => - { - await apiKeyService.RevokeAsync(organizationId, keyId, ct); - return Results.NoContent(); - }).WithName("RevokeApiKey"); - } -} diff --git a/src/api/MicCheck.Api/ApiKeys/ApiKeyHasher.cs b/src/api/MicCheck.Api/ApiKeys/ApiKeyHasher.cs deleted file mode 100755 index 63a7445..0000000 --- a/src/api/MicCheck.Api/ApiKeys/ApiKeyHasher.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Security.Cryptography; -using System.Text; - -namespace MicCheck.Api.ApiKeys; - -public static class ApiKeyHasher -{ - public static string Hash(string key) - { - var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(key)); - return Convert.ToHexString(bytes).ToLowerInvariant(); - } - - public static string GenerateKey() - { - var bytes = RandomNumberGenerator.GetBytes(32); - return Convert.ToBase64String(bytes) - .TrimEnd('=') - .Replace('+', '-') - .Replace('/', '_'); - } -} diff --git a/src/api/MicCheck.Api/ApiKeys/ApiKeyResponse.cs b/src/api/MicCheck.Api/ApiKeys/ApiKeyResponse.cs deleted file mode 100755 index 98e32d5..0000000 --- a/src/api/MicCheck.Api/ApiKeys/ApiKeyResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MicCheck.Api.ApiKeys; - -public record ApiKeyResponse( - int Id, - string Name, - string Prefix, - bool IsActive, - DateTimeOffset? ExpiresAt, - DateTimeOffset CreatedAt); diff --git a/src/api/MicCheck.Api/ApiKeys/ApiKeyService.cs b/src/api/MicCheck.Api/ApiKeys/ApiKeyService.cs deleted file mode 100755 index f697c5a..0000000 --- a/src/api/MicCheck.Api/ApiKeys/ApiKeyService.cs +++ /dev/null @@ -1,50 +0,0 @@ -using MicCheck.Api.Data; -using Microsoft.EntityFrameworkCore; - -namespace MicCheck.Api.ApiKeys; - -public class ApiKeyService(MicCheckDbContext db) -{ - public async Task<(ApiKey Key, string RawKey)> CreateAsync( - int organizationId, string name, DateTimeOffset? expiresAt, CancellationToken ct = default) - { - var rawKey = ApiKeyHasher.GenerateKey(); - var hashedKey = ApiKeyHasher.Hash(rawKey); - var prefix = rawKey.Length >= 8 ? rawKey[..8] : rawKey; - - var apiKey = new ApiKey - { - Key = hashedKey, - Prefix = prefix, - Name = name, - OrganizationId = organizationId, - IsActive = true, - ExpiresAt = expiresAt, - CreatedAt = DateTimeOffset.UtcNow - }; - - db.ApiKeys.Add(apiKey); - await db.SaveChangesAsync(ct); - - return (apiKey, rawKey); - } - - public async Task> ListAsync(int organizationId, CancellationToken ct = default) - { - return await db.ApiKeys - .Where(k => k.OrganizationId == organizationId) - .ToListAsync(ct); - } - - public async Task RevokeAsync(int organizationId, int keyId, CancellationToken ct = default) - { - var key = await db.ApiKeys - .FirstOrDefaultAsync(k => k.Id == keyId && k.OrganizationId == organizationId, ct); - - if (key is null) - return; - - key.IsActive = false; - await db.SaveChangesAsync(ct); - } -} diff --git a/src/api/MicCheck.Api/ApiKeys/CreateApiKeyRequest.cs b/src/api/MicCheck.Api/ApiKeys/CreateApiKeyRequest.cs deleted file mode 100755 index f495617..0000000 --- a/src/api/MicCheck.Api/ApiKeys/CreateApiKeyRequest.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace MicCheck.Api.ApiKeys; - -public record CreateApiKeyRequest(string Name, DateTimeOffset? ExpiresAt); diff --git a/src/api/MicCheck.Api/ApiKeys/CreateApiKeyResponse.cs b/src/api/MicCheck.Api/ApiKeys/CreateApiKeyResponse.cs deleted file mode 100755 index 615ede6..0000000 --- a/src/api/MicCheck.Api/ApiKeys/CreateApiKeyResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MicCheck.Api.ApiKeys; - -public record CreateApiKeyResponse( - int Id, - string Name, - string Key, - string Prefix, - DateTimeOffset? ExpiresAt); diff --git a/src/api/MicCheck.Api/Authentication/ApiKeyAuthenticationHandler.cs b/src/api/MicCheck.Api/Authentication/ApiKeyAuthenticationHandler.cs deleted file mode 100755 index 859bbe7..0000000 --- a/src/api/MicCheck.Api/Authentication/ApiKeyAuthenticationHandler.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Security.Claims; -using System.Text.Encodings.Web; -using MicCheck.Api.ApiKeys; -using MicCheck.Api.Data; -using Microsoft.AspNetCore.Authentication; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace MicCheck.Api.Authentication; - -public class ApiKeyAuthenticationHandler( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - MicCheckDbContext db) - : AuthenticationHandler(options, logger, encoder) -{ - public const string SchemeName = "ApiKey"; - private const string ApiKeyPrefix = "Api-Key "; - - protected override async Task HandleAuthenticateAsync() - { - if (!Request.Headers.TryGetValue("Authorization", out var authHeader)) - return AuthenticateResult.NoResult(); - - var headerValue = authHeader.FirstOrDefault(); - if (string.IsNullOrWhiteSpace(headerValue) || - !headerValue.StartsWith(ApiKeyPrefix, StringComparison.OrdinalIgnoreCase)) - return AuthenticateResult.NoResult(); - - var rawKey = headerValue[ApiKeyPrefix.Length..].Trim(); - if (string.IsNullOrWhiteSpace(rawKey)) - return AuthenticateResult.Fail("API key value is empty."); - - var hashedKey = ApiKeyHasher.Hash(rawKey); - - var apiKey = await db.ApiKeys - .FirstOrDefaultAsync(k => k.Key == hashedKey); - - if (apiKey is null) - return AuthenticateResult.Fail("Invalid API key."); - - if (!apiKey.IsActive) - return AuthenticateResult.Fail("API key is inactive."); - - if (apiKey.ExpiresAt.HasValue && apiKey.ExpiresAt.Value < DateTimeOffset.UtcNow) - return AuthenticateResult.Fail("API key has expired."); - - var claims = new[] - { - new Claim("OrganizationId", apiKey.OrganizationId.ToString()), - new Claim("OrganizationRole", "Admin") - }; - - var identity = new ClaimsIdentity(claims, Scheme.Name); - var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name); - - return AuthenticateResult.Success(ticket); - } -} diff --git a/src/api/MicCheck.Api/Authentication/EnvironmentKeyAuthenticationHandler.cs b/src/api/MicCheck.Api/Authentication/EnvironmentKeyAuthenticationHandler.cs deleted file mode 100755 index 42cfe0f..0000000 --- a/src/api/MicCheck.Api/Authentication/EnvironmentKeyAuthenticationHandler.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Security.Claims; -using System.Text.Encodings.Web; -using MicCheck.Api.Data; -using Microsoft.AspNetCore.Authentication; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace MicCheck.Api.Authentication; - -public class EnvironmentKeyAuthenticationHandler( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - MicCheckDbContext db) - : AuthenticationHandler(options, logger, encoder) -{ - public const string SchemeName = "EnvironmentKey"; - - protected override async Task HandleAuthenticateAsync() - { - if (!Request.Headers.TryGetValue("X-Environment-Key", out var keyValues)) - return AuthenticateResult.NoResult(); - - var key = keyValues.FirstOrDefault(); - if (string.IsNullOrWhiteSpace(key)) - return AuthenticateResult.Fail("X-Environment-Key header is empty."); - - var environment = await db.Environments - .FirstOrDefaultAsync(e => e.ApiKey == key); - - if (environment is null) - return AuthenticateResult.Fail("Invalid environment key."); - - var claims = new[] - { - new Claim("EnvironmentId", environment.Id.ToString()), - new Claim("ProjectId", environment.ProjectId.ToString()) - }; - - var identity = new ClaimsIdentity(claims, Scheme.Name); - var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name); - - return AuthenticateResult.Success(ticket); - } -} diff --git a/src/api/MicCheck.Api/Authorization/AuthEndpoints.cs b/src/api/MicCheck.Api/Authorization/AuthEndpoints.cs deleted file mode 100755 index 77b1477..0000000 --- a/src/api/MicCheck.Api/Authorization/AuthEndpoints.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace MicCheck.Api.Authorization; - -public static class AuthEndpoints -{ - public static void MapAuthEndpoints(this WebApplication app) - { - var group = app.MapGroup("/api/v1/auth").AllowAnonymous().WithTags("Auth"); - - group.MapPost("/login", async ( - [FromBody] LoginRequest request, - AuthService authService, - CancellationToken ct) => - { - if (string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.Password)) - return Results.BadRequest("Email and password are required."); - - var response = await authService.LoginAsync(request.Email, request.Password, ct); - return response is null - ? Results.Unauthorized() - : Results.Ok(response); - }).WithName("Login"); - - group.MapPost("/register", async ( - [FromBody] RegisterRequest request, - AuthService authService, - CancellationToken ct) => - { - if (string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.Password)) - return Results.BadRequest("Email and password are required."); - - var response = await authService.RegisterAsync( - request.Email, request.Password, - request.FirstName, request.LastName, - request.OrganizationName, ct); - - return response is null - ? Results.Conflict("An account with this email already exists.") - : Results.Ok(response); - }).WithName("Register"); - - group.MapPost("/refresh", async ( - [FromBody] RefreshRequest request, - AuthService authService, - CancellationToken ct) => - { - if (string.IsNullOrWhiteSpace(request.Token)) - return Results.BadRequest("Refresh token is required."); - - var response = await authService.RefreshAsync(request.Token, ct); - return response is null - ? Results.Unauthorized() - : Results.Ok(response); - }).WithName("RefreshToken"); - - group.MapPost("/logout", async ( - [FromBody] LogoutRequest request, - AuthService authService, - CancellationToken ct) => - { - await authService.LogoutAsync(request.Token, ct); - return Results.NoContent(); - }).WithName("Logout"); - } -} diff --git a/src/api/MicCheck.Api/Authorization/AuthService.cs b/src/api/MicCheck.Api/Authorization/AuthService.cs deleted file mode 100755 index 714ced0..0000000 --- a/src/api/MicCheck.Api/Authorization/AuthService.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System.Security.Cryptography; -using MicCheck.Api.Data; -using MicCheck.Api.Organizations; -using MicCheck.Api.Users; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; - -namespace MicCheck.Api.Authorization; - -public class AuthService( - MicCheckDbContext db, - ITokenService tokenService, - IPasswordHasher passwordHasher) -{ - public async Task LoginAsync(string email, string password, CancellationToken ct = default) - { - var user = await db.Users - .Include(u => u.Organizations) - .FirstOrDefaultAsync(u => u.Email == email && u.IsActive, ct); - - if (user is null) - return null; - - var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password); - if (result == PasswordVerificationResult.Failed) - return null; - - var accessToken = tokenService.GenerateToken(user); - var refreshTokenValue = GenerateSecureToken(); - - db.RefreshTokens.Add(new RefreshToken - { - Token = refreshTokenValue, - UserId = user.Id, - ExpiresAt = DateTimeOffset.UtcNow.AddDays(30), - CreatedAt = DateTimeOffset.UtcNow - }); - - user.LastLoginAt = DateTimeOffset.UtcNow; - await db.SaveChangesAsync(ct); - - return new LoginResponse(accessToken.Token, refreshTokenValue, accessToken.ExpiresAt); - } - - public async Task RegisterAsync( - string email, string password, - string firstName, string lastName, - string organizationName, - CancellationToken ct = default) - { - if (await db.Users.AnyAsync(u => u.Email == email, ct)) - return null; - - var user = new User - { - Email = email, - FirstName = firstName, - LastName = lastName, - IsActive = true, - CreatedAt = DateTimeOffset.UtcNow, - PasswordHash = string.Empty - }; - user.PasswordHash = passwordHasher.HashPassword(user, password); - - db.Users.Add(user); - await db.SaveChangesAsync(ct); - - var organization = new Organization - { - Name = organizationName, - CreatedAt = DateTimeOffset.UtcNow - }; - db.Organizations.Add(organization); - await db.SaveChangesAsync(ct); - - db.OrganizationUsers.Add(new OrganizationUser - { - OrganizationId = organization.Id, - UserId = user.Id, - Role = OrganizationRole.Admin - }); - await db.SaveChangesAsync(ct); - - await db.Entry(user).Collection(u => u.Organizations).LoadAsync(ct); - - var accessToken = tokenService.GenerateToken(user); - var refreshTokenValue = GenerateSecureToken(); - - db.RefreshTokens.Add(new RefreshToken - { - Token = refreshTokenValue, - UserId = user.Id, - ExpiresAt = DateTimeOffset.UtcNow.AddDays(30), - CreatedAt = DateTimeOffset.UtcNow - }); - await db.SaveChangesAsync(ct); - - return new LoginResponse(accessToken.Token, refreshTokenValue, accessToken.ExpiresAt); - } - - public async Task RefreshAsync(string refreshToken, CancellationToken ct = default) - { - var token = await db.RefreshTokens - .Include(rt => rt.User) - .ThenInclude(u => u.Organizations) - .FirstOrDefaultAsync(rt => rt.Token == refreshToken && !rt.IsRevoked, ct); - - if (token is null || token.ExpiresAt < DateTimeOffset.UtcNow) - return null; - - token.IsRevoked = true; - - var accessToken = tokenService.GenerateToken(token.User); - var newRefreshTokenValue = GenerateSecureToken(); - - db.RefreshTokens.Add(new RefreshToken - { - Token = newRefreshTokenValue, - UserId = token.UserId, - ExpiresAt = DateTimeOffset.UtcNow.AddDays(30), - CreatedAt = DateTimeOffset.UtcNow - }); - await db.SaveChangesAsync(ct); - - return new LoginResponse(accessToken.Token, newRefreshTokenValue, accessToken.ExpiresAt); - } - - public async Task LogoutAsync(string refreshToken, CancellationToken ct = default) - { - var token = await db.RefreshTokens - .FirstOrDefaultAsync(rt => rt.Token == refreshToken && !rt.IsRevoked, ct); - - if (token is null) - return; - - token.IsRevoked = true; - await db.SaveChangesAsync(ct); - } - - private static string GenerateSecureToken() - { - var bytes = RandomNumberGenerator.GetBytes(64); - return Convert.ToBase64String(bytes); - } -} diff --git a/src/api/MicCheck.Api/Authorization/AuthorizationPolicies.cs b/src/api/MicCheck.Api/Authorization/AuthorizationPolicies.cs deleted file mode 100755 index 005befd..0000000 --- a/src/api/MicCheck.Api/Authorization/AuthorizationPolicies.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MicCheck.Api.Authorization; - -public static class AuthorizationPolicies -{ - public const string FlagsApiAccess = "FlagsApiAccess"; - public const string AdminApiAccess = "AdminApiAccess"; - public const string OrganizationAdmin = "OrganizationAdmin"; -} diff --git a/src/api/MicCheck.Api/Authorization/ITokenService.cs b/src/api/MicCheck.Api/Authorization/ITokenService.cs deleted file mode 100755 index bbbfba1..0000000 --- a/src/api/MicCheck.Api/Authorization/ITokenService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using MicCheck.Api.Users; - -namespace MicCheck.Api.Authorization; - -public interface ITokenService -{ - TokenResponse GenerateToken(User user); -} diff --git a/src/api/MicCheck.Api/Authorization/LoginRequest.cs b/src/api/MicCheck.Api/Authorization/LoginRequest.cs deleted file mode 100755 index f1fbd85..0000000 --- a/src/api/MicCheck.Api/Authorization/LoginRequest.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace MicCheck.Api.Authorization; - -public record LoginRequest(string Email, string Password); diff --git a/src/api/MicCheck.Api/Authorization/LoginResponse.cs b/src/api/MicCheck.Api/Authorization/LoginResponse.cs deleted file mode 100755 index e13a8d8..0000000 --- a/src/api/MicCheck.Api/Authorization/LoginResponse.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace MicCheck.Api.Authorization; - -public record LoginResponse(string AccessToken, string RefreshToken, DateTime ExpiresAt); diff --git a/src/api/MicCheck.Api/Authorization/LogoutRequest.cs b/src/api/MicCheck.Api/Authorization/LogoutRequest.cs deleted file mode 100755 index b379da0..0000000 --- a/src/api/MicCheck.Api/Authorization/LogoutRequest.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace MicCheck.Api.Authorization; - -public record LogoutRequest(string Token); diff --git a/src/api/MicCheck.Api/Authorization/ProjectPermission.cs b/src/api/MicCheck.Api/Authorization/ProjectPermission.cs deleted file mode 100755 index 83d795b..0000000 --- a/src/api/MicCheck.Api/Authorization/ProjectPermission.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace MicCheck.Api.Authorization; - -public enum ProjectPermission -{ - ViewProject, - CreateFeature, - EditFeature, - DeleteFeature, - CreateEnvironment, - EditEnvironment, - DeleteEnvironment, - CreateSegment, - EditSegment, - DeleteSegment, - ManageWebhooks, - ViewAuditLog -} diff --git a/src/api/MicCheck.Api/Authorization/ProjectPermissionRequirement.cs b/src/api/MicCheck.Api/Authorization/ProjectPermissionRequirement.cs deleted file mode 100755 index af864db..0000000 --- a/src/api/MicCheck.Api/Authorization/ProjectPermissionRequirement.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace MicCheck.Api.Authorization; - -public class ProjectPermissionRequirement : IAuthorizationRequirement -{ - public ProjectPermission Permission { get; } - - public ProjectPermissionRequirement(ProjectPermission permission) => Permission = permission; -} diff --git a/src/api/MicCheck.Api/Authorization/ProjectPermissionRequirementHandler.cs b/src/api/MicCheck.Api/Authorization/ProjectPermissionRequirementHandler.cs deleted file mode 100755 index c550931..0000000 --- a/src/api/MicCheck.Api/Authorization/ProjectPermissionRequirementHandler.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using MicCheck.Api.Data; -using MicCheck.Api.Organizations; -using Microsoft.AspNetCore.Authorization; -using Microsoft.EntityFrameworkCore; - -namespace MicCheck.Api.Authorization; - -public class ProjectPermissionRequirementHandler( - MicCheckDbContext db, - IHttpContextAccessor httpContextAccessor) - : AuthorizationHandler -{ - protected override async Task HandleRequirementAsync( - AuthorizationHandlerContext context, - ProjectPermissionRequirement requirement) - { - var userIdClaim = context.User.FindFirst(JwtRegisteredClaimNames.Sub); - if (userIdClaim is null || !int.TryParse(userIdClaim.Value, out var userId)) - { - context.Fail(); - return; - } - - if (context.User.HasClaim("OrganizationRole", OrganizationRole.Admin.ToString())) - { - context.Succeed(requirement); - return; - } - - var httpContext = httpContextAccessor.HttpContext; - if (httpContext is null || - !httpContext.Request.RouteValues.TryGetValue("projectId", out var projectIdValue) || - !int.TryParse(projectIdValue?.ToString(), out var projectId)) - { - context.Fail(); - return; - } - - var permission = await db.UserProjectPermissions - .FirstOrDefaultAsync(p => p.UserId == userId && p.ProjectId == projectId); - - if (permission is null) - { - context.Fail(); - return; - } - - if (permission.IsAdmin || permission.Permissions.Contains(requirement.Permission)) - context.Succeed(requirement); - else - context.Fail(); - } -} diff --git a/src/api/MicCheck.Api/Authorization/RefreshRequest.cs b/src/api/MicCheck.Api/Authorization/RefreshRequest.cs deleted file mode 100755 index 94aa24e..0000000 --- a/src/api/MicCheck.Api/Authorization/RefreshRequest.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace MicCheck.Api.Authorization; - -public record RefreshRequest(string Token); diff --git a/src/api/MicCheck.Api/Authorization/RegisterRequest.cs b/src/api/MicCheck.Api/Authorization/RegisterRequest.cs deleted file mode 100755 index 58dcffe..0000000 --- a/src/api/MicCheck.Api/Authorization/RegisterRequest.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MicCheck.Api.Authorization; - -public record RegisterRequest( - string Email, - string Password, - string FirstName, - string LastName, - string OrganizationName); diff --git a/src/api/MicCheck.Api/Authorization/TokenResponse.cs b/src/api/MicCheck.Api/Authorization/TokenResponse.cs deleted file mode 100755 index 0439497..0000000 --- a/src/api/MicCheck.Api/Authorization/TokenResponse.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace MicCheck.Api.Authorization; - -public record TokenResponse(string Token, DateTime ExpiresAt); diff --git a/src/api/MicCheck.Api/Authorization/TokenService.cs b/src/api/MicCheck.Api/Authorization/TokenService.cs deleted file mode 100755 index 089fb53..0000000 --- a/src/api/MicCheck.Api/Authorization/TokenService.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; -using MicCheck.Api.Users; -using Microsoft.IdentityModel.Tokens; - -namespace MicCheck.Api.Authorization; - -public class TokenService(IConfiguration configuration) : ITokenService -{ - public TokenResponse GenerateToken(User user) - { - var secretKey = configuration["Jwt:SecretKey"] - ?? throw new InvalidOperationException("Jwt:SecretKey is not configured."); - var issuer = configuration["Jwt:Issuer"] - ?? throw new InvalidOperationException("Jwt:Issuer is not configured."); - var audience = configuration["Jwt:Audience"] - ?? throw new InvalidOperationException("Jwt:Audience is not configured."); - var expiryMinutes = int.Parse(configuration["Jwt:ExpiryMinutes"] ?? "60"); - - var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)); - var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); - var expiresAt = DateTime.UtcNow.AddMinutes(expiryMinutes); - - var claims = new List - { - new(JwtRegisteredClaimNames.Sub, user.Id.ToString()), - new(JwtRegisteredClaimNames.Email, user.Email), - new(JwtRegisteredClaimNames.GivenName, user.FirstName), - new(JwtRegisteredClaimNames.FamilyName, user.LastName), - new(JwtRegisteredClaimNames.Iat, - DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), - ClaimValueTypes.Integer64), - new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), - }; - - foreach (var org in user.Organizations) - { - claims.Add(new Claim("OrganizationId", org.OrganizationId.ToString())); - claims.Add(new Claim("OrganizationRole", org.Role.ToString())); - } - - var token = new JwtSecurityToken( - issuer: issuer, - audience: audience, - claims: claims, - expires: expiresAt, - signingCredentials: credentials); - - return new TokenResponse(new JwtSecurityTokenHandler().WriteToken(token), expiresAt); - } -} diff --git a/src/api/MicCheck.Api/Authorization/UserProjectPermission.cs b/src/api/MicCheck.Api/Authorization/UserProjectPermission.cs deleted file mode 100755 index 34001bd..0000000 --- a/src/api/MicCheck.Api/Authorization/UserProjectPermission.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MicCheck.Api.Authorization; - -public class UserProjectPermission -{ - public int UserId { get; init; } - public int ProjectId { get; init; } - public List Permissions { get; set; } = []; - public bool IsAdmin { get; set; } -} diff --git a/src/api/MicCheck.Api/MicCheck.Api.csproj b/src/api/MicCheck.Api/MicCheck.Api.csproj index b8e72e0..b650ca2 100755 --- a/src/api/MicCheck.Api/MicCheck.Api.csproj +++ b/src/api/MicCheck.Api/MicCheck.Api.csproj @@ -13,11 +13,12 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyHasherTests.cs b/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyHasherTests.cs index f3d93b5..0a0c780 100755 --- a/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyHasherTests.cs +++ b/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyHasherTests.cs @@ -1,4 +1,4 @@ -using MicCheck.Api.ApiKeys; +using MicCheck.Api.Common.Security.ApiKeys; using NUnit.Framework; namespace MicCheck.Api.Tests.Unit.ApiKeys; diff --git a/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyTests.cs b/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyTests.cs index 7993d7b..42c151d 100755 --- a/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyTests.cs +++ b/tests/api/MicCheck.Api.Tests.Unit/ApiKeys/ApiKeyTests.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using MicCheck.Api.ApiKeys; +using MicCheck.Api.Common.Security.ApiKeys; namespace MicCheck.Api.Tests.Unit.ApiKeys; diff --git a/tests/api/MicCheck.Api.Tests.Unit/Authentication/ApiKeyAuthenticationHandlerTests.cs b/tests/api/MicCheck.Api.Tests.Unit/Authentication/ApiKeyAuthenticationHandlerTests.cs index 021b1cd..e84fd79 100755 --- a/tests/api/MicCheck.Api.Tests.Unit/Authentication/ApiKeyAuthenticationHandlerTests.cs +++ b/tests/api/MicCheck.Api.Tests.Unit/Authentication/ApiKeyAuthenticationHandlerTests.cs @@ -1,6 +1,6 @@ using System.Net; using System.Net.Http.Headers; -using MicCheck.Api.ApiKeys; +using MicCheck.Api.Common.Security.ApiKeys; using MicCheck.Api.Data; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; diff --git a/tests/api/MicCheck.Api.Tests.Unit/Authorization/AuthEndpointsTests.cs b/tests/api/MicCheck.Api.Tests.Unit/Authorization/AuthEndpointsTests.cs index 092c610..251437e 100755 --- a/tests/api/MicCheck.Api.Tests.Unit/Authorization/AuthEndpointsTests.cs +++ b/tests/api/MicCheck.Api.Tests.Unit/Authorization/AuthEndpointsTests.cs @@ -1,6 +1,6 @@ using System.Net; using System.Net.Http.Json; -using MicCheck.Api.Authorization; +using MicCheck.Api.Common.Security.Authorization; using NUnit.Framework; namespace MicCheck.Api.Tests.Unit.Authorization; diff --git a/tests/api/MicCheck.Api.Tests.Unit/Authorization/ProjectPermissionRequirementHandlerTests.cs b/tests/api/MicCheck.Api.Tests.Unit/Authorization/ProjectPermissionRequirementHandlerTests.cs index f331775..f2c323d 100755 --- a/tests/api/MicCheck.Api.Tests.Unit/Authorization/ProjectPermissionRequirementHandlerTests.cs +++ b/tests/api/MicCheck.Api.Tests.Unit/Authorization/ProjectPermissionRequirementHandlerTests.cs @@ -1,6 +1,6 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; -using MicCheck.Api.Authorization; +using MicCheck.Api.Common.Security.Authorization; using MicCheck.Api.Data; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; diff --git a/tests/api/MicCheck.Api.Tests.Unit/Authorization/TokenServiceTests.cs b/tests/api/MicCheck.Api.Tests.Unit/Authorization/TokenServiceTests.cs index 8235942..36fa883 100755 --- a/tests/api/MicCheck.Api.Tests.Unit/Authorization/TokenServiceTests.cs +++ b/tests/api/MicCheck.Api.Tests.Unit/Authorization/TokenServiceTests.cs @@ -1,5 +1,5 @@ using System.IdentityModel.Tokens.Jwt; -using MicCheck.Api.Authorization; +using MicCheck.Api.Common.Security.Authorization; using MicCheck.Api.Organizations; using MicCheck.Api.Users; using Microsoft.Extensions.Configuration;