using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; namespace w4c_workflows.Filters; /// /// Authorization filter that checks the operator-key scopes resolved by /// . Scopes are stored in /// HttpContext.Items["Scopes"] as an IReadOnlyList<string>. /// /// Usage: [RequireScope("manage")] on a controller or action method. /// Multiple attributes are OR-combined (any listed scope satisfies the check). /// When no scopes are present (JWT auth, render-token auth), the check passes /// — those surfaces have their own auth gates. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] public class RequireScopeAttribute : Attribute, IAsyncActionFilter { private readonly string _scope; public RequireScopeAttribute(string scope) { _scope = scope; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // JWT auth and render-token auth don't carry scopes — they have their // own authorization gates, so skip the scope check. var authKind = context.HttpContext.Items["AuthKind"] as string; if (authKind is "jwt" or "render-token") { await next(); return; } var scopes = context.HttpContext.Items["Scopes"] as IReadOnlyList; // A key with no recorded scope set predates scope enforcement (older/seed // keys). By default it is treated as unlimited so legacy keys keep working. // With Auth:EnforceScopes=true it is denied instead: an operator must // re-mint the key with explicit scopes. This is the migration switch for // tightening P1-14 without silently locking everyone out. if (scopes == null || scopes.Count == 0) { var enforce = context.HttpContext.Items["EnforceScopes"] as bool? ?? false; if (enforce) { context.Result = new ObjectResult(new { error = "Operator key has no scopes; re-mint it with explicit scopes.", }) { StatusCode = StatusCodes.Status403Forbidden, }; return; } await next(); return; } if (!scopes.Contains(_scope, StringComparer.Ordinal)) { context.Result = new ObjectResult(new { error = $"Operator key requires the '{_scope}' scope." }) { StatusCode = StatusCodes.Status403Forbidden, }; return; } await next(); } }