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();
}
}