aclatraz 0.0.1 → 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/CHANGELOG.rdoc +21 -0
- data/README.rdoc +221 -57
- data/Rakefile +9 -2
- data/TODO.rdoc +6 -0
- data/VERSION +1 -1
- data/aclatraz.gemspec +88 -0
- data/examples/dinner.rb +71 -0
- data/lib/aclatraz.rb +34 -12
- data/lib/aclatraz/acl.rb +82 -7
- data/lib/aclatraz/guard.rb +139 -57
- data/lib/aclatraz/helpers.rb +14 -6
- data/lib/aclatraz/store.rb +3 -7
- data/lib/aclatraz/store/redis.rb +11 -11
- data/lib/aclatraz/suspect.rb +157 -57
- data/spec/aclatraz/acl_spec.rb +8 -3
- data/spec/aclatraz/guard_spec.rb +178 -121
- data/spec/aclatraz/stores_spec.rb +1 -26
- data/spec/aclatraz/suspect_spec.rb +25 -25
- data/spec/aclatraz_spec.rb +16 -2
- data/spec/alcatraz_bm.rb +54 -0
- data/spec/spec_helper.rb +7 -0
- metadata +12 -5
data/examples/dinner.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# == Dinner example.
|
2
|
+
#
|
3
|
+
# Don't forget to initialize ACLatraz datastore and your ActiveRecord
|
4
|
+
# connection with database.
|
5
|
+
|
6
|
+
class Person < ActiveRecord::Base
|
7
|
+
include Aclatraz::Suspect
|
8
|
+
end
|
9
|
+
|
10
|
+
class Dinner < ActiveRecord::Base
|
11
|
+
end
|
12
|
+
|
13
|
+
class Kitchen
|
14
|
+
include Aclatraz::Guard
|
15
|
+
|
16
|
+
suspects "@person" do
|
17
|
+
deny all
|
18
|
+
action :eat_dinner do
|
19
|
+
allow :hungry
|
20
|
+
end
|
21
|
+
action :get_dinner do
|
22
|
+
allow :servant_at => Dinner
|
23
|
+
allow :creator_of => "@dinner"
|
24
|
+
end
|
25
|
+
action :prepare_dinner do
|
26
|
+
allow :chef
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(person)
|
31
|
+
@person = person
|
32
|
+
end
|
33
|
+
|
34
|
+
def prepare_dinner
|
35
|
+
guard! :prepare_dinner
|
36
|
+
@dinner = Dinner.create
|
37
|
+
@person.is.creator_of!(@dinner)
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_dinner(id)
|
41
|
+
@dinner = Dinner.find(id)
|
42
|
+
guard! :get_dinner
|
43
|
+
end
|
44
|
+
|
45
|
+
def eat_dinner(id)
|
46
|
+
@dinner = Dinner.find(id)
|
47
|
+
guard! :eat_dinner
|
48
|
+
@dinner.destroy
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Usage...
|
53
|
+
|
54
|
+
person = Person.find(10)
|
55
|
+
kitchen = Kitchen.new(person)
|
56
|
+
|
57
|
+
kitchen.prepare_dinner # => Access denied
|
58
|
+
person.is.chef!
|
59
|
+
kitchen.prepare_dinner # => Ok
|
60
|
+
|
61
|
+
kitchen.get_dinner(10) # => Ok, he creates the @dinner
|
62
|
+
person.is_not.creator_of!(Dinner.find(10))
|
63
|
+
kitchen.get_dinner(10) # => Access denied
|
64
|
+
person.is.servant_at!(Dinner)
|
65
|
+
kitchen.get_dinner(10) # => Ok
|
66
|
+
|
67
|
+
kitchen.eat_dinner(10) # => Access denied, he is not hungry!
|
68
|
+
person.is.hungry!
|
69
|
+
kitchen.eat_dinner(10) # => Ok, enjoy your meal :)
|
70
|
+
person.is_not.hungry!
|
71
|
+
|
data/lib/aclatraz.rb
CHANGED
@@ -7,24 +7,46 @@ require 'aclatraz/guard'
|
|
7
7
|
require 'aclatraz/suspect'
|
8
8
|
|
9
9
|
module Aclatraz
|
10
|
+
# Raised when suspect don't have permission to execute action
|
10
11
|
class AccessDenied < Exception; end
|
12
|
+
|
13
|
+
# Raised when suspect specified in guarded class is invalid
|
11
14
|
class InvalidSuspect < ArgumentError; end
|
15
|
+
|
16
|
+
# Raised when invalid permission is set in ACL
|
12
17
|
class InvalidPermission < ArgumentError; end
|
18
|
+
|
19
|
+
# Raised when try to initialize invalid datastore
|
13
20
|
class InvalidStore < ArgumentError; end
|
21
|
+
|
22
|
+
# Raised when datastore is not initialized when managing permission
|
14
23
|
class StoreNotInitialized < Exception; end
|
15
24
|
|
25
|
+
# Raised when try to guard class without any ACL defined
|
26
|
+
class UndefinedAccessControlList < Exception; end
|
27
|
+
|
16
28
|
extend Helpers
|
29
|
+
|
30
|
+
# Initialize Aclatraz system with given datastore.
|
31
|
+
#
|
32
|
+
# Aclatraz.init :redis, "redis://localhost:6379/0"
|
33
|
+
# Aclatraz.init :tokyocabinet, "./permissions.tch"
|
34
|
+
# Aclatraz.init MyCustomDatastore, :option => 1 # ...
|
35
|
+
def self.init(store, *args)
|
36
|
+
store = eval("Aclatraz::Store::#{camelize(store.to_s)}") unless store.is_a?(Class)
|
37
|
+
@store = store.new(*args)
|
38
|
+
rescue NameError
|
39
|
+
raise InvalidStore, "The #{store.inspect} ACL store is not defined!"
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns current datastore object, or raises +StoreNotInitialized+ when
|
43
|
+
# +init+ method wasn't called before.
|
44
|
+
def self.store
|
45
|
+
@store or raise StoreNotInitialized, "ACLatraz is not initialized!"
|
46
|
+
end
|
17
47
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
klass = eval("Aclatraz::Store::#{camelize(klass.to_s)}") unless klass.is_a?(Class)
|
22
|
-
@store = klass.new(*args)
|
23
|
-
rescue NameError
|
24
|
-
raise InvalidStore, "The #{klass.inspect} ACL store is not defined!"
|
25
|
-
end
|
26
|
-
else
|
27
|
-
@store or raise StoreNotInitialized, "ACL store is not initialized!"
|
28
|
-
end
|
48
|
+
# Access control lists fof all classes protected by Aclatraz.
|
49
|
+
def self.acl
|
50
|
+
@acl ||= {}
|
29
51
|
end
|
30
|
-
end
|
52
|
+
end # Aclatraz
|
data/lib/aclatraz/acl.rb
CHANGED
@@ -1,46 +1,113 @@
|
|
1
1
|
module Aclatraz
|
2
2
|
class ACL
|
3
3
|
class Action
|
4
|
+
# All permissions defined in this action.
|
4
5
|
attr_reader :permissions
|
5
6
|
|
6
|
-
def initialize(parent, &block)
|
7
|
+
def initialize(parent, &block) # :nodoc:
|
7
8
|
@parent = parent
|
8
9
|
@permissions = Dictionary.new
|
9
|
-
instance_eval(&block)
|
10
|
+
instance_eval(&block) if block_given?
|
10
11
|
end
|
11
12
|
|
13
|
+
# Add permission for objects which have given role.
|
14
|
+
#
|
15
|
+
# ==== Examples
|
16
|
+
#
|
17
|
+
# allow :admin
|
18
|
+
# allow :owner_of => "object"
|
19
|
+
# allow :owner_of => :object
|
20
|
+
# allow :owner_of => object
|
21
|
+
# allow :manager_of => ClassName
|
22
|
+
# allow all
|
12
23
|
def allow(permission)
|
13
24
|
@permissions[permission] = true
|
14
25
|
end
|
15
26
|
|
27
|
+
# Add permission for objects which don't have given role.
|
28
|
+
#
|
29
|
+
# ==== Examples
|
30
|
+
#
|
31
|
+
# deny :admin
|
32
|
+
# deny :owner_of => "object"
|
33
|
+
# deny :owner_of => :object
|
34
|
+
# deny :owner_of => object
|
35
|
+
# deny :manager_of => ClassName
|
36
|
+
# deny all
|
16
37
|
def deny(permission)
|
17
38
|
@permissions[permission] = false
|
18
39
|
end
|
19
40
|
|
41
|
+
# Syntactic sugar for aliasing all permissions.
|
42
|
+
#
|
43
|
+
# ==== Examples
|
44
|
+
#
|
45
|
+
# allow all
|
46
|
+
# deny all
|
20
47
|
def all
|
21
48
|
true
|
22
49
|
end
|
23
50
|
|
51
|
+
# See <tt>Aclatraz::ACL#on</tt>.
|
24
52
|
def on(*args, &block)
|
25
53
|
@parent.on(*args, &block)
|
26
54
|
end
|
27
|
-
|
55
|
+
|
56
|
+
def clone(parent) # :nodoc:
|
57
|
+
cloned = self.class.new(parent)
|
58
|
+
cloned.instance_variable_set("@permissions", Hash.new(permissions))
|
59
|
+
cloned
|
60
|
+
end
|
61
|
+
end # Action
|
28
62
|
|
63
|
+
# All actions defined in current ACL.
|
29
64
|
attr_reader :actions
|
30
65
|
|
31
|
-
|
66
|
+
# Current suspected object.
|
67
|
+
attr_accessor :suspect
|
68
|
+
|
69
|
+
def initialize(suspect, &block) # :nodoc:
|
32
70
|
@actions = {}
|
71
|
+
@suspect = suspect
|
72
|
+
evaluate(&block) if block_given?
|
73
|
+
end
|
74
|
+
|
75
|
+
# Evaluates given block in default action.
|
76
|
+
#
|
77
|
+
# ==== Example
|
78
|
+
#
|
79
|
+
# evaluate do
|
80
|
+
# allow :foo
|
81
|
+
# deny :bar
|
82
|
+
# ...
|
83
|
+
# end
|
84
|
+
def evaluate(&block)
|
33
85
|
on(:_, &block)
|
34
86
|
end
|
35
87
|
|
88
|
+
# List of permissions defined in default action.
|
36
89
|
def permissions
|
37
90
|
@actions[:_] ? @actions[:_].permissions : Dictionary.new
|
38
91
|
end
|
39
92
|
|
93
|
+
# Syntactic sugar for actions <tt>actions[action]</tt>.
|
40
94
|
def [](action)
|
41
|
-
|
95
|
+
actions[action]
|
42
96
|
end
|
43
97
|
|
98
|
+
# Defines given action with permissions described in evaluated block.
|
99
|
+
#
|
100
|
+
# ==== Examples
|
101
|
+
#
|
102
|
+
# suspects do
|
103
|
+
# on :create do
|
104
|
+
# deny all
|
105
|
+
# allow :admin
|
106
|
+
# end
|
107
|
+
# on :delete do
|
108
|
+
# allow :owner_of => "object"
|
109
|
+
# end
|
110
|
+
# end
|
44
111
|
def on(action, &block)
|
45
112
|
raise ArgumentError, "No block given" unless block_given?
|
46
113
|
if @actions.key?(action)
|
@@ -49,5 +116,13 @@ module Aclatraz
|
|
49
116
|
@actions[action] = Action.new(self, &block)
|
50
117
|
end
|
51
118
|
end
|
52
|
-
|
53
|
-
|
119
|
+
|
120
|
+
def clone(&block) # :nodoc:
|
121
|
+
actions = Hash[*self.actions.map {|k,v| [k, v.clone(self)] }.flatten]
|
122
|
+
cloned = self.class.new(suspect)
|
123
|
+
cloned.instance_variable_set("@actions", actions)
|
124
|
+
cloned.evaluate(&block)
|
125
|
+
cloned
|
126
|
+
end
|
127
|
+
end # ACL
|
128
|
+
end # Aclatraz
|
data/lib/aclatraz/guard.rb
CHANGED
@@ -1,87 +1,169 @@
|
|
1
1
|
module Aclatraz
|
2
2
|
module Guard
|
3
|
-
def self.included(base)
|
3
|
+
def self.included(base) # :nodoc:
|
4
4
|
base.send :extend, ClassMethods
|
5
5
|
base.send :include, InstanceMethods
|
6
6
|
end
|
7
7
|
|
8
8
|
module ClassMethods
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
def acl_guard? # :nodoc:
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
# Define access controll list for current class.
|
14
|
+
#
|
15
|
+
# ==== Examples
|
16
|
+
#
|
17
|
+
# suspects :foo do # foo method result will be treated as suspect
|
18
|
+
# deny all
|
19
|
+
# allow :admin
|
20
|
+
#
|
21
|
+
# on :create do
|
22
|
+
# allow :manager
|
23
|
+
# allow :manager_of => ClassName
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# on :edit do
|
27
|
+
# # only @object_name instance variable owner is allowed to edit it.
|
28
|
+
# allow :owner_of => "object_name"
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # When called second time don't have to specify suspected object.
|
33
|
+
# suspects do
|
34
|
+
# allow :manager
|
35
|
+
# end
|
36
|
+
def suspects(suspect=nil, &block)
|
37
|
+
if acl = Aclatraz.acl[name]
|
38
|
+
acl.suspect = suspect if suspect
|
39
|
+
acl.evaluate(&block)
|
40
|
+
elsif superclass.respond_to?(:acl_guard?) && acl = Aclatraz.acl[superclass.name]
|
41
|
+
Aclatraz.acl[name] = acl.clone(&block)
|
42
|
+
else
|
43
|
+
Aclatraz.acl[name] = Aclatraz::ACL.new(suspect, &block)
|
44
|
+
end
|
15
45
|
end
|
16
46
|
alias_method :access_control, :suspects
|
17
|
-
end
|
47
|
+
end # ClassMethods
|
18
48
|
|
19
49
|
module InstanceMethods
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
50
|
+
# Returns suspected object.
|
51
|
+
#
|
52
|
+
# * when suspect name is a String then will return instance variable
|
53
|
+
# * when suspect name is a Symbol then will be returned value of instance method
|
54
|
+
# * otherwise suspect name will be treated as suspect object.
|
55
|
+
#
|
56
|
+
# ==== Examples
|
57
|
+
#
|
58
|
+
# class Bar
|
59
|
+
# suspects(:foo) { ... }
|
60
|
+
# def foo; @foo = Foo.new; end
|
61
|
+
# end
|
62
|
+
# Bar.new.suspect.class # => Foo
|
63
|
+
#
|
64
|
+
# class Bla
|
65
|
+
# suspects("foo") { ... }
|
66
|
+
# def init; @foo = Foo.new; end
|
67
|
+
# end
|
68
|
+
# Bla.new.suspect.class # => Foo
|
69
|
+
#
|
70
|
+
# class Spam
|
71
|
+
# foo = Foo.new
|
72
|
+
# suspects(foo) { ... }
|
73
|
+
# end
|
74
|
+
# Spam.new.suspect.class # => Foo
|
75
|
+
#
|
76
|
+
# You can also override this method in your protected class, and skip
|
77
|
+
# passing arguments to +#suspects+ method, eg.
|
78
|
+
#
|
79
|
+
# class Eggs
|
80
|
+
# suspects { ... }
|
81
|
+
# def suspect; @foo = Foo.new; end
|
82
|
+
# end
|
83
|
+
def suspect
|
84
|
+
@suspect ||= if acl = Aclatraz.acl[self.class.name]
|
85
|
+
case acl.suspect
|
86
|
+
when Symbol
|
87
|
+
send(acl.suspect)
|
88
|
+
when String
|
89
|
+
instance_variable_get("@#{acl.suspect}")
|
90
|
+
else
|
91
|
+
acl.suspect
|
92
|
+
end
|
28
93
|
end
|
29
94
|
end
|
30
95
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
96
|
+
# Check if current suspect have permissions to execute following code.
|
97
|
+
# If suspect hasn't required permissions, or access for any of his roles
|
98
|
+
# is denied then raises +Aclatraz::AccessDenied+ error. You can also specify
|
99
|
+
# additional permission inside given block:
|
100
|
+
#
|
101
|
+
# guard! do
|
102
|
+
# deny :foo
|
103
|
+
# allow :bar
|
104
|
+
# end
|
105
|
+
def guard!(*actions, &block)
|
106
|
+
acl = Aclatraz.acl[self.class.name] or raise UndefinedAccessControlList, "No ACL for #{self.class.name} class"
|
107
|
+
suspect.respond_to?(:acl_suspect?) or raise Aclatraz::InvalidSuspect, "Invalid ACL suspect: #{suspect.inspect}"
|
108
|
+
authorized = false
|
109
|
+
permissions = Dictionary.new
|
110
|
+
actions.unshift(:_)
|
111
|
+
|
112
|
+
if block_given?
|
113
|
+
aname = "#{__FILE__}:#{__LINE__}"
|
114
|
+
acl.on(aname, &block)
|
115
|
+
actions.push(aname)
|
116
|
+
end
|
117
|
+
|
118
|
+
actions.each do |action|
|
119
|
+
acl.actions[action].permissions.each_pair do |key, value|
|
120
|
+
permissions.delete(key)
|
121
|
+
permissions.push(key, value)
|
42
122
|
end
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
end
|
123
|
+
end
|
124
|
+
|
125
|
+
permissions.each do |permission, allow|
|
126
|
+
if permission == true
|
127
|
+
authorized = allow ? true : false
|
128
|
+
next
|
129
|
+
end
|
130
|
+
if allow
|
131
|
+
authorized ||= assert_permission(permission)
|
132
|
+
else
|
133
|
+
authorized = false if assert_permission(permission)
|
55
134
|
end
|
56
|
-
|
57
|
-
raise Aclatraz::AccessDenied unless authorized
|
58
|
-
true
|
59
|
-
else
|
60
|
-
raise Aclatraz::InvalidSuspect
|
61
135
|
end
|
136
|
+
|
137
|
+
authorized or raise Aclatraz::AccessDenied, "Access Denied"
|
138
|
+
return true
|
62
139
|
end
|
63
140
|
alias_method :authorize!, :guard!
|
64
141
|
|
65
|
-
|
142
|
+
# Check if current suspect has given permissions.
|
143
|
+
#
|
144
|
+
# ==== Examples
|
145
|
+
#
|
146
|
+
# assert_permission(:admin)
|
147
|
+
# assert_permission(:manager_of => ClassName)
|
148
|
+
# assert_permission(:owner_of => "object")
|
149
|
+
def assert_permission(permission)
|
66
150
|
case permission
|
67
151
|
when String, Symbol, true
|
68
|
-
|
152
|
+
suspect.roles.has?(permission)
|
69
153
|
when Hash
|
70
154
|
permission.each do |role, object|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
object = instance_variable_get(object)
|
75
|
-
when Symbol
|
155
|
+
if object.is_a?(String)
|
156
|
+
object = instance_variable_get(object[0] ? "@#{object}" : object)
|
157
|
+
elsif object.is_a?(Symbol)
|
76
158
|
object = send(object)
|
77
159
|
end
|
78
|
-
return true if
|
160
|
+
return true if suspect.roles.has?(role, object)
|
79
161
|
end
|
80
|
-
false
|
162
|
+
return false
|
81
163
|
else
|
82
|
-
raise Aclatraz::InvalidPermission
|
164
|
+
raise Aclatraz::InvalidPermission, "Invalid ACL permission: #{permission.inspect}"
|
83
165
|
end
|
84
166
|
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
167
|
+
end # InstanceMethods
|
168
|
+
end # Guard
|
169
|
+
end # Aclatraz
|