# Protecting certain model attributes from being exposed in an API

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


• 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

1. 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 that attr_public can automatically include all getter attribute methods as well as just the attributes. For example, if I have a custom method called full_name that I want to protect, it would be nice if attr_public would automatically protect it.

2. Is there something in ActiveRecord or Rails that provides for this functionality already?

3. What use cases am I not thinking about?

A gist of this code can be found here.

-
Some advice I've received so far: 1) Make it an include for ActiveModel instead of ActiveRecord::Base. 2) Follow the attr_accessible conventions for Blacklist / Whitelist rules. 3) Add support for roles. – Jeremy Baker Oct 1 '12 at 6:03

With regard to (1): I suppose you could consider wrapping an entire model instance in an object that presents some methods itself (like a filtered to_json, or filtered columns), and forwards only allowed methods to the model on method_missing. Basically proxying (maybe even duck typing) the wrapped model minus the private attributes/methods.

I.e. set attr_public/private on your model like now, and add a filtered_for(role) (or whatever) method, that uses the black/whitelist to construct a filtering proxy around the model itself. Sieve seems like good name for such a thing, but it's probably taken :)

Admittedly, it's a heavy-handed approach, and I haven't tried it out in any way, but it's an idea.

As for (2), I just don't know - I'm still pretty new to Rails. (So this entire answer might be wrong-headed. You've been warned.)
The issue does remind me of the strong_parameters gem, though - if only because I've just started using it. You're sort of doing the same, just on the response-side of things.

Regardless of how you implement it, a solid test suite for the API would be a boon. Write some functional tests that fail spectacularly if responses include attributes they shouldn't. While this isn't itself perfect, it has the benefit of catching cases where a gung-ho programmer simply bypasses all the protection to output an otherwise privat attribute.

A rambling answer, and perhaps entirely wrong, but if not then maybe you can use it for something.

-