Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

executable file 201 lines (160 sloc) 7.474 kB

dm-is-protectable

A DataMapper plugin that allows to protect property assignment.
This makes it very easy to implement permission checking in the model
(just like any other business rule).

Shoutouts

Big Thanks! to Bruce Perens
for inspiring this with his ModelSecurity
plugin for rails!

Overview

dm-is-protectable allows you to specify security permissions
on any or all of the properties of any DataMapper::Resource.

Security permissions are specified in the declaration of the resource’s class,
The specification includes the permission that should be granted (one of :read, :write, or :access),
the names of the properties to which the permission applies, and
an optional guard that must return true or false
depending on whether the access should be allowed or denied.

If no security permissions are declared for a property, that property
may always be accessed. This is likely to change though, I’m thinking about
a paranoid option that can be used to to either whitelist or blacklist
all properties by default.

The security tests themselves may access any data with impunity.
A thread local variable is used to disable further security testing
while a security test is in progress.

Security permissions can be installed using the let and deny class methods.


let :read,   ...   # specifies when a property _can_ be read, 
let :write,  ...   # specifies when a property _can_ be written
let :access, ...   # does both.

deny :read,   ...  # specifies when a property _cannot_ be read, 
deny :write,  ...  # specifies when a property _cannot_ be written
deny :access, ...  # does both.

If these aren’t expressive enough try

is :protectable, :extended => true

This will extend the following Module in your resource:


# Syntactic sugar
# this can be extended optionally
module MoreClassMethods
  
  # same as let without guard
  def always_let(permission, properties = [])
    let(permission, properties)
  end
  
  # same as deny without guard
  def always_deny(permission, properties = [])
    deny(permission, properties)
  end
  
  # even more sugar
  alias :never_let :always_deny
  alias :never_deny :always_let
  
end

Guards

Guards are the part that let you specify the security permissions.

  • guards can be specified using true, false, a Symbol, a String, or a Hash
    • if a Hash is used, the only two keys that are recognized are :if and :unless.
    • if a Hash is used, the only values that are recognized, are true, false, a Symbol, a String, a lambda, or a Proc.
  • no guard is the same as specifying a guard that always returns true.
  • no properties means that the guard (if present) is not bound to any property
    • guards that are not bound to any property will be evaluated for every property.
    • guards that are not bound to any property will be evaluated before all guards that are bound to a specific property
  • any guard that returns false ends the run, further tests will not be evaluated.

Examples (:read can be substituted with :write and :access)


let :read
let :read, true
let :read, false
let :read, :if     => true
let :read, :unless => true
let :read, :if     => :funny?
let :read, :unless => :funny?
let :read, :if     => "funny?"
let :read, :unless => "funny?"
let :read, :if     => lambda { |r| r.funny? }
let :read, :unless => lambda { |r| r.funny? }
let :read, :if     => Proc.new { |r| r.funny? }
let :read, :unless => Proc.new { |r| r.funny? }
                                                                 
let :read, :name
let :read, :name, true
let :read, :name, false
let :read, :name, :if     => true
let :read, :name, :unless => true
let :read, :name, :if     => :funny?
let :read, :name, :unless => :funny?
let :read, :name, :if     => "funny?"
let :read, :name, :unless => "funny?"
let :read, :name, :if     => lambda { |r| r.funny? }
let :read, :name, :unless => lambda { |r| r.funny? }
let :read, :name, :if     => Proc.new { |r| r.funny? }
let :read, :name, :unless => Proc.new { |r| r.funny? }
                                                                 
let :read, [ :name, :age ]
let :read, [ :name, :age ], true
let :read, [ :name, :age ], false
let :read, [ :name, :age ], :if     => true
let :read, [ :name, :age ], :unless => true
let :read, [ :name, :age ], :if     => :funny?
let :read, [ :name, :age ], :unless => :funny?
let :read, [ :name, :age ], :if     => "funny?"
let :read, [ :name, :age ], :unless => "funny?"
let :read, [ :name, :age ], :if     => lambda { |r| r.funny? }
let :read, [ :name, :age ], :unless => lambda { |r| r.funny? }
let :read, [ :name, :age ], :if     => Proc.new { |r| r.funny? }
let :read, [ :name, :age ], :unless => Proc.new { |r| r.funny? } 

Accessing Security Test Results

The two instance methods, readable? and writable? are available
on any instance of DataMapper::Resource that is :protectable.
They can be used to inform the program if a particular property can be accessed or not.
A call to any of the above methods will actually perform the evaluation of all the guards,
nothing is cached!

Exceptions

DataMapper provides two internal methods to access properties:
DataMapper::Property#get and DataMapper::Property#set.
Extlib::Hook before hooks are registered on these methods that will raise
various subclasses of SecurityError when an unpermitted access is attempted.
The following exceptions may occur when using dm-is-protectable


class DmIsProtectableException < SecurityError; end

class InvalidPermission     < DmIsProtectableException; end
class UnknownProperty       < DmIsProtectableException; end
class InvalidGuardCondition < DmIsProtectableException; end
class InvalidGuard          < DmIsProtectableException; end
class IllegalPropertyAccess < DmIsProtectableException; end

class IllegalReadAccess     < IllegalPropertyAccess; end
class IllegalWriteAccess    < IllegalPropertyAccess; end
class IllegalDisplayAccess  < IllegalPropertyAccess; end

Support for let :display

TODO: think about supporting this here (plus in a separate gem)

A companion mechanism could be used to control views.

let :display :phone_nr, :if => admin?

let :display could be useful for specifying if a table view should have a
column for a particular property. Its tests would have to be declared as class
methods of the resource, while the tests of let :read, let :write, and
let :access are instance methods. This is because the information declared
by let :display is accessed before iteration over the resources begins.

The class method displayable? would return true or false
depending upon whether a particular property should be displayed or not.
This could be used to modify a view so that any non-writable data
will not be presented in an editable field.

A DisplayHelper module could overload the methods that are usually used
to edit models so that they will not attempt to read or write what they
aren’t permitted, and will render appropriately for the permissions
on any resource property.

Those methods (in rails) are:

  • check_box
  • file_field
  • hidden_field
  • password_field
  • radio_button
  • text_area
  • text_field
Jump to Line
Something went wrong with that request. Please try again.