using System.Reflection;
using System.Runtime.CompilerServices;
using Elsa.Authorization;
using Elsa.Identity.Permissions;
using Elsa.Identity.Services;
using Elsa.Permissions;
using FastEndpoints;
namespace Elsa.Identity.UnitTests.Authorization;
///
/// Pins what the three endpoints that used to carry the SecurityRoot policy require now that the
/// policy is gone, and checks that what they require is something the Identity catalog advertises.
///
///
/// Two of the three (Roles/Create, Applications/Create) only lost a redundant policy line and
/// must keep the permission they already declared; Secrets/Hash gained one where it previously had
/// nothing but "any authenticated caller". The coverage gate only asks whether an endpoint declares
/// something, so without these rows either half of that could silently change.
///
public class EndpointPermissionTests
{
private static readonly Assembly Module = typeof(RoleAuthorizationService).Assembly;
public static TheoryData Declarations => new()
{
{ "Elsa.Identity.Endpoints.Secrets.Hash.Hash", IdentityPermissions.Users, CoreVerbs.Create },
{ "Elsa.Identity.Endpoints.Roles.Create.Create", IdentityPermissions.Roles, CoreVerbs.Create },
{ "Elsa.Identity.Endpoints.Applications.Create.Create", IdentityPermissions.Applications, CoreVerbs.Create }
};
[Theory]
[MemberData(nameof(Declarations))]
public void EndpointDeclaresItsExpectedPermission(string endpointTypeName, string resource, string verb) =>
Assert.Equal(new Permission(resource, verb), Declare(endpointTypeName));
[Theory]
[MemberData(nameof(Declarations))]
public void EveryDeclaredPermissionIsAdvertisedByTheCatalog(string endpointTypeName, string resource, string verb)
{
var declared = Declare(endpointTypeName);
var descriptor = new IdentityPermissionsDescriptorProvider().GetDescriptors().SingleOrDefault(x => x.Resource == declared.Resource);
Assert.True(descriptor is not null, $"{endpointTypeName} requires resource '{declared.Resource}', which the module contributes no descriptor for, so it cannot be granted through the role editor.");
Assert.True(descriptor!.Supports(declared.Verb), $"{endpointTypeName} requires '{declared}', but '{declared.Resource}' advertises only [{string.Join(", ", descriptor.SupportedVerbs)}].");
Assert.Equal(new Permission(resource, verb), declared);
}
///
/// Runs one endpoint's Configure() and returns what it recorded. The requirement is attached as an
/// inline policy, which cannot be read back off the definition, so the registry is the only way to observe
/// a declaration without booting a host. The instance skips its constructor because Configure()
/// touches none of the injected services, and substituting them would make the rows depend on which
/// dependencies happen to be interfaces.
///
private static Permission Declare(string endpointTypeName)
{
var endpointType = Module.GetType(endpointTypeName, true)!;
var endpoint = RuntimeHelpers.GetUninitializedObject(endpointType);
var (requestType, responseType) = DtoTypes(endpointType);
endpointType.GetProperty("Definition", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!
.SetValue(endpoint, new EndpointDefinition(endpointType, requestType, responseType));
endpointType.GetMethod("Configure")!.Invoke(endpoint, null);
var permission = EndpointPermissionRegistry.Find(endpointType);
Assert.True(permission.HasValue, $"{endpointTypeName} declares no permission.");
return permission!.Value;
}
private static (Type Request, Type Response) DtoTypes(Type endpointType)
{
for (var type = endpointType.BaseType; type is not null; type = type.BaseType)
{
if (!type.IsGenericType)
continue;
var definition = type.GetGenericTypeDefinition();
var arguments = type.GetGenericArguments();
if (definition == typeof(Elsa.Abstractions.ElsaEndpoint<,>))
return (arguments[0], arguments[1]);
if (definition == typeof(Elsa.Abstractions.ElsaEndpointWithoutRequest<>))
return (typeof(EmptyRequest), arguments[0]);
if (definition == typeof(Elsa.Abstractions.ElsaEndpoint<>))
return (arguments[0], typeof(object));
}
throw new InvalidOperationException($"Unsupported endpoint type '{endpointType.FullName}'.");
}
}