سؤال

I have an ASP.NET-MVC web site with a SQL Server back-end. I have a number of controller actions that require me to do an entitlement check.

Right now, I do this:

    public ActionResult SomeEntitledPage()
    {
        if (_myModel.IsMySiteAdminRole)
        {
            return View(new MyViewModel());
        }
        else
        {
            return View("NotEntitled", new NotEntitledViewModel(){Page = "[PageName]", SupportDG = "support@support.com"});
        }
    }

this works fine but it feels like I am duplicating this logic in a number of places.

What is the best way (attribute, etc) to have a number of entitlement controller action "secure" based on the below?

(Secure being that it checks the IsMySiteAdminRole and returns the "not Entitled" view if not entitled.

I also want to make sure I don't have a performance penalty on every page?

هل كانت مفيدة؟

المحلول

I prefer to use action filters for Entitlement / Privilege logic. The beauty of these filters is they can run AFTER the action method populates your Model.

For Example:

public class AdminOnlyFilterAttribute : ActionFilterAttribute
{
      public override void OnActionExecuted(ActionExecutedContext filterContext)
      {
        if (!filterContext.Controller.ViewData.Model.IsMySiteAdminRole)
            {
                filterContext.Result = new ViewResult
                {
                    ViewName = "NotEntitled",
                    Model = new NotEntitledViewModel(){Page = "[PageName]", SupportDG = "support@support.com"}
                };
            }
        base.OnActionExecuted(filterContext);
    }   
}

Action Filters allow you to selectively override your Controller's OnActionExecuted method.

This attribute can be applied to a specific action or an entire controller. The result will depend on your model values and will change your View only.

نصائح أخرى

The best way to achieve this would be to use an attribute and decorate the actions with it.

Take a look at System.Web.Mvc.AuthorizeAttribute

You can inherit it and perform your own custom logic. Here is a sample from a project I did a while ago:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
    public bool AdminRequired;
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        if (UserSession.IsLoggedIn)
            return (!AdminRequired || (AdminRequired && UserSession.IsAdmin));
        else
            return false;
    }

    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        filterContext.Result = new RedirectToRouteResult(
                                   new RouteValueDictionary 
                                   {
                                       { "action", "LogIn" },
                                       { "controller", "Account" },
                                       { "returnUrl", filterContext.HttpContext.Request.RawUrl}
                                   });
    }
}

With this attribute you can have two levels of authorization: regular user and admin user. Example:

[Authorize(AdminRequired = true)]
public ActionResult AdminOnlyAction()
{
    // perform authorized admin tasks
}

[Authorize]
public ActionResult RegularUserAction()
{
    // perform authorized regular user action
}

I agree with everyone else here that you can accomplish this with attributes but I would just like to point out another alternative. You could write a base controller from which all your controllers which require entitlement check inherit. In that controller you could write your logic to check for entitlement:

public class BaseController : Controller
{
    private readonly bool _isEntitled;

    protected override void Initialize(RequestContext requestContext)
    {
        base.Initialize(requestContext);
        // Your logic for entitlement check goes here.
        // Set _isEntitled to true if user is entitled to view page.
    }

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (!_isEntitled) {
           // Redirect user which is not entitled.
           filterContext.Result = new ViewResult
                                  {
                                     ViewName = "NotEntitled",
                                     Model = new NotEntitledViewModel(){Page = "[PageName]", SupportDG = "support@support.com"}
                                  };
        }
    }
}

Now all you have to do is inherit this controller in all your controllers which need to have entitlement check:

  public MyController : BaseController
  {
      // Action in here...
  }

I'm not arguing that this is a better option I'm just pointing out an alternative way of doing what you need. Additionally you could also implement some sort of caching in case you want to ensure that the entitlement check does not happen on every page request but only once when user has logged in...

TL;DR: Look into expanding your user profile and filling it with role information when the user is authenticated.


To me, it sounds like the problem stems from the design of the security layer in that the user that you are checking is only available in the wrong stage of execution which is causing this complication. It looks like what you're trying to do is inspect the user making the request to see if he meets a specific role requirement and show them an unauthorized page if they do not meet the requirement (pretty textbooks stuff), but for some reason, that user is in the view model and that view model is not built until the controller is fully instantiated which means you can't check it is initialized in the pipeline. The sloppy solution would be to override the initialize method on the controller, call the base method, but then do some work after the base initialize is complete. Then your data should be ready to go. But that's sloppy.

Generally speaking, your session user should be available in the session in the form of a UserProfile or extended identity. By doing so, you can fill that user's roles and what not and then check it at any stage of the controller/action execution pipeline; then you can just use a custom attribute to check User.IsInRole("Whatever"). You wouldn't need the action to return a "Not Entitled" view because you can set that in the response in the authorize attribute HandlUnauthorizedRequest override.

I do agree on the solutions suggested by others!!

What you mentioned is that you need to duplicate code, wherever you need to check the models IsInAdminRole property. So I created a common method where you pass the Model and the viewname/path. In the IsEntitled method just check the isadminrole property and take necessary action.

Note: This is just a simple and sample solution. You can check if this helps or gives some pointers.

Do something as follows.

Sample Models

public class NoRights
{
    public string Message { get; set; }
}

public class MyModel
{
    public bool IsAdminRole { get; set; }
}

Here is the home controller

public class HomeController : Controller
{

    public ActionResult Index()
    {
        var mod = new MyModel() { IsAdminRole = true };
        return IsEntitled(mod, "IndeX");
        //return View();
    }
}

And here is a sample static method. this can go in a helper class. Note: This will return a standard error view with the error message that is specified in the Message property of the NoRights Model.

    public static ViewResult IsEntitled(object model, string viewPath)
    {
        var prop = model.GetType().GetProperty("IsAdminRole");
        var hasRights = (bool)prop.GetValue(model, null);
        var viewResult = new ViewResult();
        if (hasRights)
        {
            viewResult.ViewData = new ViewDataDictionary(model);
            viewResult.ViewName = viewPath;
        }
        else
        {
            viewResult.ViewData = new ViewDataDictionary(
                new NoRights() { Message = "Your dont have rights" });
            viewResult.ViewName = "Error";

        }
        return viewResult;
    }

If I pass on true in the IsAdminRole property here is the output.

To Home Index

If I pass on false in the IsAdminRole property I get following output.

Access denied

Hope this helps.

We had a similar case wherein the requirement was the authorization logic to be set completely in one centralized location. For this you can try something like this.

First you can have an Application Level Controller which can be

public class ApplicationController : Controller
{
    protected override void OnActionExecuted(ActionExecutedContext ctx)
    {
        base.OnActionExecuted(ctx);

        if (!_myModel.IsMySiteAdminRole)
        {
            ctx.Result = View("NotEntitled", new NotEntitledViewModel(){Page = "[PageName]", SupportDG = "support@support.com"});
        }
    }
}

Now this controller can be inherited in other controllers. Your entire entitlement logic is now centralized in one controller.

public class EntitlementController: ApplicationController
{
    return View(new MyViewModel());
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top