Skip to content

Instantly share code, notes, and snippets.

@RStankov
Created May 29, 2018 07:08
Show Gist options
  • Save RStankov/0554b168d1a21cd33bc6885ee96e2dde to your computer and use it in GitHub Desktop.
Save RStankov/0554b168d1a21cd33bc6885ee96e2dde to your computer and use it in GitHub Desktop.

Idea

Top level API

ApplicationPolicy.can? user, :new, Post
ApplicationPolicy.can? user, :create, post
ApplicationPolicy.can? user, :edit, post
ApplicationPolicy.can? user, :register_ship

ApplicationPolicy.authorize! user, :new, Post
ApplicationPolicy.authorize! user, :new, Post
ApplicationPolicy.authorize! user, :create, post
ApplicationPolicy.authorize! user, :edit, post
ApplicationPolicy.authorize! user, :sign_for_ship

Define policy objects

module Ship::Policy
  extend Authorization::Policy

 # this is the same as `def can_maintain_ship_account?`
 can :maintain, ShipAccount do |user, account|
   user.admin? || member?(user, account)
 end

  can :create, UpcomingPageMessage do |user, message|
    # nested rules, all permissions are just methods
    can_maintain_ship_account?(user, message.account) && message.account.active?
  end

  # support for single rules
  can :update_account do |user|
    # ...
  end

  # allow for guest access
  can :sign_for_ship, guest: true do |user|
    !user
  end

  # private methods for helpers
  private

  def member?(user)
    # ...
  end
end

can is just a convince helper to create methods on a module:

Ship::Policy#can_maintain_ship_account?(user)
Ship::Policy#can_upcoming_page_message?(user)
Ship::Policy#can_update_account?(user)

Then all policies are grouped into a central application policy

module ApplicationPolicy
  extend Authorization::Facade

  extend Posts::Policy
  extend Ship::Policy
end

Implementation

quick and dirty

module Authorization
  class AccessDenied < StandardError
    attr_reader :user, :action, :subject

    def initialize(user, action, subject)
      @user = user
      @action = action
      @subject = subject

      super 'Not authorized'
    end
  end

  module Policy
    def can(actions, subject = nil, allow_guest: false, &block)
      Array(actions).each do |action|
        define_method Utils.rule_name(action, subject) do |*args|
          (args[0] || allow_guest) && !!block.call(*args)
        end
      end
    end
  end

  module Facade
    def can?(user, action, subject = nil)
      public_send Utils.name(action, subject), user, subject
    end

    def authorize!(*args)
      raise AccessDenied unless can?(*args)
    end
  end

  module Utils
    extend self

    def rule_name(action, subject)
      name = "can_#{ action }"
      name << "_#{ subject_to_string(subject).underscore.gsub('/', '_') }" if subject
      name << '?'
    end

    private

    def subject_to_string(subject)
      case subject
      when Class, String, Symbol then subject.to_s
      else subject.class.to_s
      end
    end
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment