objectbouncer 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.mdown +117 -0
- data/Rakefile +22 -0
- data/lib/objectbouncer.rb +4 -0
- data/lib/objectbouncer/base.rb +123 -0
- data/lib/objectbouncer/errors.rb +5 -0
- data/test/objectbouncer/base_test.rb +114 -0
- data/test/test_helper.rb +9 -0
- metadata +74 -0
data/README.mdown
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# ObjectBouncer
|
2
|
+
|
3
|
+
ObjectBouncer provides a way to restrict access to an objects properties or
|
4
|
+
methods based upon a series of preconditions.
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
|
8
|
+
Let's say we have a President who is protected by the SecretService:
|
9
|
+
|
10
|
+
class President
|
11
|
+
def shake_hands
|
12
|
+
"shaking hands"
|
13
|
+
end
|
14
|
+
|
15
|
+
def high_five
|
16
|
+
"high five!"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
And the following people:
|
21
|
+
|
22
|
+
class Nutjob
|
23
|
+
def dictator?
|
24
|
+
true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class VicePresident
|
29
|
+
def democrat?
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def friend?(other)
|
34
|
+
other.class == President
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Hippie
|
39
|
+
def democrat?
|
40
|
+
true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
To protect the President we'd define a SecretService class like so:
|
45
|
+
|
46
|
+
class SecretService
|
47
|
+
include ObjectBouncer::Doorman
|
48
|
+
door_policy do
|
49
|
+
deny :shake_hands, :if => Proc.new{|person| person.dictator? }
|
50
|
+
allow :shake_hands, :if => Proc.new{|person| person.democrat? }
|
51
|
+
deny :high_five, :unless => Proc.new{|person, president|
|
52
|
+
person.friend?(president) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
And now, to put our security detail in place we run all contact with
|
57
|
+
the President through SecretService first:
|
58
|
+
|
59
|
+
@obama = President.new
|
60
|
+
@gaddafi = Nutjob.new
|
61
|
+
@joe_biden = VicePresident.new
|
62
|
+
@tommy_chong = Hippie.new
|
63
|
+
|
64
|
+
SecretService.as(@gaddafi).on(@president).shake_hands # Raises PermissionDenied
|
65
|
+
SecretService.as(@joe_biden).on(@president).shake_hands # Allowed
|
66
|
+
SecretService.as(@tommy_chong).on(@president).shake_hands # Allowed
|
67
|
+
SecretService.as(@joe_biden).on(@president).high_five # Allowed
|
68
|
+
SecretService.as(@tommy_chong).on(@president).high_five # Raises PermissionDenied
|
69
|
+
|
70
|
+
## Why would I want to use this?
|
71
|
+
|
72
|
+
Most of the existing RBAC and other access based permission systems are
|
73
|
+
implemented at a controller or action level within the MVC stack. ObjectBouncer
|
74
|
+
allows you to provide more granular control my limiting access to discrete
|
75
|
+
methods on an instance of an object, while keeping the permissions logic
|
76
|
+
external to the Model itself.
|
77
|
+
|
78
|
+
## Compatibility
|
79
|
+
|
80
|
+
Test suite has currently only been confirmed on the following platforms:
|
81
|
+
|
82
|
+
* MRI Ruby 1.9.2
|
83
|
+
|
84
|
+
## Contributions
|
85
|
+
|
86
|
+
Patches gladly accepted. Please fork this repo, add a relevant test, and send
|
87
|
+
me a pull request.
|
88
|
+
|
89
|
+
## Status
|
90
|
+
|
91
|
+
Currently still under active development and considered alpha, the API is
|
92
|
+
liable to change without notice.
|
93
|
+
|
94
|
+
## License
|
95
|
+
|
96
|
+
ObjectBouncer is released under the MIT license.
|
97
|
+
|
98
|
+
Copyright (c) 2011 Glenn Gillen
|
99
|
+
|
100
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
101
|
+
of this software and associated documentation files (the "Software"), to deal
|
102
|
+
in the Software without restriction, including without limitation the rights
|
103
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
104
|
+
copies of the Software, and to permit persons to whom the Software is
|
105
|
+
furnished to do so, subject to the following conditions:
|
106
|
+
|
107
|
+
The above copyright notice and this permission notice shall be included in
|
108
|
+
all copies or substantial portions of the Software.
|
109
|
+
|
110
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
111
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
112
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
113
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
114
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
115
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
116
|
+
THE SOFTWARE.
|
117
|
+
ObjectBouncer
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
|
6
|
+
spec_data = File.open('objectbouncer.gemspec').read
|
7
|
+
spec = nil
|
8
|
+
Thread.new do
|
9
|
+
spec = eval("#{spec_data}")
|
10
|
+
end.join
|
11
|
+
|
12
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
13
|
+
pkg.need_zip = false
|
14
|
+
pkg.need_tar = false
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'rake/testtask'
|
18
|
+
Rake::TestTask.new(:test) do |test|
|
19
|
+
test.libs << 'lib' << 'test'
|
20
|
+
test.pattern = 'test/**/*_test.rb'
|
21
|
+
test.verbose = false
|
22
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module ObjectBouncer
|
2
|
+
module Doorman
|
3
|
+
|
4
|
+
def self.included(klass)
|
5
|
+
klass.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def door_policy(&block)
|
10
|
+
@lockdown = false
|
11
|
+
@policies = {}
|
12
|
+
yield
|
13
|
+
end
|
14
|
+
|
15
|
+
def lockdown
|
16
|
+
@lockdown = true
|
17
|
+
end
|
18
|
+
|
19
|
+
def lockdown?
|
20
|
+
@lockdown
|
21
|
+
end
|
22
|
+
|
23
|
+
def allow(method, options = {})
|
24
|
+
@policies[method] ||= blank_policy_template
|
25
|
+
if options.has_key?(:if)
|
26
|
+
@policies[method][:allow][:if] << options[:if]
|
27
|
+
elsif options.has_key?(:unless)
|
28
|
+
@policies[method][:allow][:unless] << options[:unless]
|
29
|
+
else
|
30
|
+
@policies[method][:allow][:if].unshift(Proc.new{ true == true })
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def deny(method, options = {})
|
35
|
+
@policies[method] ||= blank_policy_template
|
36
|
+
if options.has_key?(:if)
|
37
|
+
@policies[method][:deny][:if] << options[:if]
|
38
|
+
elsif options.has_key?(:unless)
|
39
|
+
@policies[method][:deny][:unless] << options[:unless]
|
40
|
+
else
|
41
|
+
@policies[method][:deny][:if].unshift(Proc.new{ true == true })
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def policies
|
46
|
+
@policies
|
47
|
+
end
|
48
|
+
|
49
|
+
def as(person)
|
50
|
+
doorman = new
|
51
|
+
doorman.send(:person=, person)
|
52
|
+
doorman
|
53
|
+
end
|
54
|
+
|
55
|
+
def on(object)
|
56
|
+
doorman = new
|
57
|
+
doorman.send(:object=, object)
|
58
|
+
doorman
|
59
|
+
end
|
60
|
+
|
61
|
+
def blank_policy_template
|
62
|
+
{ :allow => { :if => [], :unless => [] },
|
63
|
+
:deny => { :if => [], :unless => [] }
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
def on(object)
|
70
|
+
@object = object
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def as(person)
|
75
|
+
@person = person
|
76
|
+
self
|
77
|
+
end
|
78
|
+
|
79
|
+
def method_missing(meth, *args, &block)
|
80
|
+
if respond_to?(meth)
|
81
|
+
raise "adada!!!" if self.class.policies.nil? or self.class.policies.empty?
|
82
|
+
if call_allowed?(meth)
|
83
|
+
@object.send(meth, *args, &block)
|
84
|
+
elsif call_denied?(meth)
|
85
|
+
raise ObjectBouncer::PermissionDenied.new
|
86
|
+
end
|
87
|
+
else
|
88
|
+
super
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def respond_to?(meth)
|
93
|
+
@object.respond_to?(meth)
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
def call_allowed?(meth)
|
98
|
+
if policies = self.class.policies[meth]
|
99
|
+
return true if !policies[:allow][:unless].empty? && !policies[:allow][:unless].detect{|policy| policy.call(@person, @object) rescue nil}
|
100
|
+
return true if policies[:allow][:if].detect{|policy| policy.call(@person, @object) rescue nil}
|
101
|
+
return true if policies[:deny][:unless].detect{|policy| policy.call(@person, @object) rescue nil}
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def call_denied?(meth)
|
106
|
+
return true if self.class.lockdown?
|
107
|
+
if policies = self.class.policies[meth]
|
108
|
+
return true if policies[:allow][:unless].detect{|policy| policy.call(@person, @object) rescue nil}
|
109
|
+
return true if policies[:deny][:if].detect{|policy| policy.call(@person, @object) rescue nil}
|
110
|
+
return true if !policies[:deny][:unless].empty? && !call_allowed?(meth)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def person=(val)
|
115
|
+
@person = val
|
116
|
+
end
|
117
|
+
|
118
|
+
def object=(val)
|
119
|
+
@object = val
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
$:.unshift File.expand_path("..", File.dirname(__FILE__))
|
2
|
+
require "test_helper"
|
3
|
+
|
4
|
+
class SecretService
|
5
|
+
include ObjectBouncer::Doorman
|
6
|
+
door_policy do
|
7
|
+
deny :shake_hands, :if => Proc.new{|person, president| person != president}
|
8
|
+
allow :shake_hands, :if => Proc.new{|person, president| person.class == MichelleObama}
|
9
|
+
deny :high_five, :unless => Proc.new{|person, president| person.who? == "it's me, Joe!"}
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class CoastGuard
|
14
|
+
include ObjectBouncer::Doorman
|
15
|
+
door_policy do
|
16
|
+
lockdown # Overly protective are we?
|
17
|
+
allow :watch_tv_appearance
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class President
|
22
|
+
def shake_hands
|
23
|
+
"shaking hands"
|
24
|
+
end
|
25
|
+
|
26
|
+
def high_five
|
27
|
+
"high five!"
|
28
|
+
end
|
29
|
+
|
30
|
+
def watch_tv_appearance
|
31
|
+
"I'm on your TV!"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class MichelleObama
|
36
|
+
end
|
37
|
+
|
38
|
+
class JoePublic
|
39
|
+
end
|
40
|
+
|
41
|
+
class JoeBiden
|
42
|
+
def who?
|
43
|
+
"it's me, Joe!"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class ObjectBouncerTest < Test::Unit::TestCase
|
48
|
+
context "keeping the president safe" do
|
49
|
+
|
50
|
+
setup do
|
51
|
+
@president = President.new
|
52
|
+
end
|
53
|
+
|
54
|
+
should "not let the public shake hands" do
|
55
|
+
joe_public = JoePublic.new
|
56
|
+
assert_raise ObjectBouncer::PermissionDenied do
|
57
|
+
SecretService.as(joe_public).on(@president).shake_hands
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
should "let the first lady get in close" do
|
62
|
+
first_lady = MichelleObama.new
|
63
|
+
assert_equal "shaking hands", SecretService.as(first_lady).on(@president).shake_hands
|
64
|
+
end
|
65
|
+
|
66
|
+
should "high five Biden" do
|
67
|
+
vice_pres = JoeBiden.new
|
68
|
+
assert_equal "high five!", SecretService.as(vice_pres).on(@president).high_five
|
69
|
+
end
|
70
|
+
|
71
|
+
should "not let the public high five" do
|
72
|
+
joe_public = JoePublic.new
|
73
|
+
assert_raise ObjectBouncer::PermissionDenied do
|
74
|
+
SecretService.as(joe_public).on(@president).high_five
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
context "going into complete lockdown" do
|
81
|
+
|
82
|
+
setup do
|
83
|
+
@president = President.new
|
84
|
+
end
|
85
|
+
|
86
|
+
should "deny everything by default" do
|
87
|
+
joe_public = JoePublic.new
|
88
|
+
assert_raise ObjectBouncer::PermissionDenied do
|
89
|
+
CoastGuard.as(joe_public).on(@president).high_five
|
90
|
+
end
|
91
|
+
assert_raise ObjectBouncer::PermissionDenied do
|
92
|
+
CoastGuard.as(joe_public).on(@president).shake_hands
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
should "allow if explictly said it's ok" do
|
97
|
+
joe_public = JoePublic.new
|
98
|
+
assert_equal "I'm on your TV!", CoastGuard.as(joe_public).on(@president).watch_tv_appearance
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context "having a forgiving API" do
|
103
|
+
|
104
|
+
setup do
|
105
|
+
@president = President.new
|
106
|
+
end
|
107
|
+
|
108
|
+
should "let people chain methods either order" do
|
109
|
+
joe_public = JoePublic.new
|
110
|
+
assert_equal "I'm on your TV!", CoastGuard.as(joe_public).on(@president).watch_tv_appearance
|
111
|
+
assert_equal "I'm on your TV!", CoastGuard.on(@president).as(joe_public).watch_tv_appearance
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: objectbouncer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Glenn Gillen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-03-07 00:00:00 +00:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: httparty
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - "="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 0.6.1
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
description: ""
|
28
|
+
email: glenn@rubypond.com
|
29
|
+
executables: []
|
30
|
+
|
31
|
+
extensions: []
|
32
|
+
|
33
|
+
extra_rdoc_files: []
|
34
|
+
|
35
|
+
files:
|
36
|
+
- README.mdown
|
37
|
+
- Rakefile
|
38
|
+
- lib/objectbouncer/base.rb
|
39
|
+
- lib/objectbouncer/errors.rb
|
40
|
+
- lib/objectbouncer.rb
|
41
|
+
- test/objectbouncer/base_test.rb
|
42
|
+
- test/test_helper.rb
|
43
|
+
has_rdoc: true
|
44
|
+
homepage: http://github.com/rubypond/objectbouncer
|
45
|
+
licenses: []
|
46
|
+
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
|
50
|
+
require_paths:
|
51
|
+
- .
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
requirements: []
|
66
|
+
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 1.6.0
|
69
|
+
signing_key:
|
70
|
+
specification_version: 2
|
71
|
+
summary: ""
|
72
|
+
test_files:
|
73
|
+
- test/objectbouncer/base_test.rb
|
74
|
+
- test/test_helper.rb
|