A library that handles user roles and permissions. Modeled after Apache Shiro's WildcardPermission.
Please see the JavaDoc for WildcardPermission for detailed information.
See permissions_test.cljc for examples.
To install, add the following to your project :dependencies
:
[agynamix/permissions "0.2.0-SNAPSHOT"]
A permission consists of three parts; a domain, a set of actions, and a set of entities.
If any of those is not given it defaults to a wildcard permission, or "*"
.
A domain is usually an area in your application that this permission should be applicable for.
For instance if you have an area in your application managing user accounts, domain could be "users"
.
You might have privileged users that are allowed to edit user accounts and those that can only read them.
So one permission could be "users:read"
while other could receive "users:edit"
(it's up to you how you name these
actions or how many you introduce.
Finally, you might want to allow some users only to edit some user records, that's where the third part,
the entity list comes in. "users:edit:12345,12346"
would require users to either name that entity in the list of entities
they are allowed to edit. Or alternatively as in the examples above, if you omit that field it becomes "*"
the
wildcard permission. A wildcard permission in any of the three fields means the user has all permissions,
either for all domains (first part, that's something only the super power root user should get), all actions for the
mentioned domain, or all entities for a given list of actions. I've really never before used the entities field to
limit access to resources. But it's there if your use case requires it.
The permissions library consists of two parts. The permissions namespace defines the low level API of permission and
the implied-by?
method used to test if a resource permission (the first parameter) is implied (has access to)
by the second permission or list of permissions.
It also holds a factory method make-permission
that should make it trivial to create a permission a la:
(make-permission "company") ;; allows all actions on domain 'company'
(make-permission "company" "read") ;; allows read action on domain 'company'
(make-permission "company:edit,update:123,124") ;; allows edit/update action on domain 'company' and entity 123/124
The second part is for conveniently interacting with maps that contain a set of roles and permissions.
It expects a key :roles
and/or :permissions
in the given map. It will construct a set of permissions by
pulling all permissions attached to the given roles and the single permissions found into one and then checks
if any of those permissions would allow the user to access the resources (as defined by the permission attached to that
resource).
(has-permission? user-map permission-or-string)
(lacks-permission? user-map permission-or-string)
In order to know how to resolve permissions from roles it has to be initialized with a map whose keys are the role names and the values are sets of permissions (either Permission records or strings)
A role map might look like this:
(def roles {"user/admin" "user:*"
"user/all" #{"user:read" "user:write"}
"admin/all" "*"
"company/super" #{"company:read" "company:write" "company:edit" "company:delete"}
"contacts/read" #{"contacts:read"}
"timeline/edit" #{"timeline:edit" "timeline:read"}
"project/all" #{"contacts/read" "timeline/edit" "project:read"}
}
Please note that the map of roles and associated permissions can hold nested roles. If a role is found on the right side of the map (as value of another role key) it is looked up in the role map and replaced by the found values for this nested role key. This is done recursively for arbitrarily nested roles.
This feature can be used for instance to limit access to different sections (domains) of an application by checking against different permissions which are grouped together as roles by domain. Then you could define a role that would slurp all roles for these domains and assign this role to users that are allowed access to every part of the applications.
Initialize the role mapping with:
(agynamix.roles/init-roles roles)
A user might look like this:
(def user {:roles #{"user/all" "company/super"}
:permissions #{"library:read" "company:gibberish"}
... lots of other keys
}
Please have a look at roles_test.cljc for examples.
Instead of attaching literal permission strings to a user record or to a resource you can also attach one or more numbers that represent bitmasks. This works as follows:
In your application you need to define a mapping of role to permissions just as described above. But instead of choosing keywords as names for roles you choose a number that is a multiple of two (that has only 1 bit set):
(def roles {1 "user:*"
2 #{"user:read" "user:write"}
4 "*"
8 #{"company:read" "company:write" "company:edit" "company:delete"}
}
You can map the number to one or more permissions, whatever suits your application.
Initialize the role mapping like so:
(:require [agynamix.roles :refer :all]
[agynamix.bitmask-roles :refer :all])
(init-roles roles bitmask-permission-resolver bitmask-role-resolver)
It's important that you define the bitmap resolvers when initializing the role mapping!
Then in the user you declare the roles and permissions given to that user like so:
(def user {:roles 10
:permissions #{"library:read" "company:gibberish"}
... lots of other keys
}
That number 10
sets the bits at positions 2
and 8
, thus role permissions given to the
roles 2
and 8
are added to that user.
In addition you can still set arbitrary permissions using the :permissions
key in the user record if you want.
Or you could simple omit the key if it's not needed.
Similarly you give a resource one or more bitmask numbers. These numbers are reduced to the associated permissions
as described in the role mapping above. If they reduce to multiple permissions, then all resource permissions
must be met by the user (its like they are and
ed together).
Please have a look at bitmask_roles_test.cljc for examples.
Copyright © 2016 FIXME
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.