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