objectbouncer 0.0.6 → 0.1.0
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 +30 -13
- data/lib/objectbouncer.rb +1 -1
- data/lib/objectbouncer/base.rb +42 -84
- data/lib/objectbouncer/class_methods.rb +101 -0
- data/lib/objectbouncer/errors.rb +1 -0
- data/test/objectbouncer/activerecord_test.rb +78 -0
- data/test/objectbouncer/base_test.rb +23 -44
- metadata +5 -2
data/README.mdown
CHANGED
@@ -5,7 +5,7 @@ methods based upon a series of preconditions.
|
|
5
5
|
|
6
6
|
## Usage
|
7
7
|
|
8
|
-
Let's say we have a President who
|
8
|
+
Let's say we have a President who needs protection:
|
9
9
|
|
10
10
|
class President
|
11
11
|
def shake_hands
|
@@ -45,9 +45,9 @@ And the following people:
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
|
-
To protect the President we'd
|
48
|
+
To protect the President we'd add the following to our class definition:
|
49
49
|
|
50
|
-
class
|
50
|
+
class President
|
51
51
|
include ObjectBouncer::Doorman
|
52
52
|
door_policy do
|
53
53
|
deny :shake_hands, :if => Proc.new{|person| person.dictator? }
|
@@ -59,21 +59,38 @@ To protect the President we'd define a SecretService class like so:
|
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
62
|
-
And now, to put our security detail in place we
|
63
|
-
|
62
|
+
And now, to put our security detail in place we need to specify the current
|
63
|
+
user that is initiating the interaction:
|
64
64
|
|
65
65
|
@obama = President.new
|
66
66
|
@gaddafi = Nutjob.new
|
67
67
|
@joe_biden = VicePresident.new
|
68
68
|
@tommy_chong = Hippie.new
|
69
69
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
70
|
+
@obama.current_user = @gaddafi
|
71
|
+
@obama.shake_hands # Raises PermissionDenied
|
72
|
+
@obama.give(:donation) # Allowed
|
73
|
+
@obama.give(:suspect_package) # Raises PermissionDenied
|
74
|
+
|
75
|
+
@obama.current_user = @joe_biden
|
76
|
+
@obama.shake_hands # Allowed
|
77
|
+
@obama.high_five # Allowed
|
78
|
+
|
79
|
+
@obama.current_user = @tommy_chong
|
80
|
+
@obama.shake_hands # Allowed
|
81
|
+
@obama.high_five # Raises PermissionDenied
|
82
|
+
|
83
|
+
Alternatively, if you can specify the user when you instantiate the object:
|
84
|
+
|
85
|
+
@gaddafi = Nutjob.new
|
86
|
+
@obama = President.as(@gaddafi).new
|
87
|
+
|
88
|
+
@obama.shake_hands # Raises PermissionDenied
|
89
|
+
@obama.give(:donation) # Allowed
|
90
|
+
@obama.give(:suspect_package) # Raises PermissionDenied
|
91
|
+
|
92
|
+
Handy in things like application controllers where you want to pass in the
|
93
|
+
currently logged in user.
|
77
94
|
|
78
95
|
## Why would I want to use this?
|
79
96
|
|
@@ -81,7 +98,7 @@ Most of the existing RBAC and other access based permission systems are
|
|
81
98
|
implemented at a controller or action level within the MVC stack. ObjectBouncer
|
82
99
|
allows you to provide more granular control my limiting access to discrete
|
83
100
|
methods on an instance of an object, while keeping the permissions logic
|
84
|
-
external to the
|
101
|
+
external to the methods themselves.
|
85
102
|
|
86
103
|
## Compatibility
|
87
104
|
|
data/lib/objectbouncer.rb
CHANGED
data/lib/objectbouncer/base.rb
CHANGED
@@ -1,110 +1,68 @@
|
|
1
1
|
module ObjectBouncer
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
klass.extend ClassMethods
|
6
|
-
end
|
2
|
+
def self.enforce!
|
3
|
+
@enforce = true
|
4
|
+
end
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
@policies = {}
|
12
|
-
yield
|
13
|
-
end
|
6
|
+
def self.unenforce!
|
7
|
+
@enforce = false
|
8
|
+
end
|
14
9
|
|
15
|
-
|
16
|
-
|
17
|
-
|
10
|
+
def self.enforced?
|
11
|
+
@enforce
|
12
|
+
end
|
18
13
|
|
19
|
-
|
20
|
-
@lockdown
|
21
|
-
end
|
14
|
+
module Doorman
|
22
15
|
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
16
|
+
def with(user)
|
17
|
+
self
|
18
|
+
end
|
33
19
|
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
20
|
+
def current_user=(user)
|
21
|
+
@current_user = user
|
22
|
+
end
|
44
23
|
|
45
|
-
|
46
|
-
|
47
|
-
|
24
|
+
def current_user
|
25
|
+
@current_user ||= self.class.current_user
|
26
|
+
end
|
48
27
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
}
|
28
|
+
def apply_policies
|
29
|
+
policies.keys.each do |method|
|
30
|
+
self.class.protect_method!(method)
|
53
31
|
end
|
54
|
-
|
55
32
|
end
|
56
33
|
|
57
|
-
def
|
58
|
-
@
|
59
|
-
@object = object
|
60
|
-
super()
|
61
|
-
self
|
34
|
+
def policies
|
35
|
+
@policies || self.class.instance_policies
|
62
36
|
end
|
63
37
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
raise ObjectBouncer::PermissionDenied.new
|
71
|
-
else
|
72
|
-
@object.send(meth, *args, &block)
|
38
|
+
def self.included(klass)
|
39
|
+
klass.extend ClassMethods
|
40
|
+
klass.overwrite_initialize
|
41
|
+
klass.instance_eval do
|
42
|
+
def method_added(name)
|
43
|
+
overwrite_initialize if name == :initialize
|
73
44
|
end
|
74
|
-
else
|
75
|
-
super
|
76
45
|
end
|
77
46
|
end
|
78
47
|
|
79
|
-
def respond_to?(meth)
|
80
|
-
@object.respond_to?(meth)
|
81
|
-
end
|
82
|
-
|
83
48
|
private
|
84
|
-
def call_allowed?(meth, *args)
|
85
|
-
if policies = self.class.policies[meth]
|
86
|
-
return true if !policies[:allow][:unless].empty? && !policies[:allow][:unless].detect{|policy| policy.call(@accessee, @object, *args) rescue nil}
|
87
|
-
return true if policies[:allow][:if].detect{|policy| policy.call(@accessee, @object, *args) rescue nil}
|
88
|
-
return true if policies[:deny][:unless].detect{|policy| policy.call(@accessee, @object, *args) rescue nil}
|
89
|
-
end
|
90
|
-
end
|
91
49
|
|
92
50
|
def call_denied?(meth, *args)
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
51
|
+
if enforced? && current_user.nil?
|
52
|
+
raise ObjectBouncer::ArgumentError.new("You need to specify the user to execute the method as. e.g., #{self.class.to_s}.as(@some_user).#{meth.to_s}(....)")
|
53
|
+
end
|
54
|
+
return false if current_user.nil? && !enforced?
|
55
|
+
if meth_policies = policies[meth]
|
56
|
+
if !meth_policies[:unless].empty?
|
57
|
+
return false if meth_policies[:unless].detect{|policy| policy.call(current_user, @object, *args) rescue nil}
|
58
|
+
return true
|
59
|
+
end
|
60
|
+
return true if meth_policies[:if].detect{|policy| policy.call(current_user, @object, *args) rescue nil}
|
98
61
|
end
|
99
62
|
end
|
100
63
|
|
101
|
-
def
|
102
|
-
|
103
|
-
end
|
104
|
-
|
105
|
-
def object=(val)
|
106
|
-
@object = val
|
64
|
+
def enforced?
|
65
|
+
ObjectBouncer.enforced?
|
107
66
|
end
|
108
|
-
|
109
67
|
end
|
110
68
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module ObjectBouncer
|
2
|
+
module Doorman
|
3
|
+
module ClassMethods
|
4
|
+
def overwrite_initialize
|
5
|
+
class_eval do
|
6
|
+
unless method_defined?(:objectbouncer_initialize)
|
7
|
+
define_method(:objectbouncer_initialize) do |*args, &block|
|
8
|
+
original_initialize(*args, &block)
|
9
|
+
@policies = self.class.policies
|
10
|
+
apply_policies
|
11
|
+
self
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
if instance_method(:initialize) != instance_method(:objectbouncer_initialize)
|
16
|
+
alias_method :original_initialize, :initialize
|
17
|
+
alias_method :initialize, :objectbouncer_initialize
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def policies=(hash)
|
23
|
+
@policies = hash
|
24
|
+
end
|
25
|
+
|
26
|
+
def policies
|
27
|
+
@policies
|
28
|
+
end
|
29
|
+
|
30
|
+
def blank_policy_template
|
31
|
+
{ :if => [], :unless => [] }
|
32
|
+
end
|
33
|
+
|
34
|
+
def enforced?
|
35
|
+
ObjectBouncer.enforced?
|
36
|
+
end
|
37
|
+
|
38
|
+
def current_user=(user)
|
39
|
+
@current_user = user
|
40
|
+
end
|
41
|
+
|
42
|
+
def current_user
|
43
|
+
@current_user
|
44
|
+
end
|
45
|
+
|
46
|
+
def as(accessee)
|
47
|
+
new_klass = self.clone
|
48
|
+
new_klass.table_name = self.table_name if respond_to?(:table_name)
|
49
|
+
if respond_to?(:connection_handler)
|
50
|
+
new_klass.establish_connection self.connection_handler.connection_pools[name].spec.config
|
51
|
+
end
|
52
|
+
new_klass.instance_eval do
|
53
|
+
include ObjectBouncer::Doorman
|
54
|
+
end
|
55
|
+
new_klass.policies = self.policies
|
56
|
+
new_klass.current_user = accessee
|
57
|
+
new_klass.apply_policies
|
58
|
+
new_klass
|
59
|
+
end
|
60
|
+
|
61
|
+
def door_policy(&block)
|
62
|
+
@policies = {}
|
63
|
+
yield
|
64
|
+
apply_policies
|
65
|
+
end
|
66
|
+
|
67
|
+
def deny(method, options = {})
|
68
|
+
policies[method] ||= blank_policy_template
|
69
|
+
if options.has_key?(:if)
|
70
|
+
policies[method][:if] << options[:if]
|
71
|
+
elsif options.has_key?(:unless)
|
72
|
+
policies[method][:unless] << options[:unless]
|
73
|
+
else
|
74
|
+
policies[method][:if].unshift(Proc.new{ true == true })
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def apply_policies
|
79
|
+
policies.keys.each do |method|
|
80
|
+
protect_method!(method)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def protect_method!(method)
|
85
|
+
renamed_method = "#{method}_without_objectbouncer".to_sym
|
86
|
+
if method_defined?(method)
|
87
|
+
return if method_defined?(renamed_method)
|
88
|
+
alias_method renamed_method, method
|
89
|
+
define_method method do |*args, &block|
|
90
|
+
if call_denied?(method, *args)
|
91
|
+
raise ObjectBouncer::PermissionDenied.new
|
92
|
+
else
|
93
|
+
send(renamed_method, *args, &block)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/objectbouncer/errors.rb
CHANGED
@@ -0,0 +1,78 @@
|
|
1
|
+
$:.unshift File.expand_path("..", File.dirname(__FILE__))
|
2
|
+
require "test_helper"
|
3
|
+
require "active_record"
|
4
|
+
|
5
|
+
class Book < ActiveRecord::Base
|
6
|
+
establish_connection :adapter => "sqlite3",
|
7
|
+
:database => "objectbouncer_test.db"
|
8
|
+
include ObjectBouncer::Doorman
|
9
|
+
door_policy do
|
10
|
+
deny :save, :unless => Proc.new{|person| person.is_a?(Author) }
|
11
|
+
deny :save!, :unless => Proc.new{|person| person.is_a?(Author) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Author
|
16
|
+
end
|
17
|
+
|
18
|
+
class Reader
|
19
|
+
end
|
20
|
+
|
21
|
+
class ActiveRecordTest < Test::Unit::TestCase
|
22
|
+
context "protecting the library" do
|
23
|
+
|
24
|
+
setup do
|
25
|
+
ObjectBouncer.unenforce!
|
26
|
+
@author = Author.new
|
27
|
+
@reader = Reader.new
|
28
|
+
end
|
29
|
+
|
30
|
+
should "not let a reader create a book" do
|
31
|
+
assert_raise ObjectBouncer::PermissionDenied do
|
32
|
+
Book.as(@reader).create!(:name => "Real Housewives of Orange County - The Musical")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
should "let an author create a book" do
|
37
|
+
assert_nothing_raised do
|
38
|
+
Book.as(@author).create!(:name => "Guns, Germs & Steel")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
should "default to existing ActiveRecord behaviour" do
|
43
|
+
assert_nothing_raised do
|
44
|
+
Book.create!(:name => "Freakanomics")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
should "provide means of enforcing a user" do
|
49
|
+
ObjectBouncer.enforce!
|
50
|
+
assert_raise ObjectBouncer::ArgumentError do
|
51
|
+
Book.create!(:name => "Javascript: The Good Parts")
|
52
|
+
end
|
53
|
+
assert_nothing_raised do
|
54
|
+
Book.as(@author).create!(:name => "Javascript: The Good Parts")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "with an existing book" do
|
59
|
+
|
60
|
+
setup do
|
61
|
+
@book = Book.create!(:name => "On Food & Cooking",
|
62
|
+
:author => "Harold McGee",
|
63
|
+
:price => 4900)
|
64
|
+
@book_as_author = Book.as(@author).find(@book.id)
|
65
|
+
@book_as_reader = Book.as(@author).find(@book.id)
|
66
|
+
end
|
67
|
+
|
68
|
+
should "prevent reading of individual attributes" do
|
69
|
+
end
|
70
|
+
|
71
|
+
should "prevent writing of individual attributes" do
|
72
|
+
end
|
73
|
+
|
74
|
+
should "pass user through associations" do
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -1,25 +1,15 @@
|
|
1
1
|
$:.unshift File.expand_path("..", File.dirname(__FILE__))
|
2
2
|
require "test_helper"
|
3
3
|
|
4
|
-
class
|
4
|
+
class President
|
5
5
|
include ObjectBouncer::Doorman
|
6
6
|
door_policy do
|
7
|
-
deny :shake_hands, :
|
8
|
-
|
7
|
+
deny :shake_hands, :unless => Proc.new{|person, president| person.is_a?(MichelleObama) }
|
8
|
+
deny :shake_hands, :if => Proc.new{|person, president| person != president }
|
9
9
|
deny :high_five, :unless => Proc.new{|person, president| person.who? == "it's me, Joe!"}
|
10
10
|
deny :give, :unless => Proc.new{|person, president, *args| args.first == :campaign_donation }
|
11
11
|
end
|
12
|
-
end
|
13
12
|
|
14
|
-
class CoastGuard
|
15
|
-
include ObjectBouncer::Doorman
|
16
|
-
door_policy do
|
17
|
-
lockdown # Overly protective are we?
|
18
|
-
allow :watch_tv_appearance
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
class President
|
23
13
|
def shake_hands
|
24
14
|
"shaking hands"
|
25
15
|
end
|
@@ -35,6 +25,7 @@ class President
|
|
35
25
|
def give(gift)
|
36
26
|
"thanks"
|
37
27
|
end
|
28
|
+
|
38
29
|
end
|
39
30
|
|
40
31
|
class MichelleObama
|
@@ -58,70 +49,58 @@ class ObjectBouncerTest < Test::Unit::TestCase
|
|
58
49
|
|
59
50
|
should "not let the public shake hands" do
|
60
51
|
joe_public = JoePublic.new
|
61
|
-
|
52
|
+
@president.current_user = joe_public
|
62
53
|
assert_raise ObjectBouncer::PermissionDenied do
|
63
|
-
|
54
|
+
@president.shake_hands
|
64
55
|
end
|
65
56
|
end
|
66
57
|
|
67
58
|
should "let the first lady get in close" do
|
68
59
|
first_lady = MichelleObama.new
|
69
|
-
|
70
|
-
assert_equal "shaking hands",
|
60
|
+
@president.current_user = first_lady
|
61
|
+
assert_equal "shaking hands", @president.shake_hands
|
71
62
|
end
|
72
63
|
|
73
64
|
should "high five Biden" do
|
74
65
|
vice_pres = JoeBiden.new
|
75
|
-
|
76
|
-
assert_equal "high five!",
|
66
|
+
@president.current_user = vice_pres
|
67
|
+
assert_equal "high five!", @president.high_five
|
77
68
|
end
|
78
69
|
|
79
70
|
should "not let the public high five" do
|
80
71
|
joe_public = JoePublic.new
|
81
|
-
|
72
|
+
@president.current_user = joe_public
|
82
73
|
assert_raise ObjectBouncer::PermissionDenied do
|
83
|
-
|
74
|
+
@president.high_five
|
84
75
|
end
|
85
76
|
end
|
86
77
|
|
87
78
|
should "let the public give a donation" do
|
88
79
|
joe_public = JoePublic.new
|
89
|
-
|
90
|
-
assert_equal "thanks",
|
80
|
+
@president.current_user = joe_public
|
81
|
+
assert_equal "thanks", @president.give(:campaign_donation)
|
91
82
|
end
|
92
83
|
|
93
84
|
should "not let the public give a package" do
|
94
85
|
joe_public = JoePublic.new
|
95
|
-
|
86
|
+
@president.current_user = joe_public
|
96
87
|
assert_raise ObjectBouncer::PermissionDenied do
|
97
|
-
|
88
|
+
@president.give(:suspect_package)
|
98
89
|
end
|
99
90
|
end
|
100
91
|
|
101
|
-
|
102
|
-
|
103
|
-
context "going into complete lockdown" do
|
104
|
-
|
105
|
-
setup do
|
106
|
-
@president = President.new
|
107
|
-
end
|
108
|
-
|
109
|
-
should "deny everything by default" do
|
92
|
+
should "be able to specify user on creation" do
|
110
93
|
joe_public = JoePublic.new
|
111
|
-
|
94
|
+
@president = President.as(joe_public).new
|
112
95
|
assert_raise ObjectBouncer::PermissionDenied do
|
113
|
-
|
96
|
+
@president.shake_hands
|
114
97
|
end
|
115
|
-
assert_raise ObjectBouncer::PermissionDenied do
|
116
|
-
coast_guard.shake_hands
|
117
|
-
end
|
118
|
-
end
|
119
98
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
assert_equal "I'm on your TV!", coast_guard.watch_tv_appearance
|
99
|
+
first_lady = MichelleObama.new
|
100
|
+
@president = President.as(first_lady).new
|
101
|
+
assert_equal "shaking hands", @president.shake_hands
|
124
102
|
end
|
103
|
+
|
125
104
|
end
|
126
105
|
|
127
106
|
end
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: objectbouncer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.0
|
5
|
+
version: 0.1.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Glenn Gillen
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-
|
13
|
+
date: 2011-04-12 00:00:00 +01:00
|
14
14
|
default_executable:
|
15
15
|
dependencies: []
|
16
16
|
|
@@ -26,8 +26,10 @@ files:
|
|
26
26
|
- README.mdown
|
27
27
|
- Rakefile
|
28
28
|
- lib/objectbouncer/base.rb
|
29
|
+
- lib/objectbouncer/class_methods.rb
|
29
30
|
- lib/objectbouncer/errors.rb
|
30
31
|
- lib/objectbouncer.rb
|
32
|
+
- test/objectbouncer/activerecord_test.rb
|
31
33
|
- test/objectbouncer/base_test.rb
|
32
34
|
- test/test_helper.rb
|
33
35
|
has_rdoc: true
|
@@ -60,5 +62,6 @@ signing_key:
|
|
60
62
|
specification_version: 2
|
61
63
|
summary: A simple object proxy to restrict access to methods and attributes
|
62
64
|
test_files:
|
65
|
+
- test/objectbouncer/activerecord_test.rb
|
63
66
|
- test/objectbouncer/base_test.rb
|
64
67
|
- test/test_helper.rb
|