First, it’s important to acknowlege that CanCanCan is very easy to get started with and has great integrations with RailsAdmin, Devise and other gems. All permissions are defined in
ability.rb but with time this file can grow quite large. Other downsides are inability to define field level permissions and unit testing role permissions separately from other code.
Pundit separates permissions into individual policy classes which can inherit from each other. So you treat them as POROs with methods.
- Grouping policies
- Mapping roles to permissions
- Beyond RESTful actions
- Require authorize in application controller for all actions
- Headless policies
- Field level permissions
- Usefull links
Frequently you have lots of models that need to share the same permsisions. So you might not want to create policy files to each model and duplicate code. Since your policy files are just Ruby classes you can do this:
Your models may look like this. We are using Mongoid but the same design would work with ActiveRecord.
This can be a common case with Single Table Inhertiance. Often the permissions for the different models derived form the same base model are the same so you could share policies.
Alternatively you could create separate policies for Client, Account and Company and then you would not need to do
self.policy_class. You also could specify more granular permissions for Client, Account and Company models if needed.
Mapping roles to permissions
To keep things simple users can belong to only one client.
admin can edit it’s client record and do CRUD on client children records.
readonly_admin can only view all records,
account_admin can do CRUD operations on accounts and
company_admin can do the same for company records. For this we needed to create separate policies for Client, Account and Company models.
Additionally there are system wide roles (for internal users) defined directly on User model. Only internal users can create/destroy create new clients but Client Admins can modify Client attributes.
This will give internal users access to all records.
So this works great for granting application wide permissions but client specific users need to be resticted more. Additionally when we are in
destroy we can get client from the record. In
index we have multiple records and in
create the record does not exist yet so we need to get client from user.
This will give readonly access to Client records via
readonly_admin and edit/update access to other roles.
@user.user_clients.where(client: @record.client).in(roles: ...) is not DRY so we can extract it into separate class.
Permissions for Account and Company are a little different.
You also could use Rolify gem to map users to roles but we already had UserClient model for other reasons so we leveraged that.
Beyond RESTful actions
You start with
:show?, etc but then you need to define more custom permissions. Let’s say user has to be
These kinds of custom actions will usually be specific to only one model but if they are common to several you could push them into lower policy class and inherit from it in the model specific policy.
To check these custom permissions you could create a non-RESTful action in your AccountsController.
Then you just call
Require authorize in application controller for all actions
I personally prefer to require authorize for all controller actions even I put
def index? true; end to give everyone access.
Let’s say you have
report_admin role that allows user to run various reports from the dashboard.
Make sure your policy file only contains the basic permission check. When you run
rails g pundit:policy dashboard it will include placeholder for
class Scope < Scope. Otherwise you hit this github issue.
Internal users can see all records but client specific users can see only accounts and companies scoped to that client.
Field level permissions
Sometimes you need to define permissions on specific field w/in record. Let’s say that that only
sysadmin can edit Client status field.
You also want to show/hide the Status field in the Client edit page. Just call
I am working on a better solution to use CSS visibility or disabled attributes and push the logic into decorator.
In traditional erb/haml server generated UI you can use check recommended on Pundit wiki page.
But what if you are building Single Page Application? We used ActiveModelSerializers and dynamically added methods with
define_method. You could even push some of the common actions into
Your controller responds with either HTML or JSON output.
Now your frontend application JS can use output from
http://localhost:3000/accounts.json to check permissions and show/hide/disable appropriate UI controls.
Testing these policies interaction with the common
RoleCheck code can get quite repetitive. That’s where stubbing can be a valuable tool. This will simulate passing user, client and roles parameters to RoleCheck and returning true or nil.
Also checkout pundit-matchers gem.