Pundit: authorize actions within namespaced controllers - ruby-on-rails

I have a Blog model which has different states. In order to keep a skinny controller and follow the convention of only having CRUD operations per controller, I followed DHH's namespacing controllers pattern and namespaced out the Blog controller.
Now I have a Blogs::NewDraft controller, a Blogs::AwaitingApproval controller, and a Blogs::Active controller.
The issue is with writing my policies to authorize the actions within these namespaced controllers. All the actions in all the namespaced controllers are all authorizing the same Blog model object. The issue is that I need each of the namespaced controllers to authorize in a matching namespaced policy (as opposed to all of the namespaced controllers authorizing within the same blog_policy.rb file.)
Basic Example: For a restful resource with a restful controller that is NOT namespaced you do it something like this:
#app/controllers/blogs_controller.rb
class BlogsController < ApplicationController
def index
authorize :blog
#blogs = Blog.all
end
def show
#blog = Blog.find(1)
authorize #blog
end
end
And now the matching Policy
#app/policies/blogs_policy.rb
class BlogPolicy < ApplicationPolicy
def index?
user.admin?
end
def show?
record.author == current_user
end
end
You do it like that when you don't namespace.
Current Code to Try to get Namespacing to work with Pundit: I am namespacing. I am still authorizing a Blog object, but I need to authorize the actions within each namespaced controller within a namespaced policy:
#app/controllers/blogs/new_drafts.rb
class Blogs::NewDraftsController < ApplicationController
def index
# doesn't work
authorize Blog::NewDrafts
#blogs = Blog.new_drafts
end
def show
#blog = Blog.find(1)
#doesn't work either
authorize #blog, Blog::NewDraft
end
end
So I want that namespaced controller to NOT route to app/policies/blog_policy.rb, but instead to app/policies/blogs/new_draft_policy.rb
#app/policies/blogs/new_draft_policy.rb
class Blogs::NewDraftPolicy < ApplicationPolicy
def index?
user.admin?
end
def show?
# the record is a blog from the Blog Model
record.author == current_user
end
end
Pundit Documentation and Usage

Don't know how to route to namespaced policy AND pass in the Blog record. However: below is how you do it when your namespaced policy is able to authorize only based on the current user's permissions/roles:
#app/controllers/blogs/new_drafts.rb
class Blogs::NewDraftsController < ApplicationController
def index
authorize [:blogs, :new_draft]
#blogs = Blog.new_drafts
end
end
#app/policies/blogs/new_draft_policy.rb
class Blogs::NewDraftPolicy < ApplicationPolicy
def index?
user.admin?
end
end

Related

Ruby On Rails Pundit Gem Authorizing Dashboard

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)

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

authorize with a parent resource in Pundit

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.

Pundit Headless Policy

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.

Wrapping within filter

Using Rails 3.2. I am trying to secure my app by checking user permission on all crud actions. Here is one of the examples:
class StatesController < ApplicationController
def create
#country = Country.find(params[:country_id])
if can_edit(#country)
#state.save
else
redirect_to country_path
end
end
def destroy
#country = Country.find(params[:country_id])
if can_edit(#country)
#state = State.find(params[:id])
#state.destroy
else
redirect_to country_path
end
end
end
class ApplicationController < ActionController::Base
def is_owner?(object)
current_user == object.user
end
def can_edit?(object)
if logged_in?
is_owner?(object) || current_user.admin?
end
end
end
I notice that I have been wrapping can_edit? in many controllers. Is there a DRYer way to do this?
Note: All must include an object to check if the logged in user is the owner.
You are looking for an authorization library. You should look at CanCan which allows you to set certain rules about which objects can be accessed by particular users or groups of users.
It has a method, load_and_authorize_resource, which you can call in a given controller. So your statues controller would look like:
class StatesController < ApplicationController
load_and_authorize_resource :country
def create
#state.save
end
def destroy
#state = State.find(params[:id])
#state.destroy
end
end
CanCan will first load the country and determine whether or not the user has the right to view this resource. If not, it will raise an error (which you can rescue in that controller or ApplicationController to return an appropriate response".) You can then access #country in any controller action knowing that the user has the right to do so.

Resources