3

I have a Pundit policy for the Entity model and I'm trying to implement a scope. I have the following models:

Entity
  belongs_to :project

Project
  has_many :entities
  has_many :assignments
  has_many :account_users, through: :assignments, dependent: :destroy

Assignment
  belongs_to :account_user
  belongs_to :project

Project has a field, access_type, that can be: Unlimited, Partial, Admin_Only

When a project's access_type is Partial, an admin can assign users to the project, allowing them access (via the assignments join table).

How can I write a Pundit Scope to allow a non-admin user to see only those entities where at least one of the following criteria are met:

  • The entity is not assigned to a project (project.nil?)
  • The entity is assigned to a project with access_type: unlimited (this is an enum, so project.unlimited_access? also works)
  • The entity is assigned to a project with access_type: partial AND the user has been given access to the entity's project via an assignment

Here is the policy as it currently stands (the other methods are working correctly, I'm just having trouble creating the scope):

class EntityPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      if @account_user.user.admin?
        # If overall app admin then allow access
        scope.all
      elsif @account_user.admin?
        # If the user is an admin in this account then allow access
        scope.all
      else
        # If project is nil OR does not limit access OR (it allows partial access AND this user is permissioned for this project) then allow access
        # scope.includes(:project).where(project: nil).or(scope.where(project: { access_type: "unlimited" })).or(scope.where(project: {access_type: "partial", id: @account_user.project_ids}))
      end   
    end
  end

  attr_reader :user, :project

  def initialize(user, entity)
    # the "user" being passed in is actually a UserContext, see UserContext in app/models
    @account_user = user.account.account_users.find_by(user: user.user)
    @entity = entity
    @project = entity.project
  end

  def index?
    true
  end

    def show?
        if @account_user.user.admin?
            # If overall app admin then allow access
            true
        elsif @project.nil?
            # If the entity is not assigned to a project then allow access
            true
        elsif @project.unlimited_access?
            # If project does not limit access then allow access
            true
        elsif @account_user.admin?
            # If the user is an admin in this account then allow access
            true
        elsif @project.partial_access? && @account_user.projects.include?(project)
            true
        else
            false
        end
    end

    def new?
        true
    end

    def create?
        new?
    end

    def edit?
        if @account_user.user.admin?
            # If overall app admin then allow access
            true
        elsif @project.nil?
            # If the entity is not assigned to a project then allow access
            true
        elsif @project.unlimited_access?
            # If project does not limit access then allow access
            true
        elsif @account_user.admin?
            # If the user is an admin in this account then allow access
            true
        elsif @project.partial_access? && @account_user.projects.include?(project)
            true
        else
            false
        end
    end

    def update?
        edit?
    end

    def destroy?
        if @account_user.user.admin?
            # If overall app admin then allow access
            true
        elsif @project.nil?
            # If the entity is not assigned to a project then allow access
            true
        elsif @account_user.admin?
            # If the user is an admin in this account then allow access
            true
        else
            false
        end
    end
end

class UserContext
  attr_reader :user, :account

  def initialize(user, account)
    @user = user
    @account   = account
  end
end

Edit:

Here's the scope in my ProjectPolicy, which is working as far as determining what projects a user can see. The Entity scope needs to do basically the same thing but for the project's entities instead of for the project itself.

class ProjectPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      if @account_user.user.admin?
        # If overall app admin then allow access
        scope.all
      elsif @account_user.admin?
        # If the user is an admin in this account then allow access
        scope.all
      else
        # If project does not limit access or it allows partial access AND this user is permissioned for this project then allow access
        scope.where(access_type: :unlimited).or(scope.where(access_type: :partial, id: @account_user.project_ids))
      end   
    end
  end

  attr_reader :user, :project

  def initialize(user, project)
    # the "user" being passed in is actually a UserContext, see UserContext in app/models
    @account_user = user.account.account_users.find_by(user: user.user)
    @project = project
  end

Edit 2:

I got pretty close with this, however, it is not returning entities that have project_id of nil because the joins is eliminating any entities without a project assigned:

scope.joins(:project).where(project: nil)
.or(scope.joins(:project).where(projects: { access_type: :unlimited }))
.or(scope.joins(:project).where(projects: { access_type: :partial, id: @account_user.project_ids }))

So I then went with this, which seems to be working but feels less clean. Is there any way to combine this into a single query instead of combining the results of two? Is it bad to do it this way for now?

project_is_nil_ids = scope.where(project_id: nil).pluck(:id)
access_is_allowed_ids = scope.joins(:project).where(projects: { access_type: :unlimited }).or(scope.joins(:project).where(projects: { access_type: :partial, id: @account_user.project_ids })).pluck(:id)
scope.where(id: project_is_nil_ids + access_is_allowed_ids)
jackerman09
  • 2,492
  • 5
  • 29
  • 46

1 Answers1

0

I have the feeling that you need to implement ProjectPolicy instead of EntityPolicy (may be I am wrong). I wrote it according to my understanding.

class ProjectPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      if @user.admin? || @account_user.admin?
        scope.all
      else
        scope.visible_by_user(@user.id)

        # in project.rb
        # scope :visible_by_user, ->(user_id) {
        #   left_joins(:entities, :account_users).where(
        #     """
        #       projects.access_type = 'unlimited' OR
        #       entities.id IS NULL OR
        #       (projects.access_type = 'partial' AND account_users.user_id = :user_id)
        #     """, user_id: user_id
        #   )
        # }
      end
    end
  end

  attr_reader :user, :project

  def initialize(user_context, project)
    @user = user_context.user
    @account = user_context.account
    @account_user = @account.account_users.find_by(user: user)
    @record = project
  end
end
Thang
  • 811
  • 1
  • 5
  • 12
  • Thanks! I have a separate policy for projects that is working well. The scope for projects also works (I've added it above). I basically need a scope for entities that is the same as the project scope but for that project's entities instead of for the project itself. The include/join is what's throwing me off! – jackerman09 Jan 02 '22 at 15:13