I'm trying to let through a deeply nested JSON field in my model (document => field_instance => value). I was using an empty hash due to a misunderstanding of the documentation.
permit! can do what I need but I am trying to avoid simply doing params.fetch(:document).permit! due to the massive security hole this opens. So how can I permit any structures of any type under the doubly nested JSON value 'parameter' only?
I am testing with a single string under value called text, and am getting 'Unpermitted parameter: text'
Each instance_field has a specific type which has a list of required parameters, but without a way of being specific for each instance_field in the document, I've opted just to allow all parameters under that JSON field.
Here is my document_params method:
params.fetch(:document)
.permit(:structure_id, :field_instances_attributes => [
:value,
:document_id,
:field_id,
:value_attributes => {}
])
So, what am I doing wrong here?
Or, even better: each field_instance has a type that knows the exact structure the field's value expects. Can I be specific about the fields allowed under value for each field_instance?
Related logs:
service_1 | Parameters: {"utf8"=>"Ô£ô", "authenticity_token"=>" -- censored --", "document"=>{"structure_id"=>"1", "field_instances_attributes"=>[{"document_id"=>"0", "field_id"=>"1", "value_attributes"=>{"text"=>"asdf"}}]}, "commit"=>"Create Document"}
service_1 | Unpermitted parameter: text
service_1 | Unpermitted parameter: text
service_1 | #<FieldInstance id: nil, field_id: 1, document_id: nil, value: nil, created_at: nil, updated_at: nil>
It's actually pretty simple to build a params hash in multiple steps, but it's a little non-obvious.
Here's what I do:
def document_params
#document_params ||= params.require(:document).permit(:structure_id, field_instance_atributes: %i[document_id field_id]).tap do |doc|
doc[:value] = params[:document].require(:value).permit!
end
end
If the value object sanitization is really complicated, perhaps write that as its own helper and use it here.
The important part is the use of require: it returns the inner value as a new object, and so manipulations against it don't affect params. You can go back to the well, so to speak, as many times as you need.
Caveat: require will throw ActionController::ParameterMissing if the key is not found.
Related
I am trying to build a ruby on rails and graphQL app, and am working on a user update mutation. I have spent a long time trying to figure it out, and when I accidentally made a typo, it suddenly worked.
The below is the working migration:
module Mutations
class UpdateUser < BaseMutation
argument :id, Int
argument :first_name, String
argument :last_name, String
argument :username, String
argument :email, String
argument :password, String
field :user, Types::User
field :errors, [String], null: false
def resolve(id:, first_name:, last_name:, username:, email:, password:)
user = User.find(id)
user.update(first_name:, last_name:, username:, email:, password:)
{ user:, errors: [] }
rescue StandardError => e
{ user: nil, errors: [e.message] }
end
end
end
The thing I am confused about is when I define the arguments, they are colon first: eg :id or :first_name
When I pass them to the resolve method they only work if they have the colon after: eg id: or first_name:
When I pass the variables to the update method, they use the same syntax of colon after, for all variables other than ID. For some reason, when I used id: it was resolving to a string "id", and using colon first :id was returning an undefined error.
It is only when I accidentally deleted the colon, and tried id that it actually resolved to the passed through value.
My question for this, is why and how this is behaving this way? I have tried finding the answer in the docs, and reading other posts, but have been unable to find an answer.
Please someone help my brain get around this, coming from a PHP background, ruby is melting my brain.
It's going to take some time to get used to Ruby, coming from PHP, but it won't be too bad.
Essentially id is a variable, or object/model attribute when used like model_instance.id. In PHP this would be like $id or $object_instance->id.
When you see id: it is the key in a key-value pair, so it expects something (a value) after it (or assumes nil if nothing follows, often in method definitions using keyword arguments like your example). A typical use might be model_instance.update(id: 25) where you are essentially passing in a hash to the update method with id as the key and 25 as the value. The older way to write this in Ruby is with a "hash rocket" like so: model_instance.update(:id => 25).
More reading on Ruby hashes: https://www.rubyguides.com/2020/05/ruby-hash-methods
More reading on keyword arguments: https://www.rubyguides.com/2018/06/rubys-method-arguments
Now if you're paying attention that hash rocket now uses the 3rd type you're asking about. When you see a colon preceding a string like that it is called a "symbol" and it will take some time to get used to them but they are essentially strings in Ruby that are one character fewer to define (and immutable). Instead of using 'id' or "id" as a string, Ruby folks often like to use :id as a symbol and it will typically auto-convert to a string when needed. A good example might be an enumerator of sorts.
state = :ready
if state == :ready
state = :finished
else
state = :undefined
end
More reading on Ruby symbols: https://www.rubyguides.com/2018/02/ruby-symbols
Perhaps my understanding of how this is supposed to work is wrong, but I seeing strings stored in my DB when I would expect them to be a jsonb array. Here is how I have things setup:
Migration
t.jsonb :variables, array: true
Model
attribute :variables, :variable, array: true
Custom ActiveRecord::Type
ActiveRecord::Type.register(:variable, Variable::Type)
Custom Variable Type
class Variable::Type < ActiveRecord::Type::Json
include ActiveModel::Type::Helpers::Mutable
# Type casts a value from user input (e.g. from a setter). This value may be a string from the form builder, or a ruby object passed to a setter. There is currently no way to differentiate between which source it came from.
# - value: The raw input, as provided to the attribute setter.
def cast(value)
unless value.nil?
value = Variable.new(value) if !value.kind_of?(Variable)
value
end
end
# Converts a value from database input to the appropriate ruby type. The return value of this method will be returned from ActiveRecord::AttributeMethods::Read#read_attribute. The default implementation just calls #cast.
# - value: The raw input, as provided from the database.
def deserialize(value)
unless value.nil?
value = super if value.kind_of?(String)
value = Variable.new(value) if value.kind_of?(Hash)
value
end
end
So this method does work from the application's perspective. I can set the value as variables = [Variable.new, Variable.new] and it correctly stores in the DB, and retrieves back as an array of [Variable, Variable].
What concerns me, and the root of this question, is that in the database, the variable is stored using double escaped strings rather than json objects:
{
"{\"token\": \"a\", \"value\": 1, \"default_value\": 1}",
"{\"token\": \"b\", \"value\": 2, \"default_value\": 2}"
}
I would expect them to be stored something more resembling a json object like this:
{
{"token": "a", "value": 1, "default_value": 1},
{"token": "b", "value": 2, "default_value": 2}
}
The reason for this is that, from my understanding, future querying on this column directly from the DB will be faster/easier if in a json format, rather than a string format. Querying through rails would remain unaffected.
How can I get my Postgres DB to store the array of jsonb properly through rails?
So it turns out that the Rails 5 attribute api is not perfect yet (and not well documented), and the Postgres array support was causing some problems, at least with the way I wanted to use it. I used the same approach to the problem for the solution, but rather than telling rails to use an array of my custom type, I am using a custom type array. Code speaks louder than words:
Migration
t.jsonb :variables, default: []
Model
attribute :variables, :variable_array, default: []
Custom ActiveRecord::Type
ActiveRecord::Type.register(:variable_array, VariableArrayType)
Custom Variable Type
class VariableArrayType < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb
def deserialize(value)
value = super # turns raw json string into array of hashes
if value.kind_of? Array
value.map {|h| Variable.new(h)} # turns array of hashes into array of Variables
else
value
end
end
end
And now, as expected, the db entry is no longer stored as a string, but rather as searchable/indexable jsonb. The whole reason for this song and dance is that I can set the variables attribute using plain old ruby objects...
template.variables = [Variable.new(token: "a", default_value: 1), Variable.new(token: "b", default_value: 2)]
...then have it serialized as its jsonb representation in the DB...
[
{"token": "a", "default_value": 1},
{"token": "b", "default_value": 2}
]
...but more importantly, automatically deserialized and rehydrated back into the plain old ruby object, ready for me to interact with it.
Template.find(123).variables = [#<Variable:0x87654321 token: "a", default_value: 1>, #<Variable:0x12345678 token: "b", default_value: 2>]
Using the old serialize api causes a write with every save (intentionally by Rails architectural design), regardless of whether or not the serialized attribute had changed. Doing this all manually by overriding setters/getters is an unnecessary complication due to the numerous ways attributes can be assigned, and is partly the reason for the newer attributes api.
If it helps anyone else, Rails wants you to provide the possible keys to permit in the controller as well if you're using strong params:
def controller_params
params.require(:parent_key)
.permit(
jsonb_field: [:allowed_key1, :allowed_key2, :allowed_key3]
)
end
One solution could be to just parse the variable via JSON.parse, push it inside an empty array, then assign it to the attribute.
variables = []
variable = "{\"token\": \"a\", \"value\": 1, \"default_value\": 1}"
variable.class #String
parsed_variable = JSON.parse(variable) #{"token"=>"a", "value"=>1, "default_value"=>1}
parsed_variable.class #Hash
variables.push parsed_variable
I am trying to update a columns value to a hash in database. The column in database is text.
In model i have,
serialize :order_info
In controller i have update action
def update
Order.update_order_details(update_params, params[:order_info])
head :no_content
end
I am not doing strong parameters for order_info because order_info is an arbitrary hash and after doing research, strong params doesnt support an arbitrary hash
The value that i am trying to pass is like below
"order_info": {
"orders": [
{
"test": "AAAA"
}
],
"detail": "BBBB",
"type": "CCCC"
}
But when i try to update the value it gets updated in database like
--- !ruby/object:ActionController::Parameters parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess comments: - !ruby/hash:ActiveSupport::HashWithIndifferentAccess test: AAAA detail: BBBB type: CCCC permitted: false
serialize is an instance of ActiveSupport::HashWithIndifferentAccess so i am guessing thats why its in the value. How can i get rid of the extra stuff and just update the hash?
If you want to unwrap all the ActionController::Parameters stuff from params[:order_info] without any filtering then the easiest thing to do is call to_unsafe_h (or its alias to_unsafe_hash):
hash = params[:order_info].to_unsafe_h
In Rails4 that should give you a plain old Hash in hash but AFAIK Rails5 will give you an ActiveSupport::HashWithIndifferentAccess so you might want to add a to_h call:
hash = params[:order_info].to_unsafe_h.to_h
The to_h call won't do anything in Rails4 but will give you one less thing to worry about when you upgrade to Rails5.
Then your update call:
Order.update_order_details(
update_params,
params[:order_info].to_unsafe_h.to_h # <-------- Extra DWIM method calls added
)
should give you the YAML in the database that you're looking for:
"---\n:order_info:\n :orders:\n - :test: AAAA\n :detail: BBBB\n :type: CCCC\n"
You might want to throw in a deep_stringify_keys call too:
params[:order_info].to_unsafe_h.to_h.deep_stringify_keys
depending on what sort of keys you want in your YAMLizied Hash.
I'm playing around with Netflix's Workflowable gem. Right now I'm working on making a custom action where the user can choose choices.
I end up pulling {"id":1,"value":"High"} out with #options[:priority][:value]
What I want to do is get the id value of 1. Any idea how to pull that out? I tried #options[:priority][:value][:id] but that seems to through an error.
Here's what the action looks like/how I'm logging the value:
class Workflowable::Actions::UpdateStatusAction < Workflowable::Actions::Action
include ERB::Util
include Rails.application.routes.url_helpers
NAME="Update Status Action"
OPTIONS = {
:priority => {
:description=>"Enter priority to set result to",
:required=>true,
:type=>:choice,
:choices=>[{id: 1, value: "High"} ]
}
}
def run
Rails.logger.debug #options[:priority][:value]
end
end
Here's the error:
Error (3a7b2168-6f24-4837-9221-376b98e6e887): TypeError in ResultsController#flag
no implicit conversion of Symbol into Integer
Here's what #options[:priority] looks like:
{"description"=>"Enter priority to set result to", "required"=>true, "type"=>:choice, "choices"=>[{"id"=>1, "value"=>"High"}], "value"=>"{\"id\":1,\"value\":\"High\"}", "user_specified"=>true}
#options[:priority]["value"] looks to be a strong containing json, not a hash. This is why you get an error when using [:id] (this method doesn't accept symbols) and why ["id"] returns the string "id".
You'll need to parse it first, for example with JSON.parse, at which point you'll have a hash which you should be able to access as normal. By default the keys will be strings so you'll need
JSON.parse(value)["id"]
I'm assuming the error is something like TypeError: no implicit conversion of Symbol into Integer
It looks like #options[:priority] is a hash with keys :id and :value. So you would want to use #options[:priority][:id] (lose the :value that returns the string).
There seems to be an inconsistency between what is being submitted from the form, and what the rails server is identifying as params... unless I'm doing something wrong / not understanding how parameter arrays work... which is possible.
this is how I'm making my checkboxes:
current_event.competitions.map { |competition|
content_tag(:div, class: "checkbox"){
check_box_tag("attendance[competition_ids]", competition.id, #attendance.competitions.include?(competition.id)) +
label("attendance[competition_ids]", competition.id, label_with_price(competition))
}
}.join.html_safe
this is what the chrome web inspector is saying is being sent to the server:
attendance[competition_ids]:1
attendance[competition_ids]:2
attendance[competition_ids]:3
but Rails is throwing this error:
ActiveModel::ForbiddenAttributesError
and this is my params helper method is my controller
params[:attendance].permit(:package, :level, competition_ids: [])
params identified by rails:
Parameters:
{"utf8"=>"✓",
"authenticity_token"=>"qZ1gtwZoXgs9P0HdberzrsMO7L1NftmB8yGso0WquOY=",
"attendance"=>{"competition_ids"=>"3"},
"discounts"=>[""],
"commit"=>"Register"}
shouldn't my params look more like:
"attendance"=>{"competition_ids"=>["1","2","3"]}
?
There's no inconsistency actually. All of the checkboxes have the same name attribute, so you only see the "last" value. It's basically setting the param value to 1, then 2 and then 3.
If you want an array, the name attribute has to end with [], i.e. attendance[competition_ids][]
That'll be interpreted server-side as an array of values.
If you think of it as a Ruby hash, it makes sense
params["attendance"]["competition_ids"] = 1
params["attendance"]["competition_ids"] = 2
params["attendance"]["competition_ids"] = 3
the same key is being overwritten again and again. But if you add the [] to the name, the behavior is closer to
params["attendance"]["competition_ids"] = []
params["attendance"]["competition_ids"] << 1
params["attendance"]["competition_ids"] << 2
params["attendance"]["competition_ids"] << 3