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, IReadOnlyList RequiredFeatures, 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.LastOrDefault(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 : PermissionModuleCatalog.ResolvePermissionModuleCode(permission); var requiredFeatures = controller.Attributes.OfType() .Concat(action.Attributes.OfType()) .Select(attribute => attribute.FeatureCode) .Concat(module is not null && PermissionModuleCatalog.RequiredFeatures.TryGetValue(module, out var moduleFeature) && moduleFeature is not null ? [moduleFeature] : []) .Distinct(StringComparer.Ordinal) .Order(StringComparer.Ordinal) .Concat(action.Attributes.OfType() .Select(attribute => $"route:{attribute.RouteValueName}")) .ToArray(); 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, requiredFeatures, permission, operation, policies.Contains(TikuPolicies.TenantContentManageAllScope, StringComparer.Ordinal) || policies.Contains(TikuPolicies.TenantAllDataScope, 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 bool IsSafeMethod(string method) { return HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsOptions(method); } }