using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Linq; namespace StellaOps.Registry.TokenService; public static class RegistryScopeParser { public static IReadOnlyList Parse(IQueryCollection query) { ArgumentNullException.ThrowIfNull(query); var scopes = new List(); if (query.TryGetValue("scope", out var scopeValues)) { foreach (var scope in scopeValues) { if (string.IsNullOrWhiteSpace(scope)) { continue; } // Support space-delimited scopes per OAuth2 spec foreach (var component in scope.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { scopes.Add(component); } } } var requests = new List(scopes.Count); foreach (var scope in scopes) { var request = ParseScope(scope); requests.Add(request); } return requests; } private static RegistryAccessRequest ParseScope(string scope) { var segments = scope.Split(':', StringSplitOptions.TrimEntries); if (segments.Length < 1) { throw new InvalidScopeException(scope, "scope missing resource type"); } var type = segments[0]; if (!string.Equals(type, "repository", StringComparison.OrdinalIgnoreCase)) { throw new InvalidScopeException(scope, $"unsupported resource type '{type}'"); } if (segments.Length < 2 || string.IsNullOrWhiteSpace(segments[1])) { throw new InvalidScopeException(scope, "repository scope missing name"); } var name = segments[1]; var actions = segments.Length >= 3 && !string.IsNullOrWhiteSpace(segments[2]) ? segments[2].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) : Array.Empty(); if (actions.Length == 0) { actions = new[] { "pull" }; } var normalized = actions .Select(action => action.ToLowerInvariant()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); return new RegistryAccessRequest(type.ToLowerInvariant(), name, normalized); } } public sealed class InvalidScopeException : Exception { public InvalidScopeException(string scope, string reason) : base($"Invalid scope '{scope}': {reason}") { Scope = scope; Reason = reason; } public string Scope { get; } public string Reason { get; } }