I'm adding a csv upload feature to my site. I want to be able to add startups to categories that I've created.
I'm getting this error:
Startup_policy.rb file
class StartupPolicy < CategoryPolicy
def create?
user.present?
end
def import?
user.present?
end
end
Startup_controller.rb
class StartupsController < ApplicationController
before_action :authenticate_user!
def create
#startup = #startupable.startups.new startup_params
#startup.user = current_user
#startup.save
redirect_to #startupable, notice: "Your startup was added succesfully."
authorize #startup
end
def import
count = Startup.import params[:file]
redirect_to category_startups_path, notice: "Imported #{count} startups"
authorize #startups
end
private
def set_category
#startup = Startup.find(params[:id])
authorize #startup
end
def startup_params
params.require(:startup).permit(:company, :url)
end
end
Startup.rb
class Startup < ActiveRecord::Base
belongs_to :startupable, polymorphic: true
belongs_to :user
def self.import(file)
counter = 0
CSV.foreach(file.path, headers: true, header_converters: :symbol) do |row|
startup = Startup.assign_from_row(row)
if startup.save
counter += 1
else
puts "#{startup.company} - #{startup.errors.full_messages.join(",")}"
end
end
counter
end
def self.assign_from_row(row)
startup = Startup.where(company: row[:company]).first_or_initialize
startup.assign_attributes row.to_hash.slice(:url)
startup
end
end
EDIT: added Category_policy.rb
class CategoryPolicy < ApplicationPolicy
def index?
true
end
def create?
user.present?
end
def update?
return true if user.present? && (user.admin? || category.user)
end
def show?
true
end
def destroy?
user.present? && user.admin?
end
private
def category
record
end
end
What am I missing here? Pundit is pretty easy to use but for some reason I'm stumped on this one.
I don't see that you've set #startups anywhere. So it's nil, which explains the error message. I assume you meant to set it to the resulting list you get after importing the CSV. Or, since all you are checking in the policy is whether you have a user, you could just authorize :startup which will get you to the right policy, and not care which startups you have.
Related
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
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.
NoMethodError at /
protected method `policy' called for #<RailsAdmin::MainController:0x007ff5e5d1a528>
Did you mean? policies
Here's the first thing it looks at in the error page (this is in the gemfile code)
# This method is called to find authorization policy
def policy(record)
begin
#controller.policy(record)
rescue ::Pundit::NotDefinedError
::ApplicationPolicy.new(#controller.send(:pundit_user), record)
end
end
private :policy
Getting this error when I try to visit /admin - nothing changed, was working fine in 5.1.6.. I didn't change the policy.rb file, i didn't change any controller code, nothing was changed at all apart from a gemfile update to go to rails 5.2.1
My 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
def rails_admin?(action)
case action
when :dashboard
#user.admin?
when :index
#user.admin?
when :show
#user.admin?
when :new
#user.admin?
when :edit
#user.admin?
when :destroy
#user.admin?
when :export
#user.admin?
when :history
#user.admin?
when :show_in_app
#user.admin?
else
raise ::Pundit::NotDefinedError, "unable to find policy #{action} for #{record}."
end
end
end
I don't know what caused this to happen.. can't find anything on Google and the gem rails admin pundit was last updated a year ago.
Make sure you have
config.parent_controller = "::ApplicationController"
on your config/initializers/rails_admin.rb, to indicate who's the parent controller. This solved it for me.
I'm in the process of creating a very simple wiki based website where users can upgrade and downgrade their account. One of the perks of being a 'premium' member is the ability to create private wikis.
The issue I'm having is to do with this piece of code in my wikis_controller.rb:
def index
#wiki = Wiki.visible_to(current_user)
#wikis = policy_scope(Wiki)
end
When the code is written like that, I get an error message saying 'undefined method 'visible_to'. But, if I comment out the first line in that method so that it looks like this:
def index
##wiki = Wiki.visible_to(current_user)
#wikis = policy_scope(Wiki)
end
the user can see the wiki index, but they cannot see their private wikis.
Any ideas where I am going wrong? For reference, here is my wiki_policy.rb:
class WikiPolicy < ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
def update?
user.present?
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
wikis = []
if user.role == 'admin'
wikis = scope.all # if the user is an admin, show them all the wikis
elsif user.role == 'premium'
all_wikis = scope.all
all_wikis.each do |wiki|
if !wiki.private? || wiki.user == user || wiki.users.include?(user)
wikis << wiki # if the user is premium, only show them public wikis, or that private wikis they created, or private wikis they are a collaborator on
end
end
else # this is the lowly standard user
all_wikis = scope.all
wikis = []
all_wikis.each do |wiki|
if !wiki.private? || wiki.users.include?(user)
wikis << wiki # only show standard users public wikis and private wikis they are a collaborator on
end
end
end
wikis # return the wikis array we've built up
end
end
end
and here is my application_policy.rb:
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
def index?
end
def show?
if user.standard?
true
else
false
end
end
def downgrade?
if user.premium?
true
else
false
end
end
def create?
if user.present?
true
else
false
end
end
def new?
create?
end
def update?
user.present?
end
def edit?
update?
end
def destroy?
if user.present? && user.admin? #|| record.user == user)
true
else
false
end
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
Many Thanks.
But even when I do add a 'visible_to' method to Wiki controller, I still pick up the same error message.
Without seeing your code I can't be certain but I am 95% sure your problem is that you're defining an instance method when you mean to define a class method.
You are probably doing this:
class Wiki
# instance method
def visible_to test
puts test
end
end
Which would allow you to call the function like instance method:
irb(main):018:0> Wiki.new.visible_to "hello world"
# hello world
irb(main):007:0> Wiki.visible_to "hello world"
# NoMethodError: undefined method `visible_to' for Wiki:Class
But since you are actually calling the function like a class method:
Wiki.visible_to(current_user)
You need to define your method like this:
class Wiki
# class method
def self.visible_to test
puts test
end
end
Now you can call the class method:
irb(main):024:0> Wiki.visible_to "hello world"
# hello world
irb(main):007:0> Wiki.new.visible_to "hello world"
# NoMethodError: undefined method `visible_to' for #<Wiki:0x00000001d16a68>
Read more here!
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