Skip to content

Instantly share code, notes, and snippets.

@jhubert
Created September 30, 2012 10:16
Show Gist options
  • Save jhubert/3806392 to your computer and use it in GitHub Desktop.
Save jhubert/3806392 to your computer and use it in GitHub Desktop.
Protect certain attributes and methods from being exposed

Overview

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. :)

# 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
# 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
# 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment