3

I am trying to make authorize accept roles either as enum or smart enum so that I don't have to debug magic strings and their typos

but I keep hitting a dead end with these two errors:

  • Attribute constructor parameter 'roles' has type 'Role[]', which is not a valid attribute parameter type

  • An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type

here is my code:

AuthorizeRoles.cs

public class AuthorizeRoles : AuthorizeAttribute
{
    public AuthorizeRoles(params Role[] roles)
    {
        string allowed = string.Join(", ", roles.ToList().Select(x => x.Name));
        Roles = allowed;
    }
}

Role.cs

public class Role
{
    public readonly string Name;

    public enum MyEnum  // added
    {
        Admin,
        Manager
    }

    public static readonly Role Admin = new Role("Admin");
    public static readonly Role Manager = new Role("Manager");

    public Role(string name)
    {
        Name = name;
    }

    public override string ToString()
    {
        return Name;
    }

and inside my controller I did this

    [AuthorizeRoles(Role.Admin, Role.Manager)]
    [AuthorizeRoles(Role.MyEnum.Admin)] // added 
    public IActionResult Index()
    {
        return Content("hello world");
    }

I have looked at these answers but it doesn't work

Community
  • 1
  • 1
Hoshani
  • 746
  • 1
  • 10
  • 27

4 Answers4

5

Because of the CLR constraints (how attributes stored in the metadata), atribute paramters can be only primitive types or arrays of those (and Types). You can't pass a Role (a custom object) to an attribute.

Enums are valid, but the compiler cannot convert your enum (Role.MyEnum) to Role, which is the type that the constructor of AuthorizeRoles requires. So this is a compiler error.

As you can guess, the solution is to create a constructor that take array of Role.MyEnum, as the following:

public class AuthorizeRoles : Attribute
{
    public string Roles { get; private set; }

    public AuthorizeRoles(params Role.MyEnum[] roles)
    {
        string allowed = string.Join(", ", roles);
        Roles = allowed;
    }
}

public class Role
{
    public readonly string Name;

    public enum MyEnum
    {
        Admin,
        Manager
    }

    public Role(string name)
    {
        Name = name;
    }

    public override string ToString()
    {
        return Name;
    }
}

// ...

[AuthorizeRoles(Role.MyEnum.Admin)]
public IActionResult Index()
{
    // ...
}
Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
2

It admittedly sucks, but the closest you can really get to this here is doing something like:

public static class Roles
{
    public const string Admin = "Admin";
    public const string Manager = "Manager";
}

And then:

[Authorize(Roles = Roles.Admin + "," + Roles.Manager)]

Between the combo of constant strings and in place string concatenation, it's all still a "constant expression". What you cannot do is basically anything that requires a method to be run such as string.Join. That's the breaks of the game when using attributes.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • You're likely still using your "smart enum", which won't work. It requires newing up objects, which are not constants. I provided the the example static class for a reason. That's the only way that will work. – Chris Pratt Apr 17 '19 at 12:53
  • Because you can't use `ToString()`. The *only* way it will work is exactly as I posted in my answer. You keep trying other stuff and then telling it doesn't work. Of course it doesn't. This is your only option if you don't want to hardcode the string values on the attribute itself. Period. – Chris Pratt Apr 17 '19 at 12:59
2

In constructor AuthorizeRoles class you use array of Role class, but in attribute [AuthorizeRoles(Role.MyEnum.Admin)] you use parameter of type MyEnum. if you want use enum, you must create AuthorizeRoles class constructor with parameter of MyEnum type.

0

Use constants for policy names and use authorization policies

// startup.cs

services.AddAuthorization(options =>
        {
            options.AddPolicy(PolicyConstants.Admin, policy =>
            {
                // Allowed to access the resource if role admin or manager
                policy.RequireClaim(JwtClaimTypes.Role, new[] { PolicyConstants.Admin, PolicyConstants.Manager });

               // Or use LINQ here
                policy.RequireAssertion(c =>
                {

                    // c.User.Claims
                });
}

In the controller use policy name

[Authorize(PolicyConstants.Admin)]
public class TestController
{
    // here also you can use specific policy and for controller, you can use other policy. It will match Action Level policy first and then match controller policy.
    public IActionResult Index()
    {
    }
}
Rudresha Parameshappa
  • 3,826
  • 3
  • 25
  • 39