Skip to content

(RBAC like) Concise and Natural DSL for `Subject - Role(Role Group) - Permission - Resource` Management.

License

Notifications You must be signed in to change notification settings

zhandao/i_am_i_can

Repository files navigation

IAmICan

Gem Version Build Status Maintainability Test Coverage

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

  1. Concepts and Overview

  2. Installation and Setup

  3. Usage

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

  1. Subject
    • Someone who can be assigned roles, and who has permissions through the assigned roles.
    • See wiki RBAC
  2. Role
    • A job function that groups a series of permissions according to a certain dimension.
    • Also see wiki RBAC
    • Uniquely identified by name
  3. Role Group
    • A group of roles that may have the same permissions.
    • Uniquely identified by name
  4. 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)
  5. 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
  2. To assign something, you actually call one of the activerecord association methods. see here
  3. To query something, you actually call the querying interfaces of activerecord. see here

Feature List: needs you

Installation and Setup

  1. Add this line to your application's Gemfile and then bundle:

    gem 'i_am_i_can'
  2. 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 model UserRole, UserRoleGroup and UserPermission

  3. 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.

  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

  1. caller: Subject Model, like User
  2. method: have_role. aliases:
    1. have_roles
    2. 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

  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:

# === 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]
  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_<roles>, like: assoc_with_stored_roles

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
  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?
    5. is_one_of! / is_one_of_roles!
    6. is_every? / is_every_role_in?
    7. 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:

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`
  1. caller: Role / Role Group Model, like UserRole / UserRoleGroup
  2. method: have_permission. aliases:
    1. have_permissions
    2. 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
  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
  6. helpers:
    1. relation with stored permission, defaults to permissions.

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
  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!
    5. can_one_of! & can_one_of!
    6. temporarily_can?
    7. stored_can?
    8. 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

  1. caller: Resource Collection or Instance
  2. scopes:
    1. 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

  1. 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' })
  2. 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 })
  3. 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.

About

(RBAC like) Concise and Natural DSL for `Subject - Role(Role Group) - Permission - Resource` Management.

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published