Files
tiku-backend.net/Tiku.IntegrationTests/Api/AuthorizationManifestTests.cs

144 lines
6.7 KiB
C#

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 = 397;
private const string ExpectedSha256 = "fe0636f609e86c8c7540d84914f8254106d20194ee5bc715d616c5a4f84c7a94";
[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<HttpMethodAttribute>().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<EndpointDataSource>().Endpoints
.Where(endpoint => endpoint.Metadata.GetMetadata<ControllerActionDescriptor>() is not null)
.ToArray();
Assert.NotEmpty(endpoints);
foreach (var endpoint in endpoints)
{
var anonymous = endpoint.Metadata.GetMetadata<IAllowAnonymous>() is not null;
var metadata = endpoint.Metadata.GetMetadata<EndpointAuthorizationMetadata>();
if (anonymous)
{
Assert.Null(metadata);
continue;
}
Assert.NotNull(metadata);
Assert.False(string.IsNullOrWhiteSpace(metadata.AuditAction));
Assert.Contains(metadata.Realm, new[] { "authenticated", "tenant", "platform" });
if (metadata.Realm == "tenant" &&
metadata.Module is { } module &&
PermissionModuleCatalog.RequiredFeatures.TryGetValue(module, out var requiredFeature) &&
requiredFeature is not null)
{
Assert.Contains(requiredFeature, metadata.RequiredFeatures);
}
}
}
[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<ConsumerAuthorizationMetadataAttribute>();
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);
}
}
}
[Theory]
[InlineData("questions", SaasFeatureCatalog.PrivateQuestionBank)]
[InlineData("vocabulary", SaasFeatureCatalog.Vocabulary)]
[InlineData("handbook", SaasFeatureCatalog.Handbook)]
[InlineData("scoreline", SaasFeatureCatalog.Scoreline)]
[InlineData("videos", SaasFeatureCatalog.Video)]
public void Content_import_route_maps_to_an_explicit_feature(string importType, string expectedFeature)
{
Assert.Equal(expectedFeature, SaasFeatureCatalog.ResolveContentImportFeature(importType));
}
[Fact]
public void Tenant_content_permissions_are_split_by_purchasable_feature()
{
var expected = new Dictionary<string, string>(StringComparer.Ordinal)
{
[BackendPermissions.TenantContentManage] = SaasFeatureCatalog.PrivateQuestionBank,
[BackendPermissions.TenantVocabularyManage] = SaasFeatureCatalog.Vocabulary,
[BackendPermissions.TenantHandbookManage] = SaasFeatureCatalog.Handbook,
[BackendPermissions.TenantVideoManage] = SaasFeatureCatalog.Video,
[BackendPermissions.TenantScorelineManage] = SaasFeatureCatalog.Scoreline,
[BackendPermissions.TenantSiteContentManage] = SaasFeatureCatalog.SiteContent
};
foreach (var pair in expected)
{
var module = PermissionModuleCatalog.ResolvePermissionModuleCode(pair.Key);
Assert.Equal(pair.Value, PermissionModuleCatalog.RequiredFeatures[module]);
}
}
private static string Describe(Type controller, MethodInfo action)
{
var controllerRoute = controller.GetCustomAttribute<RouteAttribute>()?.Template ?? string.Empty;
var http = action.GetCustomAttributes<HttpMethodAttribute>().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<AuthorizeAttribute>()
.Concat(action.GetCustomAttributes<AuthorizeAttribute>())
.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)}";
}
}