From 6a8a62d6dd9683ee884adf18562cad8c755a5e1a Mon Sep 17 00:00:00 2001 From: "ion.dormenco" Date: Tue, 29 Sep 2026 16:17:04 +0300 Subject: [PATCH] Allow platform admins to access polling station stats across all NGOs. --- .../GetPollingStationStats/Endpoint.cs | 50 +++++++++++++------ .../GetPollingStationStats/Request.cs | 6 +-- .../GetPollingStationStats/Validator.cs | 2 - 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/api/src/Feature.Statistics/GetPollingStationStats/Endpoint.cs b/api/src/Feature.Statistics/GetPollingStationStats/Endpoint.cs index db2acdfca..55cb8cecf 100644 --- a/api/src/Feature.Statistics/GetPollingStationStats/Endpoint.cs +++ b/api/src/Feature.Statistics/GetPollingStationStats/Endpoint.cs @@ -4,6 +4,8 @@ using Feature.Statistics.Options; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Options; +using Vote.Monitor.Core.Models; +using Vote.Monitor.Core.Services.Security; using Vote.Monitor.Domain.ConnectionFactory; using ZiggyCreatures.Caching.Fusion; @@ -11,6 +13,7 @@ namespace Feature.Statistics.GetPollingStationStats; public class Endpoint( IAuthorizationService authorizationService, + ICurrentUserRoleProvider userRoleProvider, INpgsqlConnectionFactory dbConnectionFactory, IFusionCache cache, IOptions options) : Endpoint, NotFound>> @@ -23,37 +26,56 @@ public override void Configure() DontAutoTag(); Options(x => x.WithTags("statistics", "polling-stations")); Summary(s => { s.Summary = "Overview statistics for a specific polling station"; }); - Policies(PolicyNames.NgoAdminsOnly); + Policies(PolicyNames.AdminsOnly); } public override async Task, NotFound>> ExecuteAsync(Request req, CancellationToken ct) { - var authorizationResult = - await authorizationService.AuthorizeAsync(User, new MonitoringNgoAdminRequirement(req.ElectionRoundId)); - if (!authorizationResult.Succeeded) + var isPlatformAdmin = userRoleProvider.IsPlatformAdmin(); + + if (!isPlatformAdmin) { - return TypedResults.NotFound(); + var authorizationResult = + await authorizationService.AuthorizeAsync(User, new MonitoringNgoAdminRequirement(req.ElectionRoundId)); + if (!authorizationResult.Succeeded || !req.NgoId.HasValue || req.NgoId == Guid.Empty) + { + return TypedResults.NotFound(); + } } - var cacheKey = - $"polling-station-stats-{req.ElectionRoundId}-{req.NgoId}-{req.PollingStationId}-{req.DataSource}"; + var dataSource = req.DataSource ?? DataSource.Ngo; + + var cacheKey = isPlatformAdmin + ? $"polling-station-stats-{req.ElectionRoundId}-all-{req.PollingStationId}" + : $"polling-station-stats-{req.ElectionRoundId}-{req.NgoId}-{req.PollingStationId}-{dataSource}"; var response = await cache.GetOrSetAsync( cacheKey, - async _ => await GetStatisticsAsync(req, ct), + async _ => await GetStatisticsAsync(req, isPlatformAdmin, dataSource, ct), cacheOptions => cacheOptions.SetDuration(TimeSpan.FromMinutes(_options.CacheDurationInMinutes)), token: ct); return TypedResults.Ok(response); } - private async Task GetStatisticsAsync(Request req, CancellationToken ct) + private async Task GetStatisticsAsync(Request req, bool isPlatformAdmin, DataSource dataSource, + CancellationToken ct) { - const string sql = - """ + var availableObserversSql = isPlatformAdmin + ? """ + SELECT MO."Id" AS "MonitoringObserverId" + FROM "MonitoringObservers" MO + WHERE MO."ElectionRoundId" = @electionRoundId + """ + : """ + SELECT "MonitoringObserverId" + FROM "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, @dataSource) + """; + + var sql = + $""" WITH "AvailableObservers" AS ( - SELECT "MonitoringObserverId" - FROM "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, @dataSource) + {availableObserversSql} ) SELECT ( @@ -151,7 +173,7 @@ SELECT COUNT(*) electionRoundId = req.ElectionRoundId, ngoId = req.NgoId, pollingStationId = req.PollingStationId, - dataSource = req.DataSource.ToString() + dataSource = dataSource.Value }; using var dbConnection = await dbConnectionFactory.GetOpenConnectionAsync(ct); diff --git a/api/src/Feature.Statistics/GetPollingStationStats/Request.cs b/api/src/Feature.Statistics/GetPollingStationStats/Request.cs index a5f5e23be..77fce9eda 100644 --- a/api/src/Feature.Statistics/GetPollingStationStats/Request.cs +++ b/api/src/Feature.Statistics/GetPollingStationStats/Request.cs @@ -7,11 +7,11 @@ public class Request { public Guid ElectionRoundId { get; set; } - [FromClaim(ApplicationClaimTypes.NgoId)] - public Guid NgoId { get; set; } + [FromClaim(ApplicationClaimTypes.NgoId, IsRequired = false)] + public Guid? NgoId { get; set; } public Guid PollingStationId { get; set; } [QueryParam] - public DataSource DataSource { get; set; } = DataSource.Ngo; + public DataSource? DataSource { get; set; } } diff --git a/api/src/Feature.Statistics/GetPollingStationStats/Validator.cs b/api/src/Feature.Statistics/GetPollingStationStats/Validator.cs index 4f565fa66..c2091dbf0 100644 --- a/api/src/Feature.Statistics/GetPollingStationStats/Validator.cs +++ b/api/src/Feature.Statistics/GetPollingStationStats/Validator.cs @@ -5,8 +5,6 @@ public class Validator : Validator public Validator() { RuleFor(x => x.ElectionRoundId).NotEmpty(); - RuleFor(x => x.NgoId).NotEmpty(); RuleFor(x => x.PollingStationId).NotEmpty(); - RuleFor(x => x.DataSource).NotEmpty(); } }