From ce4e714fb132e240ca777268c4a14cee7af49181 Mon Sep 17 00:00:00 2001 From: Koen Metsu Date: Fri, 15 Dec 2023 14:01:12 +0100 Subject: [PATCH] feat: or-1349 add security; delete views on rebuild --- docker-compose.yml | 6 +- identityserver/vr.json | 15 +- .../VerenigingenPerInszProjection.cs | 8 +- .../Constants/Security.cs | 1 + .../ConfigurationBindings/AppSettings.cs | 10 +- .../AdminProjectionHostHttpClient.cs | 10 +- .../PublicProjectionHostHttpClient.cs | 6 +- .../ProjectionHostController.cs | 59 ++++---- .../Infrastructure}/ProjectionStatus.cs | 4 +- src/AssociationRegistry.Admin.Api/Program.cs | 18 ++- .../appsettings.development.json | 5 +- .../Program.cs | 15 +- .../BeheerVerenigingDetailProjection.cs | 1 + .../BeheerVerenigingHistoriekProjection.cs | 2 +- .../Program.cs | 12 +- .../PubliekVerenigingDetailProjection.cs | 2 +- .../AssociationRegistry.Test.Admin.Api.csproj | 3 + .../Fixtures/AdminApiClient.cs | 135 +++++++++++++++--- .../Fixtures/AdminApiFixture.cs | 5 + .../Given_An_Authorized_Client.cs | 27 ++++ .../Given_An_Unauthorized_Client.cs | 27 ++++ .../appsettings.json | 7 +- wiremock/mappings/projectionactions.json | 13 ++ 23 files changed, 303 insertions(+), 88 deletions(-) rename src/{AssociationRegistry => AssociationRegistry.Admin.Api/Infrastructure}/ProjectionStatus.cs (73%) create mode 100644 test/AssociationRegistry.Test.Admin.Api/When_Rebuilding/Given_An_Authorized_Client.cs create mode 100644 test/AssociationRegistry.Test.Admin.Api/When_Rebuilding/Given_An_Unauthorized_Client.cs create mode 100644 wiremock/mappings/projectionactions.json diff --git a/docker-compose.yml b/docker-compose.yml index aef778dad..cf9c45030 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,9 +15,9 @@ services: ELASTIC_PASSWORD: local_development discovery.type: single-node xpack.security.transport.ssl.enabled: false - cluster.routing.allocation.disk.watermark.low: 95% - cluster.routing.allocation.disk.watermark.high: 96% - cluster.routing.allocation.disk.watermark.flood_stage: 97% + cluster.routing.allocation.disk.watermark.low: 97% + cluster.routing.allocation.disk.watermark.high: 98% + cluster.routing.allocation.disk.watermark.flood_stage: 99% volumes: - es-data:/usr/share/elasticsearch/data diff --git a/identityserver/vr.json b/identityserver/vr.json index 4fe30c1ca..7f9219adf 100644 --- a/identityserver/vr.json +++ b/identityserver/vr.json @@ -54,7 +54,20 @@ "accessTokenLifetime": -1, "identityTokenLifetime": -1, "clientClaimsPrefix": "" + }, + { + "clientId": "superAdminClient", + "clientSecrets": [ + "secret" + ], + "allowedGrantTypes": "clientCredentials", + "allowedScopes": [ + "vo_info", + "dv_verenigingsregister_beheer" + ], + "accessTokenLifetime": -1, + "identityTokenLifetime": -1, + "clientClaimsPrefix": "" } ] } - diff --git a/src/AssociationRegistry.Acm.Api/Projections/VerenigingenPerInszProjection.cs b/src/AssociationRegistry.Acm.Api/Projections/VerenigingenPerInszProjection.cs index 2e20982f7..ee9a24e4d 100644 --- a/src/AssociationRegistry.Acm.Api/Projections/VerenigingenPerInszProjection.cs +++ b/src/AssociationRegistry.Acm.Api/Projections/VerenigingenPerInszProjection.cs @@ -1,14 +1,14 @@ namespace AssociationRegistry.Acm.Api.Projections; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Events; using Marten; using Marten.Events; using Marten.Events.Projections; using Schema.Constants; using Schema.VerenigingenPerInsz; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; public class VerenigingenPerInszProjection : EventProjection { @@ -18,6 +18,8 @@ public VerenigingenPerInszProjection() // the newly persisted VerenigingenPerInszDocument from FeitelijkeVerenigingWerdGeregistreerd is not in the // Query yet when we handle NaamWerdGewijzigd Options.BatchSize = 1; + Options.DeleteViewTypeOnTeardown(); + Options.DeleteViewTypeOnTeardown(); } public async Task Project(FeitelijkeVerenigingWerdGeregistreerd werdGeregistreerd, IDocumentOperations ops) diff --git a/src/AssociationRegistry.Admin.Api/Constants/Security.cs b/src/AssociationRegistry.Admin.Api/Constants/Security.cs index bb49379f6..bd1c2ce9c 100644 --- a/src/AssociationRegistry.Admin.Api/Constants/Security.cs +++ b/src/AssociationRegistry.Admin.Api/Constants/Security.cs @@ -5,6 +5,7 @@ public static class Security public static class ClaimTypes { public const string Scope = "scope"; + public const string ClientId = "client_id"; } public static class Scopes diff --git a/src/AssociationRegistry.Admin.Api/Infrastructure/ConfigurationBindings/AppSettings.cs b/src/AssociationRegistry.Admin.Api/Infrastructure/ConfigurationBindings/AppSettings.cs index 59aee565b..c43fbb9ee 100644 --- a/src/AssociationRegistry.Admin.Api/Infrastructure/ConfigurationBindings/AppSettings.cs +++ b/src/AssociationRegistry.Admin.Api/Infrastructure/ConfigurationBindings/AppSettings.cs @@ -1,19 +1,14 @@ namespace AssociationRegistry.Admin.Api.Infrastructure.ConfigurationBindings; +using System; + public class AppSettings { private string? _baseUrl; - private string? _beheerApiBaseUrl; private string? _beheerProjectionHostBaseUrl; private string? _publicApiBaseUrl; private string? _publicProjectionHostBaseUrl; - public string BeheerApiBaseUrl - { - get => _beheerApiBaseUrl?.TrimEnd(trimChar: '/') ?? string.Empty; - set => _beheerApiBaseUrl = value; - } - public string BeheerProjectionHostBaseUrl { get => _beheerProjectionHostBaseUrl?.TrimEnd(trimChar: '/') ?? string.Empty; @@ -38,6 +33,7 @@ public string BaseUrl set => _baseUrl = value; } + public string[] SuperAdminClientIds { get; set; } = Array.Empty(); public string Salt { get; set; } = null!; public ApiDocsSettings ApiDocs { get; set; } = new(); public SearchSettings Search { get; set; } = new(); diff --git a/src/AssociationRegistry.Admin.Api/Infrastructure/HttpClients/AdminProjectionHostHttpClient.cs b/src/AssociationRegistry.Admin.Api/Infrastructure/HttpClients/AdminProjectionHostHttpClient.cs index a1ff6f146..fbcbd23f3 100644 --- a/src/AssociationRegistry.Admin.Api/Infrastructure/HttpClients/AdminProjectionHostHttpClient.cs +++ b/src/AssociationRegistry.Admin.Api/Infrastructure/HttpClients/AdminProjectionHostHttpClient.cs @@ -15,20 +15,20 @@ public AdminProjectionHostHttpClient(HttpClient httpClient) } public async Task RebuildAllProjections(CancellationToken cancellationToken) - => await _httpClient.PostAsync(requestUri: "/projections/all/rebuild", content: null, cancellationToken); + => await _httpClient.PostAsync(requestUri: "/v1/projections/all/rebuild", content: null, cancellationToken); public async Task RebuildDetailProjection(CancellationToken cancellationToken) - => await _httpClient.PostAsync(requestUri: "/projections/detail/rebuild", content: null, cancellationToken); + => await _httpClient.PostAsync(requestUri: "/v1/projections/detail/rebuild", content: null, cancellationToken); public async Task RebuildHistoriekProjection(CancellationToken cancellationToken) - => await _httpClient.PostAsync(requestUri: "/projections/historiek/rebuild", content: null, cancellationToken); + => await _httpClient.PostAsync(requestUri: "/v1/projections/historiek/rebuild", content: null, cancellationToken); public async Task RebuildZoekenProjection(CancellationToken cancellationToken) - => await _httpClient.PostAsync(requestUri: "/projections/search/rebuild", content: null, cancellationToken); + => await _httpClient.PostAsync(requestUri: "/v1/projections/search/rebuild", content: null, cancellationToken); public async Task GetStatus(CancellationToken cancellationToken) { - var request = new HttpRequestMessage(HttpMethod.Get, requestUri: "/projections/status"); + var request = new HttpRequestMessage(HttpMethod.Get, requestUri: "/v1/projections/status"); request.Headers.Add(name: "X-Correlation-Id", Guid.NewGuid().ToString()); return await _httpClient.SendAsync(request, cancellationToken); diff --git a/src/AssociationRegistry.Admin.Api/Infrastructure/HttpClients/PublicProjectionHostHttpClient.cs b/src/AssociationRegistry.Admin.Api/Infrastructure/HttpClients/PublicProjectionHostHttpClient.cs index f8911ce79..08910408f 100644 --- a/src/AssociationRegistry.Admin.Api/Infrastructure/HttpClients/PublicProjectionHostHttpClient.cs +++ b/src/AssociationRegistry.Admin.Api/Infrastructure/HttpClients/PublicProjectionHostHttpClient.cs @@ -15,13 +15,13 @@ public PublicProjectionHostHttpClient(HttpClient httpClient) } public async Task RebuildDetailProjection(CancellationToken cancellationToken) - => await _httpClient.PostAsync(requestUri: "/projections/detail/rebuild", content: null, cancellationToken); + => await _httpClient.PostAsync(requestUri: "/v1/projections/detail/rebuild", content: null, cancellationToken); public async Task RebuildZoekenProjection(CancellationToken cancellationToken) - => await _httpClient.PostAsync(requestUri: "/projections/search/rebuild", content: null, cancellationToken); + => await _httpClient.PostAsync(requestUri: "/v1/projections/search/rebuild", content: null, cancellationToken); public async Task GetStatus(CancellationToken cancellationToken) - => await _httpClient.GetAsync(requestUri: "/projections/status", cancellationToken); + => await _httpClient.GetAsync(requestUri: "/v1/projections/status", cancellationToken); public void Dispose() { diff --git a/src/AssociationRegistry.Admin.Api/Infrastructure/ProjectionHostController.cs b/src/AssociationRegistry.Admin.Api/Infrastructure/ProjectionHostController.cs index a8ec7969e..fbb2f9ab1 100644 --- a/src/AssociationRegistry.Admin.Api/Infrastructure/ProjectionHostController.cs +++ b/src/AssociationRegistry.Admin.Api/Infrastructure/ProjectionHostController.cs @@ -2,7 +2,9 @@ using Be.Vlaanderen.Basisregisters.Api; using HttpClients; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using System.Threading; @@ -12,6 +14,7 @@ [AdvertiseApiVersions("1.0")] [ApiRoute("projections")] [ApiExplorerSettings(IgnoreApi = true)] +[Authorize(Policy = Program.SuperAdminPolicyName)] public class ProjectionHostController : ApiController { private readonly AdminProjectionHostHttpClient _adminHttpClient; @@ -34,9 +37,7 @@ public async Task RebuildAdminProjectionAll(CancellationToken can { var response = await _adminHttpClient.RebuildAllProjections(cancellationToken); - return response.IsSuccessStatusCode - ? Ok() - : UnprocessableEntity(); + return await OkOrForwardedResponse(cancellationToken, response); } [HttpPost("admin/detail/rebuild")] @@ -44,9 +45,7 @@ public async Task RebuildAdminProjectionDetail(CancellationToken { var response = await _adminHttpClient.RebuildDetailProjection(cancellationToken); - return response.IsSuccessStatusCode - ? Ok() - : UnprocessableEntity(); + return await OkOrForwardedResponse(cancellationToken, response); } [HttpPost("admin/historiek/rebuild")] @@ -54,9 +53,7 @@ public async Task RebuildAdminProjectionHistoriek(CancellationTok { var response = await _adminHttpClient.RebuildHistoriekProjection(cancellationToken); - return response.IsSuccessStatusCode - ? Ok() - : UnprocessableEntity(); + return await OkOrForwardedResponse(cancellationToken, response); } [HttpPost("admin/search/rebuild")] @@ -64,9 +61,7 @@ public async Task RebuildAdminProjectionZoeken(CancellationToken { var response = await _adminHttpClient.RebuildZoekenProjection(cancellationToken); - return response.IsSuccessStatusCode - ? Ok() - : UnprocessableEntity(); + return await OkOrForwardedResponse(cancellationToken, response); } [HttpGet("admin/status")] @@ -74,11 +69,7 @@ public async Task GetAdminProjectionStatus(CancellationToken canc { var response = await _adminHttpClient.GetStatus(cancellationToken); - if (!response.IsSuccessStatusCode) return BadRequest(); - - var projectionProgress = await response.Content.ReadFromJsonAsync(_jsonSerializerOptions, cancellationToken); - - return new OkObjectResult(projectionProgress); + return await OkObjectOrForwardedResponse(cancellationToken, response); } [HttpPost("public/detail/rebuild")] @@ -86,9 +77,7 @@ public async Task RebuildPublicProjectionDetail(CancellationToken { var response = await _publicHttpClient.RebuildDetailProjection(cancellationToken); - return response.IsSuccessStatusCode - ? Ok() - : UnprocessableEntity(); + return await OkOrForwardedResponse(cancellationToken, response); } [HttpPost("public/search/rebuild")] @@ -96,9 +85,7 @@ public async Task RebuildPublicProjectionZoeken(CancellationToken { var response = await _publicHttpClient.RebuildZoekenProjection(cancellationToken); - return response.IsSuccessStatusCode - ? Ok() - : UnprocessableEntity(); + return await OkOrForwardedResponse(cancellationToken, response); } [HttpGet("public/status")] @@ -106,10 +93,30 @@ public async Task GetPublicProjectionStatus(CancellationToken can { var response = await _publicHttpClient.GetStatus(cancellationToken); - if (!response.IsSuccessStatusCode) return BadRequest(); + return await OkObjectOrForwardedResponse(cancellationToken, response); + } - var projectionProgress = await response.Content.ReadFromJsonAsync(_jsonSerializerOptions, cancellationToken); + private async Task OkOrForwardedResponse(CancellationToken cancellationToken, HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) return Ok(); - return new OkObjectResult(projectionProgress); + return Problem( + title: response.ReasonPhrase, + statusCode: (int)response.StatusCode, + detail: await response.Content.ReadAsStringAsync(cancellationToken) + ); + } + + private async Task OkObjectOrForwardedResponse(CancellationToken cancellationToken, HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + return new OkObjectResult( + await response.Content.ReadFromJsonAsync(_jsonSerializerOptions, cancellationToken)); + + return Problem( + title: response.ReasonPhrase, + statusCode: (int)response.StatusCode, + detail: await response.Content.ReadAsStringAsync(cancellationToken) + ); } } diff --git a/src/AssociationRegistry/ProjectionStatus.cs b/src/AssociationRegistry.Admin.Api/Infrastructure/ProjectionStatus.cs similarity index 73% rename from src/AssociationRegistry/ProjectionStatus.cs rename to src/AssociationRegistry.Admin.Api/Infrastructure/ProjectionStatus.cs index 3d26e03ef..02d065c42 100644 --- a/src/AssociationRegistry/ProjectionStatus.cs +++ b/src/AssociationRegistry.Admin.Api/Infrastructure/ProjectionStatus.cs @@ -1,4 +1,6 @@ -namespace AssociationRegistry; +namespace AssociationRegistry.Admin.Api.Infrastructure; + +using System; public class ProjectionStatus { diff --git a/src/AssociationRegistry.Admin.Api/Program.cs b/src/AssociationRegistry.Admin.Api/Program.cs index c52311e1b..599a2b08c 100755 --- a/src/AssociationRegistry.Admin.Api/Program.cs +++ b/src/AssociationRegistry.Admin.Api/Program.cs @@ -45,7 +45,6 @@ namespace AssociationRegistry.Admin.Api; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -75,6 +74,7 @@ namespace AssociationRegistry.Admin.Api; public class Program { private const string AdminGlobalPolicyName = "Admin Global"; + public const string SuperAdminPolicyName = "Super Admin"; public static async Task Main(string[] args) { @@ -133,7 +133,10 @@ public static async Task Main(string[] args) app.UseRouting() .UseAuthentication() .UseAuthorization() - .UseEndpoints(routeBuilder => routeBuilder.MapControllers().RequireAuthorization(AdminGlobalPolicyName)); + .UseEndpoints(routeBuilder => + { + routeBuilder.MapControllers().RequireAuthorization(AdminGlobalPolicyName); + }); ConfigureLifetimeHooks(app); @@ -378,11 +381,20 @@ private static void ConfigureServices(WebApplicationBuilder builder) .AddControllersAsServices() .AddAuthorization( options => + { options.AddPolicy( AdminGlobalPolicyName, new AuthorizationPolicyBuilder() .RequireClaim(Security.ClaimTypes.Scope, Security.Scopes.Admin) - .Build())) + .Build()); + + options.AddPolicy( + SuperAdminPolicyName, + new AuthorizationPolicyBuilder() + .RequireClaim(Security.ClaimTypes.Scope, Security.Scopes.Admin) + .RequireClaim(Security.ClaimTypes.ClientId, appSettings.SuperAdminClientIds) + .Build()); + }) .AddNewtonsoftJson( opt => { diff --git a/src/AssociationRegistry.Admin.Api/appsettings.development.json b/src/AssociationRegistry.Admin.Api/appsettings.development.json index 56ee42878..fbcac5f11 100644 --- a/src/AssociationRegistry.Admin.Api/appsettings.development.json +++ b/src/AssociationRegistry.Admin.Api/appsettings.development.json @@ -35,11 +35,14 @@ "BaseUrl": "http://127.0.0.1:11004/", - "BeheerApiBaseUrl": "http://127.0.0.1:11004/", "BeheerProjectionHostBaseUrl": "http://127.0.0.1:11006/", "PublicApiBaseUrl": "http://127.0.0.1:11003/", "PublicProjectionHostBaseUrl": "http://127.0.0.1:11005/", + "SuperAdminClientIds": [ + "superAdminClient" + ], + "MagdaOptions": { "Afzender": "1234", "Hoedanigheid": "1234", diff --git a/src/AssociationRegistry.Admin.ProjectionHost/Program.cs b/src/AssociationRegistry.Admin.ProjectionHost/Program.cs index 0d041c08a..ec088c08c 100644 --- a/src/AssociationRegistry.Admin.ProjectionHost/Program.cs +++ b/src/AssociationRegistry.Admin.ProjectionHost/Program.cs @@ -84,7 +84,7 @@ public static async Task Main(string[] args) var app = builder.Build(); app.MapPost( - pattern: "projections/all/rebuild", + pattern: "v1/projections/all/rebuild", handler: async ( IDocumentStore store, IElasticClient elasticClient, @@ -93,18 +93,17 @@ public static async Task Main(string[] args) CancellationToken cancellationToken) => { var projectionDaemon = await store.BuildProjectionDaemonAsync(); + await projectionDaemon.RebuildProjection(cancellationToken); logger.LogInformation("Rebuild BeheerVerenigingDetailProjection complete"); - await projectionDaemon.RebuildProjection(cancellationToken); logger.LogInformation("Rebuild BeheerVerenigingHistoriekProjection complete"); - await RebuildElasticProjections(projectionDaemon, elasticClient, options, cancellationToken); logger.LogInformation("Rebuild ElasticSearch complete"); }); app.MapPost( - pattern: "projections/detail/rebuild", + pattern: "v1/projections/detail/rebuild", handler: async (IDocumentStore store, ILogger logger, CancellationToken cancellationToken) => { var projectionDaemon = await store.BuildProjectionDaemonAsync(); @@ -113,7 +112,7 @@ public static async Task Main(string[] args) }); app.MapPost( - pattern: "projections/historiek/rebuild", + pattern: "v1/projections/historiek/rebuild", handler: async (IDocumentStore store, ILogger logger, CancellationToken cancellationToken) => { var projectionDaemon = await store.BuildProjectionDaemonAsync(); @@ -122,7 +121,7 @@ public static async Task Main(string[] args) }); app.MapPost( - pattern: "projections/search/rebuild", + pattern: "v1/projections/search/rebuild", handler: async ( IDocumentStore store, IElasticClient elasticClient, @@ -136,7 +135,7 @@ public static async Task Main(string[] args) }); app.MapGet( - pattern: "projections/status", + pattern: "v1/projections/status", handler: async (IDocumentStore store, ILogger logger, CancellationToken cancellationToken) => { var projectionProgress = await store.Advanced.AllProjectionProgress(token: cancellationToken); @@ -161,7 +160,7 @@ private static async Task RebuildElasticProjections( { await projectionDaemon.StopShard($"{ProjectionNames.VerenigingZoeken}:All"); var oldVerenigingenIndices = await elasticClient.GetIndicesPointingToAliasAsync(options.Indices.Verenigingen); - var newIndicesVerenigingen = options.Indices.Verenigingen + "-" + SystemClock.Instance.GetCurrentInstant().ToZuluTime(); + var newIndicesVerenigingen = options.Indices.Verenigingen + "-" + SystemClock.Instance.GetCurrentInstant().ToUnixTimeMilliseconds(); await elasticClient.Indices.CreateVerenigingIndexAsync(newIndicesVerenigingen).ThrowIfInvalidAsync(); await elasticClient.Indices.DeleteAsync(options.Indices.DuplicateDetection, ct: cancellationToken).ThrowIfInvalidAsync(); diff --git a/src/AssociationRegistry.Admin.ProjectionHost/Projections/Detail/BeheerVerenigingDetailProjection.cs b/src/AssociationRegistry.Admin.ProjectionHost/Projections/Detail/BeheerVerenigingDetailProjection.cs index 1da737a01..707837303 100644 --- a/src/AssociationRegistry.Admin.ProjectionHost/Projections/Detail/BeheerVerenigingDetailProjection.cs +++ b/src/AssociationRegistry.Admin.ProjectionHost/Projections/Detail/BeheerVerenigingDetailProjection.cs @@ -15,6 +15,7 @@ public BeheerVerenigingDetailProjection() // Query yet when we handle NaamWerdGewijzigd. // see also https://martendb.io/events/projections/event-projections.html#reusing-documents-in-the-same-batch Options.BatchSize = 1; + Options.DeleteViewTypeOnTeardown(); } public void Project(IEvent @event, IDocumentOperations ops) diff --git a/src/AssociationRegistry.Admin.ProjectionHost/Projections/Historiek/BeheerVerenigingHistoriekProjection.cs b/src/AssociationRegistry.Admin.ProjectionHost/Projections/Historiek/BeheerVerenigingHistoriekProjection.cs index ef53c75fa..5421e9ec6 100644 --- a/src/AssociationRegistry.Admin.ProjectionHost/Projections/Historiek/BeheerVerenigingHistoriekProjection.cs +++ b/src/AssociationRegistry.Admin.ProjectionHost/Projections/Historiek/BeheerVerenigingHistoriekProjection.cs @@ -1,6 +1,5 @@ namespace AssociationRegistry.Admin.ProjectionHost.Projections.Historiek; -using System.Threading.Tasks; using Events; using Marten; using Marten.Events; @@ -16,6 +15,7 @@ public BeheerVerenigingHistoriekProjection() // Query yet when we handle NaamWerdGewijzigd. // see also https://martendb.io/events/projections/event-projections.html#reusing-documents-in-the-same-batch Options.BatchSize = 1; + Options.DeleteViewTypeOnTeardown(); } public void Project( diff --git a/src/AssociationRegistry.Public.ProjectionHost/Program.cs b/src/AssociationRegistry.Public.ProjectionHost/Program.cs index 68babc618..7deb4c826 100644 --- a/src/AssociationRegistry.Public.ProjectionHost/Program.cs +++ b/src/AssociationRegistry.Public.ProjectionHost/Program.cs @@ -83,7 +83,7 @@ public static async Task Main(string[] args) var app = builder.Build(); app.MapPost( - pattern: "projections/detail/rebuild", + pattern: "v1/projections/detail/rebuild", handler: async (IDocumentStore store, ILogger logger, CancellationToken cancellationToken) => { var projectionDaemon = await store.BuildProjectionDaemonAsync(); @@ -92,7 +92,7 @@ public static async Task Main(string[] args) }); app.MapPost( - pattern: "projections/search/rebuild", + pattern: "v1/projections/search/rebuild", handler: async ( IDocumentStore store, IElasticClient elasticClient, @@ -111,11 +111,9 @@ public static async Task Main(string[] args) }); app.MapGet( - "projections/status", - async (IDocumentStore store, ILogger logger, CancellationToken cancellationToken) => - { - return await store.Advanced.AllProjectionProgress(token: cancellationToken); - }); + pattern: "v1/projections/status", handler: (IDocumentStore store, ILogger logger, CancellationToken cancellationToken) + => + store.Advanced.AllProjectionProgress(token: cancellationToken)); app.SetUpSwagger(); await app.EnsureElasticSearchIsInitialized(); diff --git a/src/AssociationRegistry.Public.ProjectionHost/Projections/Detail/PubliekVerenigingDetailProjection.cs b/src/AssociationRegistry.Public.ProjectionHost/Projections/Detail/PubliekVerenigingDetailProjection.cs index 339e75400..2756d4ade 100644 --- a/src/AssociationRegistry.Public.ProjectionHost/Projections/Detail/PubliekVerenigingDetailProjection.cs +++ b/src/AssociationRegistry.Public.ProjectionHost/Projections/Detail/PubliekVerenigingDetailProjection.cs @@ -16,6 +16,7 @@ public PubliekVerenigingDetailProjection() // Query yet when we handle NaamWerdGewijzigd. // see also https://martendb.io/events/projections/event-projections.html#reusing-documents-in-the-same-batch Options.BatchSize = 1; + Options.DeleteViewTypeOnTeardown(); } public void Project(IEvent @event, IDocumentOperations ops) @@ -53,7 +54,6 @@ public async Task Project(IEvent @event, IDocumentOperations updateDocs.Add(gerelateerdeVereniging); } - PubliekVerenigingDetailProjector.Apply(@event, vereniging); PubliekVerenigingDetailProjector.UpdateMetadata(@event, vereniging); diff --git a/test/AssociationRegistry.Test.Admin.Api/AssociationRegistry.Test.Admin.Api.csproj b/test/AssociationRegistry.Test.Admin.Api/AssociationRegistry.Test.Admin.Api.csproj index b08023ffc..2bb5080ff 100644 --- a/test/AssociationRegistry.Test.Admin.Api/AssociationRegistry.Test.Admin.Api.csproj +++ b/test/AssociationRegistry.Test.Admin.Api/AssociationRegistry.Test.Admin.Api.csproj @@ -42,5 +42,8 @@ Always + + + diff --git a/test/AssociationRegistry.Test.Admin.Api/Fixtures/AdminApiClient.cs b/test/AssociationRegistry.Test.Admin.Api/Fixtures/AdminApiClient.cs index a9057a5a5..cefe00ef0 100644 --- a/test/AssociationRegistry.Test.Admin.Api/Fixtures/AdminApiClient.cs +++ b/test/AssociationRegistry.Test.Admin.Api/Fixtures/AdminApiClient.cs @@ -27,12 +27,16 @@ public async Task GetDetail(string vCode, long? expectedSeq public async Task GetHistoriek(string vCode, long? expectedSequence = null) => await GetWithPossibleSequence($"/v1/verenigingen/{vCode}/historiek", expectedSequence); - public async Task RegistreerFeitelijkeVereniging(string content, string? bevestigingsToken = null, string? initiator = "OVO000001") + public async Task RegistreerFeitelijkeVereniging( + string content, + string? bevestigingsToken = null, + string? initiator = "OVO000001") { AddOrRemoveHeader(WellknownHeaderNames.BevestigingsToken, bevestigingsToken); WithHeaders(null, initiator); var httpResponseMessage = await HttpClient.PostAsync("/v1/verenigingen/feitelijkeverenigingen", content.AsJsonContent()); AddOrRemoveHeader(WellknownHeaderNames.BevestigingsToken); + return httpResponseMessage; } @@ -40,108 +44,187 @@ public async Task RegistreerKboVereniging(string content, s { WithHeaders(null, initiator); var httpResponseMessage = await HttpClient.PostAsync($"/v1/verenigingen/kbo", content.AsJsonContent()); + return httpResponseMessage; } private async Task GetWithPossibleSequence(string? requestUri, long? expectedSequence) - => expectedSequence == null ? await HttpClient.GetAsync(requestUri) : await HttpClient.GetAsync($"{requestUri}?{WellknownParameters.ExpectedSequence}={expectedSequence}"); - - public async Task PatchVereniging(string vCode, string content, long? version = null, string? initiator = "OVO000001") + => expectedSequence == null + ? await HttpClient.GetAsync(requestUri) + : await HttpClient.GetAsync($"{requestUri}?{WellknownParameters.ExpectedSequence}={expectedSequence}"); + + public async Task PatchVereniging( + string vCode, + string content, + long? version = null, + string? initiator = "OVO000001") { WithHeaders(version, initiator); + return await HttpClient.PatchAsync($"/v1/verenigingen/{vCode}", content.AsJsonContent()); } - public async Task StopVereniging(string vCode, string content, long? version = null, string? initiator = "OVO000001") + public async Task StopVereniging( + string vCode, + string content, + long? version = null, + string? initiator = "OVO000001") { WithHeaders(version, initiator); + return await HttpClient.PostAsync($"/v1/verenigingen/{vCode}/stop", content.AsJsonContent()); } - public async Task PatchVerenigingMetRechtspersoonlijkheid(string vCode, string content, long? version = null, string? initiator = "OVO000001") + public async Task PatchVerenigingMetRechtspersoonlijkheid( + string vCode, + string content, + long? version = null, + string? initiator = "OVO000001") { WithHeaders(version, initiator); + return await HttpClient.PatchAsync($"/v1/verenigingen/{vCode}/kbo", content.AsJsonContent()); } - public async Task PostVertegenwoordiger(string vCode, string content, long? version = null, string? initiator = "OVO000001") + public async Task PostVertegenwoordiger( + string vCode, + string content, + long? version = null, + string? initiator = "OVO000001") { WithHeaders(version, initiator); + return await HttpClient.PostAsync($"/v1/verenigingen/{vCode}/vertegenwoordigers", content.AsJsonContent()); } - public async Task PostContactgegevens(string vCode, string content, long? version = null, string? initiator = "OVO000001") + public async Task PostContactgegevens( + string vCode, + string content, + long? version = null, + string? initiator = "OVO000001") { WithHeaders(version, initiator); + return await HttpClient.PostAsync($"/v1/verenigingen/{vCode}/contactgegevens", content.AsJsonContent()); } - public async Task PatchContactgegevens(string vCode, int contactgegevenId, string jsonBody, long? version = null, string? initiator = "OVO000001") + public async Task PatchContactgegevens( + string vCode, + int contactgegevenId, + string jsonBody, + long? version = null, + string? initiator = "OVO000001") { WithHeaders(version, initiator); + return await HttpClient.PatchAsync($"/v1/verenigingen/{vCode}/contactgegevens/{contactgegevenId}", jsonBody.AsJsonContent()); } - public async Task PatchContactgegevensFromKbo(string vCode, int contactgegevenId, string jsonBody, long? version = null, string? initiator = "OVO000001") + public async Task PatchContactgegevensFromKbo( + string vCode, + int contactgegevenId, + string jsonBody, + long? version = null, + string? initiator = "OVO000001") { WithHeaders(version, initiator); + return await HttpClient.PatchAsync($"/v1/verenigingen/{vCode}/kbo/contactgegevens/{contactgegevenId}", jsonBody.AsJsonContent()); } - public async Task PatchVertegenwoordiger(string vCode, int vertegenwoordigerId, string jsonBody, long? version = null, string? initiator = "OVO000001") + public async Task PatchVertegenwoordiger( + string vCode, + int vertegenwoordigerId, + string jsonBody, + long? version = null, + string? initiator = "OVO000001") { WithHeaders(version, initiator); + return await HttpClient.PatchAsync($"/v1/verenigingen/{vCode}/vertegenwoordigers/{vertegenwoordigerId}", jsonBody.AsJsonContent()); } - public async Task DeleteContactgegeven(string vCode, int contactgegevenId, long? version = null, string? initiator = "OVO000001") + public async Task DeleteContactgegeven( + string vCode, + int contactgegevenId, + long? version = null, + string? initiator = "OVO000001") { WithHeaders(version, initiator); + var request = new HttpRequestMessage { Method = HttpMethod.Delete, RequestUri = new Uri($"/v1/verenigingen/{vCode}/contactgegevens/{contactgegevenId}", UriKind.Relative), }; + return await HttpClient.SendAsync(request); } - public async Task DeleteVertegenwoordiger(string vCode, int vertegenwoordigerId, string jsonBody, long? version = null, string? initiator = "OVO000001") + public async Task DeleteVertegenwoordiger( + string vCode, + int vertegenwoordigerId, + string jsonBody, + long? version = null, + string? initiator = "OVO000001") { WithHeaders(version, initiator); + var request = new HttpRequestMessage { Method = HttpMethod.Delete, RequestUri = new Uri($"/v1/verenigingen/{vCode}/vertegenwoordigers/{vertegenwoordigerId}", UriKind.Relative), }; + return await HttpClient.SendAsync(request); } public async Task PostLocatie(string vCode, string content, long? version = null, string? initiator = "OVO000001") { WithHeaders(version, initiator); + return await HttpClient.PostAsync($"/v1/verenigingen/{vCode}/locaties", content.AsJsonContent()); } - public async Task PatchLocatie(string vCode, int locatieId, string jsonBody, long? version = null, string? initiator = "OVO000001") + public async Task PatchLocatie( + string vCode, + int locatieId, + string jsonBody, + long? version = null, + string? initiator = "OVO000001") { WithHeaders(version, initiator); + return await HttpClient.PatchAsync($"/v1/verenigingen/{vCode}/locaties/{locatieId}", jsonBody.AsJsonContent()); } - public async Task PatchMaatschappelijkeZetel(string vCode, int locatieId, string jsonBody, long? version = null, string? initiator = "OVO000001") + + public async Task PatchMaatschappelijkeZetel( + string vCode, + int locatieId, + string jsonBody, + long? version = null, + string? initiator = "OVO000001") { WithHeaders(version, initiator); - return await HttpClient.PatchAsync($"/v1/verenigingen/{vCode}/kbo/locaties/{locatieId}", jsonBody.AsJsonContent()); + return await HttpClient.PatchAsync($"/v1/verenigingen/{vCode}/kbo/locaties/{locatieId}", jsonBody.AsJsonContent()); } - public async Task DeleteLocatie(string vCode, int locatieId, string jsonBody, long? version = null, string? initiator = "OVO000001") + public async Task DeleteLocatie( + string vCode, + int locatieId, + string jsonBody, + long? version = null, + string? initiator = "OVO000001") { WithHeaders(version, initiator); + var request = new HttpRequestMessage { Method = HttpMethod.Delete, RequestUri = new Uri($"/v1/verenigingen/{vCode}/locaties/{locatieId}", UriKind.Relative), }; + return await HttpClient.SendAsync(request); } @@ -178,6 +261,24 @@ public void Dispose() public async Task GetHoofdactiviteiten() => await HttpClient.GetAsync($"/v1/hoofdactiviteitenVerenigingsloket"); + public async Task RebuildAllAdminProjections(CancellationToken cancellationToken) + => await HttpClient.PostAsync(requestUri: "/v1/projections/admin/all/rebuild", content: null, cancellationToken); + + public async Task RebuildAdminDetailProjection(CancellationToken cancellationToken) + => await HttpClient.PostAsync(requestUri: "/v1/projections/admin/detail/rebuild", content: null, cancellationToken); + + public async Task RebuildAdminHistoriekProjection(CancellationToken cancellationToken) + => await HttpClient.PostAsync(requestUri: "/v1/projections/admin/historiek/rebuild", content: null, cancellationToken); + + public async Task RebuildAdminZoekenProjection(CancellationToken cancellationToken) + => await HttpClient.PostAsync(requestUri: "/v1/projections/admin/search/rebuild", content: null, cancellationToken); + + public async Task RebuildPubliekDetailProjection(CancellationToken cancellationToken) + => await HttpClient.PostAsync(requestUri: "/v1/projections/public/detail/rebuild", content: null, cancellationToken); + + public async Task RebuildPubliekZoekenProjection(CancellationToken cancellationToken) + => await HttpClient.PostAsync(requestUri: "/v1/projections/public/search/rebuild", content: null, cancellationToken); + public async Task GetJsonLdContext(string contextName) => await HttpClient.GetAsync($"/v1/contexten/beheer/{contextName}"); } diff --git a/test/AssociationRegistry.Test.Admin.Api/Fixtures/AdminApiFixture.cs b/test/AssociationRegistry.Test.Admin.Api/Fixtures/AdminApiFixture.cs index d610a5ef4..125ed4583 100644 --- a/test/AssociationRegistry.Test.Admin.Api/Fixtures/AdminApiFixture.cs +++ b/test/AssociationRegistry.Test.Admin.Api/Fixtures/AdminApiFixture.cs @@ -268,10 +268,15 @@ public Clients(OAuth2IntrospectionOptions oAuth2IntrospectionOptions, Func CreateMachine2MachineClientFor(clientId: "vloketClient", Security.Scopes.Admin, clientSecret: "secret").GetAwaiter().GetResult(); + private HttpClient GetSuperAdminHttpClient() + => CreateMachine2MachineClientFor(clientId: "superAdminClient", Security.Scopes.Admin, clientSecret: "secret").GetAwaiter().GetResult(); public AdminApiClient Authenticated => new(GetAuthenticatedHttpClient()); + public AdminApiClient SuperAdmin + => new(GetSuperAdminHttpClient()); + public AdminApiClient Unauthenticated => new(_createClientFunc()); diff --git a/test/AssociationRegistry.Test.Admin.Api/When_Rebuilding/Given_An_Authorized_Client.cs b/test/AssociationRegistry.Test.Admin.Api/When_Rebuilding/Given_An_Authorized_Client.cs new file mode 100644 index 000000000..bea78e11d --- /dev/null +++ b/test/AssociationRegistry.Test.Admin.Api/When_Rebuilding/Given_An_Authorized_Client.cs @@ -0,0 +1,27 @@ +namespace AssociationRegistry.Test.Admin.Api.When_Rebuilding; + +using Fixtures; +using FluentAssertions; +using System.Net; +using Xunit; +using Xunit.Categories; + +[Collection(nameof(AdminApiCollection))] +[Category("AdminApi")] +[IntegrationTest] +public class Given_An_Authorized_Client +{ + private readonly AdminApiClient _client; + + public Given_An_Authorized_Client(EventsInDbScenariosFixture fixture) + { + _client = fixture.Clients.SuperAdmin; + } + + [Fact] + public async Task Then_Statuscode_Is_Ok() + { + var response = await _client.RebuildAllAdminProjections(CancellationToken.None); + response.StatusCode.Should().Be(HttpStatusCode.OK); + } +} diff --git a/test/AssociationRegistry.Test.Admin.Api/When_Rebuilding/Given_An_Unauthorized_Client.cs b/test/AssociationRegistry.Test.Admin.Api/When_Rebuilding/Given_An_Unauthorized_Client.cs new file mode 100644 index 000000000..5ed7faf89 --- /dev/null +++ b/test/AssociationRegistry.Test.Admin.Api/When_Rebuilding/Given_An_Unauthorized_Client.cs @@ -0,0 +1,27 @@ +namespace AssociationRegistry.Test.Admin.Api.When_Rebuilding; + +using Fixtures; +using FluentAssertions; +using System.Net; +using Xunit; +using Xunit.Categories; + +[Collection(nameof(AdminApiCollection))] +[Category("AdminApi")] +[IntegrationTest] +public class Given_An_Unauthorized_Client +{ + private readonly AdminApiClient _client; + + public Given_An_Unauthorized_Client(EventsInDbScenariosFixture fixture) + { + _client = fixture.DefaultClient; + } + + [Fact] + public async Task Then_Statuscode_Is_Forbidden() + { + var response = await _client.RebuildAllAdminProjections(CancellationToken.None); + response.StatusCode.Should().Be(HttpStatusCode.Forbidden); + } +} diff --git a/test/AssociationRegistry.Test.Admin.Api/appsettings.json b/test/AssociationRegistry.Test.Admin.Api/appsettings.json index 1d4ca2eca..f26d4c69c 100644 --- a/test/AssociationRegistry.Test.Admin.Api/appsettings.json +++ b/test/AssociationRegistry.Test.Admin.Api/appsettings.json @@ -9,7 +9,6 @@ } }, "BaseUrl": "http://127.0.0.1:11004/", - "PublicApiBaseUrl": "http://127.0.0.1:11003/", "ElasticClientOptions": { "Uri": "http://127.0.0.1:9200", "Username": "elastic", @@ -75,5 +74,11 @@ "GeefOndernemingVkboEndpoint": "http://localhost:8080/GeefOndernemingVkboDienst-02.00/soap/WebService", "GeefOndernemingEndpoint": "http://localhost:8080/GeefOndernemingDienst-02.00/soap/WebService" }, + "BeheerProjectionHostBaseUrl": "http://127.0.0.1:8080", + "PublicApiBaseUrl": "http://127.0.0.1:11003/", + "PublicProjectionHostBaseUrl": "http://127.0.0.1:8080", + "SuperAdminClientIds": [ + "superAdminClient" + ], "TemporaryMagdaVertegenwoordigers": "{\"TemporaryVertegenwoordigers\": [{\"Insz\": \"1234567890\",\"Voornaam\": \"Ikkeltje\",\"Achternaam\": \"Persoon\"},{\"Insz\": \"0987654321\",\"Voornaam\": \"Kramikkeltje\",\"Achternaam\": \"Persoon\"}]}" } diff --git a/wiremock/mappings/projectionactions.json b/wiremock/mappings/projectionactions.json new file mode 100644 index 000000000..4b62e8e92 --- /dev/null +++ b/wiremock/mappings/projectionactions.json @@ -0,0 +1,13 @@ +{ + "priority": 1, + "request": { + "method": "POST", + "urlPattern": "^/v1/projections/.*" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + } + } +} \ No newline at end of file