diff --git a/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs b/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs index df3e5890..d2b79e60 100644 --- a/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs +++ b/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs @@ -348,7 +348,7 @@ public async Task Turnierplan_Client_Throws_Exception_When_Version_Does_Not_Matc _ = await client.GetTournament("x"); }; - var actualVersion = typeof(TurnierplanClient).Assembly.GetName().Version!.ToString(); + var actualVersion = System.Text.RegularExpressions.Regex.Replace(typeof(TurnierplanClient).Assembly.GetName().Version!.ToString(), @"\.0$", string.Empty); var expectedMessage = sendHeader ? $"Server version '2024.0.0' does not match the Turnierplan.Adapter version '{actualVersion}'." : "Could not get 'X-Turnierplan-Version' header from response."; diff --git a/src/Turnierplan.Adapter/TurnierplanClient.cs b/src/Turnierplan.Adapter/TurnierplanClient.cs index 114af19a..d3913dc0 100644 --- a/src/Turnierplan.Adapter/TurnierplanClient.cs +++ b/src/Turnierplan.Adapter/TurnierplanClient.cs @@ -2,6 +2,7 @@ using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; using Microsoft.AspNetCore.Http.Extensions; using Turnierplan.Adapter.Models; @@ -16,9 +17,7 @@ public sealed class TurnierplanClient : IDisposable private const string ApiKeySecretHeaderName = "X-Api-Key-Secret"; private const string TurnierplanVersionHeaderName = "X-Turnierplan-Version"; - private static readonly string __turnierplanAdapterVersion = - typeof(TurnierplanClient).Assembly.GetName().Version?.ToString() - ?? throw new InvalidOperationException("Could not determine Turnierplan.Adapter version from assembly name."); + private static readonly string __turnierplanAdapterVersion = DetermineAdapterVersion(); private static readonly JsonSerializerOptions __serializerOptions = new() { @@ -118,6 +117,22 @@ public void Dispose() } } + private static string DetermineAdapterVersion() + { + var assemblyVersion = typeof(TurnierplanClient).Assembly.GetName().Version?.ToString(); + + if (assemblyVersion is null) + { + throw new InvalidOperationException("Could not determine Turnierplan.Adapter version from assembly name."); + } + + var match = Regex.Match(assemblyVersion, @"^(?\d+\.\d+\.\d+)\.0$"); + + return match.Success + ? match.Groups["Version"].Value + : throw new InvalidOperationException("Could not determine Turnierplan.Adapter version from assembly name."); + } + private static void VerifyServerVersion(HttpResponseMessage response) { if (!response.Headers.TryGetValues(TurnierplanVersionHeaderName, out var headerValue)) diff --git a/src/Turnierplan.App/Middlewares/TurnierplanVersionMiddleware.cs b/src/Turnierplan.App/Middlewares/TurnierplanVersionMiddleware.cs new file mode 100644 index 00000000..0ed5c7b9 --- /dev/null +++ b/src/Turnierplan.App/Middlewares/TurnierplanVersionMiddleware.cs @@ -0,0 +1,18 @@ +using Turnierplan.App.Constants; + +namespace Turnierplan.App.Middlewares; + +internal sealed class TurnierplanVersionMiddleware(RequestDelegate next) +{ + private const string TurnierplanVersionHeaderName = "X-Turnierplan-Version"; + + public async Task InvokeAsync(HttpContext httpContext) + { + if (httpContext.User.Identity?.IsAuthenticated == true) + { + httpContext.Response.Headers.Append(TurnierplanVersionHeaderName, TurnierplanVersion.Version); + } + + await next(httpContext); + } +} diff --git a/src/Turnierplan.App/Program.cs b/src/Turnierplan.App/Program.cs index 34a2396d..070f34b4 100644 --- a/src/Turnierplan.App/Program.cs +++ b/src/Turnierplan.App/Program.cs @@ -8,6 +8,7 @@ using Turnierplan.App.Extensions; using Turnierplan.App.Helpers; using Turnierplan.App.Mapping; +using Turnierplan.App.Middlewares; using Turnierplan.App.OpenApi; using Turnierplan.App.Options; using Turnierplan.Dal.Extensions; @@ -83,6 +84,8 @@ app.UseAuthentication(); app.UseAuthorization(); +app.UseMiddleware(); + app.Map(string.Empty, (HttpContext context) => { context.Response.StatusCode = StatusCodes.Status303SeeOther; diff --git a/src/Turnierplan.App/Security/ApiKeyAuthenticationHandler.cs b/src/Turnierplan.App/Security/ApiKeyAuthenticationHandler.cs index beb3b6e2..737547a7 100644 --- a/src/Turnierplan.App/Security/ApiKeyAuthenticationHandler.cs +++ b/src/Turnierplan.App/Security/ApiKeyAuthenticationHandler.cs @@ -12,13 +12,9 @@ namespace Turnierplan.App.Security; internal sealed class ApiKeyAuthenticationHandler : AuthenticationHandler { + private const string AuthenticationTypeName = "TurnierplanApiKeyAuthentication"; private const string ApiKeyIdHeaderName = "x-api-key"; private const string ApiKeySecretHeaderName = "x-api-key-secret"; - private const string TurnierplanVersionHeaderName = "x-turnierplan-version"; - - private static readonly string __turnierplanVersion = - typeof(ApiKeyAuthenticationHandler).Assembly.GetName().Version?.ToString() - ?? throw new InvalidOperationException("Could not determine turnierplan.NET version from assembly name."); private readonly IApiKeyRepository _apiKeyRepository; private readonly IPasswordHasher _secretHasher; @@ -72,7 +68,7 @@ protected override async Task HandleAuthenticateAsync() await _apiKeyRepository.UnitOfWork.SaveChangesAsync(); - var identity = new ClaimsIdentity(claims: [ + var identity = new ClaimsIdentity(authenticationType: AuthenticationTypeName, claims: [ new Claim(ClaimTypes.PrincipalId, apiKey.PrincipalId.ToString()), new Claim(ClaimTypes.PrincipalKind, nameof(PrincipalKind.ApiKey)) ]); @@ -80,9 +76,6 @@ protected override async Task HandleAuthenticateAsync() var principal = new ClaimsPrincipal([ identity ]); var ticket = new AuthenticationTicket(principal, Scheme.Name); - // Add the version as response header so that the Turnierplan.Adapter, if used, can detect a potential version mismatch - Context.Response.Headers.Append(TurnierplanVersionHeaderName, __turnierplanVersion); - return AuthenticateResult.Success(ticket); } }