Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
391 views
in Technique[技术] by (71.8m points)

c# - Override global authorize filter in ASP.NET Core 1.0 MVC

I am trying to set up authorization in ASP.NET Core 1.0 (MVC 6) web app.

More restrictive approach - by default I want to restrict all controllers and action methods to users with Admin role. So, I am adding a global authorize attribute like:

AuthorizationPolicy requireAdminRole = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .RequireRole("Admin")
    .Build();
services.AddMvc(options => { options.Filters.Add(new AuthorizeFilter(requireAdminRole));});

Then I want to allow users with specific roles to access concrete controllers. For example:

[Authorize(Roles="Admin,UserManager")]
public class UserControler : Controller{}

Which of course will not work, as the "global filter" will not allow the UserManager to access the controller as they are not "admins".

In MVC5, I was able to implement this by creating a custom authorize attribute and putting my logic there. Then using this custom attribute as a global. For example:

public class IsAdminOrAuthorizeAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        ActionDescriptor action = filterContext.ActionDescriptor;
        if (action.IsDefined(typeof(AuthorizeAttribute), true) ||
            action.ControllerDescriptor.IsDefined(typeof(AuthorizeAttribute), true))
        {
            return;
        }

        base.OnAuthorization(filterContext);
    }
}

I tried to create a custom AuthorizeFilter, but no success. API seems to be different.

So my question is: Is it possible to set up default policy and then override it for specific controllers and actions. Or something similar. I don't want to go with this

[Authorize(Roles="Admin,[OtherRoles]")]

on every controller/action, as this is a potential security problem. What will happen if I accidentally forget to put the Admin role.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

You will need to play with the framework a bit since your global policy is more restrictive than the one you want to apply to specific controllers and actions:

  • By default only Admin users can access your application
  • Specific roles will also be granted access to some controllers (like UserManagers accessing the UsersController)

As you have already noticied, having a global filter means that only Admin users will have access to a controller. When you add the additional attribute on the UsersController, only users that are both Admin and UserManager will have access.

It is possible to use a similar approach to the MVC 5 one, but it works in a different way.

  • In MVC 6 the [Authorize] attribute does not contain the authorization logic.
  • Instead the AuthorizeFilter is the one that has an OnAuthorizeAsync method calling the authorization service to make sure policies are satisfied.
  • A specific IApplicationModelProvider is used to add an AuthorizeFilter for every controller and action that has an [Authorize] attribute.

One option could be to recreate your IsAdminOrAuthorizeAttribute, but this time as an AuthorizeFilter that you will then add as a global filter:

public class IsAdminOrAuthorizeFilter : AuthorizeFilter
{
    public IsAdminOrAuthorizeFilter(AuthorizationPolicy policy): base(policy)
    {
    }

    public override Task OnAuthorizationAsync(Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext context)
    {
        // If there is another authorize filter, do nothing
        if (context.Filters.Any(item => item is IAsyncAuthorizationFilter && item != this))
        {
            return Task.FromResult(0);
        }

        //Otherwise apply this policy
        return base.OnAuthorizationAsync(context);
    }        
}

services.AddMvc(opts => 
{
    opts.Filters.Add(new IsAdminOrAuthorizeFilter(new AuthorizationPolicyBuilder().RequireRole("admin").Build()));
});

This would apply your global filter only when the controller/action doesn't have a specific [Authorize] attribute.


You could also avoid having a global filter by injecting yourself in the process that generates the filters to be applied for every controller and action. You can either add your own IApplicationModelProvider or your own IApplicationModelConvention. Both will let you add/remove specific controller and actions filters.

For example, you can define a default authorization policy and extra specific policies:

services.AddAuthorization(opts =>
{
    opts.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().RequireRole("admin").Build();
    opts.AddPolicy("Users", policy => policy.RequireAuthenticatedUser().RequireRole("admin", "users"));
});

Then you can create a new IApplicatioModelProvider that will add the default policy to every controller that doesn't have its own [Authorize] attribute (An application convention would be very similar and probably more aligned with the way the framework is intended to be extended. I just quickly used the existing AuthorizationApplicationModelProvider as a guide):

public class OverridableDefaultAuthorizationApplicationModelProvider : IApplicationModelProvider
{
    private readonly AuthorizationOptions _authorizationOptions;

    public OverridableDefaultAuthorizationApplicationModelProvider(IOptions<AuthorizationOptions> authorizationOptionsAccessor)
    {
        _authorizationOptions = authorizationOptionsAccessor.Value;
    }

    public int Order
    {
        //It will be executed after AuthorizationApplicationModelProvider, which has order -990
        get { return 0; }
    }

    public void OnProvidersExecuted(ApplicationModelProviderContext context)
    {
        foreach (var controllerModel in context.Result.Controllers)
        {
            if (controllerModel.Filters.OfType<IAsyncAuthorizationFilter>().FirstOrDefault() == null)
            {
                //default policy only used when there is no authorize filter in the controller
                controllerModel.Filters.Add(new AuthorizeFilter(_authorizationOptions.DefaultPolicy));
            }
        }
    }

    public void OnProvidersExecuting(ApplicationModelProviderContext context)
    {            
        //empty    
    }
}

//Register in Startup.ConfigureServices
services.TryAddEnumerable(
    ServiceDescriptor.Transient<IApplicationModelProvider, OverridableDefaultAuthorizationApplicationModelProvider>());

With this in place, the default policy will be used on these 2 controllers:

public class FooController : Controller

[Authorize]
public class BarController : Controller

And the specific Users policy will be used here:

[Authorize(Policy = "Users")]
public class UsersController : Controller

Notice that you still need to add the admin role to every policy, but at least all your policies will be declared in a single startup method. You could probably create your own methods for building policies that will always add the admin role.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...