Ouvrir l'export de configuration aux apps visiteur
`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) <noreply@anthropic.com>
This commit is contained in:
parent
1e1a36ad5a
commit
9cc45c5767
@ -12,6 +12,7 @@ using ManagerService.Data;
|
|||||||
using ManagerService.Data.SubSection;
|
using ManagerService.Data.SubSection;
|
||||||
using ManagerService.DTOs;
|
using ManagerService.DTOs;
|
||||||
using ManagerService.Services;
|
using ManagerService.Services;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
@ -380,13 +381,27 @@ namespace ManagerService.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Id of configuration to export</param>
|
/// <param name="id">Id of configuration to export</param>
|
||||||
/// <param name="language">Language to export</param>
|
/// <param name="language">Language to export</param>
|
||||||
[Authorize(Policy = ManagerService.Service.Security.Policies.AppReadAccess)]
|
/// <remarks>
|
||||||
|
/// Ouverte aux apps visiteur par <c>X-Api-Key</c> : c'est l'appel que fait
|
||||||
|
/// mymuseum-visitapp pour télécharger une visite hors ligne.
|
||||||
|
///
|
||||||
|
/// ⚠️ <c>[Authorize(AppReadAccess)]</c> ne suffisait pas : ASP.NET Core **combine**
|
||||||
|
/// les <c>[Authorize]</c> de la classe et de l'action. Le contrôleur exige
|
||||||
|
/// <c>ContentEditor</c>, 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 <c>[AllowAnonymous]</c>
|
||||||
|
/// court-circuite la policy du contrôleur ; le contrôle d'accès se fait ici.
|
||||||
|
/// Même correctif que <see cref="InstanceController.GetDetail"/>.
|
||||||
|
/// </remarks>
|
||||||
|
[AllowAnonymous]
|
||||||
[ProducesResponseType(typeof(FileContentResult), 200)]
|
[ProducesResponseType(typeof(FileContentResult), 200)]
|
||||||
[ProducesResponseType(typeof(string), 400)]
|
[ProducesResponseType(typeof(string), 400)]
|
||||||
|
[ProducesResponseType(typeof(string), 401)]
|
||||||
|
[ProducesResponseType(typeof(string), 403)]
|
||||||
[ProducesResponseType(typeof(string), 404)]
|
[ProducesResponseType(typeof(string), 404)]
|
||||||
[ProducesResponseType(typeof(string), 500)]
|
[ProducesResponseType(typeof(string), 500)]
|
||||||
[HttpGet("{id}/export")]
|
[HttpGet("{id}/export")]
|
||||||
public FileContentResult Export(string id, [FromQuery] string language)
|
public async Task<IActionResult> Export(string id, [FromQuery] string language)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -398,6 +413,26 @@ namespace ManagerService.Controllers
|
|||||||
if (configuration == null)
|
if (configuration == null)
|
||||||
throw new KeyNotFoundException("Configuration does not exist");
|
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
|
// Les entités, pas seulement leurs DTO : la collecte des ressources passe
|
||||||
// par GetReferencedResourceIds, qui vit sur le sous-type.
|
// par GetReferencedResourceIds, qui vit sur le sous-type.
|
||||||
List<Section> sections = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == configuration.Id).ToList();
|
List<Section> sections = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == configuration.Id).ToList();
|
||||||
@ -436,20 +471,19 @@ namespace ManagerService.Controllers
|
|||||||
FileDownloadName = fileName
|
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)
|
catch (ArgumentNullException ex)
|
||||||
{
|
{
|
||||||
return null;
|
return new BadRequestObjectResult(ex.Message);
|
||||||
//return new BadRequestObjectResult(ex.Message) { };
|
|
||||||
}
|
}
|
||||||
catch (KeyNotFoundException ex)
|
catch (KeyNotFoundException ex)
|
||||||
{
|
{
|
||||||
return null;
|
return new NotFoundObjectResult(ex.Message);
|
||||||
//return new NotFoundObjectResult(ex.Message) { };
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return null;
|
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||||||
//return new ObjectResult(ex.Message) { StatusCode = 500 };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user