Roles from Scratch

Episode #352 by Teacher's Avatar David Kimura

Summary

In this episode, we look at different authorization approaches from the most simple to more complex scenarios.
rails model authorization 15:11

Resources

This episode is sponsored by Honeybadger

Download Source Code

Summary

Simple User Roles

# Terminal
rails g migration add_role_to_users role:integer

# AddRoleToUsers Migration
class AddRoleToUsers < ActiveRecord::Migration[7.0]
  def change
   add_column :users, :role, :integer, default: 0, limit: 1
  end
end

# models/user.rb
  enum role: {
    normal: 0,
    admin: 1
  }

# users_controller.rb
def user_params
  allowed_attributes = [:email, :name]
  if user_signed_in? && current_user.admin?
    allowed_attributes << :role
  end
  params.require(:user).permit(allowed_attributes)
end

# Usage
if current_user.admin?

More Complex Example

# Terminal
rails g model role name reference access:integer
rails g model user_role user:belongs_to role:belongs_to

# CreateRoles Migration
class CreateRoles < ActiveRecord::Migration[7.0]
  def change
    create_table :roles do |t|
      t.string :name
      t.string :reference
      t.integer :access, limit: 1, default: 0

      t.timestamps
    end
  end
end

# db/seeds.rb
admin = User.create(email: "admin@example.com", password: "123456", password_confirmation: "123456")
editor = User.create(email: "editor@example.com", password: "123456", password_confirmation: "123456")
User.create(email: "guest@example.com", password: "123456", password_confirmation: "123456")

admin_user_role = Role.create(name: "Admin User", reference: "User", access: :createable)
admin_post_role = Role.create(name: "Admin Post", reference: "Post", access: :createable)
editor_post_role = Role.create(name: "Editor Post", reference: "Post", access: :editable)

admin.user_roles.create(role: admin_user_role)
admin.user_roles.create(role: admin_post_role)
editor.user_roles.create(role: editor_post_role)

# models/role.rb
class Role < ApplicationRecord
  has_many :user_roles, dependent: :destroy

  enum access: {
    viewable: 0,
    createable: 1,
    editable: 2,
    no_access: 3
  }
end

# models/user_role.rb
class UserRole < ApplicationRecord
  belongs_to :user
  belongs_to :role
end

# models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :user_roles, dependent: :destroy
  has_many :roles, through: :user_roles

  def can_edit?(resource)
    resource_class = resource.class.to_s == "Class" ? resource.name : resource.class.to_s
    role = roles.where(reference: resource_class)
    return false unless role

    role.map(&:editable?).any? || role.map(&:createable?).any?
  end

  def can_create?(resource)
    resource_class = resource.class.to_s == "Class" ? resource.name : resource.class.to_s
    role = roles.where(reference: resource_class)
    return false unless role

    role.map(&:createable?).any?
  end
end

# Rails Console
user.can_edit?(post)
user.can_create?(post)
user.can_edit?(Post)
user.can_create?(Post)

# helpers/application_helper.rb
module ApplicationHelper

  def can_edit?(resource)
    return false unless user_signed_in?

    current_user.can_edit?(resource)
  end

  def can_create?(resource)
    return false unless user_signed_in?

    current_user.can_create?(resource)
  end
end

# views/posts/index.html.erb
<%= link_to "Edit", edit_post_path(post) if can_edit?(post) %>