# IAmICan [![Gem Version](https://badge.fury.io/rb/i_am_i_can.svg)](https://badge.fury.io/rb/i_am_i_can) [![Build Status](https://travis-ci.org/zhandao/i_am_i_can.svg?branch=master)](https://travis-ci.org/zhandao/i_am_i_can) [![Maintainability](https://api.codeclimate.com/v1/badges/27b664da01b6cc7180e3/maintainability)](https://codeclimate.com/github/zhandao/i_am_i_can/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/27b664da01b6cc7180e3/test_coverage)](https://codeclimate.com/github/zhandao/i_am_i_can/test_coverage) Concise and Natural DSL for `Subject - Role(Role Group) - Permission - Resource` Management (RBAC like). ```ruby # 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 1. [Concepts and Overview](#concepts-and-overview) - [In one word](#in-one-word) - [Definition and uniqueness of nouns](#definition-and-uniqueness-of-nouns) - [About role group](#about-role-group) - [Three steps to use this gem](#three-steps-to-use-this-gem) - [Two Concepts of this gem](#two-concepts-of-this-gem) - [How it work?](#how-it-work) 2. [Installation and Setup](#installation-and-setup) 3. [Usage](#usage) - [Config Options](#config-options) - [Methods and helpers](#methods-and-helpers) - [A. Role Definition](#a-role-definition) - [B. Grouping Roles](#b-grouping-roles) - [C. Role Assignment](#c-role-assignment) - [D. Role / Group Querying](#d-role--group-querying) - [E. Permission Definition](#e-permission-definition) - [F. Permission Assignment](#f-permission-assignment) - [G. Permission Querying](#g-permission-querying) - [H. Shortcut Combinations - which_can](#h-shortcut-combinations---which_can) - [I. Resource Querying](#i-resource-querying) - [J. Useful Helpers](#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 0. Subject - Someone who can be assigned roles, and who has permissions through the assigned roles. - See wiki [RBAC](https://en.wikipedia.org/wiki/Role-based_access_control) 1. Role - A job function that groups a series of permissions according to a certain dimension. - Also see wiki [RBAC](https://en.wikipedia.org/wiki/Role-based_access_control) - Uniquely identified by `name` 2. Role Group - A group of roles that may have the same permissions. - Uniquely identified by `name` 3. Permission - An action, or an approval of a mode of access to a resource - Also see wiki [RBAC](https://en.wikipedia.org/wiki/Role-based_access_control) - Uniquely identified by `predicate( + object)` (name), or we can say, `action( + resource)` 4. 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 1. 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` 2. Assignment - assign role to subject, or assign permission to role / group - instance methods, like: `user.has_role :admin` 3. 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 1. Stored (save in database) TODO 2. Temporary (save in instance variable) TODO ### How it work? Very simple. Really simple. Sooooo Simple. 1. To define something, you actually `create` records. [see here](https://github.com/zhandao/i_am_i_can/blob/master/lib/i_am_i_can/helpers/dynamic.rb#L92) 2. To assign something, you actually call one of the activerecord association methods. [see here](https://github.com/zhandao/i_am_i_can/blob/master/lib/i_am_i_can/helpers/dynamic.rb#L67) 2. To query something, you actually call the querying interfaces of activerecord. [see here](https://github.com/zhandao/i_am_i_can/tree/master/lib/i_am_i_can/subject) [Feature List: needs you](https://github.com/zhandao/i_am_i_can/issues/2) ## Installation and Setup 1. Add this line to your application's Gemfile and then `bundle`: ```ruby gem 'i_am_i_can' ``` 2. Generate migrations and models by your subject name: ```bash rails g i_am_i_can:setup ``` For example, if your subject name is `user`, it will generate model `UserRole`, `UserRoleGroup` and `UserPermission` 3. Add the code returned by the generator to your subject model, like: ```ruby 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](#config-options) is some options you can pass to the declaration. 4. Run `rails db:migrate` That's all! ## Usage ### Config Options 1. auto_definition: Auto definition before assignment if it's set to `true`. defaults to `false`. 2. strict_mode: Raise error when doing wrong definition or assignment if it's set to `true`. defaults to `false`. 3. without_group: Unable `role group` feature if it's set to `true`. defaults to `false`. 4. **relation names**: you can change the names in model declarations, defaults to `stored_roles`, `permissions`, `stored_users` and so on. ### Methods and helpers #### A. [Role Definition](https://github.com/zhandao/i_am_i_can/blob/master/lib/i_am_i_can/role/definition.rb) 1. caller: Subject Model, like `User` 2. method: `have_role`. aliases: 1. `have_roles` 2. `has_role` & `has_roles` Explanation: ```ruby # === 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 ``` #### B. [Grouping Roles](https://github.com/zhandao/i_am_i_can/blob/master/lib/i_am_i_can/role/definition.rb) **Tip:** Roles that you're going to group should be defined 1. caller: Subject Model, like `User` 2. method: `group_roles`. aliases: 1. `group_role` 2. `groups_role` & `groups_roles` 3. shortcut combination method: `have_and_group_roles` (alias `has_and_groups_roles`) it will do: roles definition && roles grouping 4. helpers: 1. relation with role (member), defaults to `members`. Explanation: ```ruby # === 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] ``` #### C. [Role Assignment](https://github.com/zhandao/i_am_i_can/blob/master/lib/i_am_i_can/role/definition.rb) 1. caller: subject instance, like `User.first` 2. assignment by calling: 1. `becomes_a`, or it's aliases: 1. `is` / `is_a_role` / `is_roles` 2. `has_role` / `has_roles` 3. `role_is` / `role_are` 2. `is_a_temporary`: just like the name, the assignment occurs only in **instance variable** not in database (will be in the cache). 3. cancel assignment by calling: 1. `falls_from`, or it's aliases: 1. `removes_role` 2. `leaves` 3. `is_not_a` / `has_not_role` / `has_not_roles` 4. `will_not_be` 2. `is_not_a_temporary` 4. replacement assignment by calling: `is_only_a`, alias `currently_is`. (makes the role collection contain only the supplied roles, by adding and deleting as appropriate) 5. callbacks - before / after / around: 1. `role_assign`: assignment 2. `cancel_role_assign`: cancel assignment 3. `role_update`: 6. helpers: 1. relation with stored role, defaults to `stored_roles`. 2. `temporary_roles` and `valid_temporary_roles` 3. `roles` 4. `assoc_with_`, like: `assoc_with_stored_roles` Explanation: ```ruby 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_: 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 ``` #### D. [Role / Group Querying](https://github.com/zhandao/i_am_i_can/blob/master/lib/i_am_i_can/subject/role_querying.rb) 1. caller: subject instance, like `User.first` 2. role querying methods: 1. `is?` / `is_role?` / `has_role?` 2. `isnt?` 3. `is!` / `is_role!` / `has_role!` 4. `is_one_of?` / `is_one_of_roles?` 4. `is_one_of!` / `is_one_of_roles!` 5. `is_every?` / `is_every_role_in?` 6. `is_every!` / `is_every_role_in!` 3. group querying methods: 1. `is_in_role_group?` / `in_role_group?` 2. `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: ```ruby 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` ``` #### E. [Permission Definition](https://github.com/zhandao/i_am_i_can/blob/master/lib/i_am_i_can/permission/definition.rb) 1. caller: Role / Role Group Model, like `UserRole` / `UserRoleGroup` 2. method: `have_permission`. aliases: 1. `have_permissions` 2. `has_permission` & `has_permissions` Explanation: ```ruby # === 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 ``` #### F. [Permission Assignment](https://github.com/zhandao/i_am_i_can/blob/master/lib/i_am_i_can/permission/assignment.rb) 1. caller: role / role group instance, like `UserRole.which(name: :admin)` 2. assignment by calling `can`. alias `has_permission` 3. cancel assignment by calling `cannot`. alias `is_not_allowed_to` 4. replacement assignment by calling: `can_only`,. (makes the permission collection contain only the supplied permissions, by adding and deleting as appropriate) 5. callbacks - before / after / around: 1. `permission_assign`: assignment 2. `cancel_permission_assign`: cancel assignment 3. `permission_update`: replacement assignment 4. helpers: 1. relation with stored permission, defaults to `permissions`. Explanation: ```ruby 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 ``` #### G. [Permission Querying](https://github.com/zhandao/i_am_i_can/blob/master/lib/i_am_i_can/subject/role_querying.rb) 1. caller: 1. subject instance, like `User.find(1)` 2. role / role group instance, like `Role.which(name: :master)` (only have `can?` method) 2. methods: 1. `can?` 2. `cannot?` 3. `can!` 4. `can_each?` & `can_each!` 4. `can_one_of!` & `can_one_of!` 5. `temporarily_can?` 6. `stored_can?` 7. `group_can?` all the `?` methods will return `true` or `false` all the `!` bang methods will return `true` or raise `IAmICan::InsufficientPermission` Examples: ```ruby 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. ```ruby # === 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 1. caller: Resource Collection or Instance 2. scopes: 1. `that_allow` Explanation: ```ruby # === 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 1. for Subject (e.g. User) ```ruby # declaration in User has_and_belongs_to_many :identities # stored_roles # 1. [scope] with_ # is the same as `includes(:stored_roles)` for avoiding N+1 querying User.with_identities.where(identities: { name: 'teacher' }) ``` 2. for Role / RoleGroup (e.g. UserRole) ```ruby # 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] # 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_ # is the same as `includes(:permissions)` for avoiding N+1 querying UserRole.with_permissions.where(permissions: { id: 1 }) ``` 3. for `Permission` (e.g. UserPermission) ```ruby # 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] # 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](https://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](http://contributor-covenant.org) code of conduct. ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). ## 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](https://github.com/[USERNAME]/i_am_i_can/blob/master/CODE_OF_CONDUCT.md).