Michael Shterenberg
Michael Shterenberg

Reputation: 1016

Dependency Injection on Authorization Policy

I want to create a claim based authorization for my ASP.NET Core app:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthorization(options =>
    {
        options.AddPolicy("Founders", policy =>
                          policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));
    });
}

The problem is that I have a non trivial method to resolve the employee numbers (1 to 5) and I want to use a DI service:

public interface IEmployeeProvider {

  string[] GetAuthorizedEmployeeIds();
}

I would like to inject this service and use it in AddPolicy, something like:

services.AddAuthorization(options =>
{
    options.AddPolicy("Founders", policy =>
              policy.RequireClaim("EmployeeNumber", *employeeProvider.GetAuthorizedEmployeeIds()));
});

Note

I know that I can write my own AuthorizationHandler where I can easily inject IEmployeeProvider but I'm against this pattern because:

  1. There is a already a handler that does exactly what I need
  2. I need to write a new handler for each claim type and each different requirement
  3. This is an anti pattern because the employee ids should really be part of the requirement while the handler should be generic component that handles the requirements

So I'm looking for a way to inject services when the policy is being built

Upvotes: 8

Views: 4132

Answers (4)

Harold Meza
Harold Meza

Reputation: 87

I resolved in this way, options above show me error about service root, but i try and finally make it

Program.cs


builder.Services.AddAuthorization(options =>
{    
        new RegisterPoliciesService(builder.Services).Register(options);             
});

builder.Services.AddTransient<IAuthorizationHandler, PermissionHandler>();

RegisterPolicies.cs


public class RegisterPoliciesService {
  private IServiceCollection _serviceProvider;
  public RegisterPoliciesService(IServiceCollection serviceProvider)
  {            
     _serviceProvider = serviceProvider;           
  }

  public void Register(AuthorizationOptions _autorizationOptions)
  {            
     using (var scope = _serviceProvider.BuildServiceProvider().CreateScope())
     {
       var repositoryPermissions = scope.ServiceProvider.GetRequiredService<IPermissionRepository>();                
        foreach (var permission in repositoryPermissions.GetChildPermissions())
        {
           _autorizationOptions.AddPolicy(permission.Rule!, policy => 
             policy.Requirements.Add(new PermissionRequirement(permission.Rule!)));
         }
      }
            
   }
}

Note

You must dispose createscope, this is why i use "using" clause to dispose createscope automatically whem the using finish.I do an automatic policies based on all data in permission table whose permission doesn't have a paprent permission, that means this permission contain the rule and it belongs to a parent permission to be able to group

Upvotes: 0

Nkosi
Nkosi

Reputation: 247088

To supplement the provided answer by @MichaelShterenberg, the configuration delegate can use a IServiceProvider to allow for additional dependencies

public static IServiceCollection AddAuthorization(this IServiceCollection services,
    Action<AuthorizationOptions, IServiceProvider> configure) {
    services.AddOptions<AuthorizationOptions>().Configure<IServiceProvider>(configure);
    return services.AddAuthorization();
}

Which, based on the original example, can be used

public void ConfigureServices(IServiceCollection services) {

    //...

    service.AddScoped<IEmployeeProvider, EmployeeProvider>();

    services.AddAuthorization((options, sp) => {
        IEmployeeProvider employeeProvider = sp.GetRequiredService<IEmployeeProvider>();
        options.AddPolicy("Founders", policy =>
            policy.RequireClaim("EmployeeNumber", employeeProvider.GetAuthorizedEmployeeIds())
        );
    });

    //...
}

If there were other dependencies needed, they could be resolved from the service provider.

Upvotes: 12

Michael Shterenberg
Michael Shterenberg

Reputation: 1016

Thanks to Nkosi for the tip!

Since AddAuthorization is basically configuring AuthorizationOptions behind the scenes, I followed the same pattern only I used OptionsBuilder to configure options with dependencies

I created my own AddAuthorization method that accepts dependencies:

 public static IServiceCollection AddAuthorization<TDep>(
     this IServiceCollection services,
     Action<AuthorizationOptions, TDep> configure) where TDep : class
 {
     services.AddOptions<AuthorizationOptions>().Configure<TDep>(configure);
     return services.AddAuthorization();
 }

And now I can use it to properly configure the requirement:

services.AddAuthorization<IEmployeeProvider>((options, employeeProvider> =>
{
    options.AddPolicy("Founders", policy =>
        policy.RequireClaim("EmployeeNumber", employeeProvider.GetAuthorizedEmployeeIds())
    );
});

You can follow the same technique if you need more dependencies (OptionsBuilder.Configure supports up to 5 dependencies)

Obviously, this solution requires extra validation when upgrading to newer ASP versions, as the underlying implementation of AddAuhtorization may change

Upvotes: 10

Ryan
Ryan

Reputation: 20116

You can build a service provider using the BuildServiceProvider() method on the IServiceCollection:

public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IEmployeeProvider, EmployeeProvider>();

        var sp = services.BuildServiceProvider();
        var employeeProvider = sp.GetService<IEmployeeProvider>();
        string[] values = employeeProvider.GetAuthorizedEmployeeIds();

        services.AddAuthorization(options =>
        {

            options.AddPolicy("Founders", policy =>
                      policy.RequireClaim("EmployeeNumber", employeeProvider.GetAuthorizedEmployeeIds()));
        });
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }

interface and Class

public interface IEmployeeProvider
{
    string[] GetAuthorizedEmployeeIds();
}

public class EmployeeProvider : IEmployeeProvider
{
    public string[] GetAuthorizedEmployeeIds()
    {
        var data = new string[] { "1", "2", "3", "4", "5" };
        return data;
    }
}

Upvotes: -1

Related Questions