On the train home last night I watched the excellent Jim Weirich Play-by-play from PeepCode.
During the screencast Jim develops a library that “protects against unauthorized data model modification by users in less-privileged roles.” The screencast provides a great insight into the way Jim approaches problems, designs apis, and how he’s customised his environment to suit the way he works.
His approach is to build a proxy object which wraps the object to be updated, and provides a whitelist for fields which can be updated. He also inadvertantly demonstrates an easy mistake to make when using proxy objects.
Here is a simplified version of Jim’s solution - without any of the api for creating / finding proxies - which we will use to demonstrate this pitfall and its effects.
require 'delegate'
class ProtectionProxy < SimpleDelegator
def initialize(object, *writable_fields)
super(object)
@writable_fields = writable_fields
end
def method_missing(method, *args, &block)
method_name = method.to_s
if !method_name.end_with?('=')
super
elsif @writable_fields.include?(method_name[0...-1].to_sym)
super
end
end
end
This approach works great for silently dropping calls to the accessor methods that are not in the provided whitelist. Here are some rspec examples which show how it works.
require 'rspec-given'
require 'protection_proxy'
class User < Struct.new(:name, :email, :membership_level)
end
describe ProtectionProxy do
Given(:user) { User.new("Jim", "jim@somewhere", "Beginner") }
Given(:proxy) { ProtectionProxy.new(user, :membership_level) }
Then { proxy.name.should == "Jim" }
context "when modifiying a writable field" do
When { proxy.membership_level = "Advanced" }
Then { proxy.membership_level.should == "Advanced" }
end
context "when modifiying a non-writable field" do
When { proxy.name = "Joe" }
Then { proxy.name.should == "Jim" }
end
end
Now if we imagine we have a rails project, we can create a proxy to wrap our ActiveRecord object, and specify an attribute whitelist. This should then prevent mass-assignment of any non-whitelist attributes - it could be used in a controller like this:
class UserController < ActionController::Base
def update
user = User.find(params[:id])
proxy = ProtectionProxy.new(user, :name, :email)
if proxy.update_attributes(params[:user])
# happy path
else
# error
end
end
end
Unfortunately this won’t work as we might expect.
Proxying like this is a great way to add new behaviour to existing objects, without modifying them - or creating new subclasses. but there is one thing to be aware of when you are using delegation in this way.
Methods called on the wrapped object have no knowledge of the methods in the proxy object.
So what happens when we call proxy.update_attributes
?
The proxy object immediately delegates that method call to the user object, it will call user.update_attributes
.
If you have used ActiveRecord, you will be aware of the way that ActiveRecord::Base#update_attrbiutes
will make use of the accessor methods on its instances to set the field names.
So, user.update_attributes name: 'Joe'
will call user.name = 'Joe'
, not the accessor methods on the proxy.
As we are not calling the accessor methods on the proxy, we aren’t filtering out the fields that don’t appear in the whitelist and our attribute protection won’t work when we use update_attributes
.
Here is another example. Capitalise
wraps an object and provides a upper case version of its name method.
require 'delegate'
class Capitalise < SimpleDelegator
def initialize(source)
@source = source
super(source)
end
def name
@source.name.upcase
end
end
class Person < Struct.new(:name)
def greet
"Hello, #{name}"
end
end
john = Person.new('john')
capital_john = Capitalise.new(john)
john.greet #=> "Hello, john"
capital_john.greet #=> "Hello, john"
Because greet
is defined in the Person
class, when it calls name
it will always call Person#name
.
This has caught me out a couple times. It’s so easy in ruby to create proxy objects or decorators that its easy to forget that you have a different object.
One solution is to implement a version of update_attributes
on the proxy object.
class ProtectionProxy < SimpleDelegator
def initialize(object, *writable_fields)
super(object)
@object = object
@writable_fields = writable_fields
end
def update_attributes(attributes={})
attrs = attributes.select { |field_name|
@writable_fields.include?(field_name)
}
@object.update_attributes attrs
end
end
Here we add an update_attributes
method to the ProtectionProxy
class - this only allows attributes allowed by the whitelist through to User#update_attributes
.
The screencast ends with a note that Jim noticed this error later after recording of the screen cast finished. Jim’s complete solution, including the nice api, can be found on github.
Here is the whole of the ProxyProtection
implementation, with rspec examples.