Search code examples
ruby-on-railsrubyserialization

Updating a serialized field in Rails


I have a serialized attribute in my Rails model and am attempting to update it. The Block#preferences attribute was first generated through a migration as a text type. We use Postgresql 14.

# == Schema Information
#
# Table name: blocks
#
#  id          :bigint           not null, primary key
#  name        :string
#  preferences :text
#  created_at  :datetime         not null
#  updated_at  :datetime         not null
#  page_id     :bigint           not null
#
class Block < ApplicationRecord
  belongs_to :page

  serialize :preferences, JSON
end

I understand that the whole point of strong parameters is not allowing arbitrary parameters. But to ask a question, how do you allow dynamic keys to be updated through a serialized attribute without logging Unpermitted parameters.

Below is the parameters being passed to the update action:

Parameters: {
  "authenticity_token"=>"[FILTERED]", 
  "block"=>{
     "title_text"=>"Hello world!", 
     "description_text"=>"Send us a message.",
     "hero_image"=>"placeholder/block01.jpg",
     "bg_color"=>"#652020"
  },
  "commit"=>"Update Block",
  "id"=>"23"
}

These are saved as preferences: {"title_text"=>"Rent with us", "description_text"=>"Send us a message.", "hero_image"=>"placeholder/block01.jpg", "bg_color"=>"#000000"} on the Block object. These preferences attributes are just an example - there are other attributes such as logo_text and links_alignment as well. This list is ever growing.

The update action works if you specify the preferences attribute as the attribute to be updated.

def update
  @block = Block.find(params[:id])

  respond_to do |format|
    if @block.update(preferences: params[:block])
    end
  end
end

private

def block_params
  params.require(:block).permit(
    :name,
    :preferences
  )
end

However, if I call update with the block_params the log shows that the field is unpermitted:

Unpermitted parameters: :title_text, :description_text, :hero_image, :bg_color.

def update
  @block = Block.find(params[:id])

  respond_to do |format|
    if @block.update(block_params)
    end
  end
end

private

def block_params
  params.require(:block).permit(
    :name,
    :preferences
  )
end

As this is seem a bit novel in our environment, I'm looking to learn how serialize is able to map the preferences from the params[:block] but cannot call update using the block_params. Thanks a lot in advance for any guidance!


Solution

  • Parameter whitelisting has almost nothing to do with your schema or what the model is doing with the data in general.

    ActionContoller::Parameters is simply a hash like object and when you pass it to the ActiveRecord persistence methods (new, create, update, etc) it will raise if the passed parameters instances do not have their permitted flag set. This also applies to nested instances of ActionContoller::Parameters.

    Additionally ActionContoller::Parameters has a action_on_unpermitted_parameters configuration option which will log, raise or do nothing when you call permit and there are keys present which are not in the whitelist.

    To disable the mass assignment protection completely you can use permit!:

    def block_params
      params.require(:block).permit!
    end
    

    This permits all keys of this instance of ActionContoller::Parameters and any nested parameters.

    If you want a less nuclear option to handle hashes with arbitrary keys you can just pass the list of keys of the incoming parameters:

    def block_params
      params.require(:block).permit(*params[:block].keys.map(&:to_sym))
    end
    

    The difference here is that this will only permit permitted scalar values and not arrays and hashes.