CbSecurity has some fine mechanisms to work with user permissions (the CbAuth validator) or user roles (CFML Security validator). The cbauth validator is really flexible, but sometimes you still need more options. In one of our projects our users can have one or more fixed roles and we assigned several permissions to each role. Our rules looked like this:

{
  secureList  : "auth.Customers\.index",
  permissions : LIST_CUSTOMERS
},
{
  secureList  : "auth.Customers\.show",
  permissions : SHOW_CUSTOMERS
},
{
  secureList  : "auth.Customers\..*",
  permissions : MANAGE_CUSTOMERS
},
// ......
// block all rule
{
  secureList  : ".*",
  permissions : "JUST_A_PLACEHOLDER",
  whitelist   : whiteRules.toList()
}

This is only a very small part of our rules. We had to keep track of all kind of permission names (and we put them in variables to prevent typo’s) and still had to assign these permissions to a fixed set of roles. Quite some administration. We decided we had more wishes, in our case:

  1. each individual endpoint should have its own permission for very fine-grained control.
  2. any number of roles which we can create in an API
  3. assignment of permission to these roles in the API
  4. assignment of roles to users in the API
  5. and since we are using a VueJS frontend, we wanted a list of ALL available endpoints (based on permissions) for a user

Our system is a REST based API, and all routes translate to a Coldbox event. So why not name our permissions after the event names? Our events are nicely grouped together in packages (or modules) so when listing permissions we can easily filter on packages to get a list of related permissions (which we can assign to some role). But now we have to figure out how to check the current event against a permission (which happens to be have the same name as the event). Before we continue I will explain how cbsecurity handles authorization (permission checking).

  • cbsecurity is an interceptor on the preProcess interception point which means all requests will be evaluated by cbsecurity before anything else happens.
  • preProcess evaluate all rules in order. For every rule it checks if your event matches a whiteList property (i.e a comma-delimited list of events). If it matches the whitelist it continues to the next event.
  • if your event is not whitelisted the secureList property will be checked. Most of the time this is a comma delimited list of events, or more specific a comma delimited list of regular expressions and if your event matches one of the elements in your secure list it passed control to the ruleValidator method of a cbsecurity VALIDATOR, which can be a cbauth validator, a cfml Security validator, a JWT validator, or a custom validator.
  • The ruleValidator method will check if the user is authenticated and if it there are any permissions defined in a rule it will check the permissions. In most standard validators this is done by calling the User.hasPermission method. On succes, access will be granted.

We don’t want to create a huge list of rules with all individual permissions (we have 300+ endpoints…) so I would like to have a special validator where:
myEvent == the permission.

Actually this is a lot easier than I thought. We were using the JWT ruleValidator which has two relevant methods: a RuleValidator which just passes the rule permissions to some private ValidateSecurity method. Don’t worry too much about the validateSecurityMethod: I will not change the original. It is just checking for permissions with the user by this call: variables.cbSecurity.getAuthService().getUser().hasPermission( arguments.permissions )

struct function ruleValidator( required rule, required controller ){
  return validateSecurity( arguments.rule.permissions );
}

private function validateSecurity( required permissions ){
  var results = {
    "allow"    : false,
    "type"     : "authentication",
    "messages" : ""
  };
  try {
    // Try to get the payload from the jwt token, if we have exceptions, we have failed :(
    var payload = getPayload();
  } catch ( Any e ) {
    results.messages = e.type & ":" & e.message;
    return results;
  }

  // Are we logged in?
  if ( variables.cbSecurity.getAuthService().isLoggedIn() ) {
    // Do we have any permissions to validate?
    if ( listLen( arguments.permissions ) ) {
      // Check if the user has the right permissions?
      results.allow = (
	tokenHasScopes( arguments.permissions, payload.scope )
	  ||
	variables.cbSecurity
	  .getAuthService()
	  .getUser()
	  .hasPermission( arguments.permissions )
	);
      results.type = "authorization";
    } else {
      // We are satisfied!
	results.allow = true;
    }
  }
  return results;
}

So I only have to change ONE thing to make my event name the permission name:

struct function ruleValidator( required rule, required controller ){
  // each individual eventName is a separate permission, 
  // and there's only one rule which passes ALL events
  return validateSecurity( 
    controller.getRequestService().getContext().getCurrentEvent() 
  )
} 	

My ruleValidator has a controller reference, and from this controler I can get the event name. So now I am sending my eventName to the validateSecurity method which just asks my user object if it has this permission. So my user method looks like this

boolean function hasPermission( required permission ) {
  return ListSome(getRoles(), (role)=>{
    return SecurityService.hasPermissionForRole(role, permission);
  })
}

My user’s hasPermission method is looping through it’s roles and for each role it asks if it has a certain permission (the event name). I cached all permissions for my roles, so this call is quite fast.

So creating a new CustomValidator was easy. I copied the ruleValidator and annotationValidator method and the underlying validateSecurity method in my own CustomValidator. I just have to tell my cbsecurity now I am using my own validator:

// module setting overrides
moduleSettings = {
  cbSecurity : {
    "validator"	: "SecurityService",
//..etc

Finally I have to take care of my rules. Instead of a whole list of rules, I only need one:

rules = [
  {
    secureList  : ".*",
    permissions : "JUST_A_PLACEHOLDER",
    whitelist   : whiteRules.toList()
  }
];

This means ALL requests, except the events in my whitelist will be directly passed to my Customvalidator.

There’s two more things I want to fix: number 3 from my list. I want to have a list of all permissions in my applications so I can assign them to my roles. I have a separate table were I store RoleId’s and permission (or event names) for this. Since all endpoints are in my v1 module router this is not that hard to create a list.

property name="router" inject="router@coldbox";

var NewList = [];
router.getmoduleRoutingTable()[ "v1" ].each( function( item ) {
  item.action.each( function( key, value ) {
    var event = "#item.handler#.#value#";
    newlist.append("#item.handler#.#value#");
  }
}
return newList;

Since I also want to satisfy number 5 on my list (I want to have a list of authorized endpoints for my user, so my VueJS application has an easy list of accessible endpoints) I created a slightly modified versions of the above code. To see what’s exactly going on in the code below you could dump one of the records of the router.getmoduleRoutingTable()[ "v1" ]so you can see what’s available in each routing table item.

property name="router" inject="router@coldbox";
property name="cbSecurity" inject="@cbSecurity";

public any function getEndPoints( ) {
  // get Router info and link to handler
  // for each endpoint determine if permitted
  var NewList = [];
  router.getmoduleRoutingTable()[ "v1" ].each( function( item ) {
    item.action.each( function( key, value ) {
      var event = "#item.handler#.#value#";
      if( variables.cbSecurity
	.getAuthService()
	.getUser()
	.hasPermission( event )				
      ){ 					
	newlist.append( {
	  method   : key,
	  endpoint : "/" & left( item.pattern, item.pattern.len() - 1 )
	});
      }
    })
  })
  return newList;
}

So with this exercise we have a validator which is just passing my eventName to my Users haspermission validation. The routing table gives me a list of all available events. With some additional magic which I didn’t show here we grouped together related events such as list, show, create, update and delete for individual rest resource names.

It might not be exactly what you need, but we have the most granular permission system we could get, and it shows the flexibility of the cbsecurity module. Please feel free to comment or ask questions.