using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; namespace ManagerService.Security { /// /// Exige une clé d'API valide (X-Api-Key) ou un utilisateur du manager /// déjà authentifié. À poser sur les routes que les apps visiteur consomment. /// /// Pourquoi un filtre et pas [Authorize(AppReadAccess)]. ASP.NET Core /// combine les [Authorize] de la classe et de l'action : sur un /// contrôleur qui exige ContentEditor, ajouter une policy plus permissive sur /// l'action ne l'assouplit pas — la clé authentifierait la requête, puis l'autorisation /// la refuserait, et l'app recevrait un 403 sans corps. Seul [AllowAnonymous] /// court-circuite la policy de classe. L'action garde donc son [AllowAnonymous], /// et ce filtre redevient le contrôle d'accès. /// /// C'est exactement ce que ConfigurationController.Export faisait à la main /// depuis le commit 9cc45c5 ; ceci le rend réutilisable au lieu de le recopier /// vingt-quatre fois. /// /// ⚠️ Ce que ce filtre ne fait pas : le cloisonnement. Il vérifie qu'une clé est /// valide, pas qu'elle donne accès à cette ressource-là — ça demande de résoudre /// l'instance de la ressource, ce qui diffère à chaque endpoint. Les routes qui savent /// le faire le font en plus, dans leur corps (voir Export et /// InstanceController.GetDetail). Et la clé s'obtient anonymement, par le slug /// ou par le pincode : ceci ferme l'énumération par identifiant et rend l'accès /// révocable, ça ne rend pas le contenu confidentiel. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] public class RequireAppKeyAttribute : Attribute, IAsyncAuthorizationFilter { public const string ApiKeyScheme = "ApiKey"; public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { var http = context.HttpContext; // Le schéma ApiKey n'est pas le schéma par défaut : sur une action // [AllowAnonymous] il faut le déclencher explicitement. var apiKey = await http.AuthenticateAsync(ApiKeyScheme); if (apiKey.Succeeded) return; // Le back-office appelle les mêmes routes avec son jeton. if (http.User?.Identity?.IsAuthenticated == true && http.User.HasClaim(Service.Security.ClaimTypes.Permission, Service.Security.Permissions.Viewer)) return; context.Result = new ObjectResult("Authentication required") { StatusCode = 401 }; } } }