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 Tiku.Api.Security; namespace Tiku.IntegrationTests.Api; public sealed class AuthorizationManifestTests { private const int ExpectedActionCount = 330; private const string ExpectedSha256 = "ad09167662cb9dc25111f40902c5f16a6633465f0ca7e7da0e50cdc10cfb8bb5"; [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" }); } } 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)}"; } }