using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.RegularExpressions; namespace StellaOps.Registry.TokenService; /// /// Evaluates repository access against configured plan rules. /// public sealed class PlanRegistry { private readonly IReadOnlyDictionary _plans; private readonly IReadOnlySet _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(StringComparer.OrdinalIgnoreCase) : new HashSet(options.RevokedLicenses, StringComparer.OrdinalIgnoreCase); _defaultPlan = options.DefaultPlan; } public RegistryAccessDecision Authorize( ClaimsPrincipal principal, IReadOnlyList 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 _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 _allowedActions; public RepositoryDescriptor(RegistryTokenServiceOptions.RepositoryRule rule) { Pattern = rule.Pattern; _pattern = Compile(rule.Pattern); _allowedActions = new HashSet(rule.Actions, StringComparer.OrdinalIgnoreCase); } public string Pattern { get; } public bool Matches(string repository) { return _pattern.IsMatch(repository); } public bool AllowsActions(IReadOnlyList 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); } } }