Custom field in devise without mass assignment - ruby-on-rails

I am using devise for authentication but have also added a display_name field that I don't want the user to be able to edit after creating registration/sign up. The problem is unless I add this column/field with attr_accessible in the model, I get the following error.
Can't mass-assign protected attributes: display_name
But I can't enable attr_accessible because that will open the app up to mass assignment hack in through the edit method.
I thought about overriding Devise::RegistrationsController to supplement devise's create method, as shown in this gist, but filtering the params before calling super will obviously result in the display_name not being saved at all, and the field would show up empty in a form after validation fails for any reason.
Is there method I can override or a hook I can add into the model to prevent mass assigment of a single variable in certain conditions? Perhaps I can remove the display_name from the parameter list before_validation and put it back after_validation, while also validating it with a technique like this, and then doing a normal validation-less single assignment? Of course this still wouldn't prevent a mass assignment hack through the edit method, so I'd have to add a signature to params[:user] hash in the create method, which would be checked before removing/adding display_name from params when validating/updating the display_name attribute by itself. This seems like a mess to me, is there a better way to control mass assignment in the controller while using devise?
One approach that would be quick and dirty would be to override Devise::RegistrationsController like I mentioned first and simply cut and paste the original create method from the original devise registrations_controller.rb source, and manually add/remove display_name from params[:user] before and after the build_resource(sign_up_params) call. Just before the save call, I'd also have to add display_name individually to the model object and use single attribute validation hackery mentioned above if I want validation on display_name to work. If I update devise in the future this approach has a high probability of breaking the create method, so I've ruled it out.
After typing out these various approaches I've realized I will probably override the edit method in Devise::RegistrationsController and raise an error if params[:user][:display_name] or any other attribute not in attr_accessible is set. This sort of blacklist in multiple places approach also rubs me the wrong way, it will be too easy to forget adding this to new methods during future expansion, even if the blacklist is kept DRY in a separate private method. Still, this appears to be the least convoluted way.
Is there another approach worth trying?

Mass assignment protection only protects from assigning parameter via methods like create, save or assign_attributes, which all take a hash of arguments as an argument. You should still be able to use
user.display_name = value
user.save
in your create action.

If I don't misunderstand, your goal is just that forbids to modify display_name when updates the user.
You can put display_name in attr_accessible and have a hack in before_update:
class User
before_update :bu
def bu
if changed? && changed_attributes['display_name']
self.display_name = changed_attributes['display_name']
end
end
end
Prevent display_name to be modified, but there is no way to modify display_name after user is created.
You probably need to add an attribute as a switch and improve before_update to avoid this situation.

Related

Rails - how to protect an attribute against updating from outside of the model

In Rails 5 I have a model with an attribute, value of which is never set directly but is always calculated in before_save callback instead. I would like to protect this attribute from being updated from outside of the model, so that calls like f. e. update_attribute() would fail. I used attr_readonly inside the model to achieve what I want and it worked great until I realised that it prevents all updates! Also from within the model itself. Since, according to Rails API docs this is the correct behaviour, what would be the best way to reject modifications to a particular attribute but only from the outside?
You could override the setter. On the model:
def protected_attr_name=(val)
# raise SomeException
end
This disables:
model.protected_attr_name = "value" # => raise exception
model.update_attributes(protected_attr_name: "value") # => raise exception
And then in your before_save method/block:
write_attribute(:protected_attr_name, calculated_value)
Additional observations
Like attr_readonly, you could choose to not raise an exception, and not do anything instead. This may be confusing/frustrating to others working on the same codebase, and is potentially very non-obvious behaviour.
Also - if it is always calculated in the before_save, consider whether this protection is necessary, or whether clearer attribute naming can effectively make the issue go away.

How to make comma separated ids turn to array in rails parameters like ids[] would

So the normal way would be to have multiple input fields with a name of "obj_ids[]", then the values in rails ends up magically zipping them into an array.
Does rails have some secret way of alternately doing this with simply one field?
I'm needing to separate the input fields from the actual form, and so right now I'm basically grabbing them all, and running a JS join on them to send as one field in a separate form. I could duplicate them all as hidden fields but that seems a bit much. Also obviously could do a split in the controller, but I'm curious if there's another way.
Depending on your specific needs there are tons of ways to do this kind of thing in rails
hacking the params as #jrochkind has mentioned is one way but there are several more elegant solutions
create a virtual attribute in your model
class SomeModel
attr_reader :my_field
def my_field= value_as_comma_delimited_string
# here the the_field_i_really_want_set is an array type which is supported in mongo and postgres for exampld
self.the_field_i_really_want_set = value_as_comma_delimited_string.split(",")
end
end
You could do basically the same thing in a before_validation callback in this case you don't need to create an accessor
class User < ActiveRecord::Base
before_validation :normalize_name, on: :create
protected
def normalize_name
self.name = self.name.downcase.titleize # or whatever transformation you need
end
end
Another technique that I've been finding very useful and that is similar to the first one I showed is to override the accessor method directly. Here my_field is the actual field in your model's schema.
class SomeModel
def my_field= value_as_comma_delimited_string
self.write_attribute(:my_field,value_as_comma_delimited_string.split(","))
end
end
I don't think there's another way, I've looked into it before, for other cases.
For your case, if I understand right, you are constructing a form with JS and submitting the form with JS, and it's up to you whether to construct the form with multiple values mashed together in one input (which will turn into one URL query parameter) separated by commas, or separate fields/params.
I'd just go with separate fields/params.
Otherwise, on the controller side, you could do it in a before_filter rather than just the action method, even in a before_filter on your application_controller that applies to all actions, and you can even have the before_filter mutate Rails own params hash.
before_filter :split_commas
def split_commas
params[:my_field] = params[:my_field].split(",")
end
That should work, although mutating params hash like that has been the cause of so many frustrating bugs for me that I'm loathe to ever do it.
Adding another setter to model (or overriding existing one) as suggested by #hraynaud is one possibility. However in my opinion supporting some non-standard input format is more a controller's duty than model's. Therefore, I wouldn't put that logic into a model.
Instead, I'd introduce another controller's method which returns preprocessed parameters. Works great with Strong Parameters.
protected
def preprocessed_params
p = params.deep_dup
p[:obj_ids] = p[:obj_ids].split(",") if p[:obj_ids]
p
end
def permitted_params
preprocessed_params.permit(:obj_ids => [])
end
This snippet could be further modified by using alias_method_chain so that the preprocessed params are accessible via params whereas the raw ones are kept in params_without_preprocessing or something.

Mongoid: protect attributes from being changed

I want to protect rails model attributes from being changed, only after the model enter certain state (eg. sign_in_count > 1). Looked around and see people using
attr_readonly
to protect the attributes, can I use it with condition? If not, is there alternative solution? Thanks.
I don't really understand what you want to do, so here are some general but related purpose. Btw, in rails 4 the attributes are protected, but you can use protected_attributes gem to go through rails 3 behaviour.
Btw, in ruby you can use freeze method can be invoked to prevent from any modifications of some object you have frozen. (Toc check wether the object is indeed frozen, you can call frozen? on that object and this will return true or false).
Another remark is that you can check some change state of your model by calling changed—attributes, this returns a Hash of fields that changed relatively to their original values.
Edit : you can of course set this statement
attr_readonly :some, :attributes unless some_condition
and define
def some_condition
sign_in_count > 1
end

Sending form fields not related to passed model: Can't mass-assign protected attributes

I am using a form_for loop to submit data to my controller’s create method. In the form_for I have several text_fields that are not associated to any field on the table. They are essentially free text fields that I want to put data in and use for additional logic in the create method.
When I submit the form, the first line of the create is hit:
#user = User.new(params[:user])
The following error occurs (:test1 being my form field unrelated to the model):
Can't mass-assign protected attributes: test1
I realize it is because of the text fields I’m sending that are not related to the model. I have tried all sorts of strange syntaxes such as:
User.new(params.require(:user).except(:test1)) and User.new(params[:user].except(:test1)) to no avail.
I’ve found several sources online stating you can use the except or delete method but I can’t seem to get it to work, especially while in the User.new().
I have tried it outside the new as well:
params.delete(:test1)
#user = User.new(params[:xref_ability])
I also want to be able to read the additional attributes eventually in the create, so I don’t think the except() or delete() is going to solve all my issues (if it did work). Therefore, I tried attr_accessor :test1 on the controller class and the model class hoping it would give the method permission to read the attribute, but no success.
I've also tried adding it to attr_accessible on my model which then leads to the error:
unknown attribute: test1
Any other suggestions?
Maybe try using virtual attributes? It makes sense to use them if they don't map to a field in the DB.
RB has a good railscast on the topic: http://railscasts.com/episodes/16-virtual-attributes-revised
You probably need a attr_accessible :test1 on your User model, give it a try.
You are getting the unknown attribute error because it doesn't exist in the database.
You must create test1 in database (add it with a migration...) or just add it to your model with:
attr_accessor :test1
This way you'll have the value in the object but it won't store in the database. If you need to store it, add it to the database and use attr_accessible as #Miguelgraz told you before.

Alternative to mass assignment for HABTM

I have a similar situation to this question, which was already posted. Koraktor has asked
I'm using a simple model for user authorisation with two ActiveRecords User and Role User and Role have a HABTM relation to each other.
.
.
Manually assigning roles with #user.roles or #user.role_ids works, but not the "magic" inside User#new or User#update_attributes.
Oleg suggested that
attr_accessible :role_ids
be added to the user model. This would allow mass assignment operators to update roles. However, he cautioned against using this approach because of security concerns.
I have a follow up question to Oleg's response -
In this situation, is there a recommended method to update roles without using mass-assignment?
Also, assuming
you authenticate users,
only allow administrators to CRUD users by putting a before_filter in the users_controller,
is mass-assignment of role_ids still a valid concern?
Mass assignment is a feature of Rails provided for using less code for updating a model like this
Model.create(params[:model])
#model.update_parameters(params[:model])
instead of
#model.field1 = params[:model][:field1]
#model.field2 = params[:model][:field2]
...
#model.save
But with this feature, comes the risk of updating values which we dont intend. For example, if we want just field1, field2 and field3 to be updated by the user and you use update_parameters for mass assignment, there is a risk of updating field4, if one is passing model[user][field4]=some_value either from url or by any other ways. If we are explicitly assigning the fields in the code, we do not have this risk. But then we will have to set the values for each field(wherever we are updating or creating) which is not very productive.
So, for using the mass assignment feature, we have 2 options. First is attr_protected
attr_protected :field4
This will protect field4 from being saved from mass assignment from params[:model] even if it contains field4. For saving field4, we have to call the setter for field4 explicitly in
the code(#model.field4 =). But, the problem with attr_protected is that Rails may provide some other attributes that we may not know for mass assignment. For example, if we define
has_many :model2s
in Model.rb, Rails will provide a method model2_ids= automatically and this can be accessed by mass assignment.(If we give model[model2_ids]= in the url, it will create associations, not intended at all). So, there is a chance of missing attributes like this while using attr_protected.
So, the recommended method is to use attr_accessible
attr_accessible :field1, :field2, :field3
This will make these fields open for mass assignment and all other attributes in the model not available for mass assignment. So, the recommended way is to make those attributes which we are providing in a form for users to edit as attr_accessible and all other parameters will be protected. But, while using this you have to make sure you have included all the attributes you need for edit as attr_accessible.
In your case, since you want the user to edit the role_ids and you are providing access to CRUD for admin users only, I think you can use attr_accessible :role_ids. The alternative would be to assign role_ids explictely like .role_ids = params[:user][:role_ids]. You should use this, if you have another form where you dont want the user to edit the role_ids.

Resources