Rails 6: How to add authorization to your application
Tags: Ruby, Tutorial, Authorization, Ruby on Rails, Rails

This story covers the topic of adding authorization to a Ruby on Rails application. In general authorization means the definition of access policies e.g. which stories are editable by a signed in user on Medium. For defining access rules, you always have a relationship between an authenticated entity and resources that needs to be authorized.

Step 1: A dead simple authorization on the console

current_user = User.find_by(username: 'woodpecker21')

editable_stories = Story.where(author: current_user)

In the example above, we retrieve all stories of which the user woodpecker21 is the author and therefore has the rights to edit them.

The authentication of the user is a not part of this story, and thus a simple find_by . You need to implement a proper authentication logic in advance, e.g. by adding device to your application.

Step 2: Adding the authorization to a controller

The next step shows you how to extend a controller, so that it implements the rules defined in step 1. The current_user helper method will be available out of the box for most authentication frameworks.

class StoriesController < ApplicationController

before_action :set_story, only: [:edit, :update, :destroy]



def index

@stories = Story.all

end



def show

@story = Story.find(params[:id])

end



def new

@story = Story.new

end



def edit

end



def create

@story = Story.new(story_params)



if @story.save

redirect_to @story, notice: 'Story was successfully created.'

else

render :new

end

end



def update

if @story.update(story_params)

redirect_to @story, notice: 'Story was successfully updated.'

else

render :edit

end

end



def destroy

@story.destroy

redirect_to stories_url, notice: 'Story was successfully destroyed.'

end



private



def set_story

@story = Story.where(author: current_user).find(params[:id])

end



def story_params

params.fetch(:story, {})

end

end

The code above limits the scope of a user by saying Story.where(author: current_user).find(params[:id]) . In case the user is not the owner of the story, she will receive a 404 error when trying to edit, update or destroy the record.

Step 3: Polish up with an authentication service

While the solution above might work quite well in the beginning, there are few improvements which can be solved more elegant:

Moving the authentication logic from the controller to a service model

Ensuring every controller authorize its resources

Hiding buttons and links in the views in case of insufficient permissions

Inheriting permissions

In the next example we use a gem called consul. It’s an actively maintained alternative to pundit and cancan.

Add this line to your Gemfile:

gem 'consul'

Create a service model app/models/power.rb that handles all authorization. In the context of consul, this is called power.

class Power

include Consul::Power



def initialize(user)

@user = user

end



power :stories do

Story.all

end



power :creatable_stories do

stories

end



power :updatable_stories do

Story.where(author: @user)

end



power :destroyable_stories do

updatable_stories

end

end

Extend you application controller app/controllers/application_controller.rb to include some helper methods, that we can use in every other controller and view context.

class ApplicationController < ActionController::Base

include Consul::Controller require_power_check current_power do

Power.new(current_user)

end



end

And finally adjust the stories controller, to delegate the authorization to the power.

class StoriesController < ApplicationController power :stories, map: {

[:edit, :update] => :updatable_stories,

[:new, :create] => :creatable_stories,

[:destroy] => :destroyable_stories

}, as: :stories_scope before_action :set_story, only: [:show, :edit, :update, :destroy]



def index

@stories = stories_scope.to_a

end



def show

end



def new

@story = stories_scope.new

end



def edit

end



def create

@story = stories_scope.new(story_params)



if @story.save

redirect_to @story, notice: 'Story was successfully created.'

else

render :new

end

end



def update

if @story.update(story_params)

redirect_to @story, notice: 'Story was successfully updated.'

else

render :edit

end

end



def destroy

@story.destroy

redirect_to stories_url, notice: 'Story was successfully destroyed.'

end



private



def set_story

@story = stories_scope.find(params[:id])

end



def story_params

params.fetch(:story, {})

end

end

Now lets see how the authentication is changed with consul. By default, we map all actions to the power stories . Then we create a different mapping for :edit , :update , :new , :create and :destroy . And finally provide this scope via the method stories_scope . The power is resolved with the current_power helper method.

power :stories, map: {

[:edit, :update] => :updatable_stories,

[:new, :create] => :creatable_stories,

[:destroy] => :destroyable_stories

}, as: :stories_scope

To get a better understanding of this DSL the summary below might help you to understand the underlying mechanisms:

Action index : stories_scope = Power.new(current_user).stories

: Action show : stories_scope = Power.new(current_user).stories

: Action new : stories_scope = Power.new(current_user).creatable_stories

: Action edit : stories_scope = Power.new(current_user).updatable_stories

: Action create : stories_scope = Power.new(current_user).creatable_stories

: Action update : stories_scope = Power.new(current_user).updatable_stories

: Action destroy : stories_scope = Power.new(current_user).destroyable_stories

In the views we can hide buttons, if the required action is not accessible.

if current_power.updatable_story?(@story)

= link_to('Edit story', story_path(@story), class: 'button')

end

Finally the require_power_check statement ensures, that every controller defines the power method, so you never forget to protect your resources.

Outline