Reputation: 10215
In my Rails app I have users
who can have many projects
which in turn can have many invoices
.
How can I make sure that a user can only create an invoice for one of his projects and not for another user's projects?
class Invoice < ActiveRecord::Base
attr_accessible :number, :date, :project_id
validates :project_id, :presence => true,
:inclusion => { :in => ????????? }
end
Thanks for any help.
class InvoicesController < ApplicationController
def new
@invoice = current_user.invoices.build(:project_id => params[:project_id])
end
def create
@invoice = current_user.invoices.build(params[:invoice])
if @invoice.save
flash[:success] = "Invoice saved."
redirect_to edit_invoice_path(@invoice)
else
render :new
end
end
end
Upvotes: 0
Views: 412
Reputation: 5998
project_id
is "sensitive" attribute - so remove it from attr_accessible. You are right that you should not believe params from the form and you must check it.
def create
@invoice = current_user.invoices.build(params[:invoice])
# @invoice.project_id is nil now because this attr not in attr_accessible list
@invoice.project_id = params[:invoice][:project_id] if current_user.project_ids.include?(params[:invoice][:project_id])
if @invoice.save
flash[:success] = "Invoice saved."
redirect_to edit_invoice_path(@invoice)
else
render :new
end
end
If user tries to hack your app and change project_id
to not owned value then method create
render partial new
with invalid @invoice
. Do not forget to leave the validation of project_id
on presence.
If you get exception Can't mass-assign protected attributes...
there are several ways what to do. The simplest ways are:
1. remove line from environment configs (development, test, production)
# Raise exception on mass assignment protection for Active Record models
config.active_record.mass_assignment_sanitizer = :strict
2. Reject sensitive parameters from params before assigning.
# changes in method create
def create
project_id = params[:invoice].delete(:project_id)
@invoice = current_user.invoices.build(params[:invoice])
@invoice.project_id = project_id if current_user.project_ids.include?(project_id)
...
end
Upvotes: 1
Reputation: 10215
OK, luckily I managed to come up with a solution of my own this time.
I didn't make any changes to my controller ("let's keep 'em skinny"), but added a validation method to my model instead:
class Invoice < ActiveRecord::Base
attr_accessible :number, :date, :project_id
validates :project_id, :presence => true,
:numericality => { :only_integer => true },
:inclusion => { :in => proc { |record| record.available_project_ids } }
def available_project_ids
user.project_ids
end
end
I am not sure if this is good or bad coding practice. Maybe someone can shed some light on this. But for the moment it seems pretty safe to me and I haven't been able to hack it in any way so far.
Upvotes: 0
Reputation: 16730
I think that shouldn't be on a validation. You should ensure the project the user selected is one his projects.
You could do something on your controller like:
project = current_user.projects.find params[:project_id]
@invoice = Invoice.new(project: project)
# ...
Your create action could look something like this.
def create
@invoice = current_user.invoices.build(params[:invoice])
@invoice.project = current_user.projects.find params[:invoice][:project_id]
if @invoice.save
flash[:success] = "Invoice saved."
redirect_to edit_invoice_path(@invoice)
else
render :new
end
end
Upvotes: 1