RoleCore is a Rails engine which could provide essential industry of Role-based access control.
The dummy app shows a simple multiple roles with CanCanCan integration including a management UI.
Clone the repository.
$ git clone https://github.com/rails-engine/role_core.git
Change directory
$ cd role_core
Run bundler
$ bundle install
Preparing database
$ bin/rails db:migrate
Start the Rails server
$ bin/rails s
Open your browser, and visit http://localhost:3000
The essence of RBAC is the role, despite your application, there are many possibilities: single-role, multi-roles, extendable-role and the role may associate to different kinds of resources (e.g: users and groups)
RoleCore provides an essential definition of Role, you have to add association to adapt to your application, for example:
- single-role: adding
one-to-many
association between Role and User - multi-roles: adding
many-to-many
association between Role and User - extendable-role: adding a self-association to Role
- polymorphic-asscociated-role: consider using polymorphic association technique
Although it's not out-of-box, but it will give you fully flexibility to suit your needs.
RoleCore provides a DSL (which inspired by Redmine) that allows you define permissions for your application.
Empowered by virtual model technique,
these permissions your defined can be persisted through serialization,
and can be used with OO-style, for example: role.permissions.project.create?
There also support permission groups, and groups support nesting.
You don't need to any migration when you changing definitions.
I18n is supported too.
In fact, the essence of permissions is Hash, keys are permissions, and values are booleans. so computing of permissions with many roles, can be understood as computing of Hashes.
Building a management UI is difficult, but virtual model technique will translates permissions to a virtual model's (a class that conforms to ActiveModel) attributes, and groups will translates to nested virtual models, that means you can use all Rails view helpers including the mighty form builder, and can benefit to Strong Parameter.
The dummy app shows that rendering a permission list only about 20 lines.
If your application is API-only, you can simply dumping the role's permissions to JSON, and can still be benefit to StrongParameter.
RoleCore DOES NOT handle the authentication or authorization directly, you have to integrate with CanCanCan, Pundit or other solutions by yourself.
RoleCore can be working with CanCanCan, Pundit easily and happily.
Add this line to your Gemfile:
gem 'role_core'
Then execute:
$ bundle
Copy migrations
$ bin/rails role_core:install:migrations
Then do migrate
$ bin/rails db:migrate
Run config generator
$ bin/rails g role_core:config
Run model generator
$ bin/rails g role_core:model
Permissions are defined in config/initializers/role_core.rb
,
checking it to know how to define permissions.
In addition, there also includes a directive about how to integrate with CanCanCan.
Check config/locales/role_core.en.yml
In order to obtain maximum customizability, you need to hooking up role(s) to your user model by yourself.
Generate one-to-many
migration, adding role_id
to User
model
$ bin/rails g migration AddRoleToUsers role:references
Then do migrate
$ bin/rails db:migrate
Declare a User belongs to a Role
association
class User < ApplicationRecord
belongs_to :role
# ...
end
Declare a Role has many Users
association
class Role < RoleCore::Role
has_many :users
end
Permissions you've defined will translate to a virtual model (a Class which implemented ActiveModel interface),
permission
would be an attribute, group
would be a nested virtual model (like ActiveRecord's has_one
association).
So you can simply check permission like:
user.role.permissions.read_public?
user.role.permissions.project.read? # `project` is a `group`
For better usage, you may delegate the permissions
from Role
model to User
:
class User < ApplicationRecord
belongs_to :role
delegate :permissions, to: :role
# ...
end
Then you can
user.permissions.read_public?
user.permissions.project.read?
Keep in mind: fetching role
will made a SQL query, you may need eager loading to avoid N+1 problem in some cases.
Generate a many-to-many
intervening model
$ bin/rails g model RoleAssignment user:references role:references
Then do migrate
$ bin/rails db:migrate
Declare a User has many Roles through RoleAssignments
association
class User < ApplicationRecord
has_many :role_assignments, dependent: :destroy
has_many :roles, through: :role_assignments
# ...
end
Declare a Role has many Users through RoleAssignments
association
class Role < RoleCore::Role
has_many :role_assignments, dependent: :destroy
has_many :users, through: :role_assignments
end
Permissions you've defined will translate to a virtual model (a Class which implemented ActiveModel interface),
permission
would be an attribute, group
would be a nested virtual model (like ActiveRecord's has_one
association).
So you can simply check permission like:
user.roles.any? { |role| role.permissions.read_public? }
user.roles.any? { |role| role.permissions.project.read? } # `project` is a `group`
For better usage, you could declare a can?
helper method:
class User < ApplicationRecord
has_many :role_assignments, dependent: :destroy
has_many :roles, through: :role_assignments
def can?(&block)
roles.map(&:permissions).any?(&block)
end
# ...
end
Then you can
user.can? { |permissions| permissions.read_public? }
user.can? { |permissions| permissions.project.read? }
Keep in mind: fetching roles
will made a SQL query, you may need eager loading to avoid N+1 problem in some cases.
Just call permissions' method (see checking permission
above) in Pundit's policy.
e.g:
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def update?
user.permissions.post.update?
end
def update_my_own?
return true if user.permissions.post.update?
return unless user.permissions.post.update_my_own?
post.author == user
end
end
Open config/initializers/role_core.rb
, uncomment CanCanCan integration codes and follows samples to define permissions for CanCanCan
Open your User model:
-
For a user who has single role:
Add a delegate to User model:
delegate :computed_permissions, to: :role
-
For a user who has multiple roles:
Add a
computed_permissions
public method to User model:def computed_permissions roles.map(&:computed_permissions).reduce(RoleCore::ComputedPermissions.new, &:concat) end
Open app/models/ability.rb
, add user.computed_permissions.call(self, user)
to initialize
method.
You can check dummy app for better understanding.
See RolesController in dummy app and relates view for details.
RoleCore is a Rails engine, and following the official best practice, so you can extend RoleCore by the article suggests.
For some reason, you want to use RoleCore's ability and keeping use your own role model, e.g: integrate with rolify.
You can archive this goal by:
- Modify the migration file which RoleCore generated, changing the role table name
- Add
include RoleCore::Concerns::Models::Role
to your role model
Note: If you want another column name or there's no name in your role model, you need to lookup RoleCore::Concerns::Models::Role
source code, copy and modify to fit your needs
By design, RoleCore is for static permissions, but dynamic permissions is easy to support.
The key is RoleCore::PermissionSet#derive
, that will generate a new anonymous sub-class of PermissionSet
.
Here's an example:
- Design a model to persisting dynamic permissions, e.g
DynamicPermission(id: bigint, name: string, default: bool)
- Add
dynamic_permissions
column (text
type) to Role model to persisting dynamic permissions' configuration, and in your modelserialize :dynamic_permissions, Hash
- Generate dynamic permissions set in runtime
# Create a new permission set to containerize dynamic permissions # `"Dynamic"` can be named to other but must be a valid Ruby class name, # that's a hacking for `ActiveModel::Naming`, # and can be used as I18n key, in this case, the key is `role_core/models/dynamic`. dynamic_permission_set_class = PermissionSet.derive "Dynamic" # Fetching dynamic permissions dynamic_permissions = DynamicPermission.all # Mapping dynamic permissions to permission set dynamic_permission_set_class.draw do dynamic_permissions.each do |p| permission p.name, default: p.default end end
- Create a instance of this dynamic permission set
dynamic_permissions = dynamic_permission_set_class.new(role.dynamic_permissions)
- You can use the
dynamic_permissions
now
Rails' serialize
is static so you must do serialize and deserialize manually, you can wrap these logic into model.
Bug report or pull request are welcome.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
Please write unit test with your code if necessary.
The gem is available as open source under the terms of the MIT License.