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.
Related
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 trying to access to the current user outside of a controller and outside of a model. This is the architecture of the project
main_engine
|_bin
|_config
|_blorgh_engine
|_ —> this where devise is installed
|
|_ blorgh2_engine
|_app
|_assets
|_models
|_assets
|_queries
|_ filter_comments.rb -> Where I want to use current_user
module Blorgh2
# A class used to find comments for a commentable resource
class FilterComments < Rectify::Query
# How to get current_user here ?
...
end
end
I don't think there is a way to do it. If you have an idea, you are welcome.
If the engine is running in the same thread then perhaps you could store the current_user in the Thread.
class ApplicationController < ActionController::Base
around_action :store_current_user
def store_current_user
Thread.current[:current_user] = current_user
yield
ensure
Thread.current[:current_user] = nil
end
end
Then in your filter_comments.rb you can define a method
def current_user
Thread.current[:current_user]
end
The current_user variable is tied to the current request, and thus controller instance. In this case you should probably just parameterize your query with the user you want to filter for:
class FilterComments < Rectify::Query
def initialize(user)
#user = user
end
def query
# Query that can access user
end
end
Then, in your controller:
filtered_comments = FilterComments.new(current_user)
This makes it clear where it's coming from, allows you to reuse it with any user, and makes the query object testable, since you can just pass in any user in your test setup.
In my apps, I'm using variables that scoped to the thread currently executing. This is Rails 5 feature, and it really helping with such out of scope situations.
Idea in this blogpost.
Realisation based on Module#thread_mattr_accessor
Here example of code.
class AuthZoneController < ApplicationController
include Current
before_action :authenticate_user
around_action :set_current_user
private
def set_current_user
Current.user = current_user
yield
ensure
# to address the thread variable leak issues in Puma/Thin webserver
Current.user = nil
end
end
# /app/controllers/concerns/current.rb
module Current
thread_mattr_accessor :user
end
Now you can access Current.user in your current thread in all application scope.
I have a Campaign model which has many Applicants. I'm currently nesting Applicants within Campaigns. I'm trying to authorize a user to applicants#index based on if they are the owner of the campaign.
resources :campaigns do
..
resources :applicants do
..
end
end
What's the best way to secure the applicants#index action in Pundit? Ideally I would like to pass the #Campaign to authorize.
class ApplicantsController < ApplicationController
def index
#applicants = Applicant.where(campaign: #campaign)
authorize #campaign
respond_with(#applicants)
end
But this results in Pundit looking for campaign_policy.
I'd probably use the show? method on the CampaignPolicy
authorize #campaign, :show?
where show? (call it whatever you want...manage?, etc...) would be
def show?
record.user_id = user.id
end
Just because the resource being displayed is a list of applicants doesn't mean you need to authorize against them directly. If your logic requires authorizing the owner of the campaign, do that.
Finally, if this is some wide-spread, common thing in your application, you might consider creating some value object to wrap your current user and campaign in.
class CurrentState < Struct.new(:user, :campaign); end
and then override the pundit_user method.
def pundit_user
CurrentState.new(current_user, Campaign.find(params[:campaign_id])
end
See Additional Context in the Pundit docs.
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 :)