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).
Big Thanks! to Bruce Perens
for inspiring this with his ModelSecurity
plugin for rails!
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 are the part that let you specify the security permissions.
- guards can be specified using
true
,false
, aSymbol
, aString
, or aHash
- 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, aretrue
,false
, aSymbol
, aString
, alambda
, or aProc
.
- if a
- 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.
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? }
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!
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
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