Ruby on Rails: safely get the model class from a model_name - ruby-on-rails

I have a polymorphic route that accepts the name of an ActiveRecord model (e.g. "User", "UserGroup") as a parameter.
How can I safely access the class based on the parameter?
The naive implementation (and likely not safe) would be:
model_class = params[:modelName].constantize
How can this be achieved without causing vulnerabilities?

I would use an explicit allowlist of models that the user is allowed to constantize in this context:
allowed_classes = ["User", "UserGroup"]
class_name = params[:modelName].presence_in(allowed_classes)
if class_name.present?
model_class = class_name.safe_constantize
else
# handle error
end
presence_in returns the string if it is included in the array, nil otherwise.

I'd say that you would need a validation on that param, that checks if its value is in the collection of acceptable models, and after that, do indeed use .constantize.
If you accept all models, and all of them inherit from ApplicationRecord or something, you could generate the collection like this:
ApplicationRecord.subclasses.collect { |type| type.name }.sort
And check if params[:modelName] is in this collection.
Also take note that constantize does throw an error if no constant exists to match the result:
[1] pry(main)> "NotAConstant".constantize
NameError: uninitialized constant NotAConstant

Related

Rails ActiveAttr Gem, manipulation of attributes within Class?

I have a Rails 5 class which includes ActiveAttr::Model, ActiveAttr:MassAssignment and ActiveAttr::AttributeDefaults.
It defines a couple of attributes using the method attribute and has some instance methods. I have some trouble manipulating the defined attributes. My problem is how to set an attribute value within the initializer. Some code:
class CompanyPresenter
include ActiveAttr::Model
include ActiveAttr::MassAssignment
include ActiveAttr::AttributeDefaults
attribute :identifier
# ...
attribute :street_address
attribute :postal_code
attribute :city
attribute :country
# ...
attribute :logo
attribute :schema_org_identifier
attribute :productontology
attribute :website
def initialize(attributes = nil, options = {})
super
fetch_po_field
end
def fetch_po_field
productontology = g_i_f_n('ontology') if identifier
end
def uri
#uri ||= URI.parse(website)
end
# ...
end
As I have written it, the method fetch_po_field does not work, it thinks that productontology is a local variable (g_i_f_n(...) is defined farther down, it works and its return value is correct). The only way I have found to set this variable is to write self.productontology instead. Moreover, the instance variable #uri is not defined as an attribute, instead it is written down only in this place and visible from outside.
Probably I have simply forgotten the basics of Ruby and Rails, I've done this for so long with ActiveRecord and ActiveModel. Can anybody explain why I need to write self.productontology, using #productontology doesn't work, and why my predecessor who wrote the original code mixed the # notation in #uri with the attribute-declaration style? I suppose he must have had some reason to do it like this.
I am also happy with any pointers to documentation. I haven't been able to find docs for ActiveAttr showing manipulation of instance variables in methods of an ActiveAttr class.
Thank you :-)
To start you most likely don't need the ActiveAttr gem as it really just replicates APIs that are already available in Rails 5.
See https://api.rubyonrails.org/classes/ActiveModel.html.
As I have written it, the method fetch_po_field does not work, it thinks that productontology is a local variable.
This is really just a Ruby thing and has nothing to do with the Rails Attributes API or the ActiveAttr gem.
When using assignment you must explicitly set the recipient unless you want to set a local variable. This line:
self.productontology = g_i_f_n('ontology') if identifier
Is actually calling the setter method productontology= on self using the rval as the argument.
Can anybody explain why I need to write self.productontology, using
#productontology doesn't work
Consider this plain old ruby example:
class Thing
def initialize(**attrs)
#storage = attrs
end
def foo
#storage[:foo]
end
def foo=(value)
#storage[:foo] = value
end
end
irb(main):020:0> Thing.new(foo: "bar").foo
=> "bar"
irb(main):021:0> Thing.new(foo: "bar").instance_variable_get("#foo")
=> nil
This looks quite a bit different then the standard accessors you create with attr_accessor. Instead of storing the "attributes" in one instance variable per attribute we use a hash as the internal storage and create accessors to expose the stored values.
The Rails attributes API does the exact same thing except its not just a simple hash and the accessors are defined with metaprogramming. Why? Because Ruby does not let you track changes to simple instance variables. If you set #foo = "bar" there is no way the model can track the changes to the attribute or do stuff like type casting.
When you use attribute :identifier you're writing both the setter and getter instance methods as well as some metadata about the attribute like its "type", defaults etc. which are stored in the class.

How self[:name] works rails

I created a reader method in my users model
def name
self[:name]
end
I'm having a hard time understanding self[:name]
it looks like I'm accessing a value with a key in a Hash but from what i can tell its not a Hash.
I have also tried to create classes in ruby to emulate this but cant get them to work so i"m not sure whether this is ruby or rails thing that I'm not understanding.
ActiveRecord supplies a [] method:
[](attr_name)
Returns the value of the attribute identified by attr_name after it has been typecast...
So saying self[:name] is just a round-about way to access the name attribute of your model.
[] is a method like any other in Ruby, you can define your own in any class you want:
class C
def [](k)
# do whatever you want
end
end
c = C.new
c[:pancakes]
ActiveRecord is used with data that is, more or less, a Hash backed by a relational database so saying model[:attribute_name] is fairly natural. Hence the existence of the [] method.

Sanitize/sanity-check user-supplied content with const_get

I have an action in a Rails application in which I am taking in a "type" for a polymorphic association and looking up a class (DokiCore is the name of my engine):
def complete
model_class = DokiCore.const_get(complete_params[:pointer_type])
pointer = model_class.find(complete_params[:pointer_id])
end
def complete_params
params.require(:progress).permit(
:pointer_id,
:pointer_type
)
end
I'm concerned that a user could supply a random class with "pointer_type" and try to look up other classes in the engine namespace. Is this a worry at all? Is there a Rails-y type way of looking up models only by name or should I add a method that tests the value of pointer_type for all models I want to allow in the polymorphic association?
Yes, something like DokiCore.const_get('::User') will return top-most User class even if there is DokiCore::User.
Just add some white-list checking against your user-param and you'll be fine:
raise 'Invalid class!' unless ['GoodClass'].include? complete_params[:pointer_type]

Define which attribute to work on by passing it as an argument?

I have class that has two attributes of same type.
The class currently have two methods doing the same thing, one for each attribute.
Is it possible to reduce these two methods to only one by passing the attribute name as an argument?
This is what I have tried (but gives me "NoMethodError: undefined method `attribute_name' for ..."):
def my_method(attribute_name)
self.attribute_name = 2
self.save
end
To save without validation:
update_attribute(attribute_name.to_sym, 2)
To save with validation:
update_attributes({ attribute_name.to_sym => 2 })
To assign without saving:
assign_attributes({ attribute_name.to_sym => 2 })
I think the last method is what are you looking for
def my_method(attribute_name)
self.assign_attributes({ attribute_name.to_sym => 2 })
self.save
end
Aliasing would be a better option instead of assigning attribute values as you've attempted. Take a look at the usage of alias_attribute or alias_method
As far as the error is concerned, you cannot use attributes as variables as you've used. You can instead make use of send as follows:
def my_method(attribute_name)
send(:"#{attribute_name}=", 2)
save
end

Is it a correct / not dangerous / common approach to pass an 'ActiveRecord::Relation' object as a method parameter?

I am using Ruby on Rails 3.2.2 and I would like to know if it is a correct / not dangerous / common approach to pass an ActiveRecord::Relation object as a method parameter.
At this time I am planning to use this approach in a scope method of a my model this way:
class Article < ActiveRecord::Base
def self.with_active_associations(associations, active = nil)
# associations.class
# => ActiveRecord::Relation
case active
when nil
scoped
when 'active'
with_ids(associations.pluck(:associated_id))
when 'not_active'
...
else
...
end
end
end
Note I: I would like to use this approach for performance reasons since the ActiveRecord::Relation is lazy loaded (in my case, if the active parameter value is not active the database is not hit at all).
Note II: the usage of the pluck method may generate an error if I pass as association parameter value an Array instead of an ActiveRecord::Relation.
1) In my opinion it's a sound tradeoff, you lose the ability to send an array as argument but you gain some perfomance. It's not that strange; for example, every time you define a scope you are doing exactly that, a filter than works only on relations and not on arrays.
2) You can always add Enumerable#pluck so the method works transparently with arrays. Of course it won't work if you use more features of relations.
module Enumerable
def pluck(method, *args)
map { |x| x.send(method, *args) }
end
end

Resources