64 lines
2.2 KiB
C#
64 lines
2.2 KiB
C#
using Microsoft.AspNetCore.Http;
|
|
|
|
namespace StellaOps.Scheduler.WebService.Auth;
|
|
|
|
internal sealed class HeaderScopeAuthorizer : IScopeAuthorizer
|
|
{
|
|
private static readonly string[] ScopeHeaders = ["X-StellaOps-Scopes", "X-Scopes"];
|
|
|
|
public void EnsureScope(HttpContext context, string requiredScope)
|
|
{
|
|
Microsoft.Extensions.Primitives.StringValues values = default;
|
|
bool found = false;
|
|
foreach (var header in ScopeHeaders)
|
|
{
|
|
if (context.Request.Headers.TryGetValue(header, out values))
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
throw new UnauthorizedAccessException($"Missing required scope header (accepted: {string.Join(", ", ScopeHeaders)}).");
|
|
}
|
|
|
|
var scopeBuffer = string.Join(' ', values.ToArray());
|
|
if (string.IsNullOrWhiteSpace(scopeBuffer))
|
|
{
|
|
throw new UnauthorizedAccessException("Scope header 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}'.");
|
|
}
|
|
}
|