Things learned from creating the RoomBooking System (part 2)

The Room Booking system is now available as v 1.01; the main addition being permissions, roles and authentication. As before, I thought it might be useful to highlight a few of the thought processes behind this update, and look at some of the problems come across when beginning to think about an application which is due for distribution in all sorts of environments.

Authentication varies massively depending on the setup – here are a few common scenarios:

Using authentication external to the application, i.e at a IIS/Apache level:

  1. Block the whole website: e.g, have everything behind apache’s htpasswd authentication, or restrict to a specific IP Range, or VPN connection etc
  2. Block only “admin” level functions: e.g specific authentication triggered for all /admin/ requests: again, trigger via .htpasswd or somesuch.
  3. Using Active Directory/LDAP to authenticate a user, and use groups specified outside the main application to load a user into a specific role
  4. Other SSO systems (shibboleth etc) integrated into IIS/Apache

Authentication within the application:

  1. Using traditional cflogin, and built in CF login functions
  2. Testing for session based variables specifically, i.e session.user.loggedinAt

Obviously there are a million other variations on this, but it does highlight that whatever your doing should at least consider these other methods.

What are you trying to protect?

Obviously, this is the first question you have to ask yourself. What is it that requires some level of authentication? Nine times out of ten, it’s the privilege of changing or viewing data, that much is pretty self explanatory. But it’s worth considering all data you’re storing, and how that might be used. For instance, blocking the web interface of an application is one thing, but what about potential API usage? RSS Feeds? Other connection types which don’t require a browser? For the Room Booking system (RBS after this point, I can’t be bothered with the typing), it’s relatively straightforward, at least at the moment. But in the future I’m planning to add RSS/iCS/JSON/XML feeds – perhaps only read only, but whatever authentication system, I’ve got to consider future usage.

What I ended up with:

In order for the RBS to be as flexible as possible to the most number of users, I ended up decided on (application specific) Role based permissions. In short, if person ‘bob’ is in role ‘admin’, and tries to do ‘x’ there needs to be some sort of permissions matrix indicating the admin role’s permission to do ‘x’. The important part to note here is that I’m not ever directly testing against a role. There should be  no conditional statements like if(isAdmin(user)) then do ‘x’; this leads to a nightmare later on when you suddenly decide you want to add a role, as for each ‘isAdmin()’ check, you then have to start adding ‘isEditor()’ or somesuch, and probably all over your application.

It’s better to build a privilege called ‘canDoX’ and test against that, as roles can more easily be added in the future.

Wheels lets us handle this quite neatly with two models, users & permissions. A user has a role, such as ‘admin’ or ‘editor’ etc; Then in the permissions table, you have a permission key, which is the name of your permission, i.e canDoX, and a column for each role, with a tinyint of 1 or 0 depending. So it’s a simple matrix of yes/no type data.

Then, onApplicationStart, we can check for an load in the matrix of permissions into the application scope for referencing later:

<cfset application.rbs.permissionsQuery=model("permission").findAll()>
 <cfloop query="application.rbs.permissionsQuery">
   <cfscript>
      application.rbs.permission["#id#"]={};
      application.rbs.permission["#id#"]["admin"]=application.rbs.permissionsQuery["admin"];
      application.rbs.permission["#id#"]["editor"]=application.rbs.permissionsQuery["editor"];
      application.rbs.permission["#id#"]["user"]=application.rbs.permissionsQuery["user"];
      application.rbs.permission["#id#"]["guest"]=application.rbs.permissionsQuery["guest"];
   </cfscript>
 </cfloop>

Once we’ve got these in the application scope, all we need are four functions to handle most of the permission based work:

<cffunction name="checkPermission" hint="Checks a permission against permissions loaded into application scope for the user" returntype="boolean">
	<cfargument name="permission" required="true" hint="The permission name to check against">
	<cfscript>
		if(_permissionsSetup() AND structKeyExists(application.rbs.permission, arguments.permission)){
			return application.rbs.permission[arguments.permission][_returnUserRole()];
		}
	</cfscript>
</cffunction>

<cffunction name="checkPermissionAndRedirect" hint="Checks a permission and redirects away to access denied, useful for use in filters etc">
	<cfargument name="permission" required="true" hint="The permission name to check against">
	<cfscript>
		if(!checkPermission(arguments.permission)){
			redirectTo(route="denied", error="Sorry, you have insufficient permission to access this. If you believe this to be an error, please contact an administrator.");
		}
	</cfscript>
</cffunction>

<cffunction name="_permissionsSetup" hint="Checks for the relevant permissions structs in application scope">
	<cfscript>
		if(structKeyExists(application, "rbs") AND structKeyExists(application.rbs, "permission")){
				return true;
		}
		else
		{
			return false;
		}
	</cfscript>
</cffunction>

<cffunction name="_returnUserRole" hint="Looks for user role in session, returns guest otherwise">
	<cfscript>
		if(_permissionsSetup() AND isLoggedIn() AND structKeyExists(session.currentuser, "role")){
			return session.currentuser.role;
		} else {
			return "guest";
		}
	</cfscript>
</cffunction>

So all you’d need to do using the above is:

<cfscript>
if(checkPermission("canDoX")){
// Do stuff
}
</cfscript>

The checkPermissionAndRedirect() function is very similar, and can be used on a filter level in your controller to just push away the user if they shouldn’t be accessing a view/function etc

i.e, on my Locations.cfc controller, where CRUD updating for the RBS board locations takes place:

filters(through="checkPermissionAndRedirect", permission="accessLocations");

The advantages of this approach are:

  • You’ve got an ‘out-the-box’ system which doesn’t require extensive server setup
  • You could extend this principle to allow for 3rd party authentication – i.e, if you’re going via LDAP, once a user is approved, a ‘local’ account could easily be created which duplicates core information such as email/name from the initial authentication (see an example I did a few years ago here)
  • Permissions are cached in the application scope, meaning you potentially avoid another database hit for every user login
  • In the future, user based permissions which override role based permissions could easily be added: our checkPermission(“foo”) function could easily look for the equivalent key in the user’s session scope
  • Adding new permissions to use is as simple as adding a line in the permissions table
  • Each installation can redefine the abilities of each role to suit their needs: so in the RBS example, you could make every role do everything, and then use some other authentication method, or lock down guest users etc.

All this code can be seen in context at the GitHub Repo