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.
@@ -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
- def self.store(klass=nil, *args)
19
- if klass
20
- begin
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
- end
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
- def initialize(&block)
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
- @actions[action]
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
- end
53
- end
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
@@ -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
- attr_reader :acl_suspect
10
- attr_reader :acl_permissions
11
-
12
- def suspects(name, &block)
13
- @acl_suspect = name
14
- @acl_permissions = ACL.new(&block)
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
- def current_suspect
21
- case self.class.acl_suspect
22
- when Symbol
23
- @current_suspect ||= send(self.class.acl_suspect)
24
- when String
25
- @current_suspect ||= instance_variable_get("@#{self.class.acl_suspect}")
26
- else
27
- @current_suspect ||= self.class.acl_suspect
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
- def guard!(*actions)
32
- if current_suspect.respond_to?(:acl_suspect?)
33
- actions.unshift(:_)
34
- authorized = false
35
- permissions = Dictionary.new
36
-
37
- actions.each do |action|
38
- self.class.acl_permissions.actions[action].permissions.each_pair do |key, value|
39
- permissions.delete(key)
40
- permissions.push(key, value)
41
- end
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
- permissions.each do |permission, allow|
45
- has_permission = check_permission(permission)
46
- if permission == true
47
- authorized = allow ? true : false
48
- next
49
- end
50
- if allow
51
- authorized ||= has_permission
52
- else
53
- authorized = false if has_permission
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
- def check_permission(permission)
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
- current_suspect.has_role?(permission)
152
+ suspect.roles.has?(permission)
69
153
  when Hash
70
154
  permission.each do |role, object|
71
- case object
72
- when String
73
- object = "@#{object}" unless object[0] == "@"
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 current_suspect.has_role?(role, object)
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