Skip to main content
3 of 3
edited tags
Malachi
  • 28.7k
  • 11
  • 86
  • 188

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

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.