Question about getting Rails 5 and Pundit authorization working with Namespaces.
With Pundit, in the controller I wanted to use policy_scope([:admin, #car] which will use the Pundit policy file located in: app/policies/admin/car_policy.rb. I'm having issues trying to Pundit working with this namespace - without a namespace, it works fine.
Application is running:
Rails 5
Devise for authentication
Pundit for authorization
My namespace is for admins for example.
Standard user > http://garage.me/cars
Admin user >http://garage.me/admin/cars
The route.rb file looks like:
# config/routes.rb
devise_for :admins
root: 'cars#index'
resources :cars
namespace :admin do
root 'cars#index'
resources :cars
end
I've setup a Pundit ApplicationPolicy and to get Namespaces working with Pundit's authorize method: #record = record.is_a?(Array) ? record.last : record
# app/policies/application_policy.rb
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record.is_a?(Array) ? record.last : record
end
def scope
Pundit.policy_scope!(user, record.class)
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
scope
end
end
end
In the Admin::CarsController this works authorize [:admin, #cars]
class Admin::CarsController < Admin::BaseController
def index
#cars = Car.order(created_at: :desc)
authorize [:admin, #cars]
end
def show
#car = Car.find(params[:id])
authorize [:admin, #car]
end
end
But I would like to use Policy Scope
class Admin::CarPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user?
scope.all
else
scope.where(published: true)
end
end
end
def update?
user.admin? or not post.published?
end
end
In the Admin::CarsController
class Admin::CarssController < Admin::BaseController
def index
# #cars = Car.order(created_at: :desc) without a policy scope
#cars = policy_scope([:admin, #cars]) # With policy scope / doesn't work because of array.
authorize [:admin, #cars]
end
def show
# #car = Car.find(params[:id]) without a policy scope
#car = policy_scope([:admin, #car]) # With policy scope / doesn't work because of array.
authorize [:admin, #car]
end
end
I'm getting an error because Pundit isn't looking for the Admin::CarPolicy. I presume it's because it's an array.
I thought in the controller I could do something like policy_scope(Admin::Car) but that doesn't work :).
Any assistant is much appreciated.
Update
I found this on the Pundit Github Issues Page: https://github.com/elabs/pundit/pull/391
This fixes the namespace handling for policy_scope which is what I wanted.
It updates the Pudit gem -> policy_scope! method in lib/pundit.rb.
From:
def policy_scope!(user, scope)
PolicyFinder.new(scope).scope!.new(user, scope).resolve
end
To:
def policy_scope!(user, scope)
model = scope.is_a?(Array) ? scope.last : scope
PolicyFinder.new(scope).scope!.new(user, model).resolve
end
My question is, how do I use this in my Rails application? Is it called overloading or monkey patching?
I was thinking adding a pundit.rb in the config/initializer directory and using module_eval but unsure how to do this as the policy_scope! is inside module Pundit and class << self.
I thought this would work but it doesn't - presume it is because policy_scope! is inside class << self.
Pundit::module_eval do
def policy_scope!(user, scope)
model = scope.is_a?(Array) ? scope.last : scope
PolicyFinder.new(scope).scope!.new(user, model).resolve
end
end
Medir posted the solution on the Pundit Github Issues Page.
Just change your application_policy.rb to :
class ApplicationPolicy
index? show?....
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
# #scope = scope
#scope = scope.is_a?(Array) ? scope.last : scope #This fixes the problem
end
def resolve
scope
end
end
end
You can then use :
policy_scope([:admin, #cars])
Related
Looking to be able to authorize certain users to have the ability to view fields not just have restrictions on the entire object
Trying to help you, as part of the documentation:
With Pundit you can control which attributes a user has access to update via your policies. You can set up a permitted_attributes method in your policy like this:
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes
if user.admin? || user.owner_of?(post)
[:title, :body, :tag_list]
else
[:tag_list]
end
end
end
There is also a helper which can control permissions per action
permitted_attributes(record, action = action_name) which can be used instead.
Or, most probaby, you want to use scopes which define access to certain attributes.
From the documentation about scopes:
Often, you will want to have some kind of view listing records which a particular user has access to. When using Pundit, you are expected to define a class called a policy scope. It can look something like this:
class PostPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if user.admin?
scope.all
else
scope.where(published: true)
end
end
end
def update?
user.admin? or not record.published?
end
end
I have a Rails 4.2.7 app with rails_admin (0.8.1), pundit (1.1.0) and mongoid (5.1.4)
I created Campaign scaffold and added authorize #campaign to set_campaign in CampaignController. when I browse to http://localhost:3000/campaigns/57b34dd3f5740c23d3066e43 I get unable to find policyCampaignPolicyfor <Campaign _id ...
I ran rails g pundit:policy campaign to create CampaignPolicy and now show action works. But when I browse to RailsAdmin http://localhost:3000/admin/campaign I get:
undefined method `to_criteria' for Campaign:Class
lib/mongoid/criteria.rb merge! method
I "solved" it by modifying Scope inside CampaignPollicy like this but I am wondering if there is a better solution
class CampaignPolicy < ApplicationPolicy
def show?
true
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
end
end
end
OK, figured it out and thought I'd share the answer.
class CampaignPolicy < ApplicationPolicy
...
class Scope
def resolve
if #user.admin?
scope.all # this prevents undefined method `to_criteria'
else
scope.where(...) # put some biz logic here
end
end
end
end
I defines a Pundit policy "CompanyPolicy" as stated in the documentation , the scopez gives the expected results ( on :index ) but I get an exception trying to use the company model instance :
*** NameError Exception: undefined local variable or method `company' for #<CompanyPolicy:
here is the CompanyPolicy.rb
class CompanyPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if user.system_admin?
scope.all
else
Company.none
end
end
end
def new?
user.system_admin? ? true : false
end
def edit?
user.system_admin? ? true : false
end
def show?
user.system_admin? ? true : false
end
def destroy?
internal_name = Rails.application.secrets.internal_company_short_name
# do not destroy the internal company record
user.system_admin? && (company[:short_name] != internal_name ) ? true : false
end
end
and I check it from the Company controller
def destroy
authorize #company
##company.destroy
....
end
why (company[:short_name] is wrong ?
If I look into the Pundit doc , the example with the PostPolicy , scope and post.published is similar ...
class PostPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if user.admin?
scope.all
else
scope.where(:published => true)
end
end
end
def update?
user.admin? or not post.published?
end
end
Take a look into documentation:
Pundit makes the following assumptions about this class:
The class has the same name as some kind of model class, only suffixed with the word "Policy".
The first argument is a user. In your controller, Pundit will call the current_user method to retrieve what to send into this
argument
The second argument is some kind of model object, whose authorization you want to check. This does not need to be an
ActiveRecord or even an ActiveModel object, it can be anything
really.
The class implements some kind of query method, in this case update?. Usually, this will map to the name of a particular
controller action.
That's it really.
Usually you'll want to inherit from the application policy created by
the generator, or set up your own base class to inherit from:
class PostPolicy < ApplicationPolicy
def update?
user.admin? or not record.published?
end
end
In the generated ApplicationPolicy, the model object is called record.
just discovered that one should use #record rather than company
( read the a question related to scopes : Implementing scopes in Pundit )
but I don't understand why the Pundit doc does not mention it , and still use a model instance like 'post' for PostPolicy ...
can someone enlighten us ?
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
I am following up from a problem that I had before. I was able to get the code to work for three roles, but I need to include 4 roles in the mix.
The problem: I have 4 roles (user, business user, super user, and admin). Admins have access to everything (user index). Super users can only see both users and business users (user index).
The error: I have a functioning app that allows admins to have access to everything, but my super users can only see users (and not business users). I tried switching in the User Policy resolve method, for the super user to role: 'business_user' to see if that even worked. Well, it does not work and it only shows me users (not business_users). It's probably a simple ruby issue that I'm overlooking.
User Policy
class UserPolicy
attr_reader :current_user, :model
def initialize(current_user, model)
#current_user = current_user
#user = model
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if user.admin?
scope.all
else user.super_user?
scope.where(role: 'user')
end
end
end
def index?
#current_user.admin? or #current_user.super_user?
end
end
User Controller
class UsersController < ApplicationController
before_filter :authenticate_user!
after_action :verify_authorized
def index
#users = policy_scope(User)
authorize #users
end
[rest of the controller]
User Model
class User < ActiveRecord::Base
enum role: [:user, :business_user, :super_user, :admin]
[rest of model]
end
Can you try to change this method :
def resolve
if user.admin?
scope.all
else user.super_user?
scope.where(role: 'user')
end
end
By this :
def resolve
if user.admin?
scope.all
else user.super_user?
scope.where('role == "user" or role == "business_user"').all
end
end
You have to change your query to have both roles.
I figured out what I had to do. It was a two step process. First, I had to change the role to the numerical value that pundit stores it as instead of the string, so the role would be 0 & 1. Second, I used an array to feed them into the param so it would accept multiple options.
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if user.admin?
scope.all
elsif user.super_user?
scope.where(role: [1,0])
else
scope.none
end
end
end