Consent makes defining permissions easier by providing a clean, concise DSL for authorization
so that all abilities do not have to be in your Ability
class.
Also, Consent adds an Authorizable
model, so that you can easily grant permissions to your
ActiveRecord models.
Add this line to your application's Gemfile:
gem 'consent'
And then execute:
$ bundle
Or install it yourself as:
$ gem install consent
Then, require the engine in your application.rb
require "active_record/railtie"
require "consent/engine"
If you wish to use the activerecord adapter (accessible_by
and accessible_through
), you must load active_record/railtie
before loading the consent/engine
.
Copy and execute the migrations:
$ rails consent_engine:install:migrations
$ rails db:migrate
This will create the consent_histories
and consent_permissions
tables. If you want to use a different table prefix, you should set Consent.table_name_prefix =
before you execute the migrations. I.e.:
# config/initializers/consent.rb
require "consent"
Consent.table_name_prefix = "my_app_"
To grant permissions, you need an authorizable model. For our example we'll call it Role
:
class Role < ApplicationRecord
include ::Consent::Authorizable
end
You can now grant permissions to role with grant
, grant_all
, and grant_all!
:
role = Role.new
role.grant subject: Project, action: :update, view: :department
# OR
role.grant_all({ project: { update: :department } })
# OR
role.grant_all({ project: { update: :department } }, replace: true) # to replace everything
# OR
role.grant_all!({ project: { update: :department } }, replace: true) # to grant and save
role.permissions
=> [#<Consent::Permission subject: Project, action: :update, view: :department>]
In the above example, we're granting :department
view to perform :update
in the Project
subject.
You can now create a Consent::Ability
using the permissions granted to the role:
ability = Consent::Ability.new(user, permissions: role.permissions)
Generate permissions with the consent:permissions
generator. I.e:
$ rails g consent:permissions Project "Our Projects"
create app/permissions/projects.rb
create spec/permissions/projects_spec.rb
This will generate the permission definition:
Consent.define Project, "Our Projects" do
#in this case, Project is the subject
# and `Our Projects` is the description that makes it clear to users
# what the subject is acting upon.
…
end
We can now define the :update
action and a couple of different views:
Consent.define Project, "Our Projects" do
view :all, "All projects"
view :department, "Projects from their department" do |user|
{ department_id: user.department_id }
end
view :team, "Projects from their team" do |user|
{ team_id: user.team_id }
end
action :update, views: %i[department team all]
end
The :department
view will restrict the user to projects with matching department_id
. That
means that for Project.accessible_by(ability, :update)
, with an ability using a User with
department_id = 13, it will run a query similar to:
> user = User.new(department_id: 13)
> ability = Consent::Ability.new(user)
> ability.consent subject: Project, action: :update, view: :department
> Project.accessible_by(user).to_sql
"SELECT * FROM projects WHERE department_id = 1"
The subject is the central point of a group of actions and views. It will typically
be an ActiveRecord
, a :symbol
, or any plain ruby class.
Views are the rules that limit access to actions. For instance, a user may see a Project
from his department, but not from others. You can enforce it with a :department
view,
as in the examples below:
Probably the most commonly used. When the view can be defined using a where
scope in
an ActiveRecord context. It follows a match condition and will return all objects that meet
the criteria:
Consent.define Project, 'Projects' do
view :department, "User's department only" do |user|
{ department_id: user.id }
end
end
Although hash conditions (matching object's attributes) are recommended, the constraints can be anything you want. Since Consent does not enforce the rules, those rules are directly given to CanCan. Following CanCan rules for defining abilities is recommended.
If you're not matching for equal values, then you would need to use an object condition.
If you already have an object and want to check to see whether the user has permission to view that specific object, you would use object conditions.
If your needs can't be satisfied by hash conditions, it is recommended that a second condition is given for constraining object instances. For example, if you want to restrict a view for smaller volume projects:
Consent.define Project, 'Projects' do
view :small_volumes, "User's department only",
-> (user) {
['amount < ?', user.volume_limit]
end,
-> (user, project) {
project.amount < user.volume_limit
}
end
For object conditions, the latter argument will be the referred object, while the first will be the context given to the Permission (also check CanCan integration).
An action is anything you can perform on a given subject. In the example of Features this would look like the following using Consent's DSL:
Consent.define :features, 'Beta Features' do
action :beta_chat, 'Beta Chat App'
end
To associate different views to the same action:
Consent.define Project, 'Projects' do
# returns conditions that can be used as a matcher for objects so the matcher
# can return true or false (hash version)
view :department, "User's department only" do |user|
{ department_id: user.id }
end
view :future_projects, "User's department only",
# returns a condition to be applied to a collection of objects
-> (_) {
['starts_at > ?', Date.today]
end,
# returns true/false based on a condition -- to use this, you must pass in
# an instance of an object in order to check the permission
-> (user, project) {
project.starts_at > Date.today
}
action :read, 'Read projects', views: [:department, :future_projects]
end
If you have a set of actions with the same set of views, you can use a
with_defaults
block to simplify the writing:
with_defaults views: [:department, :small_volumes] do
action :read, 'Read projects'
action :approve, 'Approve projects'
end
Permission is what is granted to a role, or a user. It grants the ability to perform an action, on a limited scope (view) of the subject.
Consent provides a CanCan ability (Consent::Ability)
that can be initialized with a group of granted permissions. You can initialize a Consent::Ability
with:
Consent::Ability.new(*context, super_user: <true|false>, apply_defaults: <true|false>, permissions: [Consent::Permission, ...])
*context
is what is given to the view evaluating permission rules. That is typically a user;super_user
makes the ability to respond totrue
to anycan?
questions, and yields no restrictions in anyaccessible_by
andaccessible_through
queries;apply_defaults
grants actions with thedefault_view
set automatically.permissions
is a collection of permissions to grant to the user
You can manually grant permissions with consent
. You could possibly subclass
Consent::Ability
to consent some specific permissions by default:
class MyAbility < Consent::Ability
def initialize(...)
super(...)
consent action: :read, subject: Project, view: :department
end
end
You can also consent full-access by not specifying the view:
consent action: :read, subject: Project
Consenting the same permission multiple times is handled as a Union by CanCanCan:
class MyAbility < Consent::Ability
def initialize(user)
super user
consent :read, Project, :department
consent :read, Project, :future_projects
end
end
user = User.new(department_id: 13)
ability = MyAbility.new(user)
Project.accessible_by(ability, :read).to_sql
=> SELECT * FROM projects WHERE ((department_id = 13) OR (starts_at > '2021-04-06'))
Whenever the :manage
action is granted to a user through consent
or can
, that means that all actions in that subject are automatically granted with the same restrictions. Take the following example:
Consent.define User, "User permissions" do
view :all, "All users"
view :self, "Own user" do |user|
{ id: user.id }
end
action :manage, "Manage users", views: %i[self all]
action :update, "Manage users"
end
> ability = Consent::Ability.new(User.new(id: 123))
> ability.consent subject: User, action: :manage, view: :self
> User.accessible_by(ability, :manage).to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`id` = 123"
> User.accessible_by(ability, :view).to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`id` = 123"
In CanCan, :all
is a special subject which means all subjects
. Whatever is granted to :all
is applied to all subjects. I.e.:
> ability = Consent::Ability.new(User.new(id: 123))
> User.accessible_by(ability, :update).to_sql
=> "SELECT `users`.* FROM `users` WHERE 1=0"
> ability.can :update, :all
> User.accessible_by(ability, :update).to_sql
=> "SELECT `users`.* FROM `users`"
Consent is integrated into Rails with Consent::Engine
. To define where
your permission files will be, use config.consent.path
. This defaults to
#{Rails.root}/app/permissions/
to conform to Rails' standards.
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.
Bug reports and pull requests are welcome on GitHub at https://github.com/powerhome/consent.