CanCanCan System
Conventions & helpers simplifying the use of CanCanCan in complex Rails applications. CanCanCan System simplifies authorizing collaborations, memberships and more across a complex structure of models.
To describe complex abilities CanCanCan System relies on two different constructs: ActiveRecord objects, and relationships of users to those objects.
CanCanCan System uses two attributes on objects to describe abilities:
- ability: Describes the default ability of users without a special relationship with an object.
- visiblity: Specifies whether an object is visible to other users than the creator.
CanCanCan System uses one attribute on relationships to describe abilities:
- ability: Describes the ability of a user with the related object.
ability
can have any CanCanCan permission, 'admin'
(:manage
), 'user'
(:modify
) or 'guest'
(:read
) as value while visiblity
is limited to public
and private
.
Table of Contents
- Installation
- Usage
- Defining abilities
- Public abilities
- acts_as_belongable abilities
- Membership abilities
- Get abilities
- Defining abilities
- Testing
- To Do
- Contributing
- Semantic versioning
Installation
CanCanCan System works with Rails 5 onwards. You can add it to your Gemfile
with:
gem 'cancancan-system'
And then execute:
$ bundle
Or install it yourself as:
$ gem install cancancan-system
If you always want to be up to date fetch the latest from GitHub in your Gemfile
:
gem 'cancancan-system', github: 'jonhue/cancancan-system'
Now run the generator:
$ rails g cancancan_system
To wrap things up, migrate the changes to your database:
$ rails db:migrate
Usage
To get started add CanCanCan System to your Ability
class (app/models/ability.rb
) and add the required :modify
alias:
class Ability
include CanCan::Ability
include CanCanCan::System::Ability
def initialize(user)
modify([:create, :read, :update, :destroy])
end
end
Note: The aliases (:create, :read, :update, :destroy
) can be custom.
You should add the ability
attribute to ActiveRecord models you want to define abilities for:
add_column :users, :ability, :string, default: 'guest'
And you should add a visiblity
attribute to ActiveRecord models you want to define public abilities for:
add_column :users, :visiblity, :string, default: 'public'
Defining Abilities
CanCanCan System makes an abilities
method available which simplifies setting up common abilities:
def initialize(user)
abilities(Post, user)
end
This is equivalent to:
def initialize(user)
public_abilities(Post)
can(:manage, Post, user_id: user.id) if user
end
You can also use the abilities
method with custom column names and polymorphic associations. This comes in handy when using the NotificationsRails gem:
def initialize(user)
abilities(Notification, user, column: 'target', polymorphic: true, public_abilities: false)
end
Note: Set column
to nil
or ''
to use the id
attribute.
This is equivalent to:
def initialize(user)
can(:manage, Notification, target_id: user.id, target_type: user.class.name) if user
end
Learn more about the public_abilities
method here.
Public abilities
The public_abilities
method defines the object-abilities without a user
being present:
def initialize(user)
public_abilities(Post)
end
This is equivalent to:
def initialize(user)
can(:manage, Post, ability: 'admin', visibility: 'public')
can(:modify, Post, ability: 'user', visibility: 'public')
can(:read, Post, ability: 'guest', visibility: 'public')
end
acts_as_belongable abilities
CanCanCan System integrates with the acts_as_belongable gem to make defining abilities for relationships dead simple.
Let's say our users can be a member of multiple organizations:
class User < ApplicationRecord
acts_as_belongable
belongable :member_of_organizations, 'Organization', scope: :membership
has_many :organizations
end
class Organization < ApplicationRecord
acts_as_belonger
belonger :members, 'User', scope: :membership
belongs_to :user
end
We would then just do:
def initialize(user)
abilities(Organization, user) do
belonger_abilities(Organization, user, scope: :membership)
end
end
Note: This can be done in the same way with belongable_abilities
for belongable
relationships.
Now we are able to add members to our organizations and set their abilities:
Organization.first.add_belongable(User.first, scope: :membership, ability: 'admin')
Note: The scope
option is optional. If omitted, the defined abilities will apply to all belongings regardless of their scope.
Membership abilities
Now, let us assume that we have another model: Post
.
class User < ApplicationRecord
acts_as_belongable
belongable :member_of_organizations, 'Organization', scope: :membership
has_many :posts
has_many :organizations
end
class Organization < ApplicationRecord
acts_as_belonger
belonger :members, 'User', scope: :membership
has_many :posts
belongs_to :user
end
class Post < ApplicationRecord
belongs_to :user
belongs_to :organization
end
You want the posts of an organization to be accessible for its members. It doesn't get any simpler than this:
def initialize(user)
abilities(Post, user) do
membership_abilities('Organization', Post, user, scope: :membership)
end
end
Note: The scope
option is optional. If omitted, the defined abilities will apply to all belongings regardless of their scope.
You are also able to perform some customization:
class Post < ApplicationRecord
belongs_to :user
belongs_to :object, polymorphic: true
end
def initialize(user)
abilities(Post, user) do
membership_abilities('Organization', Post, user, scope: :membership, column: 'object', polymorphic: true)
end
end
Another option is to use the acts_as_belongable gem to associate posts with organizations:
class Organization < ApplicationRecord
acts_as_belonger
belonger :members, 'User', scope: :membership
belonger :posts, 'Post'
has_many :posts
belongs_to :user
end
class Post < ApplicationRecord
acts_as_belongable
belongable :organizations, 'Organization'
belongs_to :user
end
def initialize(user)
abilities(Post, user) do
organization_abilities(Post, user, scope: :membership, acts_as_belongable: true)
end
end
Note: If your acts_as_belongable
association in the Post
model is not following the CanCanCan System naming convention, you can override it by passing the column
option.
Get abilities
You can use the ability
method to get the ability of an ActiveRecord object:
Organization.first.ability
# => 'guest'
ability(Organization.first)
# => :read
It returns a symbol or nil
.
Testing
-
Fork this repository
-
Clone your forked git locally
-
Install dependencies
$ bundle install
-
Run specs
$ bundle exec rspec
-
Run RuboCop
$ bundle exec rubocop
To Do
We use GitHub projects to coordinate the work on this project.
To propose your ideas, initiate the discussion by adding a new issue.
Contributing
We hope that you will consider contributing to CanCanCan System. Please read this short overview for some information about how to get started:
Learn more about contributing to this repository, Code of Conduct
Semantic Versioning
CanCanCan System follows Semantic Versioning 2.0 as defined at http://semver.org.