Skip to content

Instantly share code, notes, and snippets.

@kidpollo
Created April 1, 2013 19:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kidpollo/5287007 to your computer and use it in GitHub Desktop.
Save kidpollo/5287007 to your computer and use it in GitHub Desktop.
Punditry on scopes

Pundit has a helper method policy_scope that can be used in controllers and views that basicaly a class finder. It is equivalent to:

@posts = policy_scope(Post)
@posts = PostPolicy::Scope.new(current_user, Post).resolve

Using the helper is a nice approach because you dont have to remember what Scope class you need. This is all fine and well on a world where the app is single teanant or the auth logic to reslove scopes needs no other context but the user and the resource that is being requested.

In our world almost all requests are nested under a company context and require a current user to determine membership role to present certain data.

I just wanted to do something like this:

# list the companies that a user has access to
@communities = policy_scope(Company, current_user).page(params[:page])

class CompanyPolicy < ApplicationPolicy
  class Scope < ApplicationPolicy::Scope
    def initialize(user,scope, person)
      @user   = user
      @scope  = scope
      @person = person
      @scope  = Company.joins(:memberships).uniq.
        includes(:links, :logo, :features)
      @scope  = @scope.where("memberships.user_id" => @person.id)
    end

    def resolve
      if @user.is_a?(SuperUser)
        @scope
      else
        if @person.id == @user.id
          @scope
        else
          raise Pundit::NotAuthorizedError, ApplicationPolicy::DEFAULT_ERROR_MESSAGE
        end
      end
    end
  end
end

I made a pull request varvet/pundit#21 to allow policy_scope to receive parameters. It was a simple addition but it was rejected.

We could continue to mantain a fork of the gem or go with a slightlu more verbose approach:

@communities = policy_scope(PersonCompanies.new(@person).scope).page(params[:page])

class PersonCompanies
  def self.policy_class
    self
  end

  def initialize(person)
    @person = person
    @scope  = Company.joins(:memberships).uniq.
        includes(:links, :logo, :features)
    @scope  = @scope.where("memberships.user_id" => person.id)
  end

  def scope
    @scope
  end

  attr_reader :person, :scope

  class Scope < ApplicationPolicy::Scope
    def resolve
      if @user.is_a?(SuperUser)
        @scope
      else
        if @scope.person.id == @user.id
          @scope
        else
          raise Pundit::NotAuthorizedError, ApplicationPolicy::DEFAULT_ERROR_MESSAGE
        end
      end
    end
  end
end

This is not the final iteration of this the wrapper class might be called MembershipScope and provide different scopes.

What do you think?

@thomasklemm
Copy link

Thanks for the hard work and thinking you put into this!

Does passing in an ActiveRecord::Relation into the policy_scope help?

class User < AR::B
  has_many :memberships
  has_many :companies, through: :memberships
end

class Membership < AR::B
  belongs_to :user
  belongs_to :company
end

class Company < AR::B
  has_many :memberships
  has_many :users, through: :memberships

  scope :active_clients_only, -> { where(active: true) }
end

class Scope < ApplicationPolicy::Scope
  def resolve
    if user.account_owner?
      scope
    else
      scope.active_clients_only
    end
  end
end

# You can pass in an ActiveRecord::Relation to the policy scope
@companies = policy_scope(current_user.companies)

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