pundit returning incorrect values - ruby-on-rails

I have this Application Policy:
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
...
def create?
user && user.admin?
end
def update?
create?
end
def edit?
create?
end
end
PostPolicy looks something like this:
class PostPolicy < ApplicationPolicy
...
def create?
super || (user && user.in_committee?)
end
def update?
super || (user && user.committees.include?(record.group))
end
def edit?
update?
end
...
end
Now when a user that is not admin i.e user returns true (it exists) and user.admin? returns false (it is not an admin) tries to edit or update a post we check the application policy. We would now expect that when the user tries to edit a post it would get access denied since update? returns super.update? and super.update? returns super.create? which should return false. ((user && user.committees.include?(record.group)) returns false)
This is not the case. If we print
user && user.admin? inside of ApplicationPolicy's create? it prints out false if we then print out create? inside update? it prints out true. I tried doing a fast check without pundit but that worked as expected
I have now solved this simply by changing the body of update? to:
def update?
user && user.admin?
end
Inside ApplicationPolicy which solves my problem. So I guess I'm mostly curious why this would happen and if someone has encountered the same issue.

Related

Why does a Pundit policy for one controller is affected by another?

I am perhaps misunderstanding Pundit policies but I am facing an issue where the UserPolicy is clashing with the SongPolicy.
What happens if that a statement in UserPolicy is being asserted ignoring what's written in SongPolicy:
Pundit::NotAuthorizedError in SongsController#edit
not allowed to edit? this User
def authorization
authorize Current.user
end
The issue emerged after introducing a new role for users but I believe that I probably haven't configured it right and for some reason only UserPolicy is looked at for asserting authorization in the SongsController?
I have two controllers that check for the user to be signed in (require_user_logged_in) and another to check on Pundit's policies (authorization):
class UsersController < ApplicationController
before_action :require_user_logged_in!, :authorization, :turbo_frame_check
# Actions were removed for brevity.
end
class SongsController < ApplicationController
before_action :require_user_logged_in!, :authorization, except: [:index, :show]
# Actions were removed for brevity.
end
The authorization methods looks like this:
def authorization
authorize Current.user
end
There's an application-level policy class, ApplicationPolicy:
# frozen_string_literal: true
class ApplicationPolicy
attr_reader :user, :params, :record
# Allows params to be part of policies.
def initialize(context, record)
if context.is_a?(Hash)
#user = context[:user]
#params = context[:params]
else
#user = context
#params = {}
end
#record = record
end
def index?
false
end
def show?
false
end
def create?
false
end
def new?
create?
end
def update?
false
end
def edit?
update?
end
def destroy?
false
end
class Scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
raise NotImplementedError, "You must define #resolve in #{self.class}"
end
private
attr_reader :user, :scope
end
end
The UserPolicy to protect user views:
class UserPolicy < ApplicationPolicy
class Scope < Scope
end
def index?
user.has_role?(:admin)
end
def show?
# Access if admin or the same user only.
user.has_role?(:admin) || is_same_user?
end
def create?
index?
end
def new?
create?
end
def update?
index? || is_same_user?
end
def edit?
update? # This is called when accessing a view for `SongsController`.
end
def destroy?
index? || is_same_user?
end
def delete?
destroy?
end
private
# Used to keep a user from editing another.
# Admins should be allowed to edit all users.
def is_same_user?
# Check if user being accessed is the one being logged in.
params[:id].to_s == Current.user.username.to_s
end
end
And the SongPolicy:
class SongPolicy < ApplicationPolicy
class Scope < Scope
end
def index?
end
def show?
end
def create?
user.has_role?(:admin) || user.has_role?(:collaborator) # This is ignored.
end
def new?
create?
end
def update?
create?
end
def edit?
create?
end
def destroy?
user.has_role?(:admin)
end
def delete?
destroy?
end
end
Not sure what else to try here, I'm sure I'm missing something, if someone with more knowledge of Pundit could let me know their thoughts on why a statement for one policy can leak into another, it would be really helpful.
You're calling authorize on the current user, which is a User, so Pundit is going to infer the UserPolicy policy. It won't automatically infer the SongPolicy policy unless you provide a Song record, even if you're in the SongController controller.
If you want to use a different policy, you'll need to provide it via authorize(policy_class:).
authorize Current.user, policy_class: SongPolicy
Implicit authorization like this is generally a code smell. Ideally, you should be explicitly authorizing the current Song record(s) against the current user context.
My opinion is that you have a misconception on Pundit's approach. Particularly you're twisting the subject of the authorization with the object of the authorization. Let's try to explain.
You have an actor who wants to apply an action on an object. The object may be authorized to receive the action.
By default Pundit's always consider the actor to be the current_user.
The action is a method on a Policy.
The object is the resource you're working on; in the most trivial scenario it could be an ActiveRecord object - but it doesn't have to.
Pundit's authorize methods is intended, in plain english, as "authorize the the resource bar to receive the action foo from the current user".
What you're trying to do is instead "authorize the current user to apply the action foo on the resource bar.
What's the difference? The subject and the object of the authorization are swapped. IMO, while doing the authorization process, you should respond to the question: "Is this object authorized to receive this action by the actor?"
object action
------------ ------
authorize Current.user, :edit?
NOTE: the actor implicitly is current_user
NOTE: if action is not declared, then it will implicitly be action_name
which resolves to the question "is this specific user authorized to receive :edit? from the current user?"
Following the reasoning, this is what I'd consider the right approach for your example scenario:
class SongsController < ApplicationController
before_action :require_user_logged_in!, :authorization, except: [:index, :show]
private
def authorization
authorize Song
end
end
I do not advise to rely on callbacks and I'd rather write more explicit code
def edit
#song = Song.find(params[:id])
authorize #song, :edit?
end
This code resolves to the question "is this specific song authorized to receive :edit? from the current user?"
A word of warning about using a custom policy_class
like in
authorize Current.user, policy_class: SongPolicy
With this code the authorization will be made by calling SongPolicy#edit? but the record will regularly be set to Current.user's value ; let's suppose to have
class SongPolicy
def edit?
record.in_my_playlist?
end
end
where in_my_playlist? is Song's instance method: you'll end having
undefined method `in_my_playlist?` for #<User>
Probably you're not doing the thing you intended to do there.
A word of warning about the use of Current.user into your logic
If Current.user is using http://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html and your entire application is relying on that singleton, then you probably want to redefine Pundit's default user as documented here
class ApplicationController < ActionController::Base
def pundit_user
Current.user
end
end
otherwise you'll end up having your business logic and your authorization logic relying on two - potentially - different sources of truth.

alias. How to optimize?

in my policies folder I have product_policy.rb
class ProductPolicy < ApplicationPolicy
def update?
creator? || admin?
end
alias create? update?
def manage?
creator?
end
def destroy?
update?
end
def color?
update?
end
etc.etc.
As you can see, it was used alias to avoid to add also
def create?
update?
end
But few lines beyound there are destroy? and color? that made the same thing.
I tried to edit alias in this way
alias create? destroy? color? update?
but it doesn't work.
How to optimize this alias?
There are some subtle differences with scoping when using alias and alias_method but in this case it is unlikely to make a difference so you could just iterate over the methods you want to create an alias for:
class ProductPolicy < ApplicationPolicy
def update?
creator? || admin?
end
%i(create? destroy? color?).each do |method|
alias_method method, :update?
end
end

Is this RSpec spec actually proving that my Pundit Policy is restricting access to the show action?

I'm struggling to understand Pundit generally, but specifically how to test it. I've gotten my test to pass by ignoring some things from examples online that seemed to have no effect on what I was doing or didn't seem to be having the right info passed to it. Now I'm not sure the thing I did is actually proving that my Policy is working or whether I've bypassed the test.
I'm testing that a visitor who tries to access someone's gallery does not have access to it. In the event that a visitor would type in http://localhost:3000/galleries/1 to the browser they wouldn't get access. This does happen in practice, the visitor is redirected to the sign in page, but I'm trying to learn how to do testing using RSpec and Pundit, so I can use the tools with confidence and accuracy.
A little help is appreciated. Below is what I believe to be the important code:
gallery_policy_spec.rb
RSpec.describe GalleryPolicy do
#Pretty sure I skipped over subject because I kept getting errors
#saying user and gallery were undefined variables
subject { GalleryPolicy.new(user, gallery) }
context "for a vistor" do #passes I know why
it "has(gallery) a valid factory" do
expect(FactoryGirl.build(:gallery)).to be_valid
end
it 'has(user) a valid factory' do #passes I know why
expect(FactoryGirl.build(:user)).to be_valid
end
permissions :show? do #passes but I don't know why
it "denies access to show if visitor" do
expect(GalleryPolicy).not_to permit(FactoryGirl.build(:user, id: nil), FactoryGirl.create(:gallery))
end
end
end
end
gallery_policy.rb
class GalleryPolicy < ApplicationPolicy
attr_reader :user, :model
def initialize(user, model)
#user = user
#gallery = model
end
class Scope < Scope
def resolve
scope.where(:user_id == user.id)
end
end
def show?
#gallery.user == #user
end
def new?
create?
end
def new?
true
end
end
application_policy.rb
I think this is standard, but I didn't want to leave out valuable info
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
raise Pundit::NotAuthorizedError, "must be logged in" unless user
#user = user
#record = record
end
def index?
false
end
def show?
false
end
def create?
false
end
def new?
create?
end
def update?
false
end
def edit?
update?
end
def destroy?
false
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
scope
end
end
end
gallery.rb #factory
FactoryGirl.define do
factory :gallery do
title { Faker::Book.title }
association :user
end
end

Rails - with Pundit Scopes in policy

I am trying to figure out how to use pundit policy scopes in my article policy.
I have written an article policy, that nests a scope and then has a resolve method in it. The resolve method has alternatives based on who the current_user is.
My article policy has:
class ArticlePolicy < ApplicationPolicy
class Scope < Scope
attr_reader :user, :scope
# I now think I don't need these actions because I have changed the action in the articles controller to look for policy scope.
# def index?
# article.state_machine.in_state?(:publish)
# end
def show?
article.state_machine.in_state?(:publish) ||
user == article.user ||
article.state_machine.in_state?(:review) && user.org_approver ||
false
end
end
def create?
article.user.has_role?(:author)
end
def update?
# user && user.article.exists?(article.id) #&& user.article.created_at < 15.minutes.ago
user.present? && user == article.user
# add current state is not published or approved
end
def destroy?
user.present? && user == article.user
# user.admin?
# user.present?
# user && user.article.exists?(article.id)
end
end
private
def article
record
end
def resolve
if user == article.user
scope.where(user_id: user_id)
elsif approval_required?
scope.where(article.state_machine.in_state?(:review)).(user.has_role?(:org_approver))
else
article.state_machine.in_state?(:publish)
end
end
def approval_required?
true if article.user.has_role?(:author)
# elsif article.user.profile.organisation.onboarding.article_approval == true
# if onboarding (currently in another branch) requires org approval
end
def org_approver
if article.user.has_role? :author
user.has_role? :editor
# if onboarding (currently in another branch) requires org approval, then the approval manager for that org
elsif article.user.has_role? :blogger
user.has_role? :editor if user.profile.organisation.id == article.user.profile.organisation.id
end
end
end
The example in the pundit docs shows how to use this for an index, but how do I use the resolve method for a show action? Can I write several resolve methods for the various other controller actions?
Pundit Scopes
I dont have much experience with pundit, however by looking at documentation and your code the code I can see 2 things.
1 - You shouldnt use methods like show? inside your scope class.
inside your scope class, you should use only methods that returns a scope. the methods that returns boolean should be in the Policy level. But in your code I can boolean methods inside the scope class.
Instances of this class respond to the method resolve, which should return some kind of result which can be iterated over. For ActiveRecord classes, this would usually be an ActiveRecord::Relation.
from the docs
2 - Given that Scope are POROs (Plain Old Ruby Object) you can have more than one resolve methods (of course with a different name :)), because resolve is just a method name.
May be you can do something like
#policy
class ArticlePolicy < ApplicationPolicy
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
class Scope < Scope
def resolve
# some scope
end
def resolve_show
#scope for show action
# E.g scope.all
end
end
def show?
article.state_machine.in_state?(:publish) ||
user == article.user ||
article.state_machine.in_state?(:review) && user.org_approver || false
end
end
in your controller
#Articles controller
class ArticlesController < ApplicationController
...
def show
authorize Article
ArticlePolicy::Scope.new(current_user, Article).resolve_show
end
...
end
This should first authorize your show method with ArticlePolicy#show? and the scope from ArticlePolicy::Scope#resolve_show
Disclaimer: Untested code, use at your own risk ;)

Cannot complete the New controller action through Pundit gem

I have a Ruby on Rails web app where users creates notes. The note belongs to the user that created it.
I installed the Pundit gem to create authorizations, specifically an admin role.
I would like for a user to be able to:
create, update, or delete their notes
And for an admin to be able to do the same for any user's notes.
When I log in as an admin, I can create a new note. When I log in as a member, I cannot create a new note. I get instantly redirected to the root page, and never even brought to the new note page.
Here is the flash error message I receive:
not allowed to new? this #<Note:0x007fe2ca17bc18>
Before installing Pundit, members were able to create a new note. So I assume it has something to do with my policies.
Here are the relevant methods from my application_policy.rb file:
def index?
false
end
def show?
scope.where(:id => record.id).exists?
end
def create?
user.present?
end
def new?
create?
end
def update?
user.present? && (record.user == user || user.admin?)
end
def edit?
update?
end
def destroy?
update?
end
Here is my note_policy.rb file:
class NotePolicy < ApplicationPolicy
def create?
user.present? && (record.user == user || user.admin?)
end
def new?
create?
end
def update?
create?
end
def destroy?
update?
end
end
And here are the relevant actions from my notes_controller.rb file:
def new
#note = Note.new
authorize #note
end
def create
#note = Note.new(notes_params)
#note.user = current_user
authorize #note
if #note.save
redirect_to notes_path
else
render :new
end
end
I am trying to figure out why an admin can create a new note, but a member cannot.
Thank you.
In the new action, the authorization check is being made using NotePolicy#create?. See chain of calls below:
NotePolicy#new? --> ApplicationPolicy#new? --> NotePolicy#create?
A new Note (#note = Note.new) won't pass the authorization check made in NotePolicy#create?.
Add a new? method to NotePolicy that implements the authorization logic you want.
ADDED
This create? method will not pass for a new note (Note.new).
def create?
user.present? && (record.user == user || user.admin?)
end
To confirm why that's the case you can debug by adding a line like puts "#{user.present?} #{record.user} #{user}" at the beginning of the create? method.
You'll see the values of these variables in the server output or logs/development.log.
Is it missing a new? method in your NotePolicy?

Resources