Ayende @ Rahien

It's a girl

Convention based security: A MonoRail Sample

I was asked how I would got about building a real world security with the concept of securing operations instead of data.

This is a quick & dirty implementation of the concept by marrying Rhino Security to MonoRail. This is so quick and dirty that I haven't even run it, so take this as a concept, not the real implementation, please.

The idea is that we can map each request to an operation, and use the convention of "id" as a special meaning to perform operation security that pertain to specific data.

Here is the code:

public class RhinoSecurityFilter : IFilter
{
    private readonly IAuthorizationService authorizationService;

    public RhinoSecurityFilter(IAuthorizationService authorizationService)
    {
        this.authorizationService = authorizationService;
    }

    public bool Perform(ExecuteWhen exec, IEngineContext context, IController controller,
                        IControllerContext controllerContext)
    {
        string operation = "/" + controllerContext.Name + "/" + controllerContext.Action;
        string id = context.Request["id"];
        object entity = null;
        if (string.IsNullOrEmpty(id) == false)
        {
            Type entityType = GetEntityType(controller);
            entity = TryGetEntity(entityType, id);
        }

        if(entity==null)
        {
            if (authorizationService.IsAllowed(context.CurrentUser, operation) == false)
            {
                DenyAccessToAction();
            }
        }
        else
        {
            if (authorizationService.IsAllowed(context.CurrentUser, entity, operation) == false)
            {
                DenyAccessToAction();
            }
        }
        return true;
    }
}

It just perform a security check using the /Controller/Action names, and it tries to get the entity from the "id" parameter if it can.

Then, we can write our base controller:

[Filter(ExecuteWhen.BeforeAction,typeof(RhinoSecurityFilter))]
public class AbstractController : SmartDispatcherController
{

}

Now you are left with configuring the security, but you already have a cross cutting security implementation.

As an example, hitting this url: /orders/list.castle?id=15

Will perform a security check that you have permission to list customer's 15 orders.

This is pretty extensive, probably overly so, however. A better alternative would be to define an attribute with the ability to override the default operation name, so you can group several action into the same operation.

You would still need a way to bypass that, however, since there are some thing where you would have to allow access and perform custom permissions, no matter how flexible Rhino Security is, or you may be required to do multiply checks to verify that, and this system doesn't allow for it.

Anyway, this is the overall idea, thoughts?

Comments

Francois Tanguay
01/23/2008 08:46 PM by
Francois Tanguay

Sounds good to me. I did the same thing in the past at the service layer instead of the mvc since I think security isn't a matter of presentation.

Operation would be deduced from the Service method being called.

What I found important is to have a hook/extension point (or really nice object constraint language) for security scenarios containing lots of business logic.

e.g.: An employee can view an account if he works for the same branch as the the account's branch, a parent branch or a branch connected through related branches and that branch connection isn't expired.

Bunter
01/23/2008 08:51 PM by
Bunter

Rather robust and dirty indeed. My first idea was "this will not work on many cases" but when I think of the apps I've done, concept itself would probably be sufficient in 90% of the cases. Simple and neat, as usual for things in your blog :)

Peter
01/23/2008 08:58 PM by
Peter

Ayende, awesome posts on this subject. There really isn't much discussion out there. I put together a similar system a while ago and have been thinking about the redesign. I commented about the original here (last comment): http://www.ayende.com/Blog/archive/2007/08/05/Alas-security-is-a-business-concern.aspx

I think of this as an Access Control model. Could you post the final data model for the system? I think of each permission row in the permissions table as ACL Entries; the EntitySecurityKey as the grouping mechanism to tie them together (like an aclid). Am I understanding correctly?

Also, this is an 'internal' security solution, as I read it. Meaning that you have to join against the permissions table when building up your entities, right? What do you think about a query like "Get all the users in groups on which I have 'Group.Edit' permission"? What about when the user store is external (LDAP, or other)?

Peter

Ayende Rahien
01/23/2008 09:10 PM by
Ayende Rahien

Francois,

Those types of constraints are usually expressed with entities groups and matching users groups in Rhino Security.

Marco
01/23/2008 10:07 PM by
Marco

Nice.. One addition.. add the "why" to the DenyAccess

DenyAccessToAction(authorizationService.GetAuthorizationInformation(user, operation);

On to the next step ;-) suppose i want to block reading a single property..Would you handle that in the view? the same for an update

How would you handle this when you have multiple different clients (web / windows form / webservice) and the same security layer should be active?

Marco
01/23/2008 10:09 PM by
Marco

@Peter.. it's already there see https://rhino-tools.svn.sourceforge.net/svnroot/rhino-tools/trunk/rhino-security

Marco
01/23/2008 10:12 PM by
Marco

oeps.. i see that you (partly) answered my question in the previous post...

Ayende Rahien
01/23/2008 11:27 PM by
Ayende Rahien

Peter,

I'll post an implementation discussion soon.

Your query assumes that Users are also persisted entity right?

If we can get them through NHibernate, it is simply:

DetachedCriteria allUsers = DetachCriteria.For();

authorizationService.AddPermissionsToQuery(CurrentUser, "/Group/Edit", allUsers);

FindAll(allUsers);

If they arrive from external store, you would need to loop on them, I am afraid.

I suppose I can add a way to get the ids of all the users that you can access, but I would like to hear more concrete reasoning for that.

Ayende Rahien
01/23/2008 11:28 PM by
Ayende Rahien

Marco,

Multiply clients will hit the same data source, possibly with distributed cache involved to make it faster.

Peter
01/24/2008 12:39 AM by
Peter

Yes, I assume that the users are also a persisted entity, as is the group, like your original model. I don't know that there is a great way to integrate with an external store although that seemed to come up frequently in my experience. You could query the authorization tables, then query the remote user store with the results but you can't use the 'in group' clause if the groups are also external. In that case, you probably have to replicate at least part of the user/group data model locally.

Aside from the functional permissions (role permissions) and operations on specific entities, the use cases that I saw most frequently were:

  1. Listing entities that the requesting user (or group containing user) has x permissions on, as in your example.

  2. Permission Management: In a multi-user system where you can create or manage entities and 'share' them, you request access to all the groups and users that you can see, in order to give them those permissions. This permission set is limited by your own access rights to the entity.

  3. Views of entities (paged) with the permissions for the requesting or specified user. So, return the list of entities where I have Read | Write | Delete permission. This is to see a quick list of the permissions along with the entity (ID, CanRead, CanEdit, CanDelete).

By the way, how many people are you? No single human can post this much AND produce all this code (and hold a job)!

Zdeslav Vojkovic
01/24/2008 08:14 AM by
Zdeslav Vojkovic

I use this solution (simplified) to filter entities based on permissions:

In the application layer I have service methods like:

[RequiresPermission("Group/Edit")]

void SomeMethod( ... parameters...) {}

Services have SecurityInterceptor implemented through Windsor/DP2 which check for RequiresPermission attribute and do something like:

CallContext[Context.Security] = new SecurityContext(CurrentUser, attr.Permission);

than in the Repository implementation, I add the permissions to query:

public IList Find(IQuerySpecification query)

{

AddPermissions(query, CallContext[Context.Security]);

return GetCriteria(query).List<T>();

}

Comments have been closed on this topic.