Running into something I don't understand with Pundit,
Using Rails 4.2.5.1, Pundit 1.1.0 with Devise for authentication.
I'm trying to use a policy scope for the BlogController#Index action.
If user is admin, display all posts (drafts, published)
If user is standard, display posts marked published only
If no user / user not logged in, display posts marked published only
Getting an error:
undefined method `admin?' for nil:NilClass
Live shell reveals:
>> user
=> nil
# ApplicationController
class ApplicationController < ActionController::Base
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
private
def user_not_authorized
flash[:error] = "You are not authorized to perform this action."
redirect_to(request.referrer || root_path)
end
end
# BlogController
# == Schema Information
#
# Table name: blogs
#
# id :integer not null, primary key
# title :string default(""), not null
# body :text default(""), not null
# published :boolean default("false"), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class BlogsController < ApplicationController
before_action :set_blog, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user!, except: [:index, :show]
after_action :verify_authorized, except: [:index, :show]
after_action :verify_policy_scoped, only: [:index]
def index
#blogs = policy_scope(Blog)
authorize #blog
end
def show
end
def new
#blog = Blog.new
authorize #blog
end
def edit
authorize #blog
end
def create
#blog = Blog.new(blog_params)
#blog.user = current_user if user_signed_in?
authorize #blog
if #blog.save
redirect_to #blog, notice: "Blog post created."
else
render :new
end
end
def update
authorize #blog
if #blog.update(blog_params)
redirect_to #blog, notice: "Blog updated."
else
render :edit
end
end
def destroy
authorize #blog
#blog.destroy
redirect_to blogs_url, notice: "Blog post deleted."
end
private
def set_blog
#blog = Blog.friendly.find(params[:id])
end
def blog_params
params.require(:blog).permit(*policy(#blog|| Blog).permitted_attributes)
end
end
# Application Policy
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
def index?
false
end
def show?
scope.where(:id => record.id).exists?
end
def create?
false
end
def new?
create?
end
def update?
false
end
def edit?
update?
end
def destroy?
false
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
# Blog Policy
class BlogPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.where(published: true)
end
end
end
def new?
user.admin?
end
def index?
true
end
def update?
user.admin?
end
def create?
user.admin?
end
def destroy?
user.admin?
end
def permitted_attributes
if user.admin?
[:title, :body]
end
end
end
In the Pundit BlogPolicy scope I've created:
class Scope < Scope
def resolve
if user.admin?
scope.order('id DESC')
else
scope.where('published: true')
end
end
end
If I log in as an admin user it works fine.
I'm able to view all blog posts.
If I log in as a standard user it works.
Standard user sees blog posts that are marked published.
If I'm not logged in where user is nil I get an error:
NoMethodError at /blog
undefined method `admin?' for nil:NilClass
I can add another clause elsif user.nil? before user.admin? or a case when statement but I thought if the user is not an admin it should just display what is in the else block?
# This seems wrong?
class Scope < Scope
def resolve
if user.nil?
scope.where('published: true')
elsif user.admin?
scope.all
else
scope.where('published: true')
end
end
end
Any pointers much appreciated
You can use try:
if user.try(:admin?)
# do something
end
http://api.rubyonrails.org/v4.2.5/classes/Object.html#method-i-try
This happens because there is no user when you are not logged in. (Probably to user variable nil value is assigned somewhere, so you are trying to call admin? method on nil object)
If you use ruby 2.3.0 or newer you had better use safe navigation
if user&.admin?
scope.order('id DESC')
else
scope.where('published: true')
end
If you user other ruby version
if user.try(:admin?)
scope.order(id: :desc)
else
scope.where(published: true)
end
Related
I'm using the Pundit gem for the user authorizations in my Rails project. The Edit function works as I expected, just user admin and whoever created the review is able to update it. However, I can't delete them with the pundit set up.
Here's my Reviews Controller:
class ReviewsController < ApplicationController
before_action :set_review, only: [:show, :edit, :update, :destroy]
def index
#reviews = Review.all
end
def new
#review = Review.new
end
def create
#review = Review.new(review_params)
#review.user_id = current_user.id
if #review.save
redirect_to review_path(#review), :alert => "Awesome! Here's your small review!"
else
error_messages(#review)
render 'new'
end
end
def show
end
def edit
authorize #review
end
def update
if #review.update(review_params)
redirect_to review_path(#review), :alert => "That's a good update!"
else
error_messages(#review)
render 'edit'
end
end
def destroy
authorize #review
#review.destroy
redirect_to reviews_path, :alert => "We'll miss that review."
end
private
def set_review
#review = Review.find_by(id: params[:id])
end
def review_params
params.require(:review).permit(:title, :content)
end
end
The Application Policy looks like this:
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
def index?
false
end
def show?
false
end
def create?
false
end
def create?
new?
end
def update?
false
end
def edit?
update?
end
def destroy?
destroy?
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
scope.all
end
end
end
And here's my customized Review Policy file:
class ReviewPolicy < ApplicationPolicy
def edit?
user.admin? || record.user == user
end
def destroy?
user.admin? || record.user == user
end
end
In case you're wondering, I created the user_not_authorized helper method. My Application Controller looks like this.
class ApplicationController < ActionController::Base
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
protect_from_forgery with: :exception
helper_method :current_user
add_flash_types :info, :error, :warning
def logged_in?
!!current_user
end
def current_user
#current_user ||= User.find_by(id: session[:user_id])
end
def error_messages(obj)
obj.errors.messages.each do |k,v|
flash[:alert] = "#{k.to_s} #{v.first.to_s}"
end
end
private
def user_not_authorized
flash[:alert] = "Ooops sorry. You don't have access to this."
redirect_to (request.referrer || root_path)
end
end
Hope my message is clear enough and would appreciate any help. Remember, '''user.admin? || record.user == user''' works for editing but the user admins and the review creators can't delete the reviews when applying the same methods.
Please let me know if any question.
I'm still trying to wrap my head around Pundit policies. I think I'm close but I've wasted too much time trying to figure this out. My Posts policy works great, but trying to authorize comments, I am getting undefined errors...
comments_controller.rb
class CommentsController < ApplicationController
before_action :find_comment, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user!, except: [:index, :show]
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.create(params[:comment].permit(:comment))
#comment.user_id = current_user.id if current_user
authorize #comment
#comment.save
if #comment.save
redirect_to post_path(#post)
else
render 'new'
end
end
def edit
authorize #comment
end
def update
authorize #comment
if #comment.update(params[:comment].permit(:comment))
redirect_to post_path(#post)
else
render 'edit'
end
end
def destroy
authorize #comment
#comment.destroy
redirect_to post_path(#post)
end
private
def find_comment
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
end
end
comment_policy.rb
class CommentPolicy < ApplicationPolicy
def owned
comment.user_id == user.id
end
def create?
comment.user_id = user.id
new?
end
def new?
true
end
def update?
edit?
end
def edit?
owned
end
def destroy?
owned
end
end
The formatting and indenting is a bit off... thats not how I code I swear
class ApplicationPolicy
attr_reader :user, :post
def initialize(user, post)
raise Pundit::NotAuthorizedError, "must be logged in" unless user
#user = user
#post = post
end
def index?
false
end
def show?
scope.where(:id => post.id).exists?
end
def create?
false
end
def new?
create?
end
def update?
false
end
def edit?
update?
end
def destroy?
false
end
def scope
Pundit.policy_scope!(user, post.class)
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
scope
end
end
end
You initialized your resource in ApplicationPolicy as #post. And since your CommentPolicy inherits from ApplicationPolicy and uses its initialization, it only has access to #post. Best option is to keep it as record:
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
raise Pundit::NotAuthorizedError, "must be logged in" unless user
#user = user
#record = record
end
## code omitted
end
class CommentPolicy < ApplicationPolicy
def owned
record.user_id == user.id
end
## code omitted
end
Basically you can call it anything you want but record makes more sense as it will be used in different policy subclasses.
I am implementing pundit and wish to restrict the user#edit and user#update actions to only the current_user
def edit
#user = current_user
authorize(#user)
end
def update
#user = current_user
authorise(#user)
if #user.update_attributes(user_params)
flash[:success] = "Profile updated"
redirect_to edit_user_path
else
render 'edit'
end
end
The following is my attempted policy which (a) does not work and (b) is illogical.
class UserPolicy
attr_reader :user, :user
def initialise(user, user)
#user = user
end
def update?
true
end
alias_method :edit?, :update?
end
I have now updated my UserPolicy as per below. I have set the actions to false for testing as everything was being authorised:
class UserPolicy < ApplicationPolicy
def new?
create?
end
def create?
false
end
def edit?
update?
end
def update?
false
#user.id == record.id
end
end
However my policies are not recognised. Upon further reading I added the following to my ApplicationController:
after_filter :verify_authorized, except: :index
after_filter :verify_policy_scoped, only: :index
When I now navigate to my user#edit action I receive:
Pundit::AuthorizationNotPerformedError
First, make sure you have...
your-app/app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Pundit
end
your-app/app/policies/application_policy.rb with default permissions for common actions.
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
def index?
false
end
def show?
scope.where(:id => record.id).exists?
end
def create?
false
end
def new?
create?
end
def update?
false
end
def edit?
update?
end
def destroy?
false
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
Then, in your UserPolicy
your-app/app/policies/section_policy.rb
class UserPolicy < ApplicationPolicy
def edit?
user.id == record.id
end
def update?
edit?
end
end
So, by default, user will be your current user and record will be the #user defined on edit and update actions.
You don't need to call authorize method explicitly. Pundit knows what to do with your #user attribute. So, your controller should be:
def edit
user
end
def update
if user.update_attributes(user_params)
flash[:success] = "Profile updated"
redirect_to edit_user_path
else
render 'edit'
end
end
private
def user
#user ||= User.find(params[:id])
end
you must know if you don't have a current_user method, yo will need to define a pundit_user in your application controller.
I'm trying to create a show view for three roles. Admin, super user, and user. An admin should see all of the users. A super user should see only users and a user should not see anyone. When I used the commented out policy method in the resolve for else user.super_user? would give me unsupported: TrueClass error. Any suggestions are welcomed.
Users Controller
def index
#users = policy_scope(User)
authorize User
end
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(user.role = 'user' )
# scope.where(user.role != 'admin') [this would not work in the views, but it would work in rails c]
end
end
end
def index?
#current_user.admin? or #current_user.super_user?
end
end
updated Users Controller
class UsersController < ApplicationController
before_filter :authenticate_user!
after_action :verify_authorized
def index
#users = policy_scope(User)
end
end
Correct Answer
I figured out what I needed to do. I was calling the role incorrectly. Updated scope below.
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
controller
class UsersController < ApplicationController
before_filter :authenticate_user!
after_action :verify_authorized
def index
#users = policy_scope(User)
authorize #users
end
Your resolve method should use elsif:
# Safer option
def resolve
if user.admin?
scope.all
elsif user.super_user?
scope.where(user.role = 'user' )
else
scope.none
end
end
or not check for the super user at all and just depend on checking the authorization of the user before the result is used:
# This option is the same as the code you added to your question
# but doesn't include the unnecessary check
def resolve
if user.admin?
scope.all
else
scope.where(user.role = 'user' )
end
end
EDIT: updated to deal with the case of not being an admin or super user
So, I'm trying to use the gem pundit. I'm just trying to figure out how to have an index view for users and admins. I want to render all results for an admin and only related posts for a user. I've googled and searched on github, but I'm not find any luck. What do I have to put in my policy and controller?
original code
class PostsPolicy
attr_reader :current_user, :model
def initialize(current_user, model)
#current_user = current_user
#post = model
end
def index?
#current_user.admin?
end
end
controller
class PostsController < ApplicationController
before_filter :load_user
before_filter :authenticate_user!
after_action :verify_authorized
def index
#posts = Post.order('title').page(params[:page]).per(25)
authorize User
end
private
def load_user
#user = User.find_by_id(params[:user_id])
end
end
second update
class PostsPolicy
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(user: user)
end
end
end
end
controller
class PostsController < ApplicationController
before_filter :load_user
before_filter :authenticate_user!
after_action :verify_authorized
def index
#posts = policy_scope(Post).order('title').page(params[:page]).per(25)
authorize User
end
private
def load_user
#user = User.find_by_id(params[:user_id])
end
end
third update
class PostPolicy
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(user: user)
end
end
end
def index?
user.admin? || user.posts.count > 0
end
end
controller
class PostsController < ApplicationController
before_filter :load_user
before_filter :authenticate_user!
after_action :verify_authorized
def index
#posts = policy_scope(Post).order('title').page(params[:page]).per(25)
authorize User
end
private
def load_user
#user = User.find_by_id(params[:user_id])
end
end
** final update with working code **
class PostPolicy
attr_reader :user, :model
def initialize(user, model)
#user = user
#post = 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
scope.where(user: user)
end
end
end
def index?
user.admin? || user.posts.count
end
end
controller
class PostsController < ApplicationController
before_filter :load_user
before_filter :authenticate_user!
after_action :verify_authorized
def index
#posts = policy_scope(Post).order('title').page(params[:page]).per(25)
authorize Post
end
private
def load_user
#user = User.find_by_id(params[:user_id])
end
end
What you're looking for is a Scope:
class PostsPolicy
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(user: user)
end
end
end
end
Then in your controller
def index
#posts = policy_scope(Post).order('title').page(params[:page]).per(25)
authorize User
end
Edit
As a side note, authorize User will probably not serve you well in the long run. You're essentially creating an index policy that would need to serve every index. If you want to to authorize visibility to the index page you can still do something like this in your policy:
def index?
user.admin? || user.posts.count > 0
end
Assuming that relationship is set up, then you would call authorize Post in your index controller before your policy_scope.