Permits
A Permit
encapsulates a set of authorization rules for a given type of context, scenario, type of user etc. It is a convenient way to group together a set of rules that conceptually/logically "belong together" and apply under the same condition.
A permit can either implement its rules directly or load its rules from JSON or some other rules storage.
A permit takes an AccessRequest
object which contains:
-
user
object asking to be authorized -
action
being attempted -
subject
being acted upon -
context
under which the whole action takes place
The permit will first match this access request to see if this permit is even relevant for it and should apply its authorization rules. If so, it will try determine if the user is allowed to perform the given action on the subject, by matching the access request on the rules to see if the rules allow or disallow this request.
permitFor
optionally take a Permit
class argument, such as AdminPermit
to further greater reuse by leveraging OOP concepts such as inheritance, module inclusion etc.
Example:
adminBarPermit = permit-for AdminPermit, 'a man walking into the bar', customPermit
The customPermit
can be either an Object or a Function.
Each time a permit is created, it is registered in the PermitRegistry by name. If the permit is not named explicitly, it will be named Permit-0
, Permit-1
etc. Only the first permit of a given name is registered.
Any attempt to later store another permit of the same name will cause an error.
You can clear all permits using PermitRegistry.clearAll!
and you can clean all permits of rules using PermitRegistry.cleanAll!
Currently the PermitRegistry
is implemented as a singleton, but in the future it will be a class to allow for multiple permit registries and then selecting one to be used for a given environment. Suggestions/ideas are always welcome!
The Permit Allower has the responsibility to determine if the permit allows a given action on a subject (see AccessRequest).
Each permit that matches for the given access context is then resolved to see if the user really can (is allowed) to perform the action on the subject.
class Permit
permitAllower (accessRequest) ->
new PermitAllower(@, accessRequest)
allows: (accessRequest) ->
permitAllower(accessRequest).allows!
disallows: (rule) ->
permitAllower(accessRequest).disallows!
The Permit Matcher is used to test if a permit matches for a given access request and should be used for that request.
A Permit class can be created via the permitFor
factory method.
Example:
sexy-permit = permit-for 'a sexy woman',
// exclude match (Permit does not apply if this match is fulfilled)
exMatch: (access) ->
user = access.user
user.gender is 'female' and user.looks is 'sexy'
rules:
manage: ->
@ucan 'seduce', ['person'], (obj) ->
obj.gender is 'male'
Note: The third argument (contextual function) is not yet supported, but will be in the near future
To facilitate creating generic matchers, a set of MatchMaker
classes have been defined for each of the keys in the access request. These are UserMatcher
, ActionMatcher
, SubjectMatcher
, ContextMatcher
.
The matchers can be used either directly by instantiation or via the matching(access)
convenience methods of Permit
, which employs the AccessMatcher
that in turn contains convenience methods for
using all the specific access matchers just mentioned!
The Permit.matching(access)
method returns an AccessMatcher
instance with convenience methods:
- user
- role
- action
- subject
- subjectClazz
- context
Access user
method:
sexy-permit = permitFor 'a sexy woman',
// only applies if this match is true
match: (access) ->
# matches if access intersects with {user: {type: 'sexy'}}
@matching(access).user(type: 'sexy')
Access action
method:
read-permit = permitFor 'a sexy woman',
match: (access) ->
# matches if access intersects with {action: 'read'}
@matching(access).action('seduce')
Access subject
method:
read-permit = permitFor 'a sexy woman',
match: (access) ->
# matches if access intersects with {subject: 'Human'}
@matching(access).subject('Human')
Access context
method:
read-permit = permitFor 'a sexy woman',
match: (access) ->
# matches if access intersects with {area: 'members'}
@matching(access).context(area: 'members')
Access role
method:
sexy-permit = permitFor 'a sexy woman',
match: (access) ->
# matches if access intersects with {user: {role: 'sexy'}}
@matching(access).role('sexy')
Access subjectClazz
method (chaining):
sexy-permit = permit-for 'a sexy woman',
match: (access) ->
# matches if {subject: sexy-woman} and sexy-woman.constructor is a Woman class
@matching(access).subjectClazz('Man').user(type: 'sexy')
The matchOn
method takes a hash and executes chaining as above:
@matching(access).matchOn(subjectClazz: 'Man', user: {type: 'sexy'}, action: 'read')