I'm trying to learn how to use pundit with my Rails 4 app.
I have a potential use policy. The potential use table has an attribute called :user_id.
I want users to be permitted to update instances if they created them. I'm trying to figure out how to get the update action to work.
My current attempts are shown below.
class PotentialUsePolicy < ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
def index?
true if user.is_admin?
end
def show?
true
end
def new?
true
end
def create?
new?
end
def update?
if #user.id == #potential_use.user_id
# if user.id == potential_use.user_id
true
else
false
end
end
def destroy?
update?
end
def edit?
true
end
def potential_use
record
end
end
When I try these, I keep getting errors saying:
undefined method `user_id' for nil:NilClass
I don't understand why I get this message. When I look in the console, I can see an entry which has a user id.
p = PotentialUse.where(project_id: 26)
PotentialUse Load (0.5ms) SELECT "potential_uses".* FROM "potential_uses" WHERE "potential_uses"."project_id" = $1 [["project_id", 26]]
=> #<ActiveRecord::Relation [#<PotentialUse id: 9, comment: "adsfsfdadfasdddxxddbbdd", project_id: 26, created_at: "2016-08-18 23:16:06", updated_at: "2016-08-24 01:06:00", user_id: 1, private_comment: false>]>
2.3.0p0 :016 >
When I look in the view (without trying to use the pundit policy, the page renders with all the right content, including the user name (which is accessed by the user_id).
The update action in my potential use controller has:
def update
authorize #potential_use
respond_to do |format|
if #potential_use.update(potential_use_params)
format.html { redirect_to #project, notice: 'Potential use was successfully updated.' }
format.json { render :show, status: :ok, location: #potential_use }
else
format.html { render #project }
format.json { render json: #potential_use.errors, status: :unprocessable_entity }
end
end
end
Can anyone see what I've done wrong?
To check if a user has permission to perform an action on a policy from a view:
<% if policy(#post).update? %>
<%= link_to "Edit post", edit_post_path(#post) %>
<% end %>
Your error is because you call #potential_use.user_id but #potential_use is never declared in the policy class. You defined potential_use as a method, but in that method you call record, instead of #record.
So, to fix it you should call potential_use instead of #potential_use and change the method to:
def potential_use
#record
end
Or simply, call #record.user_id instead of #potential_use.user_id.
Besides, as you can see in the Pundit readme, when you inherit from ApplicationPolicy, you don't need to declare:
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
As it is already in the parent class.
You access the authorized objects with your accessors. In your example you probaly called
authorize #potential_use
in your controller update action. The update? method on your controller would look like this:
def update?
user.id == record.user_id
end
Related
I don't know if I'm doing something wrong here but it seems like.
I use Pundit for authorization and I have set up a few models with it now.
Ive got a Category model which can only be created by admins. Also I don't want users to see the show/edit/destroy views either. I just want it to be accessed by admins. So far so good.
Will add some code below:
category_policy.rb
class CategoryPolicy < ApplicationPolicy
def index?
user.admin?
end
def create?
user.admin?
end
def show?
user.admin?
end
def new?
user.admin?
end
def update?
return true if user.admin?
end
def destroy?
return true if user.admin?
end
end
categories_controller.rb
class CategoriesController < ApplicationController
layout 'scaffold'
before_action :set_category, only: %i[show edit update destroy]
# GET /categories
def index
#category = Category.all
authorize #category
end
# GET /categories/1
def show
#category = Category.find(params[:id])
authorize #category
end
# GET /categories/new
def new
#category = Category.new
authorize #category
end
# GET /categories/1/edit
def edit
authorize #category
end
# POST /categories
def create
#category = Category.new(category_params)
authorize #category
if #category.save
redirect_to #category, notice: 'Category was successfully created.'
else
render :new
end
end
# PATCH/PUT /categories/1
def update
authorize #category
if #category.update(category_params)
redirect_to #category, notice: 'Category was successfully updated.'
else
render :edit
end
end
# DELETE /categories/1
def destroy
authorize #category
#category.destroy
redirect_to categories_url, notice: 'Category was successfully destroyed.'
end
private
# Use callbacks to share common setup or constraints between actions.
def set_category
#category = Category.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def category_params
params.require(:category).permit(:name)
end
end
application_policy.rb
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
def index?
false
end
def create?
create?
end
def new?
create?
end
def update?
false
end
def edit?
update?
end
def destroy?
false
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
scope.all
end
end
end
Ive got Pundit included in my ApplicationController and rescue_from Pundit::NotAuthorizedError, with: :forbidden this set up there as well.
The authorization itself works, if I'm logged in with an admin account I have access to /categories/*. If I'm logged out I get the following: NoMethodError at /categories
undefined methodadmin?' for nil:NilClass`
While writing the question I think I found the problem- I guess Pundit looks for a User that is nil because I'm not logged in. What would the correct approach of solving this issue look like?
Best regards
The most common approach is to redirect users from pages that are not accessible by not logged in users. Just add a before action in your controller:
class CategoriesController < ApplicationController
before_action :redirect_if_not_logged_in
<...>
private
def redirect_if_not_logged_in
redirect_to :home unless current_user
end
end
(I assume here that you have current_user method which returns user instance or nil. Please change :home to wherever you want to redirect users)
There are multiple ways of achieving what you want.
The most obvious (but kind of dirty) and straightforward-looking way of doing that would be to add a check for user presence in every condition:
user && user.admin?
It won't fail with nil error as the second part of the condition won't get executed. But it doesn't look very nice, right? Especially if you have to copy this over to all methods you have in CategoryPolicy.
What you can do instead, is to make Pundit "think" that you passed a User, by creating a GuestUser class which responds with false to admin? method (https://en.wikipedia.org/wiki/Null_object_pattern):
In object-oriented computer programming, a null object is an object with no referenced value or with defined neutral ("null") behavior. The null object design pattern describes the uses of such objects and their behavior (or lack thereof)
And use it when user is nil. In practice, it will look like this:
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user || GuestUser.new
#record = record
end
# ...
end
class GuestUser
def admin?
false
end
end
This way you won't have to alter any of your other code, as the model you passed responds to the method which is expected by policy (admin?). You may want to define this GuestUser somewhere else (not in the policy file), depending if you want other parts of the app to reuse that behavior.
You can also proceed with the redirect approach from P. Boro answer. It's less flexible in some sense but can totally work fine if you don't need anything besides redirecting all non-logged in users.
I have a Rails 4 App an Order Model where a user fills out a form for necessary data for price calculations etc. What I want is that they are prompted to sign in (with DEVISE gem) at the end of step_3 and are routed to a payment section but they can store their form data from the Order Model that the user has previously filled out, this data being the order form data before they signed in.
Main Issues:
Redirecting them after sign in mid way through process using devise
Saving the form data and associating it with the current user immediately before routed to the payment section
the Order.find_by section. What do I find the order by?
Background:
Rails 4
PostgreSQL
Devise Gem
class OrdersController < ApplicationController
before_action :authenticate_user!, :except => [:step_1,:process_step1,:step_2a,:process_step_2a, :step_2b, :step_2c, :step_3]
def step_1
#order = Order.find_by id: session[:order_id]
if #order.nil?
#order=Order.new
#order.save!
end
end
def process_step1
order = Order.find_by id: session[:order_id]
order.update_attributes(order_params)
if (order.building == 'Residential') || (order.building == 'Commercial' )
redirect_to step_2a_path
elsif (order.building == 'Commercial + Parking') || (order.building == 'Residential + Parking')
redirect_to step_2b_path
else
redirect_to step_2c_path
end
end
def step_2a
#order = Order.find_by id: params[:session_id]
end
def process_step_2a
order= Order.find_by status: 'cart'
# , user_id: current_user.id
order.update_attributes(order_params)
if order.save
redirect_to step_3_path
end
end
def step_2b
#order= Order.find_by status:'cart'
# , user_id: current_user.id
end
def process_step_2b
order= Order.find_by status: 'cart'
# , user_id: current_user.id
order.update_attributes(order_params)
if order.save
redirect_to step_3_path
end
end
def step_2c
#order= Order.find_by status:'cart'
# , user_id: current_user.id
end
def process_step_2c
order= Order.find_by status: 'cart'
order.update_attributes(order_params)
if order.save
redirect_to step_3_path
end
end
def step_3
#order= Order.find_by status:'cart'
# , user_id: current_user.id
end
def process_step_3
order= Order.find_by status: 'cart', user_id: current_user.id
order.update_attributes(order_params)
if order.save
redirect_to payment_path
end
end
def payment
#order= Order.find_by status:'cart', user_id: current_user.id
end
First off you should really refactor you routes. With that structure your application is going to get exponentially more confusing to navigate the larger it grows, and will make it practically impossible for anyone else to work on it/help you with debugging. Take a look at this guide on RESTful routing to learn more.
As for you issue, you can store their form data in one of the rails temporary storage mechanisms (I'd recommend a session variable) while they're logging in then redisplay it after they finish
There is a callback after_sign_in_path_for, you can add it in your ApplicationController. Usage is here
2,3. Continue to use session[:order_id]. Can't figure out why do you use find_by status: 'cart' and Order.find_by id: params[:session_id]. Check this url to save old session after log in.
Personally I do not recommend this method and I think it's better to bind order id to some secure random generated value stored in cookie.
I am trying to make an app in Rails 4.
I want to use Pundit for authorisations. I also use Devise for authentication and Rolify for role management.
I have a user model and am making my first policy, following along with this tutorial:
https://www.youtube.com/watch?v=qruGD_8ry7k
I have a users controller with:
class UsersController < ApplicationController
before_action :set_user, only: [:index, :show, :edit, :update, :destroy]
def index
if params[:approved] == "false"
#users = User.find_all_by_approved(false)
else
#users = User.all
end
end
# GET /users/:id.:format
def show
# authorize! :read, #user
end
# GET /users/:id/edit
def edit
# authorize! :update, #user
end
# PATCH/PUT /users/:id.:format
def update
# authorize! :update, #user
respond_to do |format|
if #user.update(user_params)
sign_in(#user == current_user ? #user : current_user, :bypass => true)
format.html { redirect_to #user, notice: 'Your profile was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: 'edit' }
format.json { render json: #user.errors, status: :unprocessable_entity }
end
end
end
# GET/PATCH /users/:id/finish_signup
def finish_signup
# authorize! :update, #user
if request.patch? && params[:user] #&& params[:user][:email]
if #user.update(user_params)
#user.skip_reconfirmation!
sign_in(#user, :bypass => true)
redirect_to #user, notice: 'Your profile was successfully updated.'
else
#show_errors = true
end
end
end
# DELETE /users/:id.:format
def destroy
# authorize! :delete, #user
#user.destroy
respond_to do |format|
format.html { redirect_to root_url }
format.json { head :no_content }
end
end
private
def set_user
#user = User.find(params[:id])
end
def user_params
params.require(:user).permit(policy(#user).permitted_attributes)
# accessible = [ :first_name, :last_name, :email ] # extend with your own params
# accessible << [ :password, :password_confirmation ] unless params[:user][:password].blank?
# accessible << [:approved] if user.admin
# params.require(:user).permit(accessible)
end
end
And this is my first go at the User policy.
class UserPolicy < ApplicationPolicy
def initialize(current_user, user)
#current_user = current_user
#user = user
end
def index?
#current_user.admin?
end
def show?
#current_user.admin?
end
def edit?
#current_user.admin?
end
def update?
#current_user.admin?
end
def finish_signup?
#current_user = #user
end
def destroy?
return false if #current_user == #user
#current_user.admin?
end
private
def permitted_attributes
accessible = [ :first_name, :last_name, :email ] # extend with your own params
accessible << [ :password, :password_confirmation ] unless params[:user][:password].blank?
accessible << [:approved] if user.admin
params.require(:user).permit(accessible)
end
end
My questions are:
The tutorial shows something called attr_reader. I have started learning rails from rails 4 so I don't know what these words mean. I think it has something to do with the old way of whitelisting user params in the controller, so I think I don't need to include this in my user policy. Is that correct?
is it right that i have to initialise the user model the way I have above (or is that only the case in models other than user, since I'm initialising current_user, it might already get the user initialised?
is it necessary to move the strong params to the policy, or will this work if I leave them in the controller?
The tutorial shows something called attr_reader. I have started learning rails from rails 4 so I don't know what these words mean. I think it has something to do with the old way of whitelisting user params in the controller, so I think I don't need to include this in my user policy. Is that correct?
No, it is very important.
attr_reader creates instance variables and corresponding methods that return the value of each instance variable. - From Ruby Official Documentation
Basically if you do
class A
attr_reader :b
end
a = A.new
you can do a.b to access b instance variable. It is important because in every policies you might allow read access of instance variables. #current_user and #user is instance variable.
is it right that i have to initialise the user model the way I have above (or is that only the case in models other than user, since I'm initialising current_user, it might already get the user initialised?
You have to initialise it manually. Currently, the way you did it is correctly. Good.
is it necessary to move the strong params to the policy, or will this work if I leave them in the controller?
It is the matter of choice. It will work even if you kept it into controller. Move to policy only if you want to whitelist attributes in quite complex way.
Note: device , pundit and rolify gem works good but there share some of the same functionality so be careful and consistence what to do with what.
For example, You can use devise_for :users , :students , :teachers which will give 3 different links to login the respective resources. You can do lot of things with it. You can further authenticate the urls as per the resources with authenticate method. Check https://github.com/plataformatec/devise/wiki/How-To:-Define-resource-actions-that-require-authentication-using-routes.rb This sort of thing can also be done with pundit with policies and rolify with roles.
I have a job and user(devise) form in the same view. When I am trying to submit with errors in the user fields it gives me an exception page with the validation messages. Submitting errors in the job fields works fine!
job_controller.rb
def new
#job = Job.new
if !current_user
#job.user = User.new
end
respond_to do |format|
format.html # new.html.erb
end
end
def create
#types = Type.all
#categories = Category.all
#job = Job.new(params[:job])
#if not logged in creates a user and sign in
if !current_user
#user = User.new(params[:job][:user_attributes])
else
#user = current_user
end
#job.user_id = #user.id
respond_to do |format|
if #job.save
if !current_user
sign_in(:user, #user)
end
format.html { redirect_to #job }
else
format.html { render action: "new" }
end
end
end
job.rb
attr_accessible :user_attributes, :description, :name ....
belongs_to :user
accepts_nested_attributes_for :user
Thanks!
That becuase you are calling, #user.save! which will generate an exception. Also doing it this way won't put the job in the same transaction as User. What you want are nested_attributes:
class Job < ActiveRecord::Base
accepts_nested_attributes_for :user
end
If the user is logged in, don't show that part of the form and filter those params.
See more in the Rails documentation here http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
EDIT:
Simplify your controller code, since you're using nested attributes you no longer need to manually create a user.
#if not logged in creates a user and sign in
if !current_user
#user = User.new(params[:job][:user_attributes]) # this is no longer needed
else
#user = current_user
end
#job.user_id = #user.id # this is redundant
To something more like:
# if logged in, manually assign the user (also you may want to reject any user attributes)
#job.user = current_user if current_user
Rails 3.0.3
ruby 1.9.2p0
The Problem:
I have a Users table which has many items, the item(s) in turn therefore belongs to the Users.
In my model item.rb i attempt to save the item along with the value for the user.id so i have:
self.User_ID = #user.id
this however give me the error
Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id
this is causing some confusion that it can't find this as in the show.html.erb that 'wraps' this page <%= #user.id %> displays the correct ID on the page
Many thanks in advance
** EDIT **
The Shorten action is the action upon which i want to parameter to be passed
class ItemsController < ApplicationController
def redirect
#item = Item.find_by_shortened(params[:shortened])
if #item
#redirect_to #item.original
redirect_to #item.original
else
redirect_to :shorten
end
end
def shorten
#host = request.host_with_port
#user = current_user
You need to load the #user model in every action that will require access to it. Having it render properly in the show action will not guarantee it is loaded in the update action.
Usually you need to have something like this in your controller:
class UsersController < ApplicationController
before_filter :load_user, :except => [ :index, :new, :create ]
# ...
protected
def load_user
#user = User.find(params[:user_id] || params[:id])
rescue ActiveRecord::RecordNotFound
render(:text => 'Record not found')
end
end