objectbouncer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,4 @@
1
+ base_dir = File.join(File.dirname(__FILE__), "objectbouncer")
2
+ ["base", "errors"].each do |lib|
3
+ require File.join(base_dir, lib)
4
+ 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,5 @@
1
+ module ObjectBouncer
2
+ class Error < StandardError; end
3
+ class PermissionDenied < Error; end
4
+ end
5
+
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'test/unit'
2
+ require 'shoulda'
3
+
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+ require "objectbouncer"
7
+
8
+ class Test::Unit::TestCase
9
+ end
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