There can be a business or security requirement that certain fields on a model should never be exposed in an API. One approach to this is to tell the developers not to ever put the field in the API output, but I prefer to protect them at the model level because that's where the requirement actually is.
The goal of this is to have a simple way to specify fields that should be prevented from being exposed. This is one approach and I would love feedback both on the implementation and on the approach. Ideas and criticism are very welcome.
Implementation - Extend ActiveRecord::Base
# Provide a system for specifying private attributes that shouldn't be exposed
module ActiveRecord
class Base
class << self
# Instead of setting the instance variable when this is called, we only check
# if it's defined. It's only set when attr_private is called. This allows us
# to know if the model has ever set any private attributes or not
def private_attributes
instance_variable_defined?('@private_attributes') ? @private_attributes : []
end
def is_private_attribute?(name)
private_attributes.include?(name.to_sym)
end
protected
# Set the @private_attributes variable with an array of attribute symbols
def attr_private(*args)
(@private_attributes ||= []).push(*args.collect { |a| a.to_sym }).uniq!
end
# Specify public attributes, which conversely privatizes the other attributes
# If private attributes have previously been declared, attr_public can override
# the setting. If it is the first time, make all the attributes private unless
# they are in the args.
def attr_public(*args)
if instance_variable_defined?('@private_attributes') && !@private_attributes.empty?
@private_attributes.delete_if { |n| args.include?(n.to_sym) }
else
attr_private(*attribute_names.reject { |n| args.include?(n.to_sym) })
end
end
end
# Run the to_xml options through a filter
def to_xml(options={})
super(secure_private_options(options))
end
# Run the serializable_hash options through a filter
def serializable_hash(options={})
super(secure_private_options(options))
end
protected
# Filter the options to make sure private attributes aren't included
def secure_private_options(options={})
(options[:except] ||= []).push(*self.class.private_attributes)
options[:only].delete_if { |n| self.class.is_private_attribute?(n) } if options.has_key?(:only)
options[:methods].delete_if { |n| self.class.is_private_attribute?(n) } if options.has_key?(:methods)
options
end
end
end
Example of use in a third party library - RABL
# Modify Rabl's builder to check if a method is private before exposing it
module Rabl
class Builder
protected
# Don't output the
def attribute(name, options={})
unless @_object.class.respond_to?(:is_private_attribute?) && @_object.class.is_private_attribute?(name)
@_result[options[:as] || name] = data_object_attribute(name) if @_object && @_object.respond_to?(name)
end
end
end
end
Example of general use on a model instance
# An example of usage.
# Fields: category_id, name, description, supplier
#
# Let's assume we never want supplier to be exposed.
#
class Product < ActiveRecord::Base
attr_private :supplier
end
Product.first.to_json
#=> { "category_id": 1, "name": "Bucky Balls", "description": "Awesome Magnets" }
Product.is_private_attribute?(:supplier)
#=> true
Product.is_private_attribute?(:category_id)
#=> false
Product.private_attributes
#=> [ :supplier ]
# You can also use attr_public, which takes the attributes_names and makes all of them private except the items listed in the attr_public arguments
Additional notes
- You could support role based access control by turning the underlying instance variable into a hash keyed to the role instead of a single array.
- The attributes could still be exposed by having them called directly, which allows for usage in cases where you actually WANT them exposed (like perhaps an admin panel).
Questions
As you see in the
secure_private_options
method, I check the passed in methods to make sure they aren't on the blacklist. This is fine for using attr_private to declare methods that shouldn't called, but I can't figure out how to set it so thatattr_public
can automatically include all getter attribute methods as well as just the attributes. For example, if I have a custom method calledfull_name
that I want to protect, it would be nice ifattr_public
would automatically protect it.Is there something in
ActiveRecord
or Rails that provides for this functionality already?What use cases am I not thinking about?
A gist of this code can be found here.