Module: Authorization::AuthorizationInModel

Defined in:
lib/declarative_authorization/in_model.rb,
lib/declarative_authorization/in_model.new.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object

:nodoc:



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/declarative_authorization/in_model.rb', line 65

def self.included(base) # :nodoc:
  #base.extend(ClassMethods)
  base.module_eval do
    scopes[:with_permissions_to] = lambda do |parent_scope, *args|
      options = args.last.is_a?(Hash) ? args.pop : {}
      privilege = (args[0] || :read).to_sym
      privileges = [privilege]
      context =
          if options[:context]
            options[:context]
          elsif parent_scope.respond_to?(:proxy_reflection)
            parent_scope.proxy_reflection.klass.name.tableize.to_sym
          elsif parent_scope.respond_to?(:decl_auth_context)
            parent_scope.decl_auth_context
          else
            parent_scope.name.tableize.to_sym
          end
      
      user = options[:user] || Authorization.current_user

      engine = options[:engine] || Authorization::Engine.instance
      engine.permit!(privileges, :user => user, :skip_attribute_test => true,
                     :context => context)

      obligation_scope_for( privileges, :user => user,
          :context => context, :engine => engine, :model => parent_scope)
    end
    
    # Builds and returns a scope with joins and conditions satisfying all obligations.
    def self.obligation_scope_for( privileges, options = {} )
      options = {
        :user => Authorization.current_user,
        :context => nil,
        :model => self,
        :engine => nil,
      }.merge(options)
      engine = options[:engine] || Authorization::Engine.instance

      obligation_scope = ObligationScope.new( options[:model], {} )
      engine.obligations( privileges, :user => options[:user], :context => options[:context] ).each do |obligation|
        obligation_scope.parse!( obligation )
      end

      obligation_scope.scope
    end

    # Named scope for limiting query results according to the authorization
    # of the current user.  If no privilege is given, :+read+ is assumed.
    # 
    #   User.with_permissions_to
    #   User.with_permissions_to(:update)
    #   User.with_permissions_to(:update, :context => :users)
    #   
    # As in the case of other named scopes, this one may be chained:
    #   User.with_permission_to.find(:all, :conditions...)
    # 
    # Options
    # [:+context+]
    #   Context for the privilege to be evaluated in; defaults to the
    #   model's table name.
    # [:+user+]
    #   User to be used for gathering obligations; defaults to the
    #   current user.
    #
    def self.with_permissions_to (*args)
      scopes[:with_permissions_to].call(self, *args)
    end
    
    # Activates model security for the current model.  Then, CRUD operations
    # are checked against the authorization of the current user.  The
    # privileges are :+create+, :+read+, :+update+ and :+delete+ in the
    # context of the model.  By default, :+read+ is not checked because of
    # performance impacts, especially with large result sets.
    # 
    #   class User < ActiveRecord::Base
    #     using_access_control
    #   end
    #   
    # If an operation is not permitted, a Authorization::AuthorizationError
    # is raised.
    #
    # To activate model security on all models, call using_access_control
    # on ActiveRecord::Base
    #   ActiveRecord::Base.using_access_control
    # 
    # Available options
    # [:+context+] Specify context different from the models table name.
    # [:+include_read+] Also check for :+read+ privilege after find.
    #
    def self.using_access_control (options = {})
      options = {
        :context => nil,
        :include_read => false
      }.merge(options)

      class_eval do            
        if options[:include_read]
          # If we are limiting access by options[:include_attributes], then we do not want to do the check on the entire object
          # instead we will allow the individual checks to determine what passes and what failes
          unless(options[:include_attributes])
            # after_find is only called if after_find is implemented
            after_find do |object|
              Authorization::Engine.instance.permit!(:read, :object => object,
                :context => options[:context])
            end
        
            if Rails.version < "3"
              def after_find; end
            end
          end
        end
        
        # If we are limiting access by options[:include_attributes], then we do not want to do the check on the entire object
        # instead we will allow the individual checks to determine what passes and what failes
        unless(options[:include_attributes])            
          [:create, :update, [:destroy, :delete]].each do |action, privilege|
            send(:"before_#{action}") do |object|                
              Authorization::Engine.instance.permit!(privilege || action, :object => object, :context => options[:context])
            end
          end
        end      
        
        #Inject an acl_write check for a given methid into method-chain
        def self.inject_acl_write_check(method_name)
          inject_acl_check(method_name,:write)
        end
        
        #Inject an acl_read check for a given methid into method-chain
        def self.inject_acl_read_check(method_name)
          inject_acl_check(method_name,:read)
        end
        
        #routine for helper methods
        def self.inject_acl_check(method_name,mode)
         command = <<-EOV
           alias_method :no_acl_#{method_name}, :#{method_name} unless respond_to?(:no_acl_#{method_name})
            def #{method_name}(*args,&block)
              permitted_to!(:#{mode}_#{method_name}) if !permitted_to?(:#{mode})
                return no_acl_#{method_name}(args#{',block' unless method_name.to_s.match(/=$/)})  unless args.blank? || block.blank?
                return no_acl_#{method_name}(#{'block' unless method_name.to_s.match(/=$/)}) if args.blank? && block
                return no_acl_#{method_name}(args) if !args.blank? && block.blank?
                return no_acl_#{method_name}()
            end
          EOV
          class_eval command
        end
        
        #Protecting an instance (used for generated  code, ie ActiveRecord)
        def inject_acl_object_check(method_name,mode)
          command = <<-EOV
           alias_method :no_acl_#{method_name}, :#{method_name} unless respond_to?(:no_acl_#{method_name})
            def #{method_name}(*args,&block)
              permitted_to!(:#{mode}_#{method_name}) if (!permitted_to?(:#{mode}))
                return no_acl_#{method_name}(args#{',block' unless method_name.to_s.match(/=$/)})  unless args.blank? || block.blank?
                return no_acl_#{method_name}(#{'block' unless method_name.to_s.match(/=$/)}) if args.blank? && block
                return no_acl_#{method_name}(args) if !args.blank? && block.blank?
                return no_acl_#{method_name}()
            end
          EOV
          class_eval command
        end
        
        #Inject acl-aware setter / getter methods into method-chain
        def inject_acl_object_getter_setter(method_name)
          class_eval <<-EOV
            alias_method :no_acl_#{method_name}, :#{method_name} unless respond_to? :no_acl_#{method_name}
            alias_method :no_acl_#{method_name}=, :#{method_name}= unless respond_to? :no_acl_#{method_name}=
          EOV
          logger.info "Injecting #{method_name} to #{self}"
          instance_eval <<-EOV
           
            def #{method_name}
               permitted_to!(:read_#{method_name}) unless permitted_to?(:#{read_all_privilege})
               return no_acl_#{method_name}
            end
            def #{method_name}=(value)
               permitted_to!(:write_#{method_name}) unless permitted_to?(:#{write_all_privilege})
               return no_acl_#{method_name}=(value)
            end
          EOV
        end
        
        if(options[:include_attributes]) #If attribute / getter-setter-access ought to be checekd#     
          #parse attribute hash - sane input?
          raise "Illegal syntax - :include_attributes must point to an array" unless options[:include_attributes][0].is_a?(Hash)
          
          protect_ar = options[:include_attributes][0][:protect_ar] || []
          raise "Illegal syntax :protect_ar must point to an array" unless protect_ar.blank? || protect_ar.is_a?(Array)
          protect_ar = protect_ar.to_set
          
          protect_read = options[:include_attributes][0][:protect_read]
          raise "Illegal syntax :protect_read must point to an array" unless protect_read.nil? || protect_read.is_a?(Array)
          
          protect_write = options[:include_attributes][0][:protect_write]
          raise "Illegal syntax :protect_write must point to an array" unless protect_write.nil? || protect_write.is_a?(Array)

          protect_attributes = options[:include_attributes][0][:protect_attributes]
          raise "Illegal syntax :protect_attributes point to an array" unless protect_attributes.nil? || protect_attributes.is_a?(Array)

          whitelist = options[:include_attributes][0][:whitelist] || []
          raise "Illegal syntax :whitelist must point to an array" unless whitelist.blank? || whitelist.is_a?(Array)
          whitelist = whitelist.to_set
          
          application_default_attributes = options[:include_attributes][0][:application_default_attributes] || []
          raise "Illegal syntax :application_default_attributes must point to an array" unless application_default_attributes.blank? || application_default_attributes.is_a?(Array)
          application_default_attributes = application_default_attributes.to_set
          
          #Enable callback for instance-level meta programming
          def after_initialize; end
          
          # Create helper methods, that can be called from within our code to access
          # variables that are set up during initilization
          instance_eval <<-EOV
            #
            # Determine what privilege to use for read all
            #
            def read_all_privilege
              '#{options[:include_attributes][0][:read_all_privilege].blank? ? 'read' : options[:include_attributes][0][:read_all_privilege]}'
            end            

            #
            # Determine what privilege to use for write all
            #
            def write_all_privilege
              '#{options[:include_attributes][0][:write_all_privilege].blank? ? 'write' : options[:include_attributes][0][:write_all_privilege]}'
            end
          EOV
          
          class_eval <<-EOV
            #
            # Method to return the white list
            #
            def get_white_list
              [#{whitelist.to_a.collect{|c| ":#{c}"}.join(',')}]
            end
            
            #
            # Method to return the application_default_attributes
            #
            def get_application_default_attributes
              [#{application_default_attributes.to_a.collect{|c| ":#{c}"}.join(',')}]
            end
          EOV
          
          #1a Generate guards for ar-attributes
          if protect_ar.include?(:attributes)                
            column_names.each do |name|
              class_eval "begin; alias_method :no_acl_#{name}, :#{name};rescue;end #Alias-Methods - put acl stuff into method-chain
              begin; alias_method :no_acl_#{name}=, :#{name}=; rescue; end
              def #{name} #Define getters / setter with ACL-Checks
                permitted_to!(:read_#{name}) if !permitted_to?(:#{read_all_privilege}); 
                if(respond_to? 'no_acl_#{name}')
                  return no_acl_#{name}
                else
                  return read_attribute(:#{name})
                end
              end" unless name.to_s == self.primary_key.to_s || whitelist.include?(name.to_sym) || application_default_attributes.include?(name.to_sym) || !options[:include_read] # Do not do reads, unless told so
              class_eval %{def #{name}=(n)
                permitted_to!(:write_#{name}) if !permitted_to?(:#{write_all_privilege});
                if(respond_to? 'no_acl_#{name}=')
                  return no_acl_#{name}=(n)
                else
                  return write_attribute(:#{name},n)
                end
              end} unless name.to_s == self.primary_key.to_s || whitelist.include?("#{name}=".to_sym) || application_default_attributes.include?(name.to_sym)
            end
          end
          
          #1b Generate guards for non-ar attributes
          if protect_attributes
            after_initialize do |object|
              protect_attributes.each { |attr| object.inject_acl_object_getter_setter(attr) }
            end
          end
  
          
          #2nd Generate guards for ar-proxies
          if protect_ar.include?(:proxies)
            after_initialize do |object|
              reflect_on_all_associations.each do |assoc|
                 #Respect excludes
                  #Ok, we've to intercept these calls (See: ActiveRecord::Associations::ClassMethods)
                  # one-to-one: other_id, other_id=(id), other, other=(other), build_other(attributes={}), create_other(attributes={})
                  # one-to-many / many-to-many: others, others=(other,other,...), other_ids, other_ids=(id,id,...), others<< 
                  object.inject_acl_object_getter_setter(assoc.name.to_s) unless whitelist.include?(assoc.name)
                
                  if(assoc.collection?) #Collection? if so, many-to-many case
                    object.inject_acl_object_getter_setter("#{assoc.name.to_s.singularize}_ids") unless whitelist.include?("#{assoc.name.to_s.singularize}_ids".to_sym)
                    #inject_acl_write_check("#{assoc.name}<<")
                  else
                    object.inject_acl_object_getter_setter("#{assoc.name}_id") unless assoc.macro != :has_one || whitelist.include?("#{assoc.name}_id".to_sym)
                    object.inject_acl_object_check("build_#{assoc.name}",:write) unless whitelist.include?("build_#{assoc.name}".to_sym)
                    object.inject_acl_object_check("create_#{assoc.name}",:read) unless whitelist.include?("create_#{assoc.name}".to_sym)
                  end
              end
            end
          end
          
          #3rd - generate guards for specified methods
          #3a - read-permission required
          if(protect_read)
            after_initialize do |object|
              protect_read.each { |method| object.inject_acl_object_check(method,:read) }
            end
          end
          
          #3b - write permission required
          if(protect_write)
            after_initialize do |object|
              protect_write.each { |method| object.inject_acl_object_check(method,:write) }
            end
          end              
          
          #
          # Returns a hash of key, value paris that are readable
          #
          def readable_attributes 
            return attributes if permitted_to?(self.class.read_all_privilege.to_sym)
            attributes.reject do |k,v|
              !allowed?(:read, k)
            end
          end
          
          #
          # Returns a hash of key, value paris that are showable, excluding application_default_attributes
          #
          def showable_attributes
            return attributes if permitted_to?(self.class.read_all_privilege.to_sym)
            attributes.reject do |k,v|
              !allowed?(:read, k, true)
            end
          end
          
          #
          # Returns a hash of key, value paris that are writable
          #
          def writable_attributes
            return attributes if permitted_to?(self.class.write_all_privilege.to_sym)
            attributes.reject do |k,v|
              !allowed?(:write, k)
            end
          end
          
          #
          # Returns a list of columns that are readable
          #
          def readable_columns
            readable_attributes.keys
          end
          
          #
          # Returns a list of columns that are writable
          #
          def writable_columns
            writable_attributes.keys
          end
          
          #
          # Returns a list of columns that are showable to the user
          #
          def showable_columns
            showable_attributes.keys
          end
          
          #
          # When calling permitted_to? on the model, we return true if whitelist or read/write all
          # excluding application_default_attributes
          #
          def permitted_to_with_include_attributes?(privilege, options = {}, &block)
            # Figure out what priv/attribute was passed, if it begins with read_ or write_
            if reg = privilege.to_s.match(/(^write_|^read_)(.+)/)
              mode, attribute = reg[1].chop.to_sym, reg[2] # Split the regular expression accordingly                  
              if allowed?(mode, attribute, true) # Exclude application_default_attributes
                yield if block_given?
                return true
              end
            end
            
            # Default back to old call
            permitted_to_without_include_attributes?(privilege, options, &block)  
          end
          
          alias_method_chain :permitted_to?, :include_attributes
        end

        def self.using_access_control?
          true
        end
      end
    end

    # Returns true if the model is using model security.
    def self.using_access_control?
      false
    end
  end
end

Instance Method Details

#allowed?(mode, attribute, exclude_application_defaults = false) ⇒ Boolean

Returns true or false, depending on whether we can read/write a column based on all our rules

PARAMS

mode Symbol. :read/:write
attribute String. the column we want to check
application_defaults Boolean, whether we want to incude the application defaults or not

RETURNS

boolean, true/false

Returns:

  • (Boolean)


47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/declarative_authorization/in_model.rb', line 47

def allowed?(mode, attribute, exclude_application_defaults = false)
  # Return false if mode is not read or write
  return false unless [:read, :write].include?(mode)
  
  # Variables needed to make checks
  access_all_columns_sym = (mode == :read) ? self.class.read_all_privilege.to_sym : self.class.write_all_privilege.to_sym
  whitelist_sym = (mode == :read) ? attribute.to_sym : (attribute + '=').to_sym
  acl_sym = (mode == :read) ? ('read_' + attribute).to_sym : ('write_' + attribute).to_sym
  
  # Perform checks, returns early on success
  return true if attribute.to_s == self.class.primary_key.to_s # Always return true on primary key
  return true if !exclude_application_defaults && get_application_default_attributes.include?(attribute.to_sym) # Test application defaults first
  return true if permitted_to_without_include_attributes?(access_all_columns_sym) # Are we allowed read/write all?
  return true if get_white_list.include?(whitelist_sym) # White Listed
  return true if permitted_to_without_include_attributes?(acl_sym) # read/write_{attribute} given explicitly
  false # Not allowed, return false
end

#permitted_to!(privilege, options = {}) ⇒ Object

Works similar to the permitted_to? method, but doesn’t accept a block and throws the authorization exceptions, just like Engine#permit!



24
25
26
27
28
29
30
31
32
# File 'lib/declarative_authorization/in_model.rb', line 24

def permitted_to! (privilege, options = {} )
  options = {
    :user =>  Authorization.current_user,
    :object => self
  }.merge(options)
  Authorization::Engine.instance.permit!(privilege,
      {:user => options[:user],
       :object => options[:object]})
end

#permitted_to?(privilege, options = {}, &block) ⇒ Boolean

If the user meets the given privilege, permitted_to? returns true and yields to the optional block.

Returns:

  • (Boolean)


11
12
13
14
15
16
17
18
19
20
# File 'lib/declarative_authorization/in_model.rb', line 11

def permitted_to? (privilege, options = {}, &block)
  options = {
    :user =>  Authorization.current_user,
    :object => self
  }.merge(options)
  Authorization::Engine.instance.permit?(privilege,
      {:user => options[:user],
       :object => options[:object]},
      &block)
end