I have just made the switch to Pundit from CanCan. I am unsure about a couple of things, and how Pundit is best used.
For example:
If you have a resource that can have multiple parent objects, for instance lets say a Goal belongs to a student and instructor. Therefor, a student can have many goals and an instructor can have many goals. In a controller index action you might do:
if params[:student_id].present?
#account = Student.find(params[:student_id])
#goals = #account.goals
elsif params[:instructor_id].present?
#account Instructor.find(params[:instructor_id])
#goals = #account.goals
end
params are not usable inside policies, so the logic needs to be done here. I think. For what I can tell, if you skip the policy_scope you will get an unauthorized error when viewing the index page for goals.
Would you:
#goals = policy_scope(#account.goals)
OR
#goals = policy_scope(Goal.scoped).where( account_id: #account.id)
What happens when you throw a bunch of includes in the mix?
#example = policy_scoped(#school.courses.includes(:account => :user, :teacher ))
Or when needed to order...is this correct?
policy_scope(Issue.scoped).order("created_at desc")
When using scopes: What is :scope here? Is :scope an instance of the model being evaluated? I've tried accessing its attributes via :scope, but didn't work.
class Scope < Struct.new(:user, :scope)
Reading through this from a security perspective I can see a couple things that bear mentioning. For example, if you are allowing users to specify the student_id and instructor_id param fields, what's to stop them from passing in an ID for someone other than themselves? You don't ever want to let a user specify who they are, especially when you are basing policies on the users type.
For starters, I would implement Devise and add an additional boolean field called instructor that would be true when the user was an instructor but default to false for students.
Then your Users would automatically have an instructor? method defined, which will return true if the value in the instructor column is true.
You could then add a helper for students:
def student?
!instructor?
end
Now using Devise (which gives us access to a current_user variable) we can do things like current_user.instructor? which will return true if they are an instructor.
Now on to the policy itself. I just started using Pundit a few weeks ago, but this is what I'd do in your situation:
class GoalPolicy < ApplicationPolicy
class Scope < GoalPolicy
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
#scope.where(user: #user)
end
end
end
Then your (I'm assuming GoalsController class and index method) method can look like:
def index
policy_scope(Goal) # To answer your question, Goal is the scope
end
If you wanted to order you could also do
def index
policy_scope(Goal).order(:created_at)
end
I just realized that you asked this question half a year ago, but hey! Maybe it'll answer some questions other people have and maybe I'll get some feedback on my own budding Pundit skills.
Follow #Saul's recommendation on adding devise or other means of authentication.
Then you'll want to do this (Entity is Goal in your case):
#entities = policy_scope(Entity).where(...)
In entity_policy.rb:
class EntityPolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
# Here you have access to `scope == Entity` and `user == current_user`
scope.where(entity: user.entity)
end
end
end
You might wonder why is where duplicated. The answer is (and here is the answer to your question): they serve different purposes. Although currently they are identical, but consider this:
You now have an admin user who has access to everything. Your policy changes:
class EntityPolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
if user.admin?
scope.all
else
scope.where(entity: user.entity)
end
end
end
end
If you have organizations with goals and the following restful endpoint:
/organizations/:organization_id/goals
When a user visits /organizations/1/goals you want to make sure the user is only allowed access to goals when the user is part of the organization:
scope.where(organization: user.organization) in the policy
And you also want to make sure that when an admin visits they can only see the goals related to that organization:
policy_scope(Goal).where(organization_id: params[:organization_id]) in the controller.
Related
I am trying to follow the documentation for Devise Invitable here to send different email for different user types, in my case partners and clients.
So it says to add in your Devise model, which in my case is User.rb, the following code.
....
attr_accessor :invitation_instructions
....
def self.invite_partner!(attributes={}, invited_by=nil)
self.invite!(attributes, invited_by) do |invitable|
invitable.invitation_instructions = :partner_invitation_instructions
end
end
def self.invite_client!(attributes={}, invited_by=nil)
self.invite!(attributes, invited_by) do |invitable|
invitable.invitation_instructions = :client_invitation_instructions
end
end
Then from my controller, when a new user signs up I am calling
....
if current_user.is_client?
user.invite_client!(user, current_user)
else
user.invite_partner!(user, current_user)
end
When I do that the error I get back is
undefined method 'invite_client!' for #<User:0x007ffbcdfabd08>
Which is a little confusing because the method is defined in the user model, so I would think that, at least, it was defined.
Any help on fixing this and getting this setup to work would be greatly appreciated!
I think those are class methods and you should call it like User.invite_client! and also pass your arguments in method.
Same goes for invite_partner!
We are building a Rails4 app using Trailblazer. I have never worked with Trailblazer before and I am confused about how to do things.
We are building an auction site. I was previously using a traditional controller, and this route endpoint was working fine:
def bill
#profile = Profile.find_by user_id: current_user_id
#current_order = Order.order(created_at: :desc).find_by(user_id: current_user_id)
#batch = #current_order.batch
if #batch.nil?
puts "There was no batch linked to the current order of #{#current_order.id}"
flash[:error] = "We are sorry, but we could not determine which batch your order belongs to."
else
#price_shown_to_customer = #batch.price + ENV["FUELBID_FEE_PER_GALLON"].to_f
#amount = #current_order.quantity * #price_shown_to_customer
end
But now I'm suppose to create this as a Trailblazer api, using a Representer class.
So in routes.rb I added something for "charges":
namespace :api do
get '/price' => 'info#info'
post '/order' => 'orders#create'
get '/charges' => 'charges#bill'
end
I created this Api but copying-and-pasting another:
module Api
class ChargesController < ApiApplicationController
respond_to :json
def bill
respond_with OpenStruct.new.extend(ChargesRepresenter)
end
end
end
I tested the above with a simple Representer and it all worked fine, so everything is good up to this point. If I return simple data from the Representer, then I can see it fine here:
http://localhost:3000/api/charges.json
But I need to get the current_user. How is this done? Right now, this does not work:
module ChargesRepresenter
include Roar::JSON
collection :price_shown_to_customer
def price_shown_to_customer
current_order = Order.order(created_at: :desc).find_by(user_id: current_user_id)
puts "current_order"
puts current_order.id
batch = current_order.batch
batch.price + ENV["FUELBID_FEE_PER_GALLON"].to_f
end
end
current_user_id exists in my traditional controllers because we set up Devise and so my traditional controllers inherit it:
class ChargesController < SecuredController
But is there any way to get it in a Trailblazer Representer?
Hope this answer is not too late.
If you can switch to Decorator pattern instead of a Module.
Representer really doesn't need to know and doesn't care if it is called from controller or console or test. All it needs is a hash to build your json object from. So you can just pass another attribute called current_user_id to your Representer and then use it inside r presenter like you do.
FYI:
If you need a more immediate response you can also copy your question to https://gitter.im/trailblazer/chat . There are usually several people hanging out there. But it's also good to post a question here for posterity.
I've got some simple model:
class Product < ActiveRecord::Base
has_many :categories
end
Now I would like to check in some service, if product is a bestseller and do other action for it:
class ProductService
def remind
Product.all.each do |product|
puts product unless bestseller?
end
end
end
So now what is the best place to put the bestseller? method - inside model or in the service as private method?
In future it may be used in some other services or actions.
Do you think the model is right place to put this method there?
Example of bestsellers method (bestsellers are picked manualy by adding to category 'bestsellers':
def bestseller?(product)
product.categories.include?(BESTSELLER_CATEGORY_ID)
end
or
def bestseller?(product_id)
Category.find(BESTSELLER_CATEGORY_ID).products.include?(product_id)
end
I still haven't decided which one is better (both do the same)
I really can't get my head around Rails 4 strong parameters, belongs_to association and form with fields_for.
Imagine I have model for quoting some price:
class Quote < ActiveRecord::Base
belongs_to :fee
accepts_nested_attributes_for :fee
Now, I have seeded some fees into the db, and have put some radiobuttons on my form_for #quote using fields_for. The values of the radiobuttons are simply ids of the records.
Here is the troubling part, the controller:
def create
#quote = Quote.new(quote_params)
...
end
def quote_params
params.require(:quote).permit(:amount_from, fee_attributes: [:id])
end
From my understanding, automagically Rails should fetch fee record with some id, but there is some mystic error instead.
params hash is: "quote"=>{"amount_from"=>"1200", "fee_attributes"=>{"id"=>"1"}}
Log tail:
Completed 404 Not Found in 264ms
ActiveRecord::RecordNotFound (Couldn't find Fee with ID=1 for Quote with ID=)
app/controllers/quotes_controller.rb:14:in `create'
I really don't understand what is going on here, have read Rails association guide, googled for hour for all info, but to no avail.
What I want to achieve here is to understand the correct "Rails way" to fetch some associations for new Quote object using some params I've put in the form.
Guess I got nested_attributes_for wrong, somehow thought it would call Fee.find automagically.
I've opted for ditching fields_for helpers from the form and rendering fields manually like
radio_button_tag 'fee[id]', fee.id
Then in controller I have 2 params methods now:
def quote_params
params.require(:quote).permit(:amount_from)
end
def fee_params
params.require(:fee).permit(:id)
end
And my action looks like
def create
#quote = Quote.new(quote_params)
#quote.fee = Fee.find(fee_params[:id])
...
Any additions on best practices when one has to handle lots of different objects with not so straight init logic are welcome.
Say I have a Document model that belongs to a User model. A User has_many documents. DocumentPolicy might include this...
def edit?
document.user_id == user.id
end
But, what if...to edit a document you must also be able to edit that documents parent (User). Then, the policy might look like this.
def edit?
document.user_id == user.id &&
policy(user).edit?
end
This results in the error:
undefined method `policy' for #<DocumentPolicy
I'm curious if there is a better way to do this. Am I approaching it incorrectly? It seems like something that others would have thought to do...so, Im hoping to get insight on how others have approached this.
You had the right idea, you just needed to call it through the pundit class explicitly:
def edit?
# I am assuming that a user can edit themselves, so the "or" is in there, if not, go back to using and
document.user_id == user.id or UserPolicy.new(user, User.find(document.user_id)).edit?
end
That should give you what you wanted.
In your Document Controller, declare the user variable and authorize that user.
def edit
#document = Document.find(params[:id])
#user = User.find(#document.user_id) #or how you define it
authorize #user
end
Then pundit will look in your User Policy for the edit? method.
**edit regarding the error message, it is saying that your Document model has no policy file associated to it. If you look in your policies folder you should see user_policy.rb but not document_policy.rb (app/controllers/policies)