Files
tiku-backend.net/Tiku.Api/Security/EndpointAuthorizationMetadata.cs

90 lines
4.4 KiB
C#

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<string> 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<AllowAnonymousAttribute>().Any() ||
action.Attributes.OfType<AllowAnonymousAttribute>().Any();
if (anonymous)
{
continue;
}
var policies = controller.Attributes.OfType<AuthorizeAttribute>()
.Concat(action.Attributes.OfType<AuthorizeAttribute>())
.Select(attribute => attribute.Policy)
.Where(policy => !string.IsNullOrWhiteSpace(policy))
.Cast<string>()
.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 : PermissionModuleCatalog.ResolvePermissionModuleCode(permission);
var requiredFeatures = controller.Attributes.OfType<RequireSaasFeatureAttribute>()
.Concat(action.Attributes.OfType<RequireSaasFeatureAttribute>())
.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<RequireSaasFeatureFromRouteAttribute>()
.Select(attribute => $"route:{attribute.RouteValueName}"))
.ToArray();
var httpMethods = action.Attributes.OfType<HttpMethodAttribute>()
.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) =>
HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsOptions(method);
}