Project

policies

0.0
No commit activity in last 3 years
No release in over 3 years
Authorization control
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

 Project Readme

Policies

Gem Version

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.

  1. 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
```
  1. 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
```
  1. 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 named ProjectPolicy, and a policy for users should be named UserPolicy. It is recommended to place policies in an app/policies directory.
  2. Policies should inherit from Policies::Base.
  3. 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.