3
\$\begingroup\$

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

  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.

\$\endgroup\$
1
  • \$\begingroup\$ 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. \$\endgroup\$ Commented Oct 1, 2012 at 6:03

1 Answer 1

1
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.