using System.Reflection; using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using MassTransit; using Tiku.Api.Security; using Tiku.Application.Security; using Tiku.Infrastructure.Messaging; namespace Tiku.IntegrationTests.Api; public sealed class AuthorizationManifestTests { private const int ExpectedActionCount = 332; private const string ExpectedSha256 = "a80fe477ba3021625e17c9fc639e5109bab678178f8024a51c3c732bf5a46d3f"; [Fact] public void Controller_authorization_surface_matches_reviewed_manifest() { var descriptors = typeof(Tiku.Api.ApiProgramMarker).Assembly.GetTypes() .Where(type => !type.IsAbstract && typeof(ControllerBase).IsAssignableFrom(type)) .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) .Where(method => method.GetCustomAttributes().Any()) .Select(method => Describe(type, method))) .OrderBy(value => value, StringComparer.Ordinal) .ToArray(); var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(string.Join('\n', descriptors)))) .ToLowerInvariant(); Assert.True( descriptors.Length == ExpectedActionCount && hash == ExpectedSha256, $"Authorization manifest changed. count={descriptors.Length}, sha256={hash}"); } [Fact] public async Task Runtime_controller_endpoints_have_authorization_and_audit_metadata() { await using var factory = new ApiTestFactory(); using var client = factory.CreateClient(); _ = await client.GetAsync("/api/health"); var endpoints = factory.Services.GetRequiredService().Endpoints .Where(endpoint => endpoint.Metadata.GetMetadata() is not null) .ToArray(); Assert.NotEmpty(endpoints); foreach (var endpoint in endpoints) { var anonymous = endpoint.Metadata.GetMetadata() is not null; var metadata = endpoint.Metadata.GetMetadata(); if (anonymous) { Assert.Null(metadata); continue; } Assert.NotNull(metadata); Assert.False(string.IsNullOrWhiteSpace(metadata.AuditAction)); Assert.Contains(metadata.Realm, new[] { "authenticated", "tenant", "platform" }); } } [Fact] public void Message_consumers_have_reviewed_authorization_and_audit_metadata() { var consumers = typeof(MessagingOptions).Assembly.GetTypes() .Where(type => !type.IsAbstract && type.GetInterfaces().Any(candidate => candidate.IsGenericType && candidate.GetGenericTypeDefinition() == typeof(IConsumer<>))) .ToArray(); Assert.Equal(2, consumers.Length); foreach (var consumer in consumers) { var metadata = consumer.GetCustomAttribute(); Assert.NotNull(metadata); Assert.Contains(metadata.Realm, new[] { "tenant", "platform", "system" }); Assert.False(string.IsNullOrWhiteSpace(metadata.Module)); Assert.False(string.IsNullOrWhiteSpace(metadata.AuditAction)); if (consumer.Name == "BackgroundJobRequestedConsumer") { Assert.Equal(CapabilityOperation.Write, metadata.Operation); Assert.True(metadata.RequiresSystemScope); } } } private static string Describe(Type controller, MethodInfo action) { var controllerRoute = controller.GetCustomAttribute()?.Template ?? string.Empty; var http = action.GetCustomAttributes().ToArray(); var methods = string.Join(',', http.SelectMany(attribute => attribute.HttpMethods).Distinct().Order(StringComparer.Ordinal)); var templates = string.Join(',', http.Select(attribute => attribute.Template ?? string.Empty).Distinct().Order(StringComparer.Ordinal)); var policies = controller.GetCustomAttributes() .Concat(action.GetCustomAttributes()) .Select(attribute => attribute.Policy ?? "authenticated") .Order(StringComparer.Ordinal); var anonymous = controller.IsDefined(typeof(AllowAnonymousAttribute)) || action.IsDefined(typeof(AllowAnonymousAttribute)); return $"{methods}|{controllerRoute}/{templates}|{controller.Name}.{action.Name}|anonymous={anonymous}|policies={string.Join(',', policies)}"; } }