Pundit Headless Policy - ruby-on-rails

I'm using pundit for access control in the admin section of my app. I have a dashboards controller that looks like this:
class Admin::DashboardsController < AdminController
def index
#total_revenue = Order.total_revenue
authorize :dashboards, :index?
end
...
end
and a policy that looks like this:
class DashboardPolicy < Struct.new(:user, :dashboard)
def index?
true
end
end
When I try to access /admin/dashboards/ I get a Pundit::NotDefinedError, unable to find policy SymbolPolicy for dashboards
I've also tried namespacing the policy and got the same error.

jizak's answer did not work for me, I found following solution for headless name-spaced policies, the trick being with the [:admin, :policy] first argument.
class Admin::HomeController < AdminController
def dashboard
authorize [:admin, :home], :dashboard?
end
end
And then for the policy:
Admin::HomePolicy < AdminPolicy
def dashboard?
return false unless user && user.admin?
true
end
end

I have such headless policy:
app/policies/admin/statistic_policy.rb
class Admin::StatisticPolicy < Struct.new(:user, :statistic)
def show?
user.admin?
end
end
app/controllers/admin/statistics_controller.rb
class Admin::StatisticsController < Admin::ApplicationController
def show
per_today Time.zone.now
authorize :statistics, :show?
end
...
end
and it works for me.
Try to update gem, because these changes are new (https://github.com/elabs/pundit/issues/77).
Delete your Gemfile.lock from the project and do 'bundle install'.

I recently had the same issue. The problem that I faced was that the controller was without a model.
Remember that Pundit is a model based authorization, not so much of controller based.
Before creating an Admin class (in the models), I was getting the same error as you were. Also, take note of the authorization statement on my dashboard action in the controller.
controllers/admin_controller.rb
class AdminController < ApplicationController
after_action :verify_authorized
def dashboard
authorize Admin, :dashboard?
end
end
models/admin.rb
class Admin
def self.policy_class
AdminPolicy
end
end
policies/admin_policy
class AdminPolicy < Struct.new(:user, :admin)
def dashboard?
user.admin?
end
end

I managed to use Pundit on namespaced controller actions regardless of the model by using this:
In my /private/scrapers_controller.rb I have
module Private
class ScrapersController < Private::PrivateApplicationController
# Pundit authorizations
before_action { authorize [:private, :scrapers] }
def index
end
...
And then in policies/private/scrapers_policy.rb
class Private::ScrapersPolicy < ApplicationPolicy
def index?
return true if user.has_role?(:super_admin)
return false
end
end
This will disallow visiting scrapers#index or any other action within the controller to any user who's not an :super_admin
To disallow only index explicitly you can use:
before_action { authorize [:private, :scrapers], :index? }

Check yout pundit version. You may need to run 'bundle update pundit', because headless policies were merged to master quite recently and before that you had need to install pundit from github: 'elabs/pundit' to use them.
Described issue
Merged headless policies

if you just want to render landing page for controller dashboard#index for example, where no need to authorize users you can just skip authorization like
dashboard_controller.rb
class DashboardController < ApplicationController
def index
skip_policy_scope
end
end
And therefore you don't have to create DashboardPolicy at all.

Related

Rails - Handle User roles using pundit

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 %>

Pundit::AuthorizationNotPerformedError

This is what I get going on the recipe show page:
My Controller looks like that:
class RecipesController < ApplicationController
skip_before_action :authenticate_user!, only: [:index, :show]
def index
if params[:query].present?
#recipes = policy_scope(Recipe).search_by_title_and_description(params[:query]).order(created_at: :desc)
else
#recipes = policy_scope(Recipe).order(created_at: :desc)
end
end
def show
#recipe = Recipe.find(params[:id])
#recipes = Recipe.first(5)
end
end
My policy.rb:
class RecipePolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
def index?
false
end
def show?
false
end
end
end
And this is the Error message when adding a 'authorize #recipe' to the show action:
I need Pundit Authorization for the comments for each recipe but not for the recipe show-action itself. What do I do wrong? Thanks for helping !!
authenticate_user! (which you haven't shown/explained to us, but is probably a method from devise or similar?) is presumably to do with signing in -- that's authentication, not authorization, and is therefore outside the scope of what Pundit tries to solve.
Authentication is all about checking "are you signed in?". If this check fails, then the server responds with a 401 status.
Authorization is about checking "are you allowed to perform this action (potentially as a guest)?". If this check fails, then the server responds with a 403 status
Now presumably, you have also added some code like this in your application:
class ApplicationController < ActionController::Base
include Pundit
after_action :verify_authorized, except: :index # !!!!!
end
This after_action check is a safety net; it's there to ensure that you never forget to authorize an endpoint -- as that would allow the action to be performable by any user, by default! The presence of this check is what's causing your error above.
So. With that explained, let's look how you can implement this.
Should RecipesController#show be accessible by guests, or only by logged-in users?
If, and only if, the answer is "guests" then add this:
skip_before_action :authenticate_user!, only: :show
Assuming you've already performed any necessary authentication, you want to let any user see any recipe. How can you implement that?
Option 1 (recommended):
class RecipePolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
end ## WARNING!! NOTICE THAT THE `Scope` CLASS ENDS HERE!!!
def show?
true # !!!!
end
end
class RecipesController < ApplicationController
# ...
def show
#recipe = Recipe.find(params[:id])
authorize(#recipe) # !!!
# ...
end
end
Option 2 (also works, but worse practice as it means you can't rely on unit tests for the policy class):
class RecipesController < ApplicationController
def show
skip_authorization # !!!
#recipe = Recipe.find(params[:id])
# ...
end
end

Multitenant scoping using Pundit

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

Authenticate two different devise classes in same controller in Rails

I have this view called Intranet where only authenticated "devise clients" can access.
class IntranetController < ApplicationController
before_action :authenticate_client!
def index
end
end
On the other side, I also have other "devise admin", this devise admin requires to access the same view. How can I handle this situation?
Try this:
class IntranetController < ApplicationController
before_action :authenticate_all!
def index
end
def authenticate_all!
if admin_signed_in?
true
else
authenticate_client!
end
end
end

Pundit: undefined method `authorize'

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 :)

Resources