I am trying to use Pundit to authenticate access to some static views that require no database interaction:
class StaticController < ApplicationController
include Pundit
authorize :splash, :home?
def home end
end
Below is my static policy. The home? policy always returns true, so I should be able to access the home view.
class StaticPolicy < Struct.new(:user, :static)
def initialize(user, resource)
#user = user
#resource = resource
end
def home?
true
end
end
Instead I get this:
undefined method `authorize' for StaticController:Class
Pundit works perfectly if I'm authorizing a model:
def forums_index
#forums = Forum.all
authorize #forums
end
However, if I try to use the authorize method outside of an action that doesn't make use of a model I get:
undefined method `authorize' for StaticController:Class
Well, AFAIK you'll always have to authorize against either an object or a class, while CanCan already "load_and_authorize_resource", when using Pundit you already know that you have to load and authorize something yourself (sorry if I'm being too obvious here).
That said and considering that your view doesn't have DB interation, it seems to me that the best solution for your case is make some custom authorization against your user, something like
class StaticPolicy < Struct.new(:user, :static)
def initialize(user, resource)
#user = user
#resource = resource
end
def home?
authorize #user, :admin # or suppress the second parameter and let the Policy use the 'home?' method
true
end
end
and in your UserPolicy something like
class UserPolicy < ApplicationPolicy
def admin # or def home?, it's up to you
user.admin?
end
end
I didn't test it, but that's the main idea, does it make any sense? Is it clear?
Please give it a try and post any impressions, hope it helps :)
Related
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.
I have a table of users with enum user_type [Manager, Developer, QA]. Currently, I'm handling sign in using Devise and after login I'm using the following logic to display the appropriate webpage:
class HomeController < ApplicationController
before_action :authenticate_user!
def index
if current_user.manager?
redirect_to manager_path(current_user.id)
end
if current_user.developer?
redirect_to developer_path(current_user.id)
end
if current_user.quality_assurance?
redirect_to qa_path(current_user.id)
end
end
end
I want to use pundit gem to handle this. From the documentation, it transpired that this logic will be delegated to policies but I can't figure out how. Can somebody help me in implementing pundit in my project?
This is my users table:
I have created a user_policy but its mostly empty:
class UserPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
end
end
User model:
You want to use Pundit to authorize a user, as in check if that user should be allowed to visit a controller action. If the user is not authorized for a specific action it raises a Pundit::NotAuthorizedError
You can check if a user is allowed to perform an action in the pundit policy, in which you have access to record (the instance thats passed to authorize) and user. So assuming you have a Flat Model, where only the owner can edit the Flat you might do this:
# flats_policy.rb
def edit?
record.user == user
end
Now lets say you also want to allow admins to edit you might do this
# flats_policy.rb
def owner_or_admin?
record.user == user || user.admin # where admin is a boolean
end
def edit?
owner_or_admin?
end
and the controller:
# flats_controller.rb
def edit
#flat = Flat.find(params[:id])
authorize #flat
# other code here
end
Now the index action is the odd one out because you would essentially have to call authorize on each instance, so the way Pundit handles this is with the Scope:
# flats_policy.rb
class Scope < Scope
def resolve
scope.all
end
end
and a corresponding index action might look like:
def index
#flats = policy_scope(Flat) # note that we call the model here
end
So lets say a user can only see flats that he/she owns:
# flats_policy.rb
class Scope < Scope
def resolve
scope.where(user: user)
end
end
and if admins can see all flats:
# flats_policy.rb
class Scope < Scope
def resolve
if user.admin
scope.all
else
scope.where(user: user)
end
end
end
In any case if the user is not allowed to perform an action you can rescue from the error like so:
# application_controller
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_to(root_path)
end
I guess you could do some dirty redirecting here, as in send admins to an admins_root_path, users to a default_root_path and so on...
On a final note, since this post is already too long you can check a policy in the view like this:
<% if policy(restaurant).edit? %>
You can see me if you have edit rights
<% end %>
How would I provide pundit authorization for a dashboard controller which provides data from various models?
My DashboardsController looks like this:
class DashboardsController < ApplicationController
before_action :authenticate_user!
before_action :set_user
before_action :set_business
after_action :verify_authorized
def index
end
private
def set_user
#user = current_user
end
def set_business
#business = current_user.business
end
end
How would I authorize for both #user and #business within my DashboardsPolicy?
I would argue that trying to get access to a dashboard is not a policy based on a resource named dashboard, but simply a special method in the business policy.
Therefore, I would add this to the BusinessPolicy as a method dashboard.
# in your controller
authorize #business, :dashboard?
# and the business_policy
class BusinessPolicy < ApplicationPolicy
def dashboard?
# condition depending on a `user` (current_user) and a record (business)
user.admin? || user.business == record
end
end
Or it might be even simpler. If someone is allowed to see the dashboard when she is allowed to show the business, then just re-use BusinessPolicy#show? in your controller:
authorize #business, show?
Pundit expects a current user and a model object to be passed to it. In this case I think what you would want is a DashboardsPolicy class, and you would authorize it like:
def index
authorize(#business)
end
From the README:
Pundit will call the current_user method to retrieve what to send into
this argument
The authorize method automatically infers that Post will have a
matching PostPolicy class, and instantiates this class, handing in the
current user and the given record
There is also a specific section in the README regarding headless policies that uses the Dashboard as the example action: https://github.com/varvet/pundit#headless-policies
You can also create a plain ruby object that takes two entities and use that as your object to authorize:
class UserBusiness
def initialize(user, business)
end
...other methods here
end
#model = UserBusiness.new(user, business)
authorize(#model)
I'm using the pundit gem and trying to figure out how to use it to prevent access to an index page that belongs to a user other than the current_user.
The examples only talk about how to scope the results to the current_user but no how to actually prevent access to the page itself if the current_user is NOT the owner of the record.
Any help appreciated
Thanks
Maybe you want something like this? (For class ModelName)
# /policies/model_name_policy.rb
class ModelNamePolicy
attr_reader :current_user, :resource
def initialize(current_user, resource)
#current_user = current_user
#resource = resource
end
def index?
current_user.authorized_to_edit?(resource)
end
end
# /models/user.rb
class User < ActiveRecord::Base
def authorized_to_edit?(resource)
admin? | (id == resource.created_by) # Or whatever method you want to call on your model to determine ownership
end
end
EDIT: Note that you will also need to call authorize from your controller to invoke the policy.
I'm using Pundit for authorization and I want to make use of its scoping mechanisms for multi-tenancy (driven by hostname).
I've been doing this manually to date by virtue of:
class ApplicationController < ActionController::Base
# Returns a single Client record
def current_client
#current_client ||= Client.by_host(request.host)
end
end
And then in my controllers doing things like:
class PostsController < ApplicationController
def index
#posts = current_client.posts
end
end
Pretty standard fare, really.
I like the simplicity of Pundit's verify_policy_scoped filter for ensuring absolutely every action has been scoped to the correct Client. To me, it really is worthy of a 500 error if scoping has not been officially performed.
Given a Pundit policy scope:
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
# have access to #scope => Post class
# have access to #user => User object or nil
end
end
end
Now, Pundit seems to want me to filter Posts by user, e.g.:
def resolve
scope.where(user_id: user.id)
end
However, in this scenario I actually want to filter by current_client.posts as the default case. I'm not sure how to use Pundit scopes in this situation but my feeling is it needs to look something like:
def resolve
current_client.posts
end
But current_client is naturally not going to be available in the Pundit scope.
One solution could be to pass current_client.posts to policy_scope:
def index
#posts = policy_scope(current_client.posts)
end
But I feel this decentralizes my tenancy scoping destroys the purpose of using Pundit for this task.
Any ideas? Or am I driving Pundit beyond what it was designed for?
The most "Pundit-complient" way to deal with this problem would be to create a scope in your Post model:
Class Post < ActiveRecord::Base
scope :from_user, -> (user) do
user.posts
end
end
Then, you will be able to use it in your policy, where user is filled with the current_user from your controller:
class PostPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
scope.from_user(user)
end
end
end
If you are returning an ActiveRecord::Relation from the scope, you can stop reading from here.
If your scope returns an array
The default ApplicationPolicy implement the method show using a where:
source.
So if your scope does not return an AR::Relation but an array, one work-around could be to override this show method:
class PostPolicy < ApplicationPolicy
class Scope
# same content than above
end
def show?
post = scope.find do |post_in_scope|
post_in_scope.id == post.id
end
post.present?
end
end
Whatever your implementation is, you just need to use the PostPolicy from your controller the "Pundit-way":
class PostsController < ApplicationController
def index
#posts = policy_scope(Post)
end
def show
#post = Post.find(params[:id])
authorize #post
end
end