151 lines
4.7 KiB
C#
151 lines
4.7 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);
|
|
}
|
|
}
|
|
}
|