11065: Merge branch 'master' into 11065-optional-audit-logging
[arvados.git] / services / api / app / models / arvados_model.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'arvados_model_updates'
6 require 'has_uuid'
7 require 'record_filters'
8 require 'serializers'
9 require 'request_error'
10
11 class ArvadosModel < ActiveRecord::Base
12   self.abstract_class = true
13
14   include ArvadosModelUpdates
15   include CurrentApiClient      # current_user, current_api_client, etc.
16   include DbCurrentTime
17   extend RecordFilters
18
19   after_initialize :log_start_state
20   before_save :ensure_permission_to_save
21   before_save :ensure_owner_uuid_is_permitted
22   before_save :ensure_ownership_path_leads_to_user
23   before_destroy :ensure_owner_uuid_is_permitted
24   before_destroy :ensure_permission_to_destroy
25   before_create :update_modified_by_fields
26   before_update :maybe_update_modified_by_fields
27   after_create :log_create
28   after_update :log_update
29   after_destroy :log_destroy
30   after_find :convert_serialized_symbols_to_strings
31   before_validation :normalize_collection_uuids
32   before_validation :set_default_owner
33   validate :ensure_valid_uuids
34
35   # Note: This only returns permission links. It does not account for
36   # permissions obtained via user.is_admin or
37   # user.uuid==object.owner_uuid.
38   has_many(:permissions,
39            ->{where(link_class: 'permission')},
40            foreign_key: :head_uuid,
41            class_name: 'Link',
42            primary_key: :uuid)
43
44   class PermissionDeniedError < RequestError
45     def http_status
46       403
47     end
48   end
49
50   class AlreadyLockedError < RequestError
51     def http_status
52       422
53     end
54   end
55
56   class LockFailedError < RequestError
57     def http_status
58       422
59     end
60   end
61
62   class InvalidStateTransitionError < RequestError
63     def http_status
64       422
65     end
66   end
67
68   class UnauthorizedError < RequestError
69     def http_status
70       401
71     end
72   end
73
74   class UnresolvableContainerError < RequestError
75     def http_status
76       422
77     end
78   end
79
80   def self.kind_class(kind)
81     kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
82   end
83
84   def href
85     "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}"
86   end
87
88   def self.permit_attribute_params raw_params
89     # strong_parameters does not provide security: permissions are
90     # implemented with before_save hooks.
91     #
92     # The following permit! is necessary even with
93     # "ActionController::Parameters.permit_all_parameters = true",
94     # because permit_all does not permit nested attributes.
95     if raw_params
96       serialized_attributes.each do |colname, coder|
97         param = raw_params[colname.to_sym]
98         if param.nil?
99           # ok
100         elsif !param.is_a?(coder.object_class)
101           raise ArgumentError.new("#{colname} parameter must be #{coder.object_class}, not #{param.class}")
102         elsif has_nonstring_keys?(param)
103           raise ArgumentError.new("#{colname} parameter cannot have non-string hash keys")
104         end
105       end
106     end
107     ActionController::Parameters.new(raw_params).permit!
108   end
109
110   def initialize raw_params={}, *args
111     super(self.class.permit_attribute_params(raw_params), *args)
112   end
113
114   # Reload "old attributes" for logging, too.
115   def reload(*args)
116     super
117     log_start_state
118   end
119
120   def self.create raw_params={}, *args
121     super(permit_attribute_params(raw_params), *args)
122   end
123
124   def update_attributes raw_params={}, *args
125     super(self.class.permit_attribute_params(raw_params), *args)
126   end
127
128   def self.selectable_attributes(template=:user)
129     # Return an array of attribute name strings that can be selected
130     # in the given template.
131     api_accessible_attributes(template).map { |attr_spec| attr_spec.first.to_s }
132   end
133
134   def self.searchable_columns operator
135     textonly_operator = !operator.match(/[<=>]/)
136     self.columns.select do |col|
137       case col.type
138       when :string, :text
139         true
140       when :datetime, :integer, :boolean
141         !textonly_operator
142       else
143         false
144       end
145     end.map(&:name)
146   end
147
148   def self.attribute_column attr
149     self.columns.select { |col| col.name == attr.to_s }.first
150   end
151
152   def self.attributes_required_columns
153     # This method returns a hash.  Each key is the name of an API attribute,
154     # and it's mapped to a list of database columns that must be fetched
155     # to generate that attribute.
156     # This implementation generates a simple map of attributes to
157     # matching column names.  Subclasses can override this method
158     # to specify that method-backed API attributes need to fetch
159     # specific columns from the database.
160     all_columns = columns.map(&:name)
161     api_column_map = Hash.new { |hash, key| hash[key] = [] }
162     methods.grep(/^api_accessible_\w+$/).each do |method_name|
163       next if method_name == :api_accessible_attributes
164       send(method_name).each_pair do |api_attr_name, col_name|
165         col_name = col_name.to_s
166         if all_columns.include?(col_name)
167           api_column_map[api_attr_name.to_s] |= [col_name]
168         end
169       end
170     end
171     api_column_map
172   end
173
174   def self.ignored_select_attributes
175     ["href", "kind", "etag"]
176   end
177
178   def self.columns_for_attributes(select_attributes)
179     if select_attributes.empty?
180       raise ArgumentError.new("Attribute selection list cannot be empty")
181     end
182     api_column_map = attributes_required_columns
183     invalid_attrs = []
184     select_attributes.each do |s|
185       next if ignored_select_attributes.include? s
186       if not s.is_a? String or not api_column_map.include? s
187         invalid_attrs << s
188       end
189     end
190     if not invalid_attrs.empty?
191       raise ArgumentError.new("Invalid attribute(s): #{invalid_attrs.inspect}")
192     end
193     # Given an array of attribute names to select, return an array of column
194     # names that must be fetched from the database to satisfy the request.
195     select_attributes.flat_map { |attr| api_column_map[attr] }.uniq
196   end
197
198   def self.default_orders
199     ["#{table_name}.modified_at desc", "#{table_name}.uuid"]
200   end
201
202   def self.unique_columns
203     ["id", "uuid"]
204   end
205
206   def self.limit_index_columns_read
207     # This method returns a list of column names.
208     # If an index request reads that column from the database,
209     # APIs that return lists will only fetch objects until reaching
210     # max_index_database_read bytes of data from those columns.
211     []
212   end
213
214   # If current user can manage the object, return an array of uuids of
215   # users and groups that have permission to write the object. The
216   # first two elements are always [self.owner_uuid, current user's
217   # uuid].
218   #
219   # If current user can write but not manage the object, return
220   # [self.owner_uuid, current user's uuid].
221   #
222   # If current user cannot write this object, just return
223   # [self.owner_uuid].
224   def writable_by
225     return [owner_uuid] if not current_user
226     unless (owner_uuid == current_user.uuid or
227             current_user.is_admin or
228             (current_user.groups_i_can(:manage) & [uuid, owner_uuid]).any?)
229       if ((current_user.groups_i_can(:write) + [current_user.uuid]) &
230           [uuid, owner_uuid]).any?
231         return [owner_uuid, current_user.uuid]
232       else
233         return [owner_uuid]
234       end
235     end
236     [owner_uuid, current_user.uuid] + permissions.collect do |p|
237       if ['can_write', 'can_manage'].index p.name
238         p.tail_uuid
239       end
240     end.compact.uniq
241   end
242
243   # Return a query with read permissions restricted to the union of of the
244   # permissions of the members of users_list, i.e. if something is readable by
245   # any user in users_list, it will be readable in the query returned by this
246   # function.
247   def self.readable_by(*users_list)
248     # Get rid of troublesome nils
249     users_list.compact!
250
251     # Load optional keyword arguments, if they exist.
252     if users_list.last.is_a? Hash
253       kwargs = users_list.pop
254     else
255       kwargs = {}
256     end
257
258     # Collect the UUIDs of the authorized users.
259     sql_table = kwargs.fetch(:table_name, table_name)
260     include_trash = kwargs.fetch(:include_trash, false)
261
262     sql_conds = nil
263     user_uuids = users_list.map { |u| u.uuid }
264
265     exclude_trashed_records = ""
266     if !include_trash and (sql_table == "groups" or sql_table == "collections") then
267       # Only include records that are not explicitly trashed
268       exclude_trashed_records = "AND #{sql_table}.is_trashed = false"
269     end
270
271     if users_list.select { |u| u.is_admin }.any?
272       # Admin skips most permission checks, but still want to filter on trashed items.
273       if !include_trash
274         if sql_table != "api_client_authorizations"
275           # Only include records where the owner is not trashed
276           sql_conds = "NOT EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
277                       "WHERE trashed = 1 AND "+
278                       "(#{sql_table}.owner_uuid = target_uuid)) #{exclude_trashed_records}"
279         end
280       end
281     else
282       trashed_check = ""
283       if !include_trash then
284         trashed_check = "AND trashed = 0"
285       end
286
287       # Note: it is possible to combine the direct_check and
288       # owner_check into a single EXISTS() clause, however it turns
289       # out query optimizer doesn't like it and forces a sequential
290       # table scan.  Constructing the query with separate EXISTS()
291       # clauses enables it to use the index.
292       #
293       # see issue 13208 for details.
294
295       # Match a direct read permission link from the user to the record uuid
296       direct_check = "EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
297                      "WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 #{trashed_check} AND target_uuid = #{sql_table}.uuid)"
298
299       # Match a read permission link from the user to the record's owner_uuid
300       owner_check = ""
301       if sql_table != "api_client_authorizations" and sql_table != "groups" then
302         owner_check = "OR EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
303           "WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 #{trashed_check} AND target_uuid = #{sql_table}.owner_uuid AND target_owner_uuid IS NOT NULL) "
304       end
305
306       links_cond = ""
307       if sql_table == "links"
308         # Match any permission link that gives one of the authorized
309         # users some permission _or_ gives anyone else permission to
310         # view one of the authorized users.
311         links_cond = "OR (#{sql_table}.link_class IN (:permission_link_classes) AND "+
312                        "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"
313       end
314
315       sql_conds = "(#{direct_check} #{owner_check} #{links_cond}) #{exclude_trashed_records}"
316
317     end
318
319     self.where(sql_conds,
320                user_uuids: user_uuids,
321                permission_link_classes: ['permission', 'resources'])
322   end
323
324   def save_with_unique_name!
325     uuid_was = uuid
326     name_was = name
327     max_retries = 2
328     transaction do
329       conn = ActiveRecord::Base.connection
330       conn.exec_query 'SAVEPOINT save_with_unique_name'
331       begin
332         save!
333       rescue ActiveRecord::RecordNotUnique => rn
334         raise if max_retries == 0
335         max_retries -= 1
336
337         conn.exec_query 'ROLLBACK TO SAVEPOINT save_with_unique_name'
338
339         # Dig into the error to determine if it is specifically calling out a
340         # (owner_uuid, name) uniqueness violation.  In this specific case, and
341         # the client requested a unique name with ensure_unique_name==true,
342         # update the name field and try to save again.  Loop as necessary to
343         # discover a unique name.  It is necessary to handle name choosing at
344         # this level (as opposed to the client) to ensure that record creation
345         # never fails due to a race condition.
346         err = rn.original_exception
347         raise unless err.is_a?(PG::UniqueViolation)
348
349         # Unfortunately ActiveRecord doesn't abstract out any of the
350         # necessary information to figure out if this the error is actually
351         # the specific case where we want to apply the ensure_unique_name
352         # behavior, so the following code is specialized to Postgres.
353         detail = err.result.error_field(PG::Result::PG_DIAG_MESSAGE_DETAIL)
354         raise unless /^Key \(owner_uuid, name\)=\([a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}, .*?\) already exists\./.match detail
355
356         new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
357         if new_name == name
358           # If the database is fast enough to do two attempts in the
359           # same millisecond, we need to wait to ensure we try a
360           # different timestamp on each attempt.
361           sleep 0.002
362           new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
363         end
364
365         self[:name] = new_name
366         self[:uuid] = nil if uuid_was.nil? && !uuid.nil?
367         conn.exec_query 'SAVEPOINT save_with_unique_name'
368         retry
369       ensure
370         conn.exec_query 'RELEASE SAVEPOINT save_with_unique_name'
371       end
372     end
373   end
374
375   def logged_attributes
376     attributes.except(*Rails.configuration.unlogged_attributes)
377   end
378
379   def self.full_text_searchable_columns
380     self.columns.select do |col|
381       [:string, :text, :jsonb].include?(col.type)
382     end.map(&:name)
383   end
384
385   def self.full_text_tsvector
386     parts = full_text_searchable_columns.collect do |column|
387       cast = serialized_attributes[column] ? '::text' : ''
388       "coalesce(#{column}#{cast},'')"
389     end
390     "to_tsvector('english', #{parts.join(" || ' ' || ")})"
391   end
392
393   def self.apply_filters query, filters
394     ft = record_filters filters, self
395     if not ft[:cond_out].any?
396       return query
397     end
398     query.where('(' + ft[:cond_out].join(') AND (') + ')',
399                           *ft[:param_out])
400   end
401
402   protected
403
404   def self.deep_sort_hash(x)
405     if x.is_a? Hash
406       x.sort.collect do |k, v|
407         [k, deep_sort_hash(v)]
408       end.to_h
409     elsif x.is_a? Array
410       x.collect { |v| deep_sort_hash(v) }
411     else
412       x
413     end
414   end
415
416   def ensure_ownership_path_leads_to_user
417     if new_record? or owner_uuid_changed?
418       uuid_in_path = {owner_uuid => true, uuid => true}
419       x = owner_uuid
420       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
421         begin
422           if x == uuid
423             # Test for cycles with the new version, not the DB contents
424             x = owner_uuid
425           elsif !owner_class.respond_to? :find_by_uuid
426             raise ActiveRecord::RecordNotFound.new
427           else
428             x = owner_class.find_by_uuid(x).owner_uuid
429           end
430         rescue ActiveRecord::RecordNotFound => e
431           errors.add :owner_uuid, "is not owned by any user: #{e}"
432           return false
433         end
434         if uuid_in_path[x]
435           if x == owner_uuid
436             errors.add :owner_uuid, "would create an ownership cycle"
437           else
438             errors.add :owner_uuid, "has an ownership cycle"
439           end
440           return false
441         end
442         uuid_in_path[x] = true
443       end
444     end
445     true
446   end
447
448   def set_default_owner
449     if new_record? and current_user and respond_to? :owner_uuid=
450       self.owner_uuid ||= current_user.uuid
451     end
452   end
453
454   def ensure_owner_uuid_is_permitted
455     raise PermissionDeniedError if !current_user
456
457     if self.owner_uuid.nil?
458       errors.add :owner_uuid, "cannot be nil"
459       raise PermissionDeniedError
460     end
461
462     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
463     unless rsc_class == User or rsc_class == Group
464       errors.add :owner_uuid, "must be set to User or Group"
465       raise PermissionDeniedError
466     end
467
468     if new_record? || owner_uuid_changed?
469       # Permission on owner_uuid_was is needed to move an existing
470       # object away from its previous owner (which implies permission
471       # to modify this object itself, so we don't need to check that
472       # separately). Permission on the new owner_uuid is also needed.
473       [['old', owner_uuid_was],
474        ['new', owner_uuid]
475       ].each do |which, check_uuid|
476         if check_uuid.nil?
477           # old_owner_uuid is nil? New record, no need to check.
478         elsif !current_user.can?(write: check_uuid)
479           logger.warn "User #{current_user.uuid} tried to set ownership of #{self.class.to_s} #{self.uuid} but does not have permission to write #{which} owner_uuid #{check_uuid}"
480           errors.add :owner_uuid, "cannot be set or changed without write permission on #{which} owner"
481           raise PermissionDeniedError
482         end
483       end
484     else
485       # If the object already existed and we're not changing
486       # owner_uuid, we only need write permission on the object
487       # itself.
488       if !current_user.can?(write: self.uuid)
489         logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} without write permission"
490         errors.add :uuid, "is not writable"
491         raise PermissionDeniedError
492       end
493     end
494
495     true
496   end
497
498   def ensure_permission_to_save
499     unless (new_record? ? permission_to_create : permission_to_update)
500       raise PermissionDeniedError
501     end
502   end
503
504   def permission_to_create
505     current_user.andand.is_active
506   end
507
508   def permission_to_update
509     if !current_user
510       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
511       return false
512     end
513     if !current_user.is_active
514       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
515       return false
516     end
517     return true if current_user.is_admin
518     if self.uuid_changed?
519       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
520       return false
521     end
522     return true
523   end
524
525   def ensure_permission_to_destroy
526     raise PermissionDeniedError unless permission_to_destroy
527   end
528
529   def permission_to_destroy
530     permission_to_update
531   end
532
533   def maybe_update_modified_by_fields
534     update_modified_by_fields if self.changed? or self.new_record?
535     true
536   end
537
538   def update_modified_by_fields
539     current_time = db_current_time
540     self.created_at = created_at_was || current_time
541     self.updated_at = current_time
542     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
543     self.modified_at = current_time
544     if !anonymous_updater
545       self.modified_by_user_uuid = current_user ? current_user.uuid : nil
546     end
547     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
548     true
549   end
550
551   def self.has_nonstring_keys? x
552     if x.is_a? Hash
553       x.each do |k,v|
554         return true if !(k.is_a?(String) || k.is_a?(Symbol)) || has_nonstring_keys?(v)
555       end
556     elsif x.is_a? Array
557       x.each do |v|
558         return true if has_nonstring_keys?(v)
559       end
560     end
561     false
562   end
563
564   def self.has_symbols? x
565     if x.is_a? Hash
566       x.each do |k,v|
567         return true if has_symbols?(k) or has_symbols?(v)
568       end
569     elsif x.is_a? Array
570       x.each do |k|
571         return true if has_symbols?(k)
572       end
573     elsif x.is_a? Symbol
574       return true
575     elsif x.is_a? String
576       return true if x.start_with?(':') && !x.start_with?('::')
577     end
578     false
579   end
580
581   def self.recursive_stringify x
582     if x.is_a? Hash
583       Hash[x.collect do |k,v|
584              [recursive_stringify(k), recursive_stringify(v)]
585            end]
586     elsif x.is_a? Array
587       x.collect do |k|
588         recursive_stringify k
589       end
590     elsif x.is_a? Symbol
591       x.to_s
592     elsif x.is_a? String and x.start_with?(':') and !x.start_with?('::')
593       x[1..-1]
594     else
595       x
596     end
597   end
598
599   def self.where_serialized(colname, value, md5: false)
600     colsql = colname.to_s
601     if md5
602       colsql = "md5(#{colsql})"
603     end
604     if value.empty?
605       # rails4 stores as null, rails3 stored as serialized [] or {}
606       sql = "#{colsql} is null or #{colsql} IN (?)"
607       sorted = value
608     else
609       sql = "#{colsql} IN (?)"
610       sorted = deep_sort_hash(value)
611     end
612     params = [sorted.to_yaml, SafeJSON.dump(sorted)]
613     if md5
614       params = params.map { |x| Digest::MD5.hexdigest(x) }
615     end
616     where(sql, params)
617   end
618
619   Serializer = {
620     Hash => HashSerializer,
621     Array => ArraySerializer,
622   }
623
624   def self.serialize(colname, type)
625     coder = Serializer[type]
626     @serialized_attributes ||= {}
627     @serialized_attributes[colname.to_s] = coder
628     super(colname, coder)
629   end
630
631   def self.serialized_attributes
632     @serialized_attributes ||= {}
633   end
634
635   def serialized_attributes
636     self.class.serialized_attributes
637   end
638
639   def convert_serialized_symbols_to_strings
640     # ensure_serialized_attribute_type should prevent symbols from
641     # getting into the database in the first place. If someone managed
642     # to get them into the database (perhaps using an older version)
643     # we'll convert symbols to strings when loading from the
644     # database. (Otherwise, loading and saving an object with existing
645     # symbols in a serialized field will crash.)
646     self.class.serialized_attributes.each do |colname, attr|
647       if self.class.has_symbols? attributes[colname]
648         attributes[colname] = self.class.recursive_stringify attributes[colname]
649         send(colname + '=',
650              self.class.recursive_stringify(attributes[colname]))
651       end
652     end
653   end
654
655   def foreign_key_attributes
656     attributes.keys.select { |a| a.match(/_uuid$/) }
657   end
658
659   def skip_uuid_read_permission_check
660     %w(modified_by_client_uuid)
661   end
662
663   def skip_uuid_existence_check
664     []
665   end
666
667   def normalize_collection_uuids
668     foreign_key_attributes.each do |attr|
669       attr_value = send attr
670       if attr_value.is_a? String and
671           attr_value.match(/^[0-9a-f]{32,}(\+[@\w]+)*$/)
672         begin
673           send "#{attr}=", Collection.normalize_uuid(attr_value)
674         rescue
675           # TODO: abort instead of silently accepting unnormalizable value?
676         end
677       end
678     end
679   end
680
681   @@prefixes_hash = nil
682   def self.uuid_prefixes
683     unless @@prefixes_hash
684       @@prefixes_hash = {}
685       Rails.application.eager_load!
686       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
687         if k.respond_to?(:uuid_prefix)
688           @@prefixes_hash[k.uuid_prefix] = k
689         end
690       end
691     end
692     @@prefixes_hash
693   end
694
695   def self.uuid_like_pattern
696     "#{Rails.configuration.uuid_prefix}-#{uuid_prefix}-_______________"
697   end
698
699   def self.uuid_regex
700     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
701   end
702
703   def ensure_valid_uuids
704     specials = [system_user_uuid]
705
706     foreign_key_attributes.each do |attr|
707       if new_record? or send (attr + "_changed?")
708         next if skip_uuid_existence_check.include? attr
709         attr_value = send attr
710         next if specials.include? attr_value
711         if attr_value
712           if (r = ArvadosModel::resource_class_for_uuid attr_value)
713             unless skip_uuid_read_permission_check.include? attr
714               r = r.readable_by(current_user)
715             end
716             if r.where(uuid: attr_value).count == 0
717               errors.add(attr, "'#{attr_value}' not found")
718             end
719           end
720         end
721       end
722     end
723   end
724
725   class Email
726     def self.kind
727       "email"
728     end
729
730     def kind
731       self.class.kind
732     end
733
734     def self.readable_by (*u)
735       self
736     end
737
738     def self.where (u)
739       [{:uuid => u[:uuid]}]
740     end
741   end
742
743   def self.resource_class_for_uuid(uuid)
744     if uuid.is_a? ArvadosModel
745       return uuid.class
746     end
747     unless uuid.is_a? String
748       return nil
749     end
750
751     uuid.match HasUuid::UUID_REGEX do |re|
752       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
753     end
754
755     if uuid.match(/.+@.+/)
756       return Email
757     end
758
759     nil
760   end
761
762   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
763   # an object in any class.
764   def self.find_by_uuid uuid
765     if self == ArvadosModel
766       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
767       # delegate to the appropriate subclass based on the given uuid.
768       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
769     else
770       super
771     end
772   end
773
774   def is_audit_logging_enabled?
775     return !(Rails.configuration.max_audit_log_age.to_i == 0 &&
776              Rails.configuration.max_audit_log_delete_batch.to_i > 0)
777   end
778
779   def log_start_state
780     if is_audit_logging_enabled?
781       @old_attributes = Marshal.load(Marshal.dump(attributes))
782       @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
783     end
784   end
785
786   def log_change(event_type)
787     if is_audit_logging_enabled?
788       log = Log.new(event_type: event_type).fill_object(self)
789       yield log
790       log.save!
791       log_start_state
792     end
793   end
794
795   def log_create
796     if is_audit_logging_enabled?
797       log_change('create') do |log|
798         log.fill_properties('old', nil, nil)
799         log.update_to self
800       end
801     end
802   end
803
804   def log_update
805     if is_audit_logging_enabled?
806       log_change('update') do |log|
807         log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
808         log.update_to self
809       end
810     end
811   end
812
813   def log_destroy
814     if is_audit_logging_enabled?
815       log_change('delete') do |log|
816         log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
817         log.update_to nil
818       end
819     end
820   end
821 end