views:

2386

answers:

3

I have a question about custom authorization in MVC.

I have a site that I want to limit access to certain pages, depending on their group membership. Now I have seen tons of examples on how to do this if there is a single admin group and a single user group, for example, but not any examples for a third level.

For example, only users of a company can view orders for their own company (and each company has its own admins, etc). These companies are stored in a DB. So I have seen ways to do custom authorization, overriding the AuthorizeCore method on the AuthorizeAttribute, but I don't know how to access to the parameters passed into the controller to see if the user has access to the order (order id, for example).

Is this even the best place to do the check, or should this just be handled directly from the method of the controller?

Please help or let me know if I need to be more clear.

Thanks in advance!

A: 

My answer isn't great, because it kills unit testing, but I'm pulling values from System.Web.HttpContext.Current.Session. The singleton is available throughout the project. By saving the current user in session, you can get to it from anywhere, including utility classes like AuthorizeAttribute.

I'd love to see a unit-testable solution, though.

Jarrett Meyer
+2  A: 

If the authorization is really that dynamic, I would handle it in the controller. I have one action where I do this - you can return a HttpUnauthorizedResult to redirect to the login page or you can show a custom error in your view.

I don't the default redirect to the login page when somebody is already logged in, but not in the correct role. That's very confusing for the user.

chris166
You could do the same in an attribute, which can be easily applied via decoration on other methods that need it.
tvanfosson
Yes, but you have to query the model anyway in the controller (usually). If you do it in the attribute, you have to query the model twice unless you have a clever caching mechanism
chris166
+7  A: 

The AuthorizationContext (parameter to OnAuthorize) provides access to the Controller, RouteData, HttpContext, etc. You should be able to use these in a custom authorization filter to do what you want. Below is a sample of code from a RoleOrOwnerAttribute derived from AuthorizeAttribute.

public override void OnAuthorization( AuthorizationContext filterContext )
{
    if (filterContext == null)
    {
        throw new ArgumentNullException( "filterContext" );
    }

    if (AuthorizeCore( filterContext.HttpContext )) // checks roles/users
    {
        SetCachePolicy( filterContext );
    }
    else if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
    {
        // auth failed, redirect to login page
        filterContext.Result = new HttpUnauthorizedResult();
    }
    // custom check for global role or ownership
    else if (filterContext.HttpContext.User.IsInRole( "SuperUser" ) || IsOwner( filterContext ))
    {
        SetCachePolicy( filterContext );
    }
    else
    {
        ViewDataDictionary viewData = new ViewDataDictionary();
        viewData.Add( "Message", "You do not have sufficient privileges for this operation." );
        filterContext.Result = new ViewResult { MasterName = this.MasterName, ViewName = this.ViewName, ViewData = viewData };
    }

}

// helper method to determine ownership, uses factory to get data context,
// then check the specified route parameter (property on the attribute)
// corresponds to the id of the current user in the database.
private bool IsOwner( AuthorizationContext filterContext )
{
    using (IAuditableDataContextWrapper dc = this.ContextFactory.GetDataContextWrapper())
    {
        int id = -1;
        if (filterContext.RouteData.Values.ContainsKey( this.RouteParameter ))
        {
            id = Convert.ToInt32( filterContext.RouteData.Values[this.RouteParameter] );
        }

        string userName = filterContext.HttpContext.User.Identity.Name;

        return dc.Table<Participant>().Where( p => p.UserName == userName && p.ParticipantID == id ).Any();
    }
}


protected void SetCachePolicy( AuthorizationContext filterContext )
{
    // ** IMPORTANT **
    // Since we're performing authorization at the action level, the authorization code runs
    // after the output caching module. In the worst case this could allow an authorized user
    // to cause the page to be cached, then an unauthorized user would later be served the
    // cached page. We work around this by telling proxies not to cache the sensitive page,
    // then we hook our custom authorization code into the caching mechanism so that we have
    // the final say on whether a page should be served from the cache.
    HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
    cachePolicy.SetProxyMaxAge( new TimeSpan( 0 ) );
    cachePolicy.AddValidationCallback( CacheValidateHandler, null /* data */);
}
tvanfosson
Thanks, this is what I was looking for. What is the SetCachePolicy method?
doobist
I refactored the code from AuthorizeFilter that sets the caching policy for the page so that unauthorized users don't get the page served from cache. I'll add that snippet.
tvanfosson
This is great, thanks tvanfosson!
Michael G