Thoughts On User / Page Request Security Model (Part II)
Yesterday, I started looking for a better way to handle a security model in which access permissions are based on both the permissions granted to the user and context in which the access request was made (ie. the page request settings). At first, I tried to make the permissions check a responsibility of the User; but, after some analysis and feedback, it became clear that it was neither practical nor appropriate to leave these decisions up to the User. As such, I want to explore moving the responsibility of security into a security manager (or an offshoot of it).
Before we do that, let's just recap some of our security concerns:
Access checks can and will be made for the current request and for future requests (such as with layout rendering).
Access rights depend on an event, either current or future.
Access rights depend on the target of the event.
Access rights depend on the user permissions.
Ultimately, what this boils down to is that each access check depends on some combination of the following four elements:
- Request Settings
- User
- Event
- Target
Given this information, we could easily imagine passing all four off to a security service for each access check:
SecurityService.CanAccess( Settings, User, Event [, Target ] )
Now, call me crazy, but because the User will be the same for every single access check, it seems very sloppy that we have to keep passing it in. Likewise, the Settings will be the same for every check made in a single page call. As such, what I would like to do is create a request-based security service that composes the given user and the Settings and allows for a smaller API:
<!--- Get request-based security service. --->
<cfset ReqeustSecurity = SecurityService.GetRequestSecurity(
Settings = REQUEST.Settings,
User = SESSION.User
) />
<!--- Check permissions. --->
<cfif ReqeustSecurity.CanAccess( Event [, Target ] ) />
Even if the RequestSercurity object is nothing more than a "Man In The Middle" anti-pattern that calls SecurityService.CanAccess() on the singleton and passes all four elements in, I still think it's a slightly more elegant approach.
Unfortunately, I got to the office a bit late today, so that's all the time for exploration that I have. But, there's still so much more to be tried. For example, what is the Target? Is it an ID? Is it a query row? Is it an object? And what about caching? Can we cache access rights? If so, where? And does that apply to all events? Lots to be explored!
Want to use code from this post? Check out the license.
Reader Comments
Target kind of depends on what your application's security model is. Lets say you're talking about a standard e-commerce application, shopping cart, etc. You could make the choice to make target be one (or more) of many choices:
- Each item, if you want to lock some items so that they are editable only by specific people.
- Categories of items, say if you sell Video Games, and DVDs, and you don't want the Video Game salespeople editing DVD entries
- Arbitrary groupings of items, if there are some "premium items" cross-cutting categories, and you want to lock some people out of editing them
- Static Content Pages, if you want to allow, and disallow some people from editing some static content pages
- Tax rules
- Shipping rules
I might suggest creating a "SecurityTarget" or "Lock" object, and give each thing that you want to secure one or more "Lock" properties, that allow valid user "Keys" to unlock them. You could even define a "Lockable" interface, which implements a method "canUnlock(requestSecurity,accessType)", which loops through the objects existing locks, and determines if any of the keys in requestSecurity can unlock access to it.
Now, for some apps, that may be overkill. Maybe you just want to define one "Lock" for your whole application, and grant different roles/users "Read Only", "Write", or "Create" access to your whole application, in which case "Target" becomes irrelevant.
@Adam,
The scenario that I keep going over in my mind is something that I think is pretty common: only allowing people to edit things that they have edited. In this kind of scenario, I picture looping over a query, something like:
<cfloop query="qPublication">
. . . . #qPublication.name#
. . . . <cfif ...CanAccess( "publications.edit", qPublication.id )>
. . . . . . . . edit
. . . . </cfif>
</cfloop>
Because my publications list is coming back as a query, I really should pass in the pub ID as the "Target". Yes, I could pass back the "AuthorID" or something and see if that ID matches the given User's ID. However, at that point, the "target" is not *really* a target any more... its more like meta-data.
I could also maybe pass back a Publication object instance and the security check could check the Publication.GetAuthorID() and compare that. But, in that case, I would have to create a publication instance for every record, which we all know is not feasible (from a performance standpoint in ColdFusion).
So, where does that leave us? Perhaps "target" is not the right idea? Perhaps "MetaData" is really the right idea. In that case, I *could* simply pass through the AuthorID and the security internals would simply know to expect that as the metaData being passed.
Looking over the comments from this and the last post, it looks like security and work flow management is being treated as one in the same.
I usually split these in to two distinct steps:
1st. can a user interact with the system, yes or no. security.
2nd. can the user perform the requested action, yes or no. work flow.
I like this breakdown because when defining a new system, limiting access to the system as a whole is almost always required, but in the case of trusted users work flow could be a matter of training.
From an object perspective:
systemObject.hasAccess(userObject)
businessObject.canExecute(userObject)
The systemObject knows what users can interact with it.
The businessObject knows what criteria must be met by a user for the business process to function
This is an oversimplified example, you can get in to validation patterns in a separate control object that get a structure criteria rules from the business object.
controlObject.canExecutre(userObject,businessObject)
Thanks for blogging on this, Ben -- very interesting stuff and timely, as well. We're in the midst of revamping exactly this kind of stuff in our app framework and we're wrestling with some of these same kinds of questions...
Good stuff.
@Steve,
I tend to think of those as two separate activities, but I hadn't really considered that the second would be considered "Workflow". I tend to call them "Authentication" (where the user's identity is verified), and "Authorization" (where a user's credentials are validated against various rules) Thats for pointing out a different name to call that activity by.
@Ben,
As Steve says, you may want to move your permissions checking into an object specific to the entity you're talking about. I usually stuff these methods into the Service, so if I want to find out if a user can edit the current product as I'm looping over a product query, I do this:
<cfset queryAddColumn( productQuery, "canEdit", arrayNew(1) ) />
<cfloop query="productQuery">
<cfset querySetCell( productQuery, "canEdit", productService.canUserEdit( productQuery, productQuery.currentRow, request.user.getPermissions() ), productQuery.currentRow) />
</cfloop>
As @Steve suggests though, you could certainly centralize all of the Authorization/Workflow objects into a single Control/authorization/workflow object
@Steve,
While I think it might seem, at one level, to think about the two different types of activities in two different lights - access vs. work flow, I am not sure that they are truly different. When you think about interaction with the system, each page request should map to a given action which the user either has or does not have access to execute.
So, where as one might think about a user needing "access" to the "Contacts" section but *not* to the "login" section (as all users need access to the login section)... you can easily think about that as a given user having access permissions to the Contacts section and a given user having access to the "Login" section without thinking about logged-in/logged-out nature.
Rather than thinking section X needs an authorized user and section Y does not need authorization, you can simply think of all actions as something a given user either can or cannot execute.
Even when we think about ID-specific items, the same holds. Imagine a news site where all users can view the most current news items, but only logged-in users can view the archives. In such a scenario, we don't have to think in terms of logged-in / logged-out; we can simply think about users having access to or not having access to a given page action / ID combination.
@Steve,
Also, how do you handle events that don't relate to any business objects? For example, what about a page that simply has static text on it, but still requires authorization? Do you create a business object for each event in the system?
I'm not opposed to that, just looking for ideas.
@Ben,
I may have muddied the waters with my quick post.
I've been pretty much focusing on bussiness analysis over the last year, so that's where these thoughts are coming from. Part of my job is to prevent developers from over engineering the solution, so that's why I recommended the split.
Perhaps it's better to put it is:
1st. Do I need to know who you are? - validation
2nd. Do I need to check who is allowed through a door? - security
3rd. Do I need to restrict what you do once inside? - workflow
So for your question about a static page, yes I handle it through a business object. The object would be pretty simple.
using the onRequest event in the application.cfc
new businessObject("path to page").canExecute(userObject)
The "path to page" acts a a unique id to get whatever process criteria is associated with the request.
@Ben
Looking back, I should have asked: what is your definition of a business object?
I think we are using it the same way, but it's always good to check.
I use the term to describe any object that exists to automates one or more business rules, and is subject to change if the business rules change.
@Steve,
I think we are using business objects in the same way; I am not sure I would be able to articulate my definition as well as you have, but that sounds about right.
I definitely like the idea of using business objects to handle all security, even for static pages - it just seemed like a lot of overhead. But, perhaps the benefit of the encapsulated business rules are worth it.
Although, thinking about how I might want to do it, it's not all that much different. Let's stay that our static page corresponded to the action "faq". I would definitely have a business object that could handle the security for the event "faq." I guess, my point of confusion (in my own mind) is, is that a single object? Or would that object perhaps handle more events?
"faq," is a bad example; but, take something like "news" where we might have "news" and "news.view" and "news.edit", I could easily see a single business object handling all news-related security.
I think what could be easy enough to do would be to create a bunch of security-related business objects and then let them register for different events. In that way, you could have a "news" security objects register for the "news", "news.view", "news.edit", and "news.delete" events, for example. This way, the business objects for security could be as fine-tuned or as broad as would make sense for the application.
@Ben,
I think your on the right track, it really does depend how fine-tuned or broad the rules are for a specific process.
My last approach was to try and balance the two.
My desired result was for every requested .cfm to have an associated object that acts as a facade to the combination of any underlying businessObjects required to manage the request.
To prevent too many of these facade objects from being created I used a proxy object to start, that get's replaced with a specific object as required.
So at application start I create:
APPLICATION.requestFacadeProxy = getInstance("requestFacade");
This object contains the basic rules common to all requests
At request start I set:
REQUEST.requestFacade = APPLICATION.requestFacadeProxy().getFacade(path)
The function either returns a pointer to the proxy, or a pointer to a new instance of the requestFacade composed of the required business objects.
*Note: The "getInstance" function I use is a base class function inherited by any of my components that need to access other components, it basically calls my core factory object and creates a new object, or returns the pointer to an existing one (singletons)
@Steve,
I think the fine-tuned vs. broad aspect can be encapsulated within the security service based on how many individual security objects you have and which events they register to listen for. For example, in a simple app, you could easily have one security object register for all explicit events, or even have some sort of "*" wild card registration. Then, for more complicated apps, you can have a variety of security objects.
But, I think this can all be done behind the scenes while keeping the calling code the same:
....CanAccess( "news.edit", id )
This way, the security of an app can get more complicated security without changing the rendering code while keeping it fairly straight forward at the same time.
I think the number of facades will depend on the number of specific rules that need to be checked, but I think they can get cached for performance reasons.
Still bouncing around in my head :)