Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
151 lines
4.5 KiB
C#
151 lines
4.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Security.Claims;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace StellaOps.Registry.TokenService;
|
|
|
|
/// <summary>
|
|
/// Evaluates repository access against configured plan rules.
|
|
/// </summary>
|
|
public sealed class PlanRegistry
|
|
{
|
|
private readonly IReadOnlyDictionary<string, PlanDescriptor> _plans;
|
|
private readonly IReadOnlySet<string> _revokedLicenses;
|
|
private readonly string? _defaultPlan;
|
|
|
|
public PlanRegistry(RegistryTokenServiceOptions options)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
|
|
_plans = options.Plans
|
|
.Select(plan => new PlanDescriptor(plan))
|
|
.ToDictionary(static plan => plan.Name, StringComparer.OrdinalIgnoreCase);
|
|
|
|
_revokedLicenses = options.RevokedLicenses.Count == 0
|
|
? new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
: new HashSet<string>(options.RevokedLicenses, StringComparer.OrdinalIgnoreCase);
|
|
|
|
_defaultPlan = options.DefaultPlan;
|
|
}
|
|
|
|
public RegistryAccessDecision Authorize(
|
|
ClaimsPrincipal principal,
|
|
IReadOnlyList<RegistryAccessRequest> requests)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(principal);
|
|
ArgumentNullException.ThrowIfNull(requests);
|
|
|
|
if (requests.Count == 0)
|
|
{
|
|
return new RegistryAccessDecision(false, "no_scopes_requested");
|
|
}
|
|
|
|
var licenseId = principal.FindFirstValue("stellaops:license")?.Trim();
|
|
if (!string.IsNullOrEmpty(licenseId) && _revokedLicenses.Contains(licenseId))
|
|
{
|
|
return new RegistryAccessDecision(false, "license_revoked");
|
|
}
|
|
|
|
var planName = principal.FindFirstValue("stellaops:plan")?.Trim();
|
|
if (string.IsNullOrEmpty(planName))
|
|
{
|
|
planName = _defaultPlan;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(planName) || !_plans.TryGetValue(planName, out var descriptor))
|
|
{
|
|
return new RegistryAccessDecision(false, "plan_unknown");
|
|
}
|
|
|
|
foreach (var request in requests)
|
|
{
|
|
if (!descriptor.IsRepositoryAllowed(request))
|
|
{
|
|
return new RegistryAccessDecision(false, "scope_not_permitted");
|
|
}
|
|
}
|
|
|
|
return new RegistryAccessDecision(true);
|
|
}
|
|
|
|
private sealed class PlanDescriptor
|
|
{
|
|
private readonly IReadOnlyList<RepositoryDescriptor> _repositories;
|
|
|
|
public PlanDescriptor(RegistryTokenServiceOptions.PlanRule source)
|
|
{
|
|
Name = source.Name;
|
|
_repositories = source.Repositories
|
|
.Select(rule => new RepositoryDescriptor(rule))
|
|
.ToArray();
|
|
}
|
|
|
|
public string Name { get; }
|
|
|
|
public bool IsRepositoryAllowed(RegistryAccessRequest request)
|
|
{
|
|
if (!string.Equals(request.Type, "repository", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (var repo in _repositories)
|
|
{
|
|
if (!repo.Matches(request.Name))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (repo.AllowsActions(request.Actions))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private sealed class RepositoryDescriptor
|
|
{
|
|
private readonly Regex _pattern;
|
|
private readonly IReadOnlySet<string> _allowedActions;
|
|
|
|
public RepositoryDescriptor(RegistryTokenServiceOptions.RepositoryRule rule)
|
|
{
|
|
Pattern = rule.Pattern;
|
|
_pattern = Compile(rule.Pattern);
|
|
_allowedActions = new HashSet<string>(rule.Actions, StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public string Pattern { get; }
|
|
|
|
public bool Matches(string repository)
|
|
{
|
|
return _pattern.IsMatch(repository);
|
|
}
|
|
|
|
public bool AllowsActions(IReadOnlyList<string> actions)
|
|
{
|
|
foreach (var action in actions)
|
|
{
|
|
if (!_allowedActions.Contains(action))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static Regex Compile(string pattern)
|
|
{
|
|
var escaped = Regex.Escape(pattern);
|
|
escaped = escaped.Replace(@"\*", ".*", StringComparison.Ordinal);
|
|
return new Regex($"^{escaped}$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
|
}
|
|
}
|
|
}
|