From 9cc45c57675e96732c1dc60ae8065b4eeec04a3f Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Tue, 8 Sep 2026 15:03:42 +0200 Subject: [PATCH] Ouvrir l'export de configuration aux apps visiteur MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Configuration/{id}/export` portait `[Authorize(AppReadAccess)]`, mais ASP.NET Core combine les `[Authorize]` de la classe et de l'action : le controleur exige `ContentEditor`, qu'une cle API n'a pas. La cle authentifiait la requete, puis l'autorisation la refusait — 403 sans corps. Cote mymuseum-visitapp, le telechargement d'une visite echouait donc systematiquement. Seul `[AllowAnonymous]` court-circuite la policy du controleur ; le controle d'acces se fait dans l'action, qui declenche le schema ApiKey explicitement et verifie que la cle porte bien l'instance de la configuration demandee. Meme correctif que InstanceController.GetDetail. Les trois `catch` renvoyaient `null` : l'app recevait un 200 vide et croyait la visite exportee. Ils renvoient les codes qui etaient deja ecrits, en commentaire. Co-Authored-By: Claude Opus 5 (1M context) --- .../Controllers/ConfigurationController.cs | 52 +++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/ManagerService/Controllers/ConfigurationController.cs b/ManagerService/Controllers/ConfigurationController.cs index e50fc0b..c89c7fb 100644 --- a/ManagerService/Controllers/ConfigurationController.cs +++ b/ManagerService/Controllers/ConfigurationController.cs @@ -12,6 +12,7 @@ using ManagerService.Data; using ManagerService.Data.SubSection; using ManagerService.DTOs; using ManagerService.Services; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -379,14 +380,28 @@ namespace ManagerService.Controllers /// Export a configuration /// /// Id of configuration to export - /// Language to export - [Authorize(Policy = ManagerService.Service.Security.Policies.AppReadAccess)] + /// Language to export + /// + /// Ouverte aux apps visiteur par X-Api-Key : c'est l'appel que fait + /// mymuseum-visitapp pour télécharger une visite hors ligne. + /// + /// ⚠️ [Authorize(AppReadAccess)] ne suffisait pas : ASP.NET Core **combine** + /// les [Authorize] de la classe et de l'action. Le contrôleur exige + /// ContentEditor, qu'une clé API n'a pas — la clé authentifiait donc la + /// requête, puis l'autorisation la refusait : 403 sans corps, et côté app un + /// téléchargement qui échouait sans rien dire. Seul [AllowAnonymous] + /// court-circuite la policy du contrôleur ; le contrôle d'accès se fait ici. + /// Même correctif que . + /// + [AllowAnonymous] [ProducesResponseType(typeof(FileContentResult), 200)] [ProducesResponseType(typeof(string), 400)] + [ProducesResponseType(typeof(string), 401)] + [ProducesResponseType(typeof(string), 403)] [ProducesResponseType(typeof(string), 404)] [ProducesResponseType(typeof(string), 500)] [HttpGet("{id}/export")] - public FileContentResult Export(string id, [FromQuery] string language) + public async Task Export(string id, [FromQuery] string language) { try { @@ -398,6 +413,26 @@ namespace ManagerService.Controllers if (configuration == null) throw new KeyNotFoundException("Configuration does not exist"); + // Le schéma ApiKey n'est pas le schéma par défaut : sur une action + // [AllowAnonymous] il faut le déclencher explicitement. + var apiKeyAuth = await HttpContext.AuthenticateAsync("ApiKey"); + var keyInstanceId = apiKeyAuth.Succeeded + ? apiKeyAuth.Principal?.FindFirst(ManagerService.Service.Security.ClaimTypes.InstanceId)?.Value + : null; + + // Ne PAS déduire « utilisateur du manager » d'un claim de permission : le + // handler de clé API pose lui aussi le claim Viewer. + var isManager = !apiKeyAuth.Succeeded + && User?.Identity?.IsAuthenticated == true + && User.HasClaim(ManagerService.Service.Security.ClaimTypes.Permission, + ManagerService.Service.Security.Permissions.Viewer); + + if (!isManager && keyInstanceId == null) + return new ObjectResult("Authentication required") { StatusCode = 401 }; + + if (!isManager && keyInstanceId != configuration.InstanceId) + return new ObjectResult("This API key does not grant access to this configuration") { StatusCode = 403 }; + // Les entités, pas seulement leurs DTO : la collecte des ressources passe // par GetReferencedResourceIds, qui vit sur le sous-type. List
sections = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == configuration.Id).ToList(); @@ -436,20 +471,19 @@ namespace ManagerService.Controllers FileDownloadName = fileName }; } + // Les trois `catch` renvoyaient `null` : l'app recevait un 200 vide et croyait + // la visite exportée. Les codes ci-dessous étaient déjà écrits, en commentaire. catch (ArgumentNullException ex) { - return null; - //return new BadRequestObjectResult(ex.Message) { }; + return new BadRequestObjectResult(ex.Message); } catch (KeyNotFoundException ex) { - return null; - //return new NotFoundObjectResult(ex.Message) { }; + return new NotFoundObjectResult(ex.Message); } catch (Exception ex) { - return null; - //return new ObjectResult(ex.Message) { StatusCode = 500 }; + return new ObjectResult(ex.Message) { StatusCode = 500 }; } }