https://github.com/zhandao/i_am_i_can
(RBAC like) Concise and Natural DSL for `Subject - Role(Role Group) - Permission - Resource` Management.
https://github.com/zhandao/i_am_i_can
Last synced: 9 months ago
JSON representation
(RBAC like) Concise and Natural DSL for `Subject - Role(Role Group) - Permission - Resource` Management.
- Host: GitHub
- URL: https://github.com/zhandao/i_am_i_can
- Owner: zhandao
- License: mit
- Created: 2018-09-22T08:22:20.000Z (over 7 years ago)
- Default Branch: master
- Last Pushed: 2023-04-07T19:05:55.000Z (about 3 years ago)
- Last Synced: 2025-08-13T06:52:51.024Z (10 months ago)
- Language: Ruby
- Homepage:
- Size: 182 KB
- Stars: 4
- Watchers: 1
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.txt
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# IAmICan
[](https://badge.fury.io/rb/i_am_i_can)
[](https://travis-ci.org/zhandao/i_am_i_can)
[](https://codeclimate.com/github/zhandao/i_am_i_can/maintainability)
[](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).