Files
git.stella-ops.org/src/Scheduler/StellaOps.Scheduler.WebService/Auth/HeaderScopeAuthorizer.cs
2026-02-17 00:51:35 +02:00

53 lines
1.9 KiB
C#

using Microsoft.AspNetCore.Http;
namespace StellaOps.Scheduler.WebService.Auth;
internal sealed class HeaderScopeAuthorizer : IScopeAuthorizer
{
private const string ScopeHeader = "X-StellaOps-Scopes";
public void EnsureScope(HttpContext context, string requiredScope)
{
if (!context.Request.Headers.TryGetValue(ScopeHeader, out var values))
{
throw new UnauthorizedAccessException($"Missing required header '{ScopeHeader}'.");
}
var scopeBuffer = string.Join(' ', values.ToArray());
if (string.IsNullOrWhiteSpace(scopeBuffer))
{
throw new UnauthorizedAccessException($"Header '{ScopeHeader}' cannot be empty.");
}
var scopes = scopeBuffer
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
if (scopes.Contains(requiredScope))
{
return;
}
// Hierarchical match: fine-grained scope "scheduler.runs.read" is satisfied
// by OIDC coarse-grained scope "scheduler:read" or "scheduler:admin".
// Format: "{service}.{resource}.{action}" -> check "{service}:{action}" and "{service}:admin"
var dotParts = requiredScope.Split('.');
if (dotParts.Length >= 2)
{
var service = dotParts[0];
var action = dotParts[^1];
if (scopes.Contains($"{service}:{action}") || scopes.Contains($"{service}:admin"))
{
return;
}
// Also check "operate" scope for write/manage actions
if (action is "write" or "manage" or "preview" && scopes.Contains($"{service}:operate"))
{
return;
}
}
throw new InvalidOperationException($"Missing required scope '{requiredScope}'.");
}
}