Skip to content

Instantly share code, notes, and snippets.

@ciembor
Last active August 27, 2018 14:01
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 ciembor/8985278ee02bb25b4bf9c2ad68839571 to your computer and use it in GitHub Desktop.
Save ciembor/8985278ee02bb25b4bf9c2ad68839571 to your computer and use it in GitHub Desktop.
require "pundit/version"
require "active_support/concern"
require "active_support/core_ext/string/inflections"
require "active_support/core_ext/object/blank"
require "active_support/core_ext/module/introspection"
require "active_support/dependencies/autoload"
module Pundit
  class PolicyFinder
    attr_reader :object
    def initialize(object)
      @object = object
    end
    def scope
      "#{policy}::Scope".safe_constantize
    end
    def policy
      klass = find(object)
      klass.is_a?(String) ? klass.safe_constantize : klass
    end
    def scope!
      scope or raise NotDefinedError, "unable to find scope `#{find(object)}::Scope` for `#{object.inspect}`"
    end
    def policy!
      policy or raise NotDefinedError, "unable to find policy `#{find(object)}` for `#{object.inspect}`"
    end
    def param_key
      model = object.is_a?(Array) ? object.last : object
      if model.respond_to?(:model_name)
        model.model_name.param_key.to_s
      elsif model.is_a?(Class)
        model.to_s.demodulize.underscore
      else
        model.class.to_s.demodulize.underscore
      end
    end
    private
    def find(subject)
      if subject.is_a?(Array)
        modules = subject.dup
        last = modules.pop
        context = modules.map { |x| find_class_name(x) }.join("::")
        [context, find(last)].join("::")
      elsif subject.respond_to?(:policy_class)
        subject.policy_class
      elsif subject.class.respond_to?(:policy_class)
        subject.class.policy_class
      else
        klass = find_class_name(subject)
        "#{klass}#{SUFFIX}"
      end
    end
    def find_class_name(subject)
      if subject.respond_to?(:model_name)
        subject.model_name
      elsif subject.class.respond_to?(:model_name)
        subject.class.model_name
      elsif subject.is_a?(Class)
        subject
      elsif subject.is_a?(Symbol)
        subject.to_s.camelize
      else
        subject.class
      end
    end
  end
  SUFFIX = "Policy".freeze
  module Generators; end
  class Error < StandardError; end
  class NotAuthorizedError < Error
    attr_reader :query, :record, :policy
    def initialize(options = {})
      if options.is_a? String
        message = options
      else
        @query  = options[:query]
        @record = options[:record]
        @policy = options[:policy]
        message = options.fetch(:message) { "not allowed to #{query} this #{record.inspect}" }
      end
      super(message)
    end
  end
  class InvalidConstructorError < Error; end
  class AuthorizationNotPerformedError < Error; end
  class PolicyScopingNotPerformedError < AuthorizationNotPerformedError; end
  class NotDefinedError < Error; end
  extend ActiveSupport::Concern
  class << self
    def authorize(user, record, query, policy_class: nil)
      policy = policy_class ? policy_class.new(user, record) : policy!(user, record)
      raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
      record
    end
    def policy_scope(user, scope)
      policy_scope = PolicyFinder.new(scope).scope
      policy_scope.new(user, pundit_model(scope)).resolve if policy_scope
    rescue ArgumentError
      raise InvalidConstructorError, "Invalid #<#{policy_scope}> constructor is called"
    end
    def policy_scope!(user, scope)
      policy_scope = PolicyFinder.new(scope).scope!
      policy_scope.new(user, pundit_model(scope)).resolve
    rescue ArgumentError
      raise InvalidConstructorError, "Invalid #<#{policy_scope}> constructor is called"
    end
    def policy(user, record)
      policy = PolicyFinder.new(record).policy
      policy.new(user, pundit_model(record)) if policy
    rescue ArgumentError
      raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
    end
    def policy!(user, record)
      policy = PolicyFinder.new(record).policy!
      policy.new(user, pundit_model(record))
    rescue ArgumentError
      raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
    end
    private
    def pundit_model(record)
      record.is_a?(Array) ? record.last : record
    end
  end
  module Helper
    def policy_scope(scope)
      pundit_policy_scope(scope)
    end
  end
  included do
    helper Helper if respond_to?(:helper)
    if respond_to?(:helper_method)
      helper_method :policy
      helper_method :pundit_policy_scope
      helper_method :pundit_user
    end
  end
  protected
  def pundit_policy_authorized?
    !!@_pundit_policy_authorized
  end
  def pundit_policy_scoped?
    !!@_pundit_policy_scoped
  end
  def verify_authorized
    raise AuthorizationNotPerformedError, self.class unless pundit_policy_authorized?
  end
  def verify_policy_scoped
    raise PolicyScopingNotPerformedError, self.class unless pundit_policy_scoped?
  end
  def authorize(record, query = nil, policy_class: nil)
    query ||= "#{action_name}?"
    @_pundit_policy_authorized = true
    policy = policy_class ? policy_class.new(pundit_user, record) : policy(record)
    raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
    record
  end
  def skip_authorization
    @_pundit_policy_authorized = true
  end
  def skip_policy_scope
    @_pundit_policy_scoped = true
  end
  def policy_scope(scope, policy_scope_class: nil)
    @_pundit_policy_scoped = true
    policy_scope_class ? policy_scope_class.new(pundit_user, scope).resolve : pundit_policy_scope(scope)
  end
  def policy(record)
    policies[record] ||= Pundit.policy!(pundit_user, record)
  end
  def permitted_attributes(record, action = action_name)
    policy = policy(record)
    method_name = if policy.respond_to?("permitted_attributes_for_#{action}")
                    "permitted_attributes_for_#{action}"
                  else
                    "permitted_attributes"
                  end
    pundit_params_for(record).permit(*policy.public_send(method_name))
  end
  def pundit_params_for(record)
    params.require(PolicyFinder.new(record).param_key)
  end
  def policies
    @_pundit_policies ||= {}
  end
  def policy_scopes
    @_pundit_policy_scopes ||= {}
  end
  def pundit_user
    current_user
  end
  private
  def pundit_policy_scope(scope)
    policy_scopes[scope] ||= Pundit.policy_scope!(pundit_user, scope)
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment