Concise and Natural DSL for Subject - Role(Role Group) - Permission - Resource
Management (RBAC like).
# our Subject is People, and subject is he:
he = People.take
# let: Roles means PeopleRole, Groups means PeopleRoleGroup
# Role
People.have_role :admin # role definition
he.becomes_a :admin # role assignment
he.is? :admin # role querying => true
he.is? :someone_else # role querying => false
# Role Group
People.have_and_group_roles :dev, :master, :committer, by_name: :team
he.becomes_a :master # role assignment
he.in_role_group? :team # role group querying => true
# Role - Permission
People.have_role :coder # role definition
Roles.have_permission :fly # permission definition
Roles.which(name: :coder).can :fly # permission assignment (by predicate)
he.becomes_a :coder # role assignment
he.can? :fly # permission querying
# Role Group - Permission
Groups.have_permission :manage, obj: User # permission definition
Groups.which(name: :team).can :manage, obj: User # permission assignment (by predicate and object)
he.is? :master # yes
he.can? :manage, User # permission querying
# more concise and faster way
he.becomes_a :magician, which_can: [:perform], obj: :magic
he.is? :magician # => true
Roles.which(name: :magician).can? :perform, :magic # => true
he.can? :perform, :magic # => true
# Cancel Assignment
he.falls_from :admin
Roles.which(name: :coder).cannot :fly
# Get allowed resources:
Resource.that_allow(user, to: :manage) # => ActiveRecord_Relation[]
- role has permissions
- subject has the roles
> subject has the permissions through the roles.
- Subject
- Someone who can be assigned roles, and who has permissions through the assigned roles.
- See wiki RBAC
- Role
- A job function that groups a series of permissions according to a certain dimension.
- Also see wiki RBAC
- Uniquely identified by
name
- Role Group
- A group of roles that may have the same permissions.
- Uniquely identified by
name
- Permission
- An action, or an approval of a mode of access to a resource
- Also see wiki RBAC
- Uniquely identified by
predicate( + object)
(name), or we can say,action( + resource)
- Object (Resource)
- Polymorphic association with permissions
- role group has permissions
- roles are in the group
- subject has one or more of the roles
> subject has the permissions through the role which is in the group
- Querying
- Find if the given role is assigned to the subject
- Find if the given permission is assigned to the subject's roles / group
- instance methods, like:
user.can? :fly
- Assignment
- assign role to subject, or assign permission to role / group
- instance methods, like:
user.has_role :admin
- Definition
- the role or permission you want to assign MUST be defined before
- option :auto_definition (before assignment) you may need in some cases
- class methods, like:
UserRoleGroup.have_permission :fly
Definition => Assignment => Querying
- Stored (save in database) TODO
- Temporary (save in instance variable) TODO
Very simple. Really simple. Sooooo Simple.
- To define something, you actually
create
records. see here - To assign something, you actually call one of the activerecord association methods. see here
- To query something, you actually call the querying interfaces of activerecord. see here
-
Add this line to your application's Gemfile and then
bundle
:gem 'i_am_i_can'
-
Generate migrations and models by your subject name:
rails g i_am_i_can:setup <subject_name>
For example, if your subject name is
user
, it will generate modelUserRole
,UserRoleGroup
andUserPermission
-
Add the code returned by the generator to your subject model, like:
class User has_and_belongs_to_many :stored_roles, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) }, join_table: 'users_and_user_roles', foreign_key: 'user_id', class_name: 'UserRole', association_foreign_key: 'user_role_id' has_many_temporary_roles acts_as_subject end
here is some options you can pass to the declaration.
-
Run
rails db:migrate
That's all!
-
auto_definition: Auto definition before assignment if it's set to
true
. defaults tofalse
. -
strict_mode: Raise error when doing wrong definition or assignment if it's set to
true
. defaults tofalse
. -
without_group: Unable
role group
feature if it's set totrue
. defaults tofalse
. -
relation names: you can change the names in model declarations, defaults to
stored_roles
,permissions
,stored_users
and so on.
- caller: Subject Model, like
User
- method:
have_role
. aliases:have_roles
has_role
&has_roles
Explanation:
# === method signature ===
have_role *names, which_can: [ ], obj: nil
# === examples ===
User.have_roles :admin, :master # => 'Role Definition Done' or error message
# is the same as: `UserRole.create([{ name: :admin }, ...])`
# then:
UserRole.count # => 2
Tip: Roles that you're going to group should be defined
- caller: Subject Model, like
User
- method:
group_roles
. aliases:group_role
groups_role
&groups_roles
- shortcut combination method:
have_and_group_roles
(aliashas_and_groups_roles
)
it will do: roles definition && roles grouping - helpers:
- relation with role (member), defaults to
members
.
- relation with role (member), defaults to
Explanation:
# === method signature ===
group_roles *members, by_name:, which_can: [ ], obj: nil
# === examples ===
# 1. normal usage
User.have_roles :vip1, :vip2, :vip3
User.group_roles :vip1, :vip2, :vip3, by_name: :vip
# 2. shortcut combination
User.have_and_group_roles :vip1, :vip2, :vip3, by_name: :vip
UserRoleGroup.count # => 1
UserRoleGroup.which(name: :vip).members.names # => %i[vip1 vip2 vip3]
- caller: subject instance, like
User.first
- assignment by calling:
becomes_a
, or it's aliases:is
/is_a_role
/is_roles
has_role
/has_roles
role_is
/role_are
is_a_temporary
: just like the name, the assignment occurs only in instance variable not in database (will be in the cache).
- cancel assignment by calling:
falls_from
, or it's aliases:removes_role
leaves
is_not_a
/has_not_role
/has_not_roles
will_not_be
is_not_a_temporary
- replacement assignment by calling:
is_only_a
, aliascurrently_is
. (makes the role collection contain only the supplied roles, by adding and deleting as appropriate) - callbacks - before / after / around:
role_assign
: assignmentcancel_role_assign
: cancel assignmentrole_update
:
- helpers:
- relation with stored role, defaults to
stored_roles
. temporary_roles
andvalid_temporary_roles
roles
assoc_with_<roles>
, like:assoc_with_stored_roles
- relation with stored role, defaults to
Explanation:
he = User.take
# Dont't forget to define roles before assignment
User.have_roles :admin, :coder
# === Stored Assignment ===
# method signature
becomes_a *roles, which_can: [ ], obj: nil,
_d: config.auto_definition,
auto_definition: _d || which_can.present?,
expires_in: nil, expires_at: (expires_in.after if expires_in)
# 1. example of giving Symbol to `roles` params
he.becomes_a :admin # => 'Role Assignment Done' or error message
he.stored_roles # => [<#UserRole id: 1>]
# 2. example of giving role instances to `roles` params
he.becomes_a UserRole.all # => 'Role Assignment Done' or error message
he.stored_roles # => [<#UserRole id: 1>, <#UserRole id: 2>]
# 3. `expires` (subject assocates roles with a `expire_at` scope)
he.is_a :visitor, expires_in: 1.hour # or `expires_at: 1.hour.after`
he.is? :visitor # => true
# an hour later ...
he.is? :visitor # => false
# assoc_with_<roles>: for getting the relation records between subject and it's roles
he.assoc_with_stored_roles # => UsersAndUserRoles::ActiveRecord_Associations_CollectionProxy
# === Temporary Assignment ===
# signature as `becomes_a`
# examples
he.is_a_temporary :coder # => 'Role Assignment Done' or error message
he.temporary_roles # => [<#UserRole id: 2>]
he.roles # => [:admin, :coder]
# === Cancel Assignment ===
# method signature
falls_from *roles
is_not_a_temporary *roles
# examples
he.falls_from :admin # => 'Role Assignment Done' or error message
he.is_not_a_temporary :coder # => 'Role Assignment Done' or error message
he.roles # => []
# === Replacement Assignment ===
# method signature
is_only_a *roles
# examples
he.is_only_a :role1, :role2
- caller: subject instance, like
User.first
- role querying methods:
is?
/is_role?
/has_role?
isnt?
is!
/is_role!
/has_role!
is_one_of?
/is_one_of_roles?
is_one_of!
/is_one_of_roles!
is_every?
/is_every_role_in?
is_every!
/is_every_role_in!
- group querying methods:
is_in_role_group?
/in_role_group?
is_in_one_of?
/in_one_of?
all the ?
methods will return true
or false
all the !
bang methods will return true
or raise IAmICan::VerificationFailed
Examples:
he = User.take
he.is? :admin
he.isnt? :admin
he.is! :admin
he.is_every? :admin, :master # return false if he is not a `admin` or `master`
he.is_one_of! :admin, :master # return true if he is a `master` or `admin`
he.is_in_role_group? :vip # return true if he has at least one role of the group `vip`
- caller: Role / Role Group Model, like
UserRole
/UserRoleGroup
- method:
have_permission
. aliases:have_permissions
has_permission
&has_permissions
Explanation:
# === method signature ===
have_permission *actions, obj: nil
# It is not recommended to pass an array of objects
# === examples ===
UserRole.have_permission :fly # => 'Permission Definition Done' or error message
UserPermission.count # => 1
UserRoleGroup.have_permissions :read, :write, obj: book # => 'Permission Definition Done' or error message
UserPermission.count # => 1 + 2
- caller: role / role group instance, like
UserRole.which(name: :admin)
- assignment by calling
can
. aliashas_permission
- cancel assignment by calling
cannot
. aliasis_not_allowed_to
- replacement assignment by calling:
can_only
,. (makes the permission collection contain only the supplied permissions, by adding and deleting as appropriate) - callbacks - before / after / around:
permission_assign
: assignmentcancel_permission_assign
: cancel assignmentpermission_update
: replacement assignment
- helpers:
- relation with stored permission, defaults to
permissions
.
- relation with stored permission, defaults to
Explanation:
role = UserRole.which(name: :admin)
# Dont't forget to define permission before assginment
UserRole.have_permission :fly
# === Assignment ===
# method signature
can *actions, resource: nil, obj: resource, # you can use `resource` or `obj`
_d: config.auto_definition, auto_definition: _d
# examples
role.can :fly # => 'Permission Assignment Done' or error message
role.permissions # => [<#UserPermission id: ..>]
# you can also passing permission instances to `actions` params, like:
role.can UserPermission.all
# === Cancel Assignment ===
# method signature
cannot *actions, resource: nil, obj: resource
# examples
role.cannot :fly
# === Replacement Assignment ===
# method signature
can_only *actions, resource: nil, obj: resource
# examples
role.can_only :run
- caller:
- subject instance, like
User.find(1)
- role / role group instance, like
Role.which(name: :master)
(only havecan?
method)
- subject instance, like
- methods:
can?
cannot?
can!
can_each?
&can_each!
can_one_of!
&can_one_of!
temporarily_can?
stored_can?
group_can?
all the ?
methods will return true
or false
all the !
bang methods will return true
or raise IAmICan::InsufficientPermission
Examples:
he = User.take
# `perform` is action, and `magic` is object (resource)
he.can? :perform, :magic
# the same as:
he.can? :perform, obj: :magic
he.cannot? :perform, :magic
he.can! :perform, :magic
he.can_each? %i[ fly jump ] # return false if he can not `fly` or `jump`
he.can_one_of! %i[ fly jump ] # return true if he can `fly` or `jump`
Faster way to assign, define roles and their permissions.
You can use it when defining role even assigning role.
# === use when defining role ===
# it does:
# 1. define the role to Subject Model
# 2. define & assign the permission to the role
User.have_role :coder, which_can: [:perform], obj: :magic
UserRole.which(name: :coder).can? :perform, :magic # => true
# === use when assigning role ===
# it does:
# 1. define the role to Subject Model
# 2. assign the role to subject instance
# 2. define & assign the permission to the role
user = User.take
user.becomes_a :master, which_can: [:read], obj: :book
user.is? :master # => true
user.can? :read, :book # => true
- caller: Resource Collection or Instance
- scopes:
that_allow
Explanation:
# === method signature ===
scope :that_allow, -> (subject, to:) { }
# === examples ===
Book.that_allow(User.all, to: :read)
Book.that_allow(User.last, to: :write)
-
for Subject (e.g. User)
# declaration in User has_and_belongs_to_many :identities # stored_roles # 1. [scope] with_<stored_roles> # is the same as `includes(:stored_roles)` for avoiding N+1 querying User.with_identities.where(identities: { name: 'teacher' })
-
for Role / RoleGroup (e.g. UserRole)
# declaration in UserRole has_and_belongs_to_many :related_users has_and_belongs_to_many :related_role_groups has_and_belongs_to_many :permissions # 1. [class method] which(name:, **conditions) # the same as `find_by!` UserRole.which(name: :admin) # 2. [class method] names UserRole.all.names # => symbol array # 3. [class method] <related_*> # returns a ActiveRecord_Relation # for example, to get the users of the role `admin` and `dev`: UserRole.where(name: ['admin', 'dev']).related_users # to get the groups of the role `admin` and `dev`: UserRole.where(name: ['admin', 'dev']).related_role_groups # 4. [scope] with_<permissions> # is the same as `includes(:permissions)` for avoiding N+1 querying UserRole.with_permissions.where(permissions: { id: 1 })
-
for
Permission
(e.g. UserPermission)# declaration in UserPermission has_and_belongs_to_many :related_roles has_and_belongs_to_many :related_role_groups # 1. [class method] which(action:, obj: nil, **conditions) # the same as `find_by!` UserPermission.which(action: :read, obj: Book.first) UserPermission.which(action: :read, obj_type: 'Book', obj_id: 1) # 2. [class method] names UserPermission.all.names # => symbol array # 3. [class method] <related_*> # returns a ActiveRecord_Relation as above UserPermission.where(..).related_roles UserPermission.where(..).related_role_groups # 4. [instance method] name UserPermission.first.name # => :read_Book_1 # 5. [instance method] obj UserPermission.first.obj # => nil / Book / book / :book
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/i_am_i_can. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the IAmICan project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.