objectbouncer 0.0.6 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 is protected by the SecretService:
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 define a SecretService class like so:
48
+ To protect the President we'd add the following to our class definition:
49
49
 
50
- class SecretService
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 run all contact with
63
- the President through SecretService first:
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
- SecretService.new(@gaddafi, @president).shake_hands # Raises PermissionDenied
71
- SecretService.new(@joe_biden, @president).shake_hands # Allowed
72
- SecretService.new(@tommy_chong, @president).shake_hands # Allowed
73
- SecretService.new(@joe_biden, @president).high_five # Allowed
74
- SecretService.new(@tommy_chong, @president).high_five # Raises PermissionDenied
75
- SecretService.new(@gaddafi, @president).give(:donation) # Allowed
76
- SecretService.new(@gaddafi, @president).give(:suspect_package) # Raises PermissionDenied
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 Model itself.
101
+ external to the methods themselves.
85
102
 
86
103
  ## Compatibility
87
104
 
@@ -1,4 +1,4 @@
1
1
  base_dir = File.join(File.dirname(__FILE__), "objectbouncer")
2
- ["base", "errors"].each do |lib|
2
+ ["class_methods", "base", "errors"].each do |lib|
3
3
  require File.join(base_dir, lib)
4
4
  end
@@ -1,110 +1,68 @@
1
1
  module ObjectBouncer
2
- module Doorman
3
-
4
- def self.included(klass)
5
- klass.extend ClassMethods
6
- end
2
+ def self.enforce!
3
+ @enforce = true
4
+ end
7
5
 
8
- module ClassMethods
9
- def door_policy(&block)
10
- @lockdown = false
11
- @policies = {}
12
- yield
13
- end
6
+ def self.unenforce!
7
+ @enforce = false
8
+ end
14
9
 
15
- def lockdown
16
- @lockdown = true
17
- end
10
+ def self.enforced?
11
+ @enforce
12
+ end
18
13
 
19
- def lockdown?
20
- @lockdown
21
- end
14
+ module Doorman
22
15
 
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
16
+ def with(user)
17
+ self
18
+ end
33
19
 
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
20
+ def current_user=(user)
21
+ @current_user = user
22
+ end
44
23
 
45
- def policies
46
- @policies
47
- end
24
+ def current_user
25
+ @current_user ||= self.class.current_user
26
+ end
48
27
 
49
- def blank_policy_template
50
- { :allow => { :if => [], :unless => [] },
51
- :deny => { :if => [], :unless => [] }
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 initialize(accessee, object)
58
- @accessee = accessee
59
- @object = object
60
- super()
61
- self
34
+ def policies
35
+ @policies || self.class.instance_policies
62
36
  end
63
37
 
64
- def method_missing(meth, *args, &block)
65
- if respond_to?(meth)
66
- raise "TODO!!!" if self.class.policies.nil? or self.class.policies.empty?
67
- if call_allowed?(meth, *args)
68
- @object.send(meth, *args, &block)
69
- elsif call_denied?(meth, *args)
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
- return true if self.class.lockdown?
94
- if policies = self.class.policies[meth]
95
- return true if policies[:allow][:unless].detect{|policy| policy.call(@accessee, @object, *args) rescue nil}
96
- return true if policies[:deny][:if].detect{|policy| policy.call(@accessee, @object, *args) rescue nil}
97
- return true if !policies[:deny][:unless].empty? && !call_allowed?(meth)
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 accessee=(val)
102
- @accessee = val
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
@@ -1,5 +1,6 @@
1
1
  module ObjectBouncer
2
2
  class Error < StandardError; end
3
3
  class PermissionDenied < Error; end
4
+ class ArgumentError < Error; end
4
5
  end
5
6
 
@@ -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 SecretService
4
+ class President
5
5
  include ObjectBouncer::Doorman
6
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}
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
- secret_service = SecretService.new(joe_public, @president)
52
+ @president.current_user = joe_public
62
53
  assert_raise ObjectBouncer::PermissionDenied do
63
- secret_service.shake_hands
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
- secret_service = SecretService.new(first_lady, @president)
70
- assert_equal "shaking hands", secret_service.shake_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
- secret_service = SecretService.new(vice_pres, @president)
76
- assert_equal "high five!", secret_service.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
- secret_service = SecretService.new(joe_public, @president)
72
+ @president.current_user = joe_public
82
73
  assert_raise ObjectBouncer::PermissionDenied do
83
- secret_service.high_five
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
- secret_service = SecretService.new(joe_public, @president)
90
- assert_equal "thanks", secret_service.give(:campaign_donation)
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
- secret_service = SecretService.new(joe_public, @president)
86
+ @president.current_user = joe_public
96
87
  assert_raise ObjectBouncer::PermissionDenied do
97
- secret_service.give(:suspect_package)
88
+ @president.give(:suspect_package)
98
89
  end
99
90
  end
100
91
 
101
- end
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
- coast_guard = CoastGuard.new(joe_public, @president)
94
+ @president = President.as(joe_public).new
112
95
  assert_raise ObjectBouncer::PermissionDenied do
113
- coast_guard.high_five
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
- should "allow if explictly said it's ok" do
121
- joe_public = JoePublic.new
122
- coast_guard = CoastGuard.new(joe_public, @president)
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.6
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-03-29 00:00:00 +01:00
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