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). // Treat it as unlimited rather than silently locking the operator out; a key that // actually declares a scope set is still enforced below. if (scopes == null || scopes.Count == 0) { 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(); } }