Is there a way to bypass mass assignment protection? - ruby-on-rails

I have a Rails 3 app which JSON encodes objects in order to store them in a Redis key/value store.
When I retrieve the objects, I'm trying to decode the JSON and instantiate them from the data like so:
def decode(json)
self.new(ActiveSupport::JSON.decode(json)["#{self.name.downcase}"])
end
The problem is that doing this involves mass assignment which is disallowed (for good reason I'm told!) for attributes I haven't given attr_writer ability to.
Is there a way I can bypass the mass assignment protection just for this operation only?

assign_attributes with without_protection: true seems less intrusive:
user = User.new
user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
user.name # => "Josh"
user.is_admin? # => true
#tovodeverett mentioned in the comment you can also use it with new, like this in 1 line
user = User.new({ :name => 'Josh', :is_admin => true }, :without_protection => true)

EDIT: kizzx2's Answer is a much better solution.
Kind of a hack, but...
self.new do |n|
n.send "attributes=", JSON.decode( json )["#{self.name.downcase}"], false
end
This invokes attributes= passing false for the guard_protected_attributes parameter which will skip any mass assignment checks.

You can create a user also in this way which is not doing the mass assignment.
User.create do |user|
user.name = "Josh"
end
You may want to put this into a method.
new_user(name)
User.create do |user|
user.name = name
end
end

Related

Ruby - Ignore protected attributes

How can I tell Ruby (Rails) to ignore protected variables which are present when mass-assigning?
class MyClass < ActiveRecord::Base
attr_accessible :name, :age
end
Now I will mass-assign a hash to create a new MyClass.
MyClass.create!({:name => "John", :age => 25, :id => 2})
This will give me an exception:
ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes: id
I want it to create a new MyClass with the specified (unprotected) attributes and ignore the id attribute.
On the side note: How can I also ignore unknown attributes. For example, MyClass doesn't have a location attribute. If I try to mass-assign it, just ignore it.
Use Hash#slice to only select the keys you're actually interested in assigning:
# Pass only :name and :age to create!
MyClass.create!(params.slice(:name, :age))
Typically, I'll add wrapper method for params to my controller which filters it down to only the fields that I know I want assigned:
class MyController
# ...
def create
#my_instance = MyClass.create!(create_params)
end
protected
def create_params
params.slice(:name, :age)
end
end
Setting mass_assignment_sanitizer to :logger solved the issue in development and test.
config.active_record.mass_assignment_sanitizer = :logger
You can use strong_parameters gem, that will be in rails 4.
See the documentation here.
This way you can specify the params you want by action or role, for example.
If you want to get down and dirty with it, and dynamically let only a model's attributes through, without disabling ActiveModel::MassAssignmentSecurity::Errors globally:
params = {:name => "John", :age => 25, :id => 2}
MyClass.create!(params.slice(*MyClass.new.attributes.symbolize_keys.keys)
The .symbolize_keys is required if you are using symbols in your hash, like in this situation, but you might not need that.
Personally, I like to keep things in the model by overriding assign_attributes.
def assign_attributes(new_attributes, options = {})
if options[:safe_assign]
authorizer = mass_assignment_authorizer(options[:as])
new_attributes = new_attributes.reject { |key|
!has_attribute?(key) || authorizer.deny?(key)
}
end
super(new_attributes, options)
end
Use it similarly to :without_protection, but for when you want to ignore unknown or protected attributes:
MyModel.create!(
{ :asdf => "invalid", :admin_field => "protected", :actual_data => 'hello world!' },
:safe_assign => true
)
# => #<MyModel actual_data: "hello world!">

In Rails, how do I limit which attributes can be updated, without preventing them from being created?

I have a situation where an attribute can be created through a JSON API. But once it is created, I want to prevent it from ever being updated.
This constraint causes my first solution, which is using attr_accessible, to be insufficient. Is there a nice way to handle this type of situation in rails, or do I have to perform a manual check in the update method?
You can use attr_readonly, this will allow the value to be set on creation, but ignored on update.
Example:
class User < ActiveRecord::Base
attr_accessible :name
attr_readonly :name
end
> User.create(name: "lorem")
> u = User.first
=> #<User id: 1, name: "lorem">
> u.name = "ipsum"
=> "ipsum"
> u.save
=> true
> User.first.name
=> "lorem"
There is not a nice way to do that as far as I know, you have to write a custom filter
before_update :prevent_attributes_update
def prevent_attribute_updates
%w(attr1, attr2).each do |a|
send("#{attr1}=", send("#{attr1}_was")) unless self.send("#{attr1}_was").blank?
end
end

Rails 3.2 Prevent Object from being Saved using Errors

I have an ActiveRecord object and I would like to prevent it from being saved, without having permanent validations on the model. You used to be able to do something like this using errors.add but it doesn't look like it works anymore.
user = User.last
user.errors.add :name, "name doesn't rhyme with orange"
user.valid? # => true
user.save # => true
or
user = User.last
user.errors.add :base, "my unique error"
user.valid? # => true
user.save # => true
How can I prevent the user object from getting saved in Rails 3.2 without modifying it's model?
You can set errors, but do it within a validate method, e.g.:
validate :must_rhyme_with_orange
def must_rhyme_with_orange
unless rhymes_with_orange?
errors.add(:name, "doesn't rhyme with orange")
end
end
If you want to conditionally run the validation, one trick is to use attr_accessor and a guard condition:
attr_accessor :needs_rhyming
validate :must_rhyme_with_orange, :if => Proc.new {|o| o.needs_rhyming}
> u = User.last
> u.needs_rhyming = true
> u.valid? # false
Your issue is running valid? will rerun the validations.. resetting your errors.
pry(main)> u.errors[:base] << "This is some custom error message"
=> ["This is some custom error message"]
pry(main)> u.errors
=> {:base=>["This is some custom error message"]}
pry(main)> u.valid?
=> true
pry(main)> u.errors
=> {}
pry(main)>
Instead, just check if u.errors.blank?
This is a slight deviation from the original question, but I found this post after trying a few things. Rails has built in functionality to reject an object from saving if it has the _destroy attribute assigned as true. Quite helpful if you're handling model creation on the controller level.

Passing attributes to Mongoid update_attributes()

I want to use this function from mongoid:
person.update_attributes(first_name: "Jean", last_name: "Zorg")
But I want to pass in all the attributes from another variable. How do I do that?
Edit: Thanks everyone for your reply. I'm new to ruby so at first I thought I just made a silly mistake with this. The bug was in a completely different place, the correct code, for your enjoyment:
def twitter
# Scenarios:
# 1. Player is already signed in with his fb account:
# we link the accounts and update the information.
# 2. Player is new: we create the account.
# 3. Player is old: we update the player's information.
# login with a safe write.
puts "twitter"
twitter_details = {
twitter_name: env["omniauth.auth"]['user_info']['name'],
twitter_nick: env["omniauth.auth"]['user_info']['nickname'],
twitter_uid: env["omniauth.auth"]['uid']
}
if player_signed_in?
#player = Player.find(current_player['_id'])
else
#player = Player.first(conditions: {twitter_uid: env['omniauth.auth']['uid']})
end
if #player.nil?
#player = Player.create!(twitter_details)
else
#player.update_attributes(twitter_details)
end
flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Twitter"
sign_in_and_redirect #player, :event => :authentication
end
The update_attributes method takes a Hash argument so if you have a Hash, h, with just :first_name and :last_name keys then:
person.update_attributes(h)
If your Hash has more keys then you can use slice to pull out just the ones you want:
person.update_attributes(h.slice(:first_name, :last_name))
if you look at the source code of Mongoid, you'll see the definition of update_attributes in the file
.rvm/gems/ruby-1.9.2-p0/gems/mongoid-2.3.1/lib/mongoid/persistence.rb
# Update the document attributes in the datbase.
#
# #example Update the document's attributes
# document.update_attributes(:title => "Sir")
#
# #param [ Hash ] attributes The attributes to update.
#
# #return [ true, false ] True if validation passed, false if not.
def update_attributes(attributes = {})
write_attributes(attributes); save
end
It takes a Hash -- that means you can use a Hash as the variable that's passed in.
e.g.
my_attrs = {first_name: "Jean", last_name: "Zorg"}
person.update_attributes( my_attrs )
What's happening in the update_attributes method and, indeed, across the Rails platform is variables get put into a hash internally, when necessary.
So the following are equivalent:
person.update_attributes(first_name: "Jean", last_name: "Zorg")
person.update_attributes({first_name: "Jean", last_name: "Zorg"})
person.update_attributes(name_hash)
Where name_hash is:
name_hash = {first_name: "Jean", last_name: "Zorg"}

FactoryGirl: attributes_for not giving me associated attributes

I have a Code model factory like this:
Factory.define :code do |f|
f.value "code"
f.association :code_type
f.association(:codeable, :factory => :portfolio)
end
But when I test my controller with a simple test_should_create_code like this:
test "should create code" do
assert_difference('Code.count') do
post :create, :code => Factory.attributes_for(:code)
end
assert_redirected_to code_path(assigns(:code))
end
... the test fails. The new record is not created.
In the console, it seems that attributes_for does not return all required attributes like the create does.
rob#compy:~/dev/my_rails_app$ rails console test
Loading test environment (Rails 3.0.3)
irb(main):001:0> Factory.create(:code)
=> #<Code id: 1, code_type_id: 1, value: "code", codeable_id: 1, codeable_type: "Portfolio", created_at: "2011-02-24 10:42:20", updated_at: "2011-02-24 10:42:20">
irb(main):002:0> Factory.attributes_for(:code)
=> {:value=>"code"}
Any ideas?
Thanks,
You can try something like this:
(Factory.build :code).attributes.symbolize_keys
Check this: http://groups.google.com/group/factory_girl/browse_thread/thread/a95071d66d97987e)
This one doesn't return timestamps etc., only attributes that are accessible for mass assignment:
(FactoryGirl.build :position).attributes.symbolize_keys.reject { |key, value| !Position.attr_accessible[:default].collect { |attribute| attribute.to_sym }.include?(key) }
Still, it's quite ugly. I think FactoryGirl should provide something like this out of the box.
I opened a request for this here.
I'd suggest yet an other approach, which I think is clearer:
attr = attributes_for(:code).merge(code_type: create(:code_type))
heres what I end up doing...
conf = FactoryGirl.build(:conference)
post :create, {:conference => conf.attributes.slice(*conf.class.accessible_attributes) }
I've synthesized what others have said, in case it helps anyone else. To be consistent with the version of FactoryGirl in question, I've used Factory.build() instead of FactoryGirl.build(). Update as necessary.
def build_attributes_for(*args)
build_object = Factory.build(*args)
build_object.attributes.slice(*build_object.class.accessible_attributes).symbolize_keys
end
Simply call this method in place of Factory.attributes_for:
post :create, :code => build_attributes_for(:code)
The full gist (within a helper module) is here: https://gist.github.com/jlberglund/5207078
In my APP/spec/controllers/pages_controllers_spec.rb I set:
let(:valid_attributes) { FactoryGirl.attributes_for(:page).merge(subject: FactoryGirl.create(:theme), user: FactoryGirl.create(:user)) }
Because I have two models associated. This works too:
FactoryGirl.define do
factory :page do
title { Faker::Lorem.characters 12 }
body { Faker::Lorem.characters 38 }
discution false
published true
tags "linux, education, elearning"
section { FactoryGirl.create(:section) }
user { FactoryGirl.create(:user) }
end
end
Here's another way. You probably want to omit the id, created_at and updated_at attributes.
FactoryGirl.build(:car).attributes.except('id', 'created_at', 'updated_at').symbolize_keys
Limitations:
It does not generate attributes for HMT and HABTM associations (as these associations are stored in a join table, not an actual attribute).
Association strategy in the factory must be create, as in association :user, strategy: :create. This strategy can make your factory very slow if you don't use it wisely.

Resources