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