IAuthorizationHandler with multiple registration - how the dependency resolver selects the right implementation?
Asked Answered
T

2

19

Consider the following code in the ConfigureServices method of the Startup class -

services.AddTransient<IAuthorizationHandler, BlockUsersHandler>();
services.AddTransient<IAuthorizationHandler, BlockClaimHandler>();

services.AddAuthorization(option =>
{                
    option.AddPolicy("NotHacker", policy =>
    {
        policy.AddRequirements(new BlockUsersRequirement("Hacker"));
    });                
    option.AddPolicy("NotThatClaim", policy =>
    {
        policy.AddRequirements(new BlockClaimRequirement(new Claim("ThatClaimType", "ThatClaim")));
    });
});

and these are the custom class implementations -

public class BlockUsersRequirement : IAuthorizationRequirement
{
    // Code goes here
}

public class BlockUsersHandler : AuthorizationHandler<BlockUsersRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BlockUsersRequirement requirement)
    {
        // Code goes here
    }
}

public class BlockClaimRequirement : IAuthorizationRequirement
{
    // Code goes here
}

public class BlockClaimHandler : AuthorizationHandler<BlockClaimRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BlockClaimRequirement requirement)
    {            
        // Code goes here
    }
}

My understanding was that whenever a dependency on a service is faced, the built-in dependency resolver provides the concrete implementation registered for that service and if I register multiple implementation of a service, then the last registration will take effect.

In the code above, two implementations are registered for IAuthorizationHandler and the two authorization policies are working fine with that.

So, how is the dependency resolver deciding when to select which implementation? And based on what?

EDIT - 2019.07.28
So, as @Martin answered below, looks like the dependency resolver can infer the implementation from the IAuthorizationRequirement in the AuthorizationHandler<TRequirement>, from which the Handler implementations are deriving.

But you actually can create a Handler class by directly implementing the IAuthorizationHandler interface without deriving from AuthorizationHandler<TRequirement> -

public class DeletePermissionRequirement : IAuthorizationRequirement
{
    // Nothing here
}

public class DeletePermissionHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        // Code goes here
    }
}

So, now there is no IAuthorizationRequirement in the Handler's signature to infer from.

Also, you can add multiple Handler implementations for a single Requirement -

public class BuildingEntryRequirement : IAuthorizationRequirement
{
    // Nothing here
}

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BuildingEntryRequirement requirement)
    {
        // Code goes here
    }
}

public class TemporaryPassHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BuildingEntryRequirement requirement)
    {
        // Code goes here
    }
}

Taking the these new implementations into account the code in the ConfigureServices method looks like -

services.AddTransient<IAuthorizationHandler, BlockUsersHandler>();
services.AddTransient<IAuthorizationHandler, BlockClaimHandler>();
services.AddTransient<IAuthorizationHandler, DeletePermissionHandler>();
services.AddTransient<IAuthorizationHandler, BadgeEntryHandler>();
services.AddTransient<IAuthorizationHandler, TemporaryPassHandler>();

services.AddAuthorization(option =>
{                
    option.AddPolicy("NotHacker", policy =>
    {
        policy.AddRequirements(new BlockUsersRequirement("Hacker"));
    });                
    option.AddPolicy("NotThatClaim", policy =>
    {
        policy.AddRequirements(new BlockClaimRequirement(new Claim("ThatClaimType", "ThatClaim")));
    });
    option.AddPolicy("CanDelete", policy =>
    {
        policy.AddRequirements(new DeletePermissionRequirement());
    });
    option.AddPolicy("BadgeEntry", policy =>
    {
        policy.AddRequirements(new BuildingEntryRequirement());
    });
});

and, of course, all the authorization policies are working fine.

So again, how the dependency resolver is selecting the right implementation?

Tritheism answered 27/7, 2019 at 16:41 Comment(0)
M
8

It is using the type of the requirement to decide which handler to use.

There can be more simultaneous authorization handlers that differ in type of the requirement.

It is possible to check more requirements in one policy.

Also when the authorization service is called, it picks the right handler:

IAuthorizationService _authorizationService; // injected

_authorizationService.AuthorizeAsync(User, resourceId, new MyAuthorizationRequirement(UserAccessScope.Account, resourceId, requiredRole));

Update:

The default behavior is

  • for a given AuthorizationHandlerContext all registered IAuthorizationHandler handlers are evaluated
  • for each of these handlers the method HandleAsync is called
  • a handler that derives from the abstract AuthorizationHandler<TRequirement> class implements the method HandleAsync this way:
public virtual async Task HandleAsync(AuthorizationHandlerContext context)
{
    foreach (var req in context.Requirements.OfType<TRequirement>())
    {
        await HandleRequirementAsync(context, req);
    }
}

It means the handler filters the requirements by type. It also means a handler that does not derive from the abstract class AuthorizationHandler<TRequirement> has its own implementation of the HandleAsync method.

The difference between an authorization handler that does inherit from the abstract class AuthorizationHandler<TRequirement> and one that doesn't boils down to how the HandleAsync method of the interface IAuthorizationHandler is implemented. AuthorizationHandler<TRequirement> has a default implementation described above, the implementor of the generic interface IAuthorizationHandler needs to supply its own implementation.

The answer to the second part concerning multiple Handler implementations for a single Requirement is, all handlers which have requirement of the given type will be evaluated, and if any one of them succeeds and none of them explicitly fails (Fail method has been called on the context) then the operation will be authorized.

Macropterous answered 28/7, 2019 at 7:22 Comment(5)
I kinda guessed so, after posting the question and spending some time on it, but then I found that you can actually create a Handler without inheriting from AuthorizationHandler<TRequirement> and directly implementing the IAuthorizationHandler interface, in which case there is no way to infer the Requirement from the Handler's signature. I'll try to add an **Edit on that. Also, you can add multiple Handler implementations for a single Requirement.Tritheism
Please check the Edit.Tritheism
I don't see how that answers my question :(Tritheism
Martin, you are explaining "how the Handlers are executed". My question is how the Handlers get chosen at the first place. Take a look at the last code snippet in my Edit. Now consider an action method decorated with [Authorize(Policy = "CanDelete")]. When this action method is reached, how the dependency resolver choose the DeletePermissionHandler out of the five Handlers registered for the IAuthorizationHandler? All their implementation signatures are given above.Tritheism
@Tritheism I'm thinking maybe the handlers are been resolved based on their class name as this is unique among the different handlers. Internally, the DI might be using their class names as keys when registering the handlers, and also uses that to determine what handler to resolve.Fishworm
V
1

ASP .NET Core dependency injection container can resolve to a single implementation per Interface and can resolve to all registered implementations based on the signature that's resolved.

Signature Interface is resolved to a single implementation - last one registered. Signature IEnumerable<Interface> is resolved to all registered implementations.

I assume that IAuthorizationHandler is resolved using IEnumerable<IAuthorizationHandler> and therefore all implementations are created.

Vigen answered 25/2, 2020 at 16:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.