123 lines
5.8 KiB
C#
123 lines
5.8 KiB
C#
using System.Reflection;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Mvc.Controllers;
|
|
using Microsoft.AspNetCore.Mvc.Routing;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Tiku.Api;
|
|
using Tiku.Api.Security;
|
|
using Tiku.Application.Security;
|
|
|
|
namespace Tiku.IntegrationTests.Api;
|
|
|
|
public sealed class AuthorizationManifestTests
|
|
{
|
|
// Reviewed additions: platform approval, typed configuration, notification governance,
|
|
// and operation-level authorization metadata.
|
|
private const int ExpectedActionCount = 465;
|
|
private const string ExpectedSha256 = "d31e8f91cc261863272ceef6868db68c9a1288aaaef29332f4e1a5b11b617fcd";
|
|
|
|
[Fact]
|
|
public void Controller_authorization_surface_matches_reviewed_manifest()
|
|
{
|
|
var descriptors = typeof(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/system/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);
|
|
}
|
|
}
|
|
|
|
[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 = string.Join(',', controller.GetCustomAttributes<RouteAttribute>()
|
|
.Select(attribute => attribute.Template ?? string.Empty)
|
|
.Order(StringComparer.Ordinal));
|
|
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)}";
|
|
}
|
|
} |