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
# role definition and grouping
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. :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. :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) # => Active::Relation
Concepts and Overview
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)
- Resource
- Polymorphic association with permissions
In one word:
- role has permissions
- subject has the roles
> subject has the permissions through the roles.
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 of 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_define_before (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
- Local (variable value) TODO
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,UserRoleGroupandUserPermissionAdd the code returned by the generator to your subject model, like:
class User has_and_belongs_to_many :stored_roles, join_table: 'users_and_user_roles', foreign_key: 'user_role_id', class_name: 'UserRole', association_foreign_key: 'user_id' act_as_subject endhere is some options you can pass to the declaration.
Run
rails db:migrate
That's all!
Usage
Customization
- association names TODO
Config Options
TODO
Methods and their Aliases
A. Role Definition
- Caller: Subject Model, like
User - methods:
- save to database:
have_role. aliases:have_roleshas_role&has_roles
- save to local variable:
declare_role. aliasdeclare_roles
- save to database:
- helpers:
defined_local_rolesdefined_stored_roles&defined_stored_role_namesdefined_roles
Methods Explanation:
# === Save to DB ===
# method signature
have_role *names, desc: nil, save: default_save#, which_can: [ ], obj: nil
# examples
User.have_roles :admin, :master # => 'Role Definition Done' or error message
User.defined_stored_roles.keys.count # => 2
# === Save in Local ===
# signature as `have_role`
# examples
User.declare_role :coder # => 'Role Definition Done' or error message
User.defined_local_roles.keys.count # => 1
User.defined_roles.keys.count # => 3
B. Grouping Roles
Tips:
- Role Group must be saved in database currently
- Roles that you're going to group should be defined
Overview:
- Caller: Subject Model, like
User - method:
group_roles. aliases:group_rolegroups_role&groups_roles
- shortcut combination method:
have_and_group_roles(aliashas_and_groups_roles)
it will do: roles definition => roles grouping - helpers:
defined_role_groups&defined_role_group_namesmembers_of_role_group
Methods Explanation:
# method signature
group_roles *members, by_name:, #which_can: [ ], obj: nil
# examples
User.have_and_group_roles :vip1, :vip2, :vip3, by_name: :vip
User.defined_role_group_names # => [:vip]
User.members_of_role_group(:vip) # => %i[vip1 vip2 vip3]
C. Role Assignment
- Caller: subject instance, like
User.find(1) - assign methods:
- save to database:
becomes_a. aliases:is&is_a_role&is_roleshas_role&has_rolesrole_is&role_are
- save to local variable:
temporarily_is. aliaslocally_is
- save to database:
- cancel assign method:
falls_from. aliases:removes_roleleavesis_not_a&has_not_role&has_not_roleswill_not_be
- helpers:
local_roles&local_role_namesstored_roles&stored_role_namesroles
Methods Explanation:
he = User.take
# === Save to DB ===
# method signature
becomes_a *roles, auto_define_before: auto_define_before, save: default_save#, which_can: [ ], obj: nil
# examples
he.becomes_a :admin # => 'Role Definition Done' or error message
he.stored_roles # => [<#UserRole id: 1>]
# === Save in Local ===
# signature as `becomes_a`
# examples
he.temporarily_is :coder # => 'Role Assignment Done' or error message
he.local_roles # => [{ coder: { .. } }]
he.roles # => [:admin, :coder]
# === Cancel ===
# method signature
falls_from *roles, saved: default_save
# examples
he.falls_from :admin # => 'Role Assignment Done' or error message
he.removes_roles :coder, saved: false # => 'Role Assignment Done' or error message
he.roles # => []
D. Role / Group Querying
- Caller: subject instance, like
User.find(1) - 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
Methods 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 a role which is in the group :vip
E. Permission Definition
- Caller: Role / Role Group Model, like
UserRole/UserRoleGroup - methods:
- save to database:
have_permission. aliases:have_permissionshas_permission&has_permissions
- save to local variable:
declare_permission. aliasdeclare_permissions
- save to database:
- helpers:
defined_local_permissionsdefined_stored_permissionsdefined_permissions
- class method:
which(name:) - Permission
- class method:
which(pred:, obj:) - instance methods:
#pred,#obj,#name
- class method:
Methods Explanation:
# === Save to DB ===
# method signature
*preds, obj: nil, desc: nil, save: default_save
# examples
UserRole. :fly # => 'Permission Definition Done' or error message
UserRole..keys.count # => 1
UserRoleGroup. *%i[read write], obj: Book.find(1) # => 'Permission Definition Done' or error message
UserRoleGroup..keys.count # => 1
# === Save in Local ===
# signature as `have_permission`
# examples
UserRole. :perform, obj: :magic # => 'Permission Definition Done' or error message
UserRole..keys.count # => 1
UserRole..keys.count # => 2
# === class methods ===
UserRole.which(name: :admin)
# as same as
UserRole.find_by_name!(:admin)
# === Permission ===
p = UserPermission.which(pred: :read, obj: Book.find(1))
p.pred == 'read'
p.obj == Book.find(1)
p.name == :read_Book_1
F. Permission Assignment
What is Wrong Assignment - Covered?
Before: he can manage User
When you do: he can manage User.find(1)
will get an Error, tell you that User is cover User.find(1), no need to assign
Overview:
- Caller: role / role group instance, like
UserRole.which(name: :admin) - methods:
- save to database:
can. aliases:has_permission - save to local variable:
temporarily_can. aliaslocally_can
- save to database:
- cancel assign method:
cannot. aliasis_not_allowed_to - helpers:
local_permissionsstored_permissionspermissions
Methods Explanation:
role = UserRole.which(name: :admin)
# === Save to DB ===
# method signature
can *preds, obj: nil, strict_mode: false, auto_define_before: auto_define_before
# examples
role.can :fly # => 'Permission Assignment Done' or error message
role. # => [<#UserPermission id: ..>]
# === Save in Local
# signature as `can`
# examples
role.temporarily_can :perform, obj: :magic # => 'Permission Assignment Done' or error message
role. # => [:perform_magic]
role..keys.count # => 3
G. Permission Querying
- Caller:
- subject instance, like
User.find(1) - role / role group instance, like
Role.which(name: :master)
notice that this caller have onlycan?andtemporarily_can?methods.
- subject instance, like
- methods:
can?cannot?can!can_each?&can_each!can_one_of!&can_one_of!temporarily_can?/locally_can?stored_can?group_can?
- helpers:
permissions_of_stored_rolespermissions_of_local_rolespermissions_of_role_groups
all the ? methods will return true or false
all the ! bang methods will return true or raise IAmICan::InsufficientPermission
Methods Examples:
he = User.take
he.can? :perform, :magic
he.cannot? :perform, :magic
he.can! :perform, :magic
he.can_each? :fly, :jump # return false if he can not fly or jump
he.can_one_of! :fly, :jump # return true if he can fly or jump
H. Shortcut Combinations - which_can
Faster way to assign, define roles and thier 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
# save in local
User.local_role_which(name: :local_role, can: [:perform], obj: :magic)
UserRole.new(name: :local_role).temporarily_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
TODO
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.