using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Routing; using Tiku.Application.Security; namespace Tiku.Api.Security; public sealed record EndpointAuthorizationMetadata( string Realm, string? Module, string? Permission, CapabilityOperation Operation, bool RequiresAllDataScope, string AuditAction); internal sealed class EndpointAuthorizationMetadataConvention : IApplicationModelConvention { public void Apply(ApplicationModel application) { foreach (var controller in application.Controllers) { foreach (var action in controller.Actions) { var anonymous = controller.Attributes.OfType().Any() || action.Attributes.OfType().Any(); if (anonymous) { continue; } var policies = controller.Attributes.OfType() .Concat(action.Attributes.OfType()) .Select(attribute => attribute.Policy) .Where(policy => !string.IsNullOrWhiteSpace(policy)) .Cast() .ToArray(); var permission = policies.FirstOrDefault(policy => BackendPermissions.Tenant.Contains(policy) || BackendPermissions.Platform.Contains(policy)); var realm = permission is not null && BackendPermissions.Platform.Contains(permission) || policies.Any(policy => policy.StartsWith("platform", StringComparison.Ordinal)) ? "platform" : permission is not null && BackendPermissions.Tenant.Contains(permission) || policies.Any(policy => policy.StartsWith("tenant", StringComparison.Ordinal)) ? "tenant" : "authenticated"; var module = permission is null ? null : ResolveModule(permission); var httpMethods = action.Attributes.OfType() .SelectMany(attribute => attribute.HttpMethods) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); var operation = httpMethods.All(IsSafeMethod) ? CapabilityOperation.Read : CapabilityOperation.Write; var route = $"{controller.ControllerName}.{action.ActionName}"; var metadata = new EndpointAuthorizationMetadata( realm, module, permission, operation, policies.Contains(TikuPolicies.TenantContentManageAllScope, StringComparer.Ordinal) || policies.Contains(TikuPolicies.TenantCommerceOperateAllScope, StringComparer.Ordinal), $"{string.Join(',', httpMethods.Order(StringComparer.Ordinal))}:{route}"); foreach (var selector in action.Selectors) { selector.EndpointMetadata.Add(metadata); } } } } private static string ResolveModule(string permission) { var parts = permission.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); return parts.Length >= 2 ? parts[1].ToLowerInvariant() : permission.ToLowerInvariant(); } private static bool IsSafeMethod(string method) => HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsOptions(method); }