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, soproject.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 anassignment
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)