objectbouncer 0.0.6 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|