zuul 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. data/lib/generators/zuul/orm_helpers.rb +21 -0
  2. data/lib/generators/zuul/permission_generator.rb +57 -0
  3. data/lib/generators/zuul/permission_role_generator.rb +40 -0
  4. data/lib/generators/zuul/permission_subject_generator.rb +40 -0
  5. data/lib/generators/zuul/role_generator.rb +58 -0
  6. data/lib/generators/zuul/role_subject_generator.rb +40 -0
  7. data/lib/generators/zuul/subject_generator.rb +39 -0
  8. data/lib/generators/zuul/templates/permission.rb +18 -0
  9. data/lib/generators/zuul/templates/permission_existing.rb +25 -0
  10. data/lib/generators/zuul/templates/permission_role.rb +17 -0
  11. data/lib/generators/zuul/templates/permission_role_existing.rb +24 -0
  12. data/lib/generators/zuul/templates/permission_subject.rb +17 -0
  13. data/lib/generators/zuul/templates/permission_subject_existing.rb +24 -0
  14. data/lib/generators/zuul/templates/role.rb +20 -0
  15. data/lib/generators/zuul/templates/role_existing.rb +27 -0
  16. data/lib/generators/zuul/templates/role_subject.rb +17 -0
  17. data/lib/generators/zuul/templates/role_subject_existing.rb +24 -0
  18. data/lib/tasks/zuul.rake +56 -0
  19. data/lib/zuul.rb +14 -5
  20. data/lib/zuul/action_controller.rb +108 -0
  21. data/lib/zuul/action_controller/dsl.rb +384 -0
  22. data/lib/zuul/action_controller/evaluators.rb +60 -0
  23. data/lib/zuul/active_record.rb +338 -0
  24. data/lib/zuul/active_record/context.rb +38 -0
  25. data/lib/zuul/active_record/permission.rb +31 -0
  26. data/lib/zuul/active_record/permission_role.rb +29 -0
  27. data/lib/zuul/active_record/permission_subject.rb +29 -0
  28. data/lib/zuul/active_record/role.rb +117 -0
  29. data/lib/zuul/active_record/role_subject.rb +29 -0
  30. data/lib/zuul/active_record/scope.rb +71 -0
  31. data/lib/zuul/active_record/subject.rb +239 -0
  32. data/lib/zuul/configuration.rb +149 -0
  33. data/lib/zuul/context.rb +53 -0
  34. data/lib/zuul/exceptions.rb +3 -0
  35. data/lib/zuul/exceptions/access_denied.rb +9 -0
  36. data/lib/zuul/exceptions/invalid_context.rb +9 -0
  37. data/lib/zuul/exceptions/undefined_scope.rb +9 -0
  38. data/lib/zuul/railtie.rb +5 -0
  39. data/lib/zuul/version.rb +3 -0
  40. data/lib/zuul_viz.rb +195 -0
  41. data/spec/db/schema.rb +172 -0
  42. data/spec/spec_helper.rb +25 -0
  43. data/spec/support/capture_stdout.rb +12 -0
  44. data/spec/support/models.rb +167 -0
  45. data/spec/zuul/active_record/context_spec.rb +55 -0
  46. data/spec/zuul/active_record/permission_role_spec.rb +84 -0
  47. data/spec/zuul/active_record/permission_spec.rb +174 -0
  48. data/spec/zuul/active_record/permission_subject_spec.rb +84 -0
  49. data/spec/zuul/active_record/role_spec.rb +694 -0
  50. data/spec/zuul/active_record/role_subject_spec.rb +84 -0
  51. data/spec/zuul/active_record/scope_spec.rb +75 -0
  52. data/spec/zuul/active_record/subject_spec.rb +1186 -0
  53. data/spec/zuul/active_record_spec.rb +624 -0
  54. data/spec/zuul/configuration_spec.rb +254 -0
  55. data/spec/zuul/context_spec.rb +128 -0
  56. data/spec/zuul_spec.rb +15 -0
  57. metadata +181 -70
  58. data/.document +0 -5
  59. data/.gitignore +0 -23
  60. data/LICENSE +0 -20
  61. data/README.rdoc +0 -65
  62. data/Rakefile +0 -54
  63. data/VERSION +0 -1
  64. data/lib/zuul/restrict_access.rb +0 -104
  65. data/lib/zuul/valid_roles.rb +0 -37
  66. data/spec/rails_root/app/controllers/application_controller.rb +0 -2
  67. data/spec/rails_root/app/models/user.rb +0 -8
  68. data/spec/rails_root/config/boot.rb +0 -110
  69. data/spec/rails_root/config/database.yml +0 -5
  70. data/spec/rails_root/config/environment.rb +0 -7
  71. data/spec/rails_root/config/environments/test.rb +0 -7
  72. data/spec/rails_root/config/initializers/session_store.rb +0 -15
  73. data/spec/rails_root/config/routes.rb +0 -4
  74. data/spec/rails_root/db/test.sqlite3 +0 -0
  75. data/spec/rails_root/log/test.log +0 -5388
  76. data/spec/rails_root/spec/controllers/require_user_spec.rb +0 -138
  77. data/spec/rails_root/spec/controllers/restrict_access_spec.rb +0 -64
  78. data/spec/rails_root/spec/models/user_spec.rb +0 -37
  79. data/spec/rails_root/spec/spec_helper.rb +0 -34
  80. data/zuul.gemspec +0 -78
@@ -0,0 +1,60 @@
1
+ module Zuul
2
+ module ActionController
3
+ module Evaluators
4
+ class ForTarget
5
+ def for_target(&block)
6
+ return self if @dsl.nil?
7
+ if match?
8
+ @controller.instance_eval do
9
+ yield
10
+ end if block_given?
11
+ end
12
+ self
13
+ end
14
+
15
+ def else(&block)
16
+ return self if @dsl.nil?
17
+ if !match?
18
+ @controller.instance_eval do
19
+ yield
20
+ end if block_given?
21
+ end
22
+ self
23
+ end
24
+
25
+ def else_for(target, context=nil, force_context=nil, &block)
26
+ return self.class.new(@controller, target, context, force_context, &block)
27
+ end
28
+
29
+ protected
30
+
31
+ def initialize(controller, target, context=nil, force_context=nil, &block)
32
+ @controller = controller
33
+ @dsl = @controller.acl_dsl
34
+ @target = target
35
+ @context = context
36
+ @force_context = force_context
37
+ for_target &block
38
+ end
39
+ end
40
+
41
+ class ForRole < ForTarget
42
+ def match?
43
+ (@dsl.subject.nil? && @target == @dsl.logged_out) || (!@dsl.subject.nil? && (@target == @dsl.logged_in || @dsl.subject.has_role?(@target, @context, @force_context)))
44
+ end
45
+ end
46
+
47
+ class ForRoleOrHigher < ForTarget
48
+ def match?
49
+ (@dsl.subject.nil? && @target == @dsl.logged_out) || (!@dsl.subject.nil? && (@target == @dsl.logged_in || @dsl.subject.has_role_or_higher?(@target, @context, @force_context)))
50
+ end
51
+ end
52
+
53
+ class ForPermission < ForTarget
54
+ def match?
55
+ !@dsl.subject.nil? && @dsl.subject.has_permission?(@target, @context, @force_context)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,338 @@
1
+ require 'zuul/active_record/scope'
2
+ require 'zuul/active_record/role'
3
+ require 'zuul/active_record/permission'
4
+ require 'zuul/active_record/context'
5
+ require 'zuul/active_record/subject'
6
+ require 'zuul/active_record/role_subject'
7
+ require 'zuul/active_record/permission_role'
8
+ require 'zuul/active_record/permission_subject'
9
+
10
+ module Zuul
11
+ module ActiveRecord
12
+ def self.included(base)
13
+ base.send :extend, ClassMethods
14
+ base.send :include, InstanceMethods
15
+ end
16
+
17
+ module ClassMethods
18
+ def self.extended(base)
19
+ end
20
+
21
+ # Includes auth methods into the model and configures auth options and scopes
22
+ #
23
+ # The args parameter is an optional hash of configuration options.
24
+ def acts_as_authorization_model(args={}, &block)
25
+ include AuthorizationMethods
26
+ auth_config = Zuul.configuration.clone.configure(args, &block)
27
+ @auth_scopes ||= {}
28
+ raise "Scope already in use: #{auth_config.scope}" if @auth_scopes.has_key?(auth_config.scope)
29
+ @auth_scopes[auth_config.scope] = Scope.new(auth_config)
30
+ @auth_scopes[:default] ||= @auth_scopes[auth_config.scope]
31
+ @auth_scopes[auth_config.scope]
32
+ end
33
+
34
+ # Configure the model to act as a zuul authorization role
35
+ #
36
+ # The args parameter is an optional hash of configuration options.
37
+ def acts_as_authorization_role(args={}, &block)
38
+ scope = acts_as_authorization_model(args.merge({:role_class => self.name}), &block)
39
+ prepare_join_classes scope.name
40
+ include Role
41
+ end
42
+
43
+ # Configure the model to act as a zuul authorization permission
44
+ #
45
+ # The args parameter is an optional hash of configuration options.
46
+ def acts_as_authorization_permission(args={}, &block)
47
+ scope = acts_as_authorization_model(args.merge({:permission_class => self.name}), &block)
48
+ prepare_join_classes scope.name
49
+ include Permission
50
+ end
51
+
52
+ # Configure the model to act as a zuul authorization subject
53
+ #
54
+ # The args parameter is an optional hash of configuration options.
55
+ def acts_as_authorization_subject(args={}, &block)
56
+ scope = acts_as_authorization_model(args.merge({:subject_class => self.name}), &block)
57
+ prepare_join_classes scope.name
58
+ include Subject
59
+ end
60
+
61
+ # Configure the model to act as a zuul authorization context (or resource)
62
+ #
63
+ # The args parameter is an optional hash of configuration options.
64
+ def acts_as_authorization_context(args={}, &block)
65
+ acts_as_authorization_model(args, &block)
66
+ include Context
67
+ end
68
+
69
+ # Sets up the join models for a newly defined scope.
70
+ #
71
+ # This is similar the the acts_as_authorization_* methods, but it handles all the joining models for a scope.
72
+ def prepare_join_classes(scope)
73
+ scope_config = auth_scope(scope).config
74
+
75
+ unless auth_scope(scope).role_subject_class.ancestors.include?(RoleSubject)
76
+ auth_scope(scope).role_subject_class.instance_eval do
77
+ acts_as_authorization_model(scope_config.to_h)
78
+ include RoleSubject
79
+ end
80
+ end
81
+
82
+ if auth_scope(scope).config.with_permissions
83
+ unless auth_scope(scope).permission_subject_class.ancestors.include?(PermissionSubject)
84
+ auth_scope(scope).permission_subject_class.instance_eval do
85
+ acts_as_authorization_model(scope_config.to_h)
86
+ include PermissionSubject
87
+ end
88
+ end
89
+ unless auth_scope(scope).permission_role_class.ancestors.include?(PermissionRole)
90
+ auth_scope(scope).permission_role_class.instance_eval do
91
+ acts_as_authorization_model(scope_config.to_h)
92
+ include PermissionRole
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ # Checks if the model is setup to act as a zuul authorization role
99
+ def acts_as_authorization_role?
100
+ ancestors.include?(Zuul::ActiveRecord::Role)
101
+ end
102
+
103
+ # Checks if the model is setup to act as a zuul authorization permission
104
+ def acts_as_authorization_permission?
105
+ ancestors.include?(Zuul::ActiveRecord::Permission)
106
+ end
107
+
108
+ # Checks if the model is setup to act as a zuul authorization context/resource
109
+ def acts_as_authorization_context?
110
+ ancestors.include?(Zuul::ActiveRecord::Context)
111
+ end
112
+
113
+ # Checks if the model is setup to act as a zuul authorization subject
114
+ def acts_as_authorization_subject?
115
+ ancestors.include?(Zuul::ActiveRecord::Subject)
116
+ end
117
+ end
118
+
119
+ # Defines acts_as_authorization_*? methods to pass through to the class
120
+ module InstanceMethods
121
+ [:role, :permission, :subject, :context].each do |auth_type|
122
+ method_name = "acts_as_authorization_#{auth_type}?"
123
+ define_method method_name do
124
+ self.class.send method_name
125
+ end
126
+ end
127
+ end
128
+
129
+ module AuthorizationMethods
130
+ def self.included(base)
131
+ base.class.send :attr_reader, :auth_scopes
132
+ base.class.send :attr_reader, :current_auth_scope
133
+ base.send :instance_variable_set, :@current_auth_scope, :default
134
+ base.send :extend, ClassMethods
135
+ base.send :include, InstanceMethods
136
+ end
137
+
138
+ module ClassMethods
139
+ # Return the requested scope, call a method within a scope or execute an optional block within that scope
140
+ #
141
+ # If an optional block is passed, it will be executed within the provided scope. This allows
142
+ # you to call methods on the model or the auth scope without having to specify a scope
143
+ # each time. The exec_args hash can be used to pass arguments through to the block.
144
+ #
145
+ # If a block is not passed, exec_args can be used to provide a method and arguments to be called on the
146
+ # object within the requested scope.
147
+ #
148
+ # The reason this is defined separately at the class and instance level is because it uses
149
+ # instance_exec to execute the block within the scope of the object (either class or instance)
150
+ # and then uses method_missing temporarily to provide the auth scope methods.
151
+ def auth_scope(scope=nil, *exec_args, &block)
152
+ scope ||= current_auth_scope
153
+ raise ::Zuul::Exceptions::UndefinedScope unless auth_scopes.has_key?(scope)
154
+
155
+ if block_given? || (exec_args.length > 0 && exec_args[0].is_a?(Symbol) && respond_to?(exec_args[0]))
156
+ old_scope = current_auth_scope
157
+ self.current_auth_scope = scope
158
+
159
+ instance_eval do
160
+ def method_missing (meth,*args)
161
+ return auth_scopes[current_auth_scope].send(meth, *args) if auth_scopes[current_auth_scope].respond_to?(meth)
162
+ raise NoMethodError, "#{self.name}.#{meth} does not exist."
163
+ end
164
+ end
165
+ exec_result = block_given? ? instance_exec(*exec_args, &block) : send(exec_args.slice!(0), *exec_args)
166
+ instance_eval do
167
+ undef method_missing
168
+ end
169
+
170
+ self.current_auth_scope = old_scope
171
+ return exec_result
172
+ end
173
+
174
+ auth_scopes[scope]
175
+ end
176
+
177
+ # Evaluate a block within the requested scope
178
+ def auth_scope_eval(scope=nil, &block)
179
+ auth_scope(scope).instance_eval &block
180
+ end
181
+
182
+ # Set the current auth scope
183
+ #
184
+ # The current_auth_scope is the scope that is currently active on the model for all auth operations
185
+ def current_auth_scope=(scope)
186
+ @current_auth_scope = scope.to_sym
187
+ end
188
+ end
189
+
190
+ module InstanceMethods
191
+ def self.included(base)
192
+ # TODO figure out how to delegate tasks to self.class
193
+ end
194
+
195
+ def auth_scopes
196
+ self.class.auth_scopes
197
+ end
198
+
199
+ # Return the requested scope, call a method within a scope or execute an optional block within that scope
200
+ #
201
+ # If an optional block is passed, it will be executed within the provided scope. This allows
202
+ # you to call methods on the model or the auth scope without having to specify a scope
203
+ # each time. The exec_args hash can be used to pass arguments through to the block.
204
+ #
205
+ # If a block is not passed, exec_args can be used to provide a method and arguments to be called on the
206
+ # object within the requested scope.
207
+ #
208
+ # The reason this is defined separately at the class and instance level is because it uses
209
+ # instance_exec to execute the block within the scope of the object (either class or instance)
210
+ # and then uses method_missing temporarily to provide the auth scope methods.
211
+ def auth_scope(scope=nil, *exec_args, &block)
212
+ scope ||= current_auth_scope
213
+ raise ::Zuul::Exceptions::UndefinedScope unless auth_scopes.has_key?(scope)
214
+
215
+ if block_given? || (exec_args.length > 0 && exec_args[0].is_a?(Symbol) && respond_to?(exec_args[0]))
216
+ old_scope = current_auth_scope
217
+ self.current_auth_scope = scope
218
+
219
+ instance_eval do
220
+ def method_missing (meth,*args)
221
+ return auth_scopes[current_auth_scope].send(meth, *args) if auth_scopes[current_auth_scope].respond_to?(meth)
222
+ raise NoMethodError, "#{self.class.name}##{meth} does not exist."
223
+ end
224
+ end
225
+ exec_result = block_given? ? instance_exec(*exec_args, &block) : send(exec_args.slice!(0), *exec_args)
226
+ instance_eval do
227
+ undef method_missing
228
+ end
229
+
230
+ self.current_auth_scope = old_scope
231
+ return exec_result
232
+ end
233
+
234
+ auth_scopes[scope]
235
+ end
236
+
237
+ def auth_scope_eval(scope=nil, &block)
238
+ self.class.auth_scope_eval(scope, &block)
239
+ end
240
+
241
+ def current_auth_scope
242
+ self.class.current_auth_scope
243
+ end
244
+
245
+ def current_auth_scope=(scope)
246
+ self.class.current_auth_scope = scope
247
+ end
248
+
249
+ # Looks for the role slug with the closest contextual match, working it's way up the context chain.
250
+ #
251
+ # If the provided role is already a Role, just return it without checking for a match.
252
+ #
253
+ # This allows a way to provide a specific role that isn't necessarily the best match
254
+ # for the provided context to methods like assign_role, but still assign them in the
255
+ # provided context, letting you assign a role like ['admin', SomeThing, nil] to the
256
+ # resource SomeThing.find(1), even if you also have a ['admin', SomeThing, 1] role.
257
+ def target_role(role, context, force_context=nil)
258
+ auth_scope_eval do
259
+ return role if role.is_a?(role_class)
260
+ force_context ||= config.force_context
261
+
262
+ context = Zuul::Context.parse(context)
263
+ target_role = role_class.where(:slug => role.to_s.underscore, :context_type => context.class_name, :context_id => context.id).first
264
+ return target_role if force_context
265
+ target_role ||= role_class.where(:slug => role.to_s.underscore, :context_type => context.class_name, :context_id => nil).first unless context.id.nil?
266
+ target_role ||= role_class.where(:slug => role.to_s.underscore, :context_type => nil, :context_id => nil).first unless context.class_name.nil?
267
+ target_role
268
+ end
269
+ end
270
+
271
+ # Looks for the permission slug with the closest contextual match, working it's way upwards.
272
+ #
273
+ # If the provided permission is already a Permission, just return it without checking for a match.
274
+ #
275
+ # This allows a way to provide a specific permission that isn't necessarily the best match
276
+ # for the provided context to metods like assign_permission, but still assign them in the
277
+ # provided context, letting you assign a permission like ['edit', SomeThing, nil] to the
278
+ # resource SomeThing.find(1), even if you also have a ['edit', SomeThing, 1] permission.
279
+ def target_permission(permission, context, force_context=nil)
280
+ auth_scope_eval do
281
+ return permission if permission.is_a?(permission_class)
282
+ force_context ||= config.force_context
283
+
284
+ context = Zuul::Context.parse(context)
285
+ target_permission = permission_class.where(:slug => permission.to_s.underscore, :context_type => context.class_name, :context_id => context.id).first
286
+ return target_permission if force_context
287
+ target_permission ||= permission_class.where(:slug => permission.to_s.underscore, :context_type => context.class_name, :context_id => nil).first unless context.id.nil?
288
+ target_permission ||= permission_class.where(:slug => permission.to_s.underscore, :context_type => nil, :context_id => nil).first unless context.class_name.nil?
289
+ target_permission
290
+ end
291
+ end
292
+
293
+ # Verifies whether a role or permission (target) is allowed to be used within the provided context.
294
+ # The target's context must either match the one provided or be higher up the context chain.
295
+ #
296
+ # [SomeThing, 1] CANNOT be used with [SomeThing, nil] or [OtherThing, 1]
297
+ # [SomeThing, nil] CAN be used for [SomeThing, 1], [SomeThing, 2], etc.
298
+ # [nil, nil] global targets can be used for ANY context
299
+ def verify_target_context(target, context, force_context=nil)
300
+ return false if target.nil?
301
+ force_context ||= auth_scope.config.force_context
302
+ context = Zuul::Context.parse(context)
303
+ return (target.context.class_name == context.class_name && target.context.id == context.id) if force_context
304
+ (target.context.class_name.nil? && target.context.id.nil?) || (target.context.class_name == context.class_name && (target.context.id.nil? || target.context.id == context.id))
305
+ end
306
+
307
+ # Simple helper for "IS NULL" vs "= 'VALUE'" SQL syntax
308
+ # (this *must* already exist somewhere in AREL? can't find it though...)
309
+ def sql_is_or_equal(value)
310
+ value.nil? ? "IS" : "="
311
+ end
312
+ end
313
+ end
314
+
315
+ # These are included in roles & permissions objects and assigned roles & permissions objects
316
+ # to provide easy access to the context for that object.
317
+ module ContextMethods
318
+ def self.included(base)
319
+ base.send :attr_accessible, :context
320
+ end
321
+
322
+ # Return a Zuul::Context object representing the context for the role
323
+ def context
324
+ Zuul::Context.new(context_type, context_id)
325
+ end
326
+
327
+ # Parse a context into an Zuul::Context and set the type and id
328
+ def context=(context)
329
+ context = Zuul::Context.parse(context)
330
+ self.context_type = context.class_name
331
+ self.context_id = context.id
332
+ end
333
+ end
334
+
335
+ end
336
+ end
337
+
338
+ ActiveRecord::Base.send :include, Zuul::ActiveRecord
@@ -0,0 +1,38 @@
1
+ module Zuul
2
+ module ActiveRecord
3
+ module Context
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ base.send :include, InstanceMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def self.extended(base)
11
+ base.send :extend, RoleMethods
12
+ base.send :extend, PermissionMethods if base.auth_scope.config.with_permissions
13
+ end
14
+ end
15
+
16
+ module InstanceMethods
17
+ def self.included(base)
18
+ base.send :include, RoleMethods
19
+ base.send :include, PermissionMethods if base.auth_scope.config.with_permissions
20
+ end
21
+ end
22
+
23
+ module RoleMethods
24
+ # Checks whether the subject possesses the specified role within the context of self
25
+ def allowed?(subject, role)
26
+ subject.has_role?(role, self)
27
+ end
28
+ end
29
+
30
+ module PermissionMethods
31
+ # Checks whether the subject possesses the specified permission within the context of self
32
+ def allowed_to?(subject, permission)
33
+ subject.has_permission?(permission, self)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ module Zuul
2
+ module ActiveRecord
3
+ module Permission
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ base.send :include, ContextMethods # defined in lib/zuul/active_record.rb
7
+ end
8
+
9
+ module ClassMethods
10
+ def self.extended(base)
11
+ base.send :attr_accessible, :context, :context_id, :context_type, :slug
12
+ add_validations base
13
+ add_associations base
14
+ end
15
+
16
+ def self.add_validations(base)
17
+ base.send :validates_presence_of, :slug
18
+ base.send :validates_uniqueness_of, :slug, :scope => [:context_id, :context_type], :case_sensitive => false
19
+ base.send :validates_format_of, :slug, :with => /\A[a-z0-9_]+\Z/
20
+ end
21
+
22
+ def self.add_associations(base)
23
+ base.send :has_many, base.auth_scope.permission_roles_table_name.to_sym
24
+ base.send :has_many, base.auth_scope.roles_table_name.to_sym, :through => base.auth_scope.permission_roles_table_name.to_sym
25
+ base.send :has_many, base.auth_scope.permission_subjects_table_name.to_sym
26
+ base.send :has_many, base.auth_scope.subjects_table_name.to_sym, :through => base.auth_scope.permission_subjects_table_name.to_sym
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end