forked from xiongyuxing/tiku-backend.net
144 lines
6.7 KiB
C#
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)}";
|
|
}
|
|
}
|