Policies
Policies is an authorization control library for Ruby on Rails.
It was primarily designed for use in applications where a user's authorization may change depending on a particular context. For example, in an application where users may belong to one or more projects, it may be ideal for them to edit the settings of a project they own, but not necessarily edit the settings of a project in which they are a member.
This gem helps facilitate the creation of those authorization rules through simple, well defined Ruby classes.
Installation
In your Gemfile, include the policies
gem.
gem 'policies'
Prerequisites
Policies makes a few logical assumptions for the ease of implementation.
- It requires a
current_user
method to be defined.
```ruby
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
```
- It requires a
current_role
method to be defined.
```ruby
# On an intermediary object, such as membership
def current_role
if @project.present? && @project.persisted?
@current_role ||= @project.memberships.find_by(user: current_user).role
end
end
# On the user
def current_role
@current_role ||= current_user.role
end
```
- The names of policy classes must be a combination of an object's class suffixed with
Policy
. For example, a policy for projects should be namedProjectPolicy
, and a policy for users should be namedUserPolicy
. It is recommended to place policies in anapp/policies
directory. - Policies should inherit from
Policies::Base
. - Method names within a policy should be suffixed with a
?
.
Getting Started
Take the following example, in which a user may belong to one or more projects through an intermediary membership.
# app/models/user.rb
class User < ActiveRecord::Base
has_many :memberships
has_many :projects, through: :memberships
end
# app/models/project.rb
class Project < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships
end
# app/models/membership.rb
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
# app/models/role.rb
class Role < ActiveRecord::Base
def member?
%w(Member Administrator Owner).include?(name)
end
def admin?
%w(Administrator Owner).include?(name)
end
def owner?
name == 'Owner'
end
end
Imagine a user is an owner of Project A and a member of Project B. In this specific case, the role of the user will change depending on which project they are viewing. Owners of a project should have the ability to edit its settings or invite new members, while members of a project should only be allowed to view it.
With that in mind, a new policy class may be created to limit the authorization depending on the current role.
Creating a New Policy
Within app/policies
, create a new file named project_policy.rb
. Remember to restart your application server to
pick up the new directory.
# app/policies/project_policy.rb
class ProjectPolicy < Policies::Base
end
Limiting Access
Let's assume we want to limit the edit and update actions to a project owner.
# app/policies/project_policy.rb
class ProjectPolicy < Policies::Base
def edit?
current_role.owner?
end
alias_method :update?, :edit?
end
An instance variable named after the object's class is also available for use within the policy.
# app/policies/project_policy.rb
class ProjectPolicy < Policies::Base
def destroy?
@project.can_be_destroyed? && current_role.owner?
end
end
Using a different example, a user may only be allowed to edit their own account.
# app/policies/user_policy.rb
class UserPolicy < Policies::Base
def edit?
current_user == @user
end
alias_method :update?, :edit?
end
Updating Views and Controllers
After the policy is written, views may be updated with the authorized?
helper.
<% if authorized?(:edit, @project) %>
<%= link_to @project, project_path(@project) %>
<% end %>
Controllers may be updated with the authorize
and authorized?
methods.
# app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
def edit
@project = current_user.projects.find(params[:id])
authorize(@project)
end
def update
@project = current_user.projects.find(params[:id])
authorize(@project)
if @project.update(project_params)
redirect_to @project, success: translate('.success')
else
render :edit
end
end
end
A better, more DRY approach may be using authorize
in a before_action
.
# app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
before_action :set_project, only: [:edit, :update]
def update
if @project.update(project_params)
redirect_to @project, success: translate('.success')
else
render :edit
end
end
private
def set_project
@project = current_user.projects.find(params[:id])
authorize(@project)
end
end
authorize
will raise Policies::UnauthorizedError
if the user is restricted from accessing the particular action.
authorized?
may be used when a boolean should be returned. If no action argument is passed, it will default to the
current action.
# app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
def edit
@project = Project.find(params[:id])
if authorized?(@project)
...
else
redirect_to projects_path, error: translate('.unauthorized')
end
end
end
In a situation where an instantiated object is not available, a symbol may be passed to authorized?
and authorize
.
If no action argument is passed, it defaults to the current action_name
.
<% if authorized?(:index, :projects) %>
<%= link_to @project, projects_path %>
<% end %>
# app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
def index
authorize(:projects)
@projects = current_user.projects
end
end
Acknowledgments
Special thanks to Pundit for the inspiration for this project.