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