IAmICan
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[]
Table of Content
-
Concepts and Overview
- In one word
- Definition and uniqueness of nouns
- About role group
- Three steps to use this gem
- Two Concepts of this gem
- How it work?
-
Installation and Setup
-
Usage
- Config Options
- Methods and helpers
- A. Role Definition
- B. Grouping Roles
- C. Role Assignment
- D. Role / Group Querying
- E. Permission Definition
- F. Permission Assignment
- G. Permission Querying
- H. Shortcut Combinations - which_can
- I. Resource Querying
- J. Useful Helpers
Concepts and Overview
In one word:
- role has permissions
- subject has the roles
> subject has the permissions through the roles.
Definition and uniqueness of nouns
- 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
About role group?
- 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
Three steps to use this gem
- 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
Two Concepts of this gem
- Stored (save in database) TODO
- Temporary (save in instance variable) TODO
How it work?
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
Installation and Setup
-
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!
Usage
Config Options
-
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.
Methods and helpers
- 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
: assignment -
cancel_role_assign
: cancel assignment -
role_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
: assignment -
cancel_permission_assign
: cancel assignment -
permission_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`
H. Shortcut Combinations - which_can
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
I. Resource Querying
- 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)
J. Useful Helpers
-
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
Development
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.
Contributing
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.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the IAmICan project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.