At aercolino/mean-app you’ll see a partially developed NodeJS app. I’m still working on it, but I’ve recently added a permission model that works with permissions like this:
{[ .anybody_canEdit_theirStuff | 1.hilite(=javascript=) ]}
I like it because it’s quite clean and very self-documenting. I personally find it very frustrating that permissions use to be buried into code in many places and need always some computer guy to translate them into business terms.
The name / value pair above completely defines a permission. The name is not only an English description but also active code to be interpreted by the model using the value. The name is SUBJECT + ACTION + OBJECT and the value should contain descriptors for each of those parts.
Notice that the meaning of all those parts comes from their descriptors in the value. For example, there is no conventional meaning assigned to the ´canEdit´ action except for it to be matched against the action passed to the Can function. Compare with this:
{[ .anAdmin_canDo_everything | 1.hilite(=javascript=) ]}
And here are some console logs just to see how it works.
What do you think? 🙂
Usage
To illustrate how to use these permissions I’m going to implement a use case where a developer wants to check permissions before allowing updates to models. I’m going to only show needed changes to existing files: the complete files are in the repository on GitHub.
Load the Can function
File ´/server/start.js´: Make the permission model available to the app. The ´Can´ function returns a promise which will be resolved with the first matching permission name or ´false´.
{[ .=start.js= | 1.hilite(=javascript=) ]}
Add support for the Can function
File ´/server/app/shared/CRUD.controller.js´ (old): This is how the ´Update´ function looked like before adding a check for verifying whether the update is allowed or not.
{[ .=function Update old= | 1.hilite(=javascript=) ]}
File ´/server/app/shared/CRUD.controller.js´ (new): This is how the ´Update´ function looks like after adding a check for verifying whether the update is allowed or not.
{[ .=function Update new= | 1.hilite(=javascript=) ]}
Instead of directly calling the ´Can´ function from the ´Update´ function, I prefer to call it from an ´AllowUpdate´ method, optionally defined on the model. This solution is much more flexible because it allows to easily tell apart when a check is required and when it is not: if the ´AllowUpdate´ is defined, it will be used, otherwise the update will take place straight away. Additionally, this solution decouples the permissions functionality from the (shared) CRUD functionality, enabling the developer to allow an update or not for whatever reason, not necessarily by checking a permission.
Notice that:
- I had to wrap the call to ´FilterFields(…)´ (which is the updating block) into a local function so that it can be called after the check or immediately.
- The ´AllowUpdate´ result could be a promise or not, and ´Promise.resolve(self.AllowUpdate(item, req))´ takes care of that.
- The ´AllowUpdate´ arguments could be a subject and an object, i.e. a user and an item, thus mimicking the arguments for the descriptors of the permissions. I decided instead to pass the item and the request.
- The order is important for highlighting that this is not (necessarily) related to a permission.
- The request carries the session, whose current user is the big deal.
- The error we signal when the update is not allowed is independent from the permission model: We just say that the ´AllowUpdate´ check failed.
Call the Can function
File ´/server/app/components/users/user.controller.js (old)´: This is how the ´User´ controller looked like before adding an ´AllowUpdate´ method.
{[ .=user.controller.js old= | 1.hilite(=javascript=) ]}
Notice that if no ´AllowUpdate´ method was necessary, this code would still work perfectly well with the permission setup I’m describing here.
File ´/server/app/components/users/user.controller.js (new)´: This is how the ´User´ controller looks like after adding an ´AllowUpdate´ method.
{[ .=user.controller.js new= | 1.hilite(=javascript=) ]}
Notice how I take advantage of the resolved value to ´log.info()´ about the matching permission right here, while the ´AllowUpdate´ result is going to be taken into account from the shared CRUD controller code.
Permissions
File ´/server/app/components/permissions/permissions.js´: Here is where the available permissions are listed. This is an example with all the different shapes (hopefully). Add and remove as needed.
{[ .=permissions.js= | 1.hilite(=javascript=) ]}
Here is a bunch of things to know about permissions.
- ´Translators canTranslate DocumentsNeedingTranslation´
- A permission name is a sentence SUBJECT + ACTION + OBJECT.
- An action must begin with ´can´.
- An actor is either a subject or an object.
- Actors starting with an uppercase letter are role names.
- Roles are defined by instances of the ´Role´ model, thus they cannot be defined in the permission value.
- ´Translators canTranslate documentsNeedingTranslation´
- Actors starting with a lowercase letter are item names.
- Items must be defined by descriptors in the permission value.
- A descriptor can be a hash with a ´model´ string and a ´restriction´ function.
- The corresponding actor must then be an item of the given model, which also satisfies the given restriction.
- The restriction function always gets two arguments: the subject item and the object item, in this order.
- An underscore in the signature means that the corresponding item is not relevant. (by convention)
- The name of an argument should always be whatever makes the most sense for self documentation.
- Permissions can overlap with each other, like this permission overlaps with the previous one. They’re not nice, one is probably superfluous, but the permission model gets happily along with them.
- ´anybody canEdit TheirStuff´
- ´anybody canEdit theirStuff´
- A descriptor can be a model string. The corresponding actor must then be an item of the given model.
- A descriptor can contain a ´model´ RegEx. The corresponding actor must then be an item of a matching model.
- As a policy, to keep things simple, a RegEx R (fully) matches a string S if
- ´S.replace(R, ”) === ”´
- The descriptor of the item ´theirStuff´ is an implementation of Duck Typing.
- Compare the descriptor of the item ´theirStuff´ with the role ´TheirStuff´ and its setup below.
- ´anAdmin canDo everything´
- Actions can be defined by RegEx descriptors in the permission value.
- A match-all RegEx for the action and the object makes this permission VERY generic.
Role and Permission Models
Role Model
File ´/server/app/components/roles/role.model.js´: This is the role model.
{[ .=role.model.js= | 1.hilite(=javascript=) ]}
Here is a bunch of things to know about the permission model.
- The model must be a string, even when it should be a RegEx.
- To differentiate a RegEx string from a non-RegEx string, wrap it into slashes (with optional modifiers at the end).
- In other words, the string to use for a RegEx is its source with properly escaped backslashes.
- If the restriction R is a JSON object, then an item I of the model M has this role if
- ´M.count(Extend({_id: I._id}, R)) === 1´.
- R is considered like Mongoose / MongoDB criteria.
- If the restriction R is a non-empty string, then an item I of the model M has this role if
- ´R(subject, object) == true´.
- R is considered like a function name.
- The function name can contain dots.
- If the function name is a (static) method of an unavailable Model, it will be automatically required.
- If the restriction R is FALSEy, then an item I of the model M has this role if
- ´item.roles.indexOf(name) > -1´.
- This is the case corresponding to a classical ´Role´ model, where a given role is associated to a given user by adding the role name to the user roles.
- If the restriction R is TRUEy, then each item I of the model M has this role.
- This role acts like an alias of the model.
- The usefulness of this case is still unknown…
Their Stuff
Here is how the role ´TheirStuff´ I used in a permission above could be implemented.
{[ .TheirStuff | 1.hilite(=javascript=) ]}
File ´/server/app/components/users/user.model.js´: This is the user model, after adding both the instance method ´isAdmin´ the static method ´owns´ used before and right above respectively.
{[ .=User.owns= | 1.hilite(=javascript=) ]}
Permission Model
File ´/server/app/components/permissions/permission.model.js´: This is the permission model.
I’m not going to illustrate its supporting functions (peruse them here, if you like), but here is its main code.
{[ .=permission.model.js main= | 1.hilite(=javascript=) ]}
Here is a bunch of things to know about the permission model.
- When the permission model is required the permissions list is loaded and compiled.
- The compilation process builds a structure that the global ´Can´ function will later use.
- A permission answers a ´Can´ question if their actions, subjects, and objects respectively match each other.
- Permissions are selected, sorted, chained, and then all checked in turn, one after the other.
- Sorting is based on a very rough complexity measure, detected during compilation.
- As soon as a full match is found, the ´Can´ function immediately resolves with the permission name.
- If no full match exists, then the ´Can´ function finally resolves with a ´false´.
Enjoy.