Pundit policy for personalized routes - ruby-on-rails

I'm working on a rails app where I wrote a personalized route called "all_designs"; with the corresponding method on the controller and the view, before I add pundit to my project it was working fine.
Now I'm having this error:
Pundit::AuthorizationNotPerformedError in DesignsController#all_designs
I understand that I'm missing a policy for this action, but the way I'm trying is not working.
How can I add a policy for this method?
Controller:
class DesignsController < ApplicationController
before_action :set_design, only: [:show,:edit,:update,:destroy]
def index
#designs = policy_scope(Design.where(user: current_user, status: 'activo'))
#user = current_user
end
def all_designs
#designs = Design.where(user: current_user)
#user = current_user
end
...
end
Policy:
class DesignPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
end
def create?
true
end
def show?
true
end
def destroy?
user == record.user
end
def update?
# If the user is the owner of the design
user == record.user
end
def all_designs?
true
end
end

I would consider separate controller and policy for this as what you're doing is really just a nested route (designs belonging to a singleton resource).
scope 'user', module: :users do
resources :designs, only: :index
end
module Users
class DesignsPolicy
class Scope < Scope
def resolve
#user.designs # make sure user has a `has_many :designs` assocation.
end
end
end
def index?
true
end
end
# Represents designs belonging to the current user
module Users
class DesignsController < ApplicationController
# GET /user/designs
def index
#designs = policy_scope
end
end
end
This lets you separate the logic of displaying the the current users designs from /designs which would display everything in a clean way.

Every method on the controller which needs to be authorized, needs to contains an explicit declaration like this:
def all_designs
#designs = Design.where(user: current_user)
#user = current_user
authorize #designs
end
The reason it wasn't working was: I missed the authorize line

Related

How to restrict access to different pages in Rails?

I have controller Users:
class UsersController < ApplicationController
......
def show
#user = User.find(params[:id])
end
def index
#users = User.paginate(page: params[:page], per_page: 25)
end
......
end
Now users profiles are at /users/1, /users/2, etc. and list of users is at /users/.
I want to give special access:
user can see only own profile
admin can see the list of users and any profile
How can I restrict access this way?
Assuming you have a current_user defined and your User class has an admin attribute you can do the following:
class UsersController < ApplicationController
......
def show
#user = User.find(params[:id])
if current_user.admin || #user == current_user
# render the show screen
else
# redirect to wherever
end
end
def index
if current_user.admin
#users = User.paginate(page: params[:page], per_page: 25)
# render the index screen
else
# redirect to wherever
end
end
......
end
Or you could just use one of the plenty of authorization gems out there, like cancancan or pundit.
You should use ACL libraries like cancancan or pundit or from ruby-toolbox.com
I would probably handle this by having two different endpoints, something like /profile and /admin/users/1. Then you have different controllers for them:
UserProfileController < ApplicationController
def show
#user = current_user
end
end
and:
class Admin::UsersController < AdminController
def show
#user = User.find(params[:id])
render 'user_profile/show' # or another template if you like
end
end
class AdminController < ApplicationController
before_action :ensure_admin
def ensure_admin
if !current_user.admin?
raise ActionController::RoutingError, 'Not Found'
end
end
end
Considering your url user/1/ you grab the param id and compare it to the current user ID in a hook :
before_action :auth_user
private
def auth_user
unless params[:id].to_s == current_user.id.to_s
redirect_to root_path
end
Regarding the admin you probably have a dedicated namespace, with even more thorough checks, where you can see user profiles.

Pundit with namespaced controllers

The policy_scope works perfectly finding the correct policy named Admin::RemittancePolicy but authorize method not.
module Admin
class RemittancesController < AdminController # :nodoc:
...
def index
#remittances = policy_scope(Remittance).all
render json: #remittances
end
def show
authorize #remittance
render json: #remittance
end
...
end
end
Take a look at output error:
"#<Pundit::NotDefinedError: unable to find scope `RemittancePolicy::Scope` for `Remittance(...)`>"
Perhaps a error with pundit, I really not know how fix it. Thanks.
More information below:
# policies/admin/admin_policy.rb
module Admin
class AdminPolicy < ApplicationPolicy # :nodoc:
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 # :nodoc:
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope.is_a?(Array) ? scope.last : scope
end
def resolve
scope
end
end
end
end
# controllers/admin/admin_controller.rb
module Admin
class AdminController < ActionController::API # :nodoc:
include Knock::Authenticable
include Pundit
before_action :authenticate_user
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
# def policy_scope!(user, scope)
# model = scope.is_a?(Array) ? scope.last : scope
# PolicyFinder.new(scope).scope!.new(user, model).resolve
# end
def policy_scope(scope)
super [:admin, scope]
end
def authorize(record, query = nil)
super [:admin, record], query
end
end
end
Your stacktrace says the error comes from
app/policies/admin/admin_policy.rb:9:in 'scope'
That's this:
def scope
Pundit.policy_scope! user, record.class
end
record.class evaluates to Remittance, so if I understand what you're trying to do, you need to change scope to
def scope
Pundit.policy_scope! user, [:admin, record.class]
end

Scope for nested resources using pundit resolve method

I am referring to my own question Rails Nested Resources with Pundit Allowing Index and finally came up with a working solution but is there not any much better solution defining scope.where(?) or scope.select(?) in the property_policy? How to get all the properties that only belongs to one specific deal using the pundit resolve method?
What I finally did :
properties_controller.rb
class PropertiesController < ApplicationController
before_action :set_deal, except: [:index, :all]
before_action :set_property, only: [:show, :edit, :update, :destroy]
def all
#properties = Property.all
authorize #properties
end
def index
#deal = Deal.find(params[:deal_id])
#properties = policy_scope(Deal)
end
def set_deal
#deal = Deal.find(params[:deal_id])
# pundit ######
authorize #deal
###############
end
(...)
end
property_policy.rb
class PropertyPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all if user.admin?
end
def all?
user_is_admin?
end
def user_is_admin?
user.try(:admin?)
end
(...)
end
What I'd like better:
properties_controller.rb
def index
#deal = Deal.find(params[:deal_id])
#properties = policy_scope(Property) # => for # #properties = #deal.properties
authorize #deal
end
and in the property_policy.rb something like
def resolve
# scope.where(???) if user.admin? # only an admin user can see the #deal.properties
# or any other solution using scope
end
As a reminder 1 deal has many properties and 1 property belongs to one specific deal. My routes are nested deals/id/properties except for the full list of properties I have simple "/properties". Thanks a lot for helping.
** UPDATE **
I finally went for
properties_controller.rb
def index
#deal = Deal.find(params[:deal_id])
#properties = policy_scope(#deal.properties)
authorize #properties, :index?
end
and in property_policy.rb
class PropertyPolicy < ApplicationPolicy
class Scope < Scope
def resolve
user.admin? ? scope.all : scope.none
end
end
def index?
user_is_admin?
end
def user_is_admin?
user.try(:admin?)
end
end
Not sure if it is the proper way
What you want to do is pass a scope to the policy - not just a class.
#properties = policy_scope(#deal.policies)
class PropertiesPolicy
class Scope < Scope
def resolve
user.admin? ? scope.all : scope.none
end
end
end
Another problem with your controller is that authorize #deal will call DealsPolicy#index? which is not what you want.
To authorize an index action you want to call authorize with the model class (and not an instance):
def index
authorize Property # calls PropertiesPolicy#index?
#deal = Deal.find(params[:deal_id])
#properties = policy_scope(#deal.properties)
end
In that case you don't have to do anything special in your Scope#resolve method really. Just return scope since you can assume at that point that the user is an admin.

Authorise user to edit a particular field using Pundit in Rails

I'm running Pundit in my Rails app for authorisation. I seem to be getting the hang of it all but want to know how to restrict the edit or update actions to a certain field.
For example, a user can edit their user.first_name, user.mobile or user.birthday etc but can't edit their user.role. Essentially my logic is, let the user edit anything that's cosmetic but not if it is functional.
These fields should only be able to be edited by a user who has a 'super_admin' role (which I have setup on the user.rb with methods such as the below).
def super_admin?
role == "super admin"
end
def account?
role == "account"
end
def segment?
role == "segment"
end
def sales?
role == "sale"
end
def regional?
role == "regional"
end
def national?
role == "national"
end
def global?
role == "global"
end
I pretty much have a clean slate user_policy.rb file where the update and edit actions are the default
def update?
false
end
def edit?
update?
end
Maybe I am thinking entirely wrong about this and should just wrap a user.super_admin? if statement around the role field on the user show page but this feels wrong if I am only using that tactic for security.
Use Pundit's permitted_attributes helper which is described on the gem's README page: https://github.com/elabs/pundit
# 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
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def update
#post = Post.find(params[:id])
if #post.update_attributes(post_params)
redirect_to #post
else
render :edit
end
end
private
def post_params
params.require(:post).permit(policy(#post).permitted_attributes)
end
end
In your views, you can limit what users can see based on their role.
User View
- if current_user.super_admin?
= f.select(:role, User.roles.keys.map {|role| [role.titleize.role]})
- else
= user.role
And in the policy you can call the role of the user to make sure they are able to edit.
class UserPolicy
attr_reader :current_user, :model
def initialize(current_user, model)
#current_user = current_user
#user = model
end
def edit?
#current_user.super_admin || #current_user == #user
end
end

inheritance in controllers

I use inheritance in my model. An event has different types:
Event < activity
Event < training
Event < game
I want to set session data to every event type like
game.user_id = session[:user_id]
training.user_id = session[:user_id]
activity.user_id = session[:user_id]
I want to avoid writing #game.user_id = session[:user_id] , ..., ... in every create method in the controller of activity, game and training
Someone knows how to approach this best.
Thanks
Perhaps you are looking for a before_filter that resides in your ApplicationController? Then in each controller, you can set the before_filter to run on create actions.
ApplicationController
def set_user_ids
game.user_id = session[:user_id]
training.user_id = session[:user_id]
activity.user_id = session[:user_id]
end
...
end
OneController < ApplicationController
before_filter :set_user_ids, :only => [:create]
...
end
TwoController < ApplicationController
before_filter :set_user_ids, :only => [:create]
...
end
Don't use game.user_id, instead you can do this:
game = current_user.games.build(params[:game])
if game.save
# do something
else
# do something else
end
Repeat for your other controllers too!
The associations guide may be helpful too.
Generally you'll want to use the built-in scoping that Rails provides. Just to flesh out what #Radar already posted:
class ApplicationController < ActionController::Base
before_filter :find_current_user
private
def find_current_user
#current_user = User.find( session[:user_id] )
end
end
class EventsController < ApplicationController
def create
#event = #current_user.events.build( params[:event] )
#event.save!
end
end
This assumes that you have setup the associations in your model:
class User
has_many :events
end
class Event
belongs_to :user
end
This is also a rather handy mechanism if you need to restrict what a user can see or edit:
class EventsController < ApplicationController
def index
#events = #current_user.events # only fetch current users events
end
def update
#event = #current_user.events.find( params[:id] ) # can't update other user's events
#event.update_attributes!( params[:event] )
end
end

Resources