zuul 0.1.1 → 0.2.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.
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