I have a simple ActiveRecord class that logs an event and stores some additional data using the serialize method. It has one string column for an event and a text column to store a data object.
# DB Columns
# event => string
# data => text
#
class MyLog < ActiveRecord::Base
serialize :data
validates :event, :data, :presence => true
end
In my controller I would like to take user submitted information and include it as the data:
class ContactFormController < ApplicationController
def send_message
...
data = {name: params[:name], email: params[:email], message: params[:message]}
MyLog.create(event: "User submitted contact form", data: data)
...
end
end
Questions
The serialize method uses YAML by default to store objects like this. Is there a security risk here in the case that a user submits some crafty code that is passed through the parameters? Is there a chance for user submitted Ruby code to be executed when the data field is retrieved and deserialized?
My goal is to provide a way to log events from anywhere in my app and store data of any sort about that event. Is there a better way to preform this than what I have set up here?
In general terms, it's perfectly fine to serialize anything you want. User data of any kind is acceptable.
This presumes you're patched up to the absolutely latest version of Rails 3.2 or 4.0. There have been some issues with YAML and JSON serialization in the past but these have been patched and resolved.
Test your application against known vulnerabilities using a tool like GemCanary to be sure you're current, and to catch future problems.
Related
I am new to rails and I try to find a validation method corresponding to validate the presence of an attribute when updating a record. If that attribute is not present, meaning the attribute does not exist from the request body, Rails should not update the record.
validates :description, presence: true
and
validates_presence_of :description
doesn't seem to do the job. Is there a method for this purpose? It seems quite common in every day scenarios.
If you say:
model.update(hash_that_has_no_description_key)
then you're not touching :description: sending a hash without a :description key to update is not the same as sending in a hash with :description => nil. If model is already valid (i.e. it has a description) then that update won't invalidate it because it won't touch :description.
You say this:
If that attribute is not present, meaning the attribute does not exist from the request body, Rails should not update the record.
Since you're talking about the request body (which models really shouldn't know anything about) then you should be dealing with this logic in the controller as it prepares the incoming data for the update call.
You could check in the controller and complain:
data = whatever_params
if(!data.has_key?(:description))
# Complain in an appropriate manner...
end
# Continue as now...
or you could include :description => nil if there is no :description:
def whatever_params
data = params.require(...).permit(...)
data[:description] = data[:description].presence # Or however you prefer to do this...
data
end
maybe you should use before_update..
see this: http://edgeguides.rubyonrails.org/active_record_callbacks.html#conditional-callbacks
but use before_update instead before_save..
Suppose there is a Rails model with a custom setter/accessor and a uniqueness constraint on the name column:
class Person < ActiveRecord::Base
validates :name, presence: true, uniqueness: true
def name=(name)
# Example transformation only.
# Could be substituted for a more complex operation/transformation.
title_cased = name.titleize
self[:name] = title_cased
end
end
Now, consider the following:
Person.create! name: "John Citizen"
Person.find_or_create_by! name: "john citizen" # Error: validation fails
The find operation will not find any results, since there are no entries that match "john citizen". Then, the create! operation will throw an error as there is already an existing entry "John Citizen" (create! creates a new record and raises an exception if the validation fails).
How do you elegantly prevent such errors from occurring? For loose coupling and encapsulation purposes, is it possible to not transform names (to titlecase, in this case) before I perform operations like find_or_create_by! or other operations like find_by?
EDIT:
As #harimohanraj alludes to, the issue seems to be around equivalence. Should the model transparently deal with the understanding/translating input to its boiled-down, canonical state. Or should this be the responsibility of consumers of the class/model?
Also, is active record callbacks a recommended approach to this kind of scenario?
If you have defined a custom setter method, the implicit decision that you have made is: values for the name attribute, no matter what form they come in (eg. a user's input in a text field), should be handled in titleized form in your DB. If that's the case, then it makes sense that find_or_create_by! name: 'john citizen' fails! In other words, your custom setter method represents your decision that "John Citizen" and "john citizen" are one and the same.
If you find yourself wanting to store John Citizen and john citizen in your DB, then I would revisit your decision to create a custom setter method. One cool way to achieve "loose coupling" is to put all of the logic that sanitizes data (ex. data from a user filling out a form) into a separate Ruby object.
There isn't much context in the question, so here is a bit of an abstract example to demonstrate what I mean.
# A class to house the logic of sanitizing your parameters
class PersonParamsSanitizer
# It is initialized with dirty user parameters
def initialize(params)
#params = params
end
# It spits out neat, titleized params
def sanitized_params
{
name: #params[:name].titleize
}
end
end
class PersonController < ApplicationController
def create
# Use your sanitizer object to convert dirty user parameters into neat
# titleized params for your new perons
sanitized_params = UserParamsSanitizer.new(params).sanitized_params
person = Person.new(sanitized_params)
if person.save
redirect_to person
else
render :new
end
end
end
This way, you don't override the setter method in your User model, and are free to use find_or_create_by! fearlessly if you so choose!
You can set a validation to be case-insensitive by using:
class Person < ActiveRecord::Base
validates :name,
presence: true,
uniqueness: { case_sensitive: false }
end
However you also need a case-insensitive database index backing it since just using a validation in Rails will lead to race conditions. How to achieve that depends on the RBDMS.
Which leaves the issue of querying. The classic way of performing a intensive search is by WHERE LOWER(name) = LOWER(?). Although Postgres lets you use WHERE ILIKE name = ?.
If you want to encapsulate this into the model which is a good idea you would create a scope:
class Person
scope :find_by_name, lambda{ |name| where('LOWER(name) = LOWER(?)', name) }
end
However, you cannot use .find_or_create_by! in this case as the query not just a hash. Instead you would call .first_or_create.
Person.find_by_name("John Citizen").first_or_create(attrs)
see also
PostgreSQL: How to make "case-insensitive" query
The problem is the find_or_create_by and similar methods are already not tansforming the name... as you say there is no record "john citizen" but to work properly you'd need to titleize it for the find_or_create_by, find_or_create_by!, or find_by
(you don't need this solution for find as that only retrieves record by primary key)
so...
def self.find_or_create_by(options)
super(rectify_options(options))
end
def self.find_or_create_by!(options)
super(rectify_options(options))
end
def self.find_by(options)
super(rectify_options(options))
end
private
def self.rectify_options(options)
options[:name] = (new.name = options[:name]) if options[:name]
options
end
I have to prepare a rather large amount of seed data for a Rails application. Based on my limited experience, I know of two ways to do it but want to know which offers the most flexibility.
One, I could do it with Rails and just prepare the seed data the way it's used in the seeds.rb file
User.create!( name: "John" )
Or, I could create a json document of the data. For example, I know that Mongodb lets you import json documents directly into the database. I'm not sure about other databases...
It occurred to me that a json document might be the most flexible, because I suppose you could also use a regular expression script to turn the json into something like this User.create!( name: "John" )
However, I'm wondering if there's any other issues I should consider...
I'm a StackOverflow newbie, so can't reply to the comments above.
I've been trying to do something similar, the mass assignment protection issue came up for me. An idea that I came across in my research was to use:
without_protection: true
I found this worked using rails 3.2.3 For example:
json = ActiveSupport::JSON.decode(File.read('db/seeds/data.json'))
json.each do |a|
Country.create!(a['data'], without_protection: true)
end
See http://snippets.aktagon.com/snippets/401-How-to-seed-your-database-with-JSON-YAML-data-in-Rails
One issue to consider is that you can't always pass all the parameters through the constructor. Consider this:
class User
include Mongoid::Document
field :name
field :role
validates :name, presence: true
validates :role, presence: true
attr_accessible :name
end
If you have something like User.create({name: 'John', role: 'admin'}), then your seed will fail because role will not be able to be assigned.
Let's say a model catches a validation error, usually this is handled by the controller, but is it possible to handle it automatically by the model?
Practically I want to generate a unique id uid for each Note, the model looks like this:
class Note < ActiveRecord::Base
validates_uniqueness_of :uid
# ... some code to generate uid on after_initialize
end
The closest I got is:
class Note < ActiveRecord::Base
validates_uniqueness_of :uid
# ... some code to generate uid on after_initialize
after_rollback :repair
protected
def repair
if self.errors[:uid].size > 0
self.uid = generate_uid
end
self.save # Try again
end
end
Some immediate problems with my solution: (1) The model instance still has errors that the controller can see, I'm not sure how to clear the errors. (2) The repair method is recursive.
While I'm sure there is a way to catch and handle the errors in the model (maybe the after_validation callback could be of use), perhaps you can avoid the issue in this case by ensuring that the uid you generate is unique when you create it.
Ryan Bates offered this method for generating unique tokens in a RailsCast:
def generate_token(column)
begin
self[column] = SecureRandom.urlsafe_base64
end while User.exists?(column => self[column])
end
With the use of a before_create callback, i.e. before_create { generate_token(:uid) }, each model will have a unique id.
All this said, #Beerlington raises a really good point about UUIDs.
Update: Note that the method given is expecting to be defined in a User model. For your example, you'd want to change it to ...while Note.exists?....
I would use a true UUID that is guaranteed to be unique, and not add the overhead to your model. Having a uniqueness validation in the model adds some overhead because it has to hit the database to figure out if something exists, and it's still not even guaranteed.
Check out this Ruby project to generate UUIDs: https://github.com/assaf/uuid/
I have an ActiveRecord model whose fields mostly come from the database. There are additional attributes, which come from a nested serialised blob of stuff. This has been done so that I can use these attributes from forms without having to jump through hoops (or so I thought in the beginning, anyway) while allowing forwards and backwards compatibility without having to write complicated migrations.
Basically I am doing this:
class Licence < ActiveRecord::Base
attr_accessor :load_worker_count
strip_attributes!
validates_numericality_of :load_worker_count,
:greater_than => 2, :allow_nil => true, :allow_blank => true
before_save :serialise_fields_into_properties
def serialise_fields_into_properties
...
end
def after_initialize
...
end
...
end
The problem I noticed was that I can't get empty values in :load_worker_count to be accepted by the validator, because:
If I omit :allow_blank, it fails validation complaining about it being blank
If I put in :allow_blank, it converts the blank to 0, which when fails on the :greater_than => 2
In tracking down why these blank values are getting to the validation stage in the first place, I discovered the root of the problem: strip_attributes! only affects actual attributes, as returned by the attributes method. So the values which should be nil at time of validation are not. So it feels like the root cause is that the synthetic attributes I added in aren't seen when setting which attributes to strip, so therefore I ask:
Is there a proper way of creating synthetic attributes which are recognised as proper attributes by other code which integrates with ActiveRecord?
I assume you are talking of the strip_attributes plugin; looking at the code, it uses the method attributes, defined in active_record/base.rb, which uses #attributes, which is initialized (in initialize) as #attributes = attributes_from_column_definition.
Maybe it's possible to hack ActiveRecord::Base somehow, but it would be a hard work: #attributes is also used when getting/putting stuff from/to db, so you would have to do a lot of hacking.
There's a much simpler solution:
before_validate :serialise_fields_into_properties
...
def serialise_fields_into_properties
if load_worker_count.respond_to? :strip
load_worker_count = load_worker_count.blank? ? nil : load_worker_count.strip
end
...
end
After all, this is what strip_attributes! does.
Wouldn't it be easier to just use Rails' serialize macro here?
class License < ActiveRecord::Base
serialize :special_attributes
end
Now you can assign a hash or array or whatever you need to special_attributes and Rails will serialize it a text field in the database.
license = License.new
license.special_attributes = { :beer => true, :water => false }
This will keep your code clean and you don't have to worry about serializing/deserializing attributes yourself.