using System.Security.Claims; using System.Text.Json; using Microsoft.AspNetCore.Authorization; namespace SatelliteProvider.Api.Authentication; // AZ-488: enforces a required permission from the `permissions` JWT claim. // The claim may arrive as either: // - a JWT array claim (multiple ClaimType="permissions" entries — Microsoft.IdentityModel // splits arrays this way), OR // - a single JSON-array string ("[\"GPS\",\"FL\"]") when a producer mis-encodes. // Both shapes are matched so the satellite-provider remains tolerant of upstream // producers that do not split array claims out of the box. public sealed class PermissionsRequirement : IAuthorizationRequirement { public PermissionsRequirement(string requiredPermission) { if (string.IsNullOrWhiteSpace(requiredPermission)) { throw new ArgumentException("Required permission must be a non-empty string.", nameof(requiredPermission)); } RequiredPermission = requiredPermission; } public string RequiredPermission { get; } } public sealed class PermissionsAuthorizationHandler : AuthorizationHandler { public const string ClaimType = "permissions"; protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionsRequirement requirement) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(requirement); var user = context.User; if (user?.Identity?.IsAuthenticated != true) { return Task.CompletedTask; } if (UserHasPermission(user, requirement.RequiredPermission)) { context.Succeed(requirement); } return Task.CompletedTask; } private static bool UserHasPermission(ClaimsPrincipal user, string requiredPermission) { foreach (var claim in user.FindAll(ClaimType)) { if (string.Equals(claim.Value, requiredPermission, StringComparison.Ordinal)) { return true; } if (TryReadJsonArray(claim.Value, out var values)) { foreach (var value in values) { if (string.Equals(value, requiredPermission, StringComparison.Ordinal)) { return true; } } } } return false; } private static bool TryReadJsonArray(string value, out IReadOnlyList items) { items = Array.Empty(); if (string.IsNullOrWhiteSpace(value) || value[0] != '[') { return false; } try { using var document = JsonDocument.Parse(value); if (document.RootElement.ValueKind != JsonValueKind.Array) { return false; } var result = new List(document.RootElement.GetArrayLength()); foreach (var element in document.RootElement.EnumerateArray()) { if (element.ValueKind == JsonValueKind.String) { var text = element.GetString(); if (!string.IsNullOrEmpty(text)) { result.Add(text); } } } items = result; return result.Count > 0; } catch (JsonException) { return false; } } } public static class SatellitePermissions { public const string Gps = "GPS"; public const string UavUploadPolicy = "RequiresGpsPermission"; }