How to get custom annotation attributes for a cont

2020-02-08 02:56发布

I am working with a permission based authorization system for my app in ASP.NET MVC. For this I have created a custom authorization attribute

public class MyAuthorizationAttribute : AuthorizeAttribute
{
    string Roles {get; set;}
    string Permission {get; set;}
}

so that I can authorize a user by both role or a specific permission key with annotation for actions like

public class UserController : Controller
{
    [MyAuthorization(Roles="ADMIN", Permissions="USER_ADD")]
    public ActionResult Add()

    [MyAuthorization(Roles="ADMIN", Permissions="USER_EDIT")]
    public ActionResult Edit()

    [MyAuthorization(Roles="ADMIN", Permissions="USER_DELETE")]
    public ActionResult Delete()
}

then I override AuthorizeCore() method in MyAuthorizationAttribute class with similar logic(pseudo code)

protected override bool AuthorizeCore(HttpContextBase httpContext)
{
    if(user not authenticated)
        return false;

    if(user has any role of Roles)
        return true;

    if(user has any permission of Permissions)
        return true;

    return false;
}

Up to this is working fine.

Now I need some sort of extension methods so that I can dynamically generate action url in view pages that will return action url based on MyAuthorization attribute authorization logic for the given action. Like

@Url.MyAuthorizedAction("Add", "User")

will return url to "User/Add" if user has admin role or has permission of "USER_ADD" (as defined in attributes for the action) or return empty string otherwise.

But after searching in internet for few days I could not figure it out. :(

So far I have found only this "Security aware" action link? which works by executing all action filters for the action until it fails.

It's nice, but I think it would be an overhead to execute all the action filters for each time I call the MyAuthorizedAction() method. Besides It also didn't work with my version (MVC 4 and .NET 4.5)

What all I need is to check authenticated user's role, permissions (will be stored in session) against authorized role and permission for the given action. Like something as following (pseudo code)

MyAuthorizedAction(string actionName, string controllerName)
{
    ActionObject action = SomeUnknownClass.getAction(actionName, controllerName)
    MyAuthorizationAttribute attr = action.returnsAnnationAttributes()

    if(user roles contains any in attr.Roles 
       or 
       user permissions contains any attr.Permissions)
    {
        return url to action
    }
    return empty string
}

I am searching the solution of getting action attributes value for quite a long time, could not find enough good resources at all. Am I missing out right keywords? :/

If anyone can provide me the solution that would be truly a great help. Thanks in advance for the solutions

3条回答
劳资没心,怎么记你
2楼-- · 2020-02-08 03:50

While I agree that generating urls based on permissions is probably not best practices, if you want to continue anyway you can find the actions and their attributes using these:

Retrieve 'Action' methods: This retrieves a collection of method infos because it is possible to have multiple Controller classes with the same name and multiple methods of the same name in particular with the use of areas. If you have to worry about this I'll leave the disambiguation up to you.

public static IEnumerable<MethodInfo> GetActions(string controller, string action)
{
    return Assembly.GetExecutingAssembly().GetTypes()
           .Where(t =>(t.Name == controller && typeof(Controller).IsAssignableFrom(t)))
           .SelectMany(
                type =>
                type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
                    .Where(a => a.Name == action && a.ReturnType == typeof(ActionResult))
             );

}

Retrieve Permissions from MyAuthorizationAttributes:

public static MyAuthorizations GetMyAuthorizations(IEnumerable<MethodInfo> actions)
{
    var myAuthorization = new MyAuthorizations();
    foreach (var methodInfo in actions)
    {
        var authorizationAttributes =  methodInfo
                .GetCustomAttributes(typeof (MyAuthorizationAttribute), false)
                .Cast<MyAuthorizationAttribute>();
        foreach (var myAuthorizationAttribute in authorizationAttributes)
        {
            myAuthorization.Roles.Add(MyAuthorizationAttribute.Role);
            myAuthorization.Permissions.Add(MyAuthorizationAttribute.Permission);
        }
    }
    return myAuthorization;
}
public class MyAuthorizations
{
    public MyAuthorizations()
    {
        Roles = new List<string>();
        Permissions = new List<string>();
    }
    public List<string> Roles { get; set; }
    public List<string> Permissions { get; set; }
}

Finally the AuthorizedAction extension: warning:If you do have more than one match for a given controller/action pair this will give the 'authorized' url if the user is authorized for any of them...

public static string AuthorizedAction(this UrlHelper url, string controller, string action)
{
    var actions = GetActions(controller, action);
    var authorized = GetMyAuthorizations(actions);
    if(user.Roles.Any(userrole => authorized.Roles.Any(role => role == userrole)) ||
       user.Permissions.Any(userPermission => authorized.Permissions.Any(permission => permission == userPermission)))
    {
        return url.Action(controller,action)
    }
    return string.empty;
}

A Note on Generating Urls Based on Permissions:
I state this is probably not best practice because of many little things. Each may have their own level of relevance depending on your situation.

  • Gives the impression of trying to achieve security through obscurity. If I don't show them the url, they won't know it is there.
  • If you are already checking permissions in other ways to control the rendering of the page (as it appears you are doing based on your comments elsewhere) this explicitly not writing the url out is pointless. Better to not even call the Url.Action method.
  • If you are not already controlling the rendering of the page according to the user's permissions, simply returning empty string for urls will leave a lot of broken or seemingly broken content on your pages. Hey this button doesn't do anything when I press it!
  • It can make testing and debugging more complicated: Is the url not showing up because the permissions are not right, or is there another bug?
  • The behavior of the AuthorizedAction method seems inconsistent. Returns a url sometimes, and an empty string other times.

Controlling Page Rendering Via Action Authorization attributes: Modify the AuthorizedAction method to be a boolean, then use the result of that to control page rendering.

public static bool AuthorizedAction(this HtmlHelper helper, string controller, string action)
{
    var actions = GetActions(controller, action);
    var authorized = GetMyAuthorizations(actions);
    return user.Roles.Any(userrole => authorized.Roles.Any(role => role == userrole)) ||
       user.Permissions.Any(userPermission => authorized.Permissions.Any(permission => permission == userPermission))
}

Then use this in your razor pages.

@if(Html.AuthorizedAction("User","Add")){
   <div id='add-user-section'>
        If you see this, you have permission to add a user.
        <form id='add-user-form' submit='@Url.Action("User","Add")'>
             etc
        </form>
   </div>
}
else {
  <some other content/>

}
查看更多
做自己的国王
3楼-- · 2020-02-08 03:51

My only recommendation would be to write an extensions methods on IPrincipal instead which would look like

public static bool HasRolesAndPermissions(this IPrincipal instance,
    string roles,
    string permissions,)
{
  if(user not authenticated)
    return false;

  if(user has any role of Roles)
    return true;

  if(user has any permission of Permissions)
    return true;

return false;
}

Then your code in the views/partials is a little more readable in terms of what it's actually doing (not doing anything with html, but validating a user) then the code in the views/partials looks like

@if (User.HasRolesAndPermissions(roles, permissions)) 
{ 
   @Html.ActionLink(..);
}

Each MVC Page has the property WebViewPage.User for the current user.

The problem with your purposed solution (and the link to security aware link) is that the creation of the link, and the Authorize on the controllers could be different (and mixing responsibilities in this type of fashion in MY opinion is bad practice). By extending IPrincipal a new authorization would look like:

protected override bool AuthorizeCore(HttpContextBase httpContext)
{
  return user.HasRolesAndPermissions(roles, permissions)
}

Now both your Authorize Attribute and Views use the same roles/permissions data logic.

查看更多
狗以群分
4楼-- · 2020-02-08 03:55

I don't think you should check the action annotations each time you want to create an url with Url.Action(). If the action is secured with custom authorization filter it won't be executed for unprivileged user, so what's the point in hiding URL to that action? Instead you could implement extension method on HtmlHelper to check if the current user has the given premission, like:

public static bool HasPermission(this HtmlHelper helper, params Permission[] perms)
{
    if (current user session has any permission from perms collection)
    {
        return true;
    }
    else
    {
        return false;
    }
}

Then you could use the helper inside the views to hide buttons and links that are not accessible for the current user, like:

@if (Html.HasPermission(Permission.CreateItem))
{
    <a href="@Url.Action("Items", "Create")">Create item</a>
}

Of course this hiding of specific links is only for UI purposes - the real controll of access is done by the custom authorization attribute.

查看更多
登录 后发表回答