ninja_nugget
ninja_nugget

Reputation: 732

Authorize Users to perform various CRUD actions for each controller without using Pundit; Ruby on Rails

I am currently building a simple web app with Ruby on Rails that allows logged in users to perform CRUD actions to the User model. I would like to add a function where:

  1. Users can select which actions they can perform per controller; Ex: User A can perform actions a&b in controller A, whereas User B can only perform action B in controller A. These will be editable via the view.
  2. Only authorized users will have access to editing authorization rights of other users. For example, if User A is authorized, then it can change what User B will be able to do, but User B, who is unauthorized, will not be able to change its own, or anyone's performable actions.

I already have my users controller set up with views and a model

class UsersController < ApplicationController
  skip_before_action :already_logged_in?
  skip_before_action :not_authorized, only: [:index, :show]

  def index
    @users = User.all
  end
  
  def new
    @user = User.new
  end
  
  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to users_path
    else
      render :new
    end
  end

  def show
    set_user
  end

  def edit
    set_user
  end
  
  def update
    if set_user.update(user_params)
      redirect_to user_path(set_user)
    else
      render :edit
    end
  end

  def destroy
    if current_user.id == set_user.id
      set_user.destroy
      session[:user_id] = nil
      redirect_to root_path
    else
      set_user.destroy
      redirect_to users_path
    end
  end
  
  private

  def user_params
    params.require(:user).permit(:email, :password)
  end

  def set_user
    @user = User.find(params[:id])
  end
end

My sessions controller:
class SessionsController < ApplicationController
  skip_before_action :login?, except: [:destroy]
  skip_before_action :already_logged_in?, only: [:destroy]
  skip_before_action :not_authorized
  
  def new
  end
  
  def create
    user = User.find_by(email: params[:email])
    if user && user.authenticate(params[:password])
      session[:user_id] = user.id
      redirect_to user_path(user.id), notice: 'You are now successfully logged in.'
    else
      flash.now[:alert] = 'Email or Password is Invalid'
      render :new
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_path, notice: 'You have successfully logged out'
  end
end

The login/logout function works, no problem there.

I started off by implementing a not_authorized method in the main application controller which by default prevents users from accessing the respective actions if the user role is not equal to 1.

  def not_authorized
    return if current_user.nil?
    
    redirect_to users_path, notice: 'Not Authorized' unless current_user.role == 1
  end  

the problem is that I would like to make this editable. So users with role = 1 are able to edit each user's access authorization, if that makes sense.

How would I go about developing this further? I also do not want to use gems, as the sole purpose of this is for me to learn. Any insights are appreciated. Thank you!

Upvotes: 1

Views: 1809

Answers (1)

max
max

Reputation: 101811

The basics of an authorization system is an exception class:

# app/errors/authorization_error.rb
class AuthorizationError < StandardError; end

And a rescue which will catch when your application raises the error:

class ApplicationController < ActionController::Base
  rescue_from 'AuthorizationError', with: :deny_access

  private
  def deny_access
    # see https://stackoverflow.com/questions/3297048/403-forbidden-vs-401-unauthorized-http-responses
    redirect_to '/somewhere', status: :forbidden
  end
end

This avoids repeating the logic all over your controllers while you can still override the deny_access method in subclasses to customize it.

You would then perform authorization checks in your controllers:

class ThingsController
  before_action :authorize!, only: [:update, :edit, :destroy]

  def create
     @thing = current_user.things.new(thing_params)
     if @thing.save
       redirect_to :thing
     else
       render :new
     end
  end

  # ...

  private
  def authorize!
    @thing.find(params[:id])
    raise AuthorizationError unless @thing.user == current_user || current_user.admin?
  end
end

In this pretty typical scenario anybody can create a Thing, but the users can only edit things they have created unless they are admins. "Inlining" everything like this into your controllers can quickly become an unwieldy mess through as the level of complexity grows - which is why gems such as Pundit and CanCanCan extract this out into a separate layer.

Creating a system where the permissions are editable by users of the application is several degrees of magnitude harder to both conceptualize and implement and is really beyond what you should be attempting if you are new to authorization (or Rails). You would need to create a separate table to hold the permissions:

class User < ApplicationRecord
  has_many :privileges
end
class Privilege < ApplicationRecord
  belongs_to :thing
  belongs_to :user
end
class ThingsController
  before_action :authorize!, only: [:update, :edit, :destroy]
  # ...

  private
  def authorize!
    @thing.find(params[:id])
    raise AuthorizationError unless owner? || admin? || privileged?
  end

  def owner?
    @thing.user == current_user
  end

  def admin?
    current_user.admin?
  end

  def privileged?
    current_user.privileges.where(
      thing: @thing,
      name: params[:action]
    )
  end
end

This is really a rudimentary Role-based access control system (RBAC).

Upvotes: 1

Related Questions