codette
codette

Reputation: 12583

Allow multiple roles to access controller action

Right now I decorate a method like this to allow "members" to access my controller action

[Authorize(Roles="members")]

How do I allow more than one role? For example the following does not work but it shows what I am trying to do (allow "members" and "admin" access):

[Authorize(Roles="members", "admin")] 

Upvotes: 333

Views: 270887

Answers (14)

Paul Manley
Paul Manley

Reputation: 71

If you have anything beyond a basic Role based security you will have custom authorization attributes. It’s harder to have multiple “OR”ing authorize filter attributes.

I wanted:

"somethingIDontHave.oops" | "SRE.View" | "SRE.List" | "SRE-BADBADBAD.List" | "SRE.Edit"

Where those are custom filters and I wanted them in their own Attributes.

At our company we have Resources and Actions that you can perform as well as Phylums that you can be. It’s a little more complicated than just Roles.

I wanted something like this:

[AuthorizeByResourceAction("somethingIDontHave", "oops")]
[AuthorizeByResourceAction("SRE", "View")]
[AuthorizeByResourceAction("SRE-BADBADBAD", "List")]
[AuthorizeByResourceAction("SRE", "Edit")]
public class DashboardModel : PageModel
{}

And have each AuthorizeByResourceAction OR each other.

My solution is to have OnAuthorizationAsync get all the attributes on the Target and then run over all of them until I get a ‘Yes’. You will get check amplification with this solution, so you should be caching your results. You also have to pass in the type.

How to specify which attributes can be OR’ed.

public interface IOrAbleAuthorization
{
    Task<bool> IsAuthorizedAsync(HttpContext httpContextAccessor);
}

For the actual class that does the Authorization Filtering

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class AuthorizeByResourceActionAttribute : Attribute, IAsyncAuthorizationFilter, IOrAbleAuthorization
{
    protected readonly string _resource;
    protected readonly string _action;
    protected readonly Type _controller;

    public AuthorizeByResourceActionAttribute(Type Controller, string resource, string action = null) 
    { 
        _resource = resource;
        _action = action;
        _controller = Controller;
    }
}

The code that does the actual custom authorization is part of the Interface's IsAuthorizedAsync instead of OnAuthorizationAsync. OnAuthorizationAsync is responsible for calling all the other IOrAbleAuthorization classes.

public async Task<bool> IsAuthorizedAsync(HttpContext httpContextAccessor)
{
    // Custom Auth Code here
    string ActorJWT = httpContextAccessor.GetJWTFromCookie(false);

    bool hasAuth = await PM.Instance.HasPermission(ActorJWT, _resource, _action);
    if ( !hasAuth)
    {
        L.Warn($"User does not have permission to {_resource}.{_action}");
        return false;
    }

    return true;
}

That is a simple true/false for IsAuthorized.

OnAuthorizationAsync will iterate over all of the attached attributes looking for any "True/Yes".

public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
    object[] attributes = _controller.GetCustomAttributes(false);

    IOrAbleAuthorization[] filteredAttributes = attributes
        .Where(attribute => attribute.GetType().GetInterfaces().Contains(typeof(IOrAbleAuthorization)))
        .Cast<IOrAbleAuthorization>()
        .ToArray();

    bool isAuthorized = false;
    foreach(IOrAbleAuthorization filteredAttribute in filteredAttributes)
    {
        isAuthorized = await filteredAttribute.IsAuthorizedAsync(context.HttpContext);
        if ( isAuthorized )
            break;
    }

    if ( !isAuthorized) // All of them were false
    {
        context.Result = new ForbidResult();
    }
}

I could not find a way to get the Target type in my attribute without passing it in directly. So the final call will look like this:

[AuthorizeByResourceAction(typeof(DashboardModel), "somethingIDontHave", "oops")]
[AuthorizeByResourceAction(typeof(DashboardModel), "SRE", "View")]
[AuthorizeByResourceAction(typeof(DashboardModel), "SRE", "List")]
[AuthorizeByResourceAction(typeof(DashboardModel), "SRE.History", "List")]
[AuthorizeByResourceAction(typeof(DashboardModel), "SRE", "Edit")]
public class DashboardModel : PageModel
{ }

Problems:

  1. Call amplification
  2. Passing in the Type from the Model/Controller

Call Amplification - You can see 5 calls for somethingIDontHave.oops

You can see the call amplification here with 5 calls for the Auth check for "somethingIDontHave", but zero for "SRE-BADBADBAD". This is because "somethingIDontHave" is first, and the identity doesn't have that Resource. But the identity does have the "SRE.View" ResourceAction so it doesn't need to proceed to the following Authorization filter checks.

I would put in caching here to alleviate the multiple same checks.

I also don't like the passing in of the type. That's error-prone when copy/pasta the AuthorizeByResourceAction attribute onto the next controller/page.

Upvotes: 0

nerkn
nerkn

Reputation: 1980

For "Or" you need comma between roles

[Authorize(Roles="members,admin")]

both members and admins can access

But for "and" you need

[Authorize(Roles="manager")]
[Authorize(Roles="sales")]

managers who are in sales can access.

Upvotes: 5

Rahim Lotfi
Rahim Lotfi

Reputation: 59

I mixed answers and proposed this method.

Firstly, We create an enum for role accesses.

public enum ERoleAccess
{
     [Description("Admin User")]
     Admin = 1,

     [Description("General User")]
     User = 2,

     [Description("Editor User")]
     Editor = 3,
}

Secondly, we need an attribute filter for customer MVC authorize.

public class RolesAttribute:AuthorizeAttribute
{
   public RolesAttribute(params ERoleAccess[] roles)
   {
      Roles = string.Join(",", roles);
   }
}

Finally, we can use "RolesAttribute" on the controllers or actions.


[Roles(ERoleAccess.Admin, ERoleAccess.Editor, ERoleAccess.User)]

In this approach, we use numbers of alternative string values. (1= Admin, 2=User,...)

It's good for decreasing token size and comparing performance.

Upvotes: 4

gawkface
gawkface

Reputation: 2322

[Authorize(Roles="admin")]
[Authorize(Roles="members")]

will work when the AND is needed (as asked by question) whereas this answer shows the OR version. See more at https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles?view=aspnetcore-6.0#adding-role-checks

Upvotes: 0

Sedat Y
Sedat Y

Reputation: 601

You can use Authorization Policy in Startup.cs

    services.AddAuthorization(options =>
    {
        options.AddPolicy("admin", policy => policy.RequireRole("SuperAdmin","Admin"));
        options.AddPolicy("teacher", policy => policy.RequireRole("SuperAdmin", "Admin", "Teacher"));
    });

And in Controller Files:

 [Authorize(Policy = "teacher")]
 [HttpGet("stats/{id}")]
 public async Task<IActionResult> getStudentStats(int id)
 { ... }

"teacher" policy accept 3 roles.

Upvotes: 13

Orsit Moel
Orsit Moel

Reputation: 1

Intent promptInstall = new Intent(android.content.Intent.ACTION_VIEW);
promptInstall.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
promptInstall.setDataAndType(Uri.parse("http://10.0.2.2:8081/MyAPPStore/apk/Teflouki.apk"), "application/vnd.android.package-archive" );

startActivity(promptInstall);

Upvotes: -11

Jim Schmehil
Jim Schmehil

Reputation: 7059

Another option is to use a single authorize filter as you posted but remove the inner quotations.

[Authorize(Roles="members,admin")]

Upvotes: 693

DirtyNative
DirtyNative

Reputation: 2854

Using AspNetCore 2.x, you have to go a little different way:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class AuthorizeRoleAttribute : AuthorizeAttribute
{
    public AuthorizeRoleAttribute(params YourEnum[] roles)
    {
        Policy = string.Join(",", roles.Select(r => r.GetDescription()));
    }
}

just use it like this:

[Authorize(YourEnum.Role1, YourEnum.Role2)]

Upvotes: 4

GER
GER

Reputation: 2042

If you find yourself applying those 2 roles often you can wrap them in their own Authorize. This is really an extension of the accepted answer.

using System.Web.Mvc;

public class AuthorizeAdminOrMember : AuthorizeAttribute
{
    public AuthorizeAdminOrMember()
    {
        Roles = "members, admin";
    }
}

And then apply your new authorize to the Action. I think this looks cleaner and reads easily.

public class MyController : Controller
{
    [AuthorizeAdminOrMember]
    public ActionResult MyAction()
    {
        return null;
    }
}

Upvotes: 3

Pablo Claus
Pablo Claus

Reputation: 5920

If you want use custom roles, you can do this:

CustomRoles class:

public static class CustomRoles
{
    public const string Administrator = "Administrador";
    public const string User = "Usuario";
}

Usage

[Authorize(Roles = CustomRoles.Administrator +","+ CustomRoles.User)]

If you have few roles, maybe you can combine them (for clarity) like this:

public static class CustomRoles
{
     public const string Administrator = "Administrador";
     public const string User = "Usuario";
     public const string AdministratorOrUser = Administrator + "," + User;  
}

Usage

[Authorize(Roles = CustomRoles.AdministratorOrUser)]

Upvotes: 164

kinzzy goel
kinzzy goel

Reputation: 29

Better code with adding a subclass AuthorizeRole.cs

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
    class AuthorizeRoleAttribute : AuthorizeAttribute
    {
        public AuthorizeRoleAttribute(params Rolenames[] roles)
        {
            this.Roles = string.Join(",", roles.Select(r => Enum.GetName(r.GetType(), r)));
        }
        protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
        {
            if (filterContext.HttpContext.Request.IsAuthenticated)
            {
                filterContext.Result = new RedirectToRouteResult(
                new RouteValueDictionary {
                  { "action", "Unauthorized" },
                  { "controller", "Home" },
                  { "area", "" }
                  }
              );
                //base.HandleUnauthorizedRequest(filterContext);
            }
            else
            {
                filterContext.Result = new RedirectToRouteResult(
                new RouteValueDictionary {
                  { "action", "Login" },
                  { "controller", "Account" },
                  { "area", "" },
                  { "returnUrl", HttpContext.Current.Request.Url }
                  }
              );
            }
        }
    }

How to use this

[AuthorizeRole(Rolenames.Admin,Rolenames.Member)]

public ActionResult Index()
{
return View();
}

Upvotes: 3

Ren&#234; R. Silva
Ren&#234; R. Silva

Reputation: 57

Another clear solution, you can use constants to keep convention and add multiple [Authorize] attributes. Check this out:

public static class RolesConvention
{
    public const string Administrator = "Administrator";
    public const string Guest = "Guest";
}

Then in the controller:

[Authorize(Roles = RolesConvention.Administrator )]
[Authorize(Roles = RolesConvention.Guest)]
[Produces("application/json")]
[Route("api/[controller]")]
public class MyController : Controller

Upvotes: 4

Bernardo Loureiro
Bernardo Loureiro

Reputation: 344

For MVC4, using a Enum (UserRoles) with my roles, I use a custom AuthorizeAttribute.

On my controlled action, I do:

[CustomAuthorize(UserRoles.Admin, UserRoles.User)]
public ActionResult ChangePassword()
{
    return View();
}

And I use a custom AuthorizeAttribute like that:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class CustomAuthorize : AuthorizeAttribute
{
    private string[] UserProfilesRequired { get; set; }

    public CustomAuthorize(params object[] userProfilesRequired)
    {
        if (userProfilesRequired.Any(p => p.GetType().BaseType != typeof(Enum)))
            throw new ArgumentException("userProfilesRequired");

        this.UserProfilesRequired = userProfilesRequired.Select(p => Enum.GetName(p.GetType(), p)).ToArray();
    }

    public override void OnAuthorization(AuthorizationContext context)
    {
        bool authorized = false;

        foreach (var role in this.UserProfilesRequired)
            if (HttpContext.Current.User.IsInRole(role))
            {
                authorized = true;
                break;
            }

        if (!authorized)
        {
            var url = new UrlHelper(context.RequestContext);
            var logonUrl = url.Action("Http", "Error", new { Id = 401, Area = "" });
            context.Result = new RedirectResult(logonUrl);

            return;
        }
    }
}

This is part of modifed FNHMVC by Fabricio Martínez Tamayo https://github.com/fabriciomrtnz/FNHMVC/

Upvotes: 19

Mihkel M&#252;&#252;r
Mihkel M&#252;&#252;r

Reputation: 2192

One possible simplification would be to subclass AuthorizeAttribute:

public class RolesAttribute : AuthorizeAttribute
{
    public RolesAttribute(params string[] roles)
    {
        Roles = String.Join(",", roles);
    }
}

Usage:

[Roles("members", "admin")]

Semantically it is the same as Jim Schmehil's answer.

Upvotes: 107

Related Questions