Skip to content

Instantly share code, notes, and snippets.

@tbcooney
Last active November 25, 2021 12:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tbcooney/aca326ab31d2d3ba820ec092be00a7cb to your computer and use it in GitHub Desktop.
Save tbcooney/aca326ab31d2d3ba820ec092be00a7cb to your computer and use it in GitHub Desktop.

Here is a plain-ruby approach for a flexible and expendable authorization solution to authorize actions that a user can? perform within an account or organization based on their access_level.

class CreateMembers < ActiveRecord::Migration[6.0]
  def change
    create_table :members do |t|
      t.references :account, null: false, foreign_key: true
      t.integer :access_level
      t.references :user, foreign_key: true

      t.timestamps
    end
    add_index :members, :access_level
  end
end

In a single class Ability, we have defined a number of class methods to set the permissions. For instance, for a Manager:

# app/models/ability.rb
def manager_rules
  @manager_rules ||= staff_rules + [
    :edit_post
  ]
end

Ability has defined an allowed? method that check whether a permission exists for a certain object, based on the abilities that you've defined for that class (such as above for a Manager). To make use of a similar syntax as the often-used Cancan authorization gem, you can use a similar can? method with Ability:

# app/models/user.rb
def can?(object, action, subject)
  Ability.allowed?(self, action, subject)
end

Then all we have to do is check the permission wherever we need it. For instance to check whether someone is allowed to edit a Post:

def destroy
  return access_denied! unless can?(current_user, :edit_post, @post)

  @post.update(post_params)
  redirect_to @post
end
@tbcooney
Copy link
Author

tbcooney commented Nov 21, 2021

Library module to define access levels

# frozen_string_literal: true

#
# Define allowed roles that can be used
# to determine authorization level
#
module Access
  # Member access levels
  NO_ACCESS = 0
  STAFF     = 10
  REPORTER  = 20
  MANAGER   = 30
  OWNER     = 40

  class << self
    def values
      options.values
    end

    def all_values
      options_with_owner.values
    end

    def options
      {
        "Staff"     => STAFF,
        "Reporter"  => REPORTER,
        "Manager"   => MANAGER
      }
    end

    def options_with_owner
      options.merge(
        "Owner" => OWNER
      )
    end
  end

  def owner?
    access_level == OWNER
  end
end

@stevepolitodesign
Copy link

I really like how simple this is. I do have a few questions:

  1. What do you think about updating the can? method to be called on an instance of a user?
# app/models/user.rb
def can?(action, subject)
  Ability.allowed?(self, action, subject)
end

current_user.can?(:edit_post, @post)

I think it reads a little better this way, but maybe I'm overlooking why you had an object parameter 🤔

  1. Is there a way to set default values?
  2. Is there a way to dynamically set an ability? For example, you may want someone with a low access level to still be able to edit their post, but not others.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment