Merge branch 'master' into 15106-trgm-text-search
[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_coalesce
423     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   end
429
430   def self.full_text_trgm
431     "(#{full_text_coalesce.join(" || ' ' || ")})"
432   end
433
434   def self.full_text_tsvector
435     parts = full_text_searchable_columns.collect do |column|
436       is_jsonb = self.columns.select{|x|x.name == column}[0].type == :jsonb
437       cast = (is_jsonb || serialized_attributes[column]) ? '::text' : ''
438       "coalesce(#{column}#{cast},'')"
439     end
440     "to_tsvector('english', substr(#{parts.join(" || ' ' || ")}, 0, 8000))"
441   end
442
443   def self.apply_filters query, filters
444     ft = record_filters filters, self
445     if not ft[:cond_out].any?
446       return query
447     end
448     query.where('(' + ft[:cond_out].join(') AND (') + ')',
449                           *ft[:param_out])
450   end
451
452   protected
453
454   def self.deep_sort_hash(x)
455     if x.is_a? Hash
456       x.sort.collect do |k, v|
457         [k, deep_sort_hash(v)]
458       end.to_h
459     elsif x.is_a? Array
460       x.collect { |v| deep_sort_hash(v) }
461     else
462       x
463     end
464   end
465
466   def ensure_ownership_path_leads_to_user
467     if new_record? or owner_uuid_changed?
468       uuid_in_path = {owner_uuid => true, uuid => true}
469       x = owner_uuid
470       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
471         begin
472           if x == uuid
473             # Test for cycles with the new version, not the DB contents
474             x = owner_uuid
475           elsif !owner_class.respond_to? :find_by_uuid
476             raise ActiveRecord::RecordNotFound.new
477           else
478             x = owner_class.find_by_uuid(x).owner_uuid
479           end
480         rescue ActiveRecord::RecordNotFound => e
481           errors.add :owner_uuid, "is not owned by any user: #{e}"
482           throw(:abort)
483         end
484         if uuid_in_path[x]
485           if x == owner_uuid
486             errors.add :owner_uuid, "would create an ownership cycle"
487           else
488             errors.add :owner_uuid, "has an ownership cycle"
489           end
490           throw(:abort)
491         end
492         uuid_in_path[x] = true
493       end
494     end
495     true
496   end
497
498   def set_default_owner
499     if new_record? and current_user and respond_to? :owner_uuid=
500       self.owner_uuid ||= current_user.uuid
501     end
502   end
503
504   def ensure_owner_uuid_is_permitted
505     raise PermissionDeniedError if !current_user
506
507     if self.owner_uuid.nil?
508       errors.add :owner_uuid, "cannot be nil"
509       raise PermissionDeniedError
510     end
511
512     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
513     unless rsc_class == User or rsc_class == Group
514       errors.add :owner_uuid, "must be set to User or Group"
515       raise PermissionDeniedError
516     end
517
518     if new_record? || owner_uuid_changed?
519       # Permission on owner_uuid_was is needed to move an existing
520       # object away from its previous owner (which implies permission
521       # to modify this object itself, so we don't need to check that
522       # separately). Permission on the new owner_uuid is also needed.
523       [['old', owner_uuid_was],
524        ['new', owner_uuid]
525       ].each do |which, check_uuid|
526         if check_uuid.nil?
527           # old_owner_uuid is nil? New record, no need to check.
528         elsif !current_user.can?(write: check_uuid)
529           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}"
530           errors.add :owner_uuid, "cannot be set or changed without write permission on #{which} owner"
531           raise PermissionDeniedError
532         end
533       end
534     else
535       # If the object already existed and we're not changing
536       # owner_uuid, we only need write permission on the object
537       # itself.
538       if !current_user.can?(write: self.uuid)
539         logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} without write permission"
540         errors.add :uuid, "is not writable"
541         raise PermissionDeniedError
542       end
543     end
544
545     true
546   end
547
548   def ensure_permission_to_save
549     unless (new_record? ? permission_to_create : permission_to_update)
550       raise PermissionDeniedError
551     end
552   end
553
554   def permission_to_create
555     current_user.andand.is_active
556   end
557
558   def permission_to_update
559     if !current_user
560       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
561       return false
562     end
563     if !current_user.is_active
564       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
565       return false
566     end
567     return true if current_user.is_admin
568     if self.uuid_changed?
569       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
570       return false
571     end
572     return true
573   end
574
575   def ensure_permission_to_destroy
576     raise PermissionDeniedError unless permission_to_destroy
577   end
578
579   def permission_to_destroy
580     permission_to_update
581   end
582
583   def maybe_update_modified_by_fields
584     update_modified_by_fields if self.changed? or self.new_record?
585     true
586   end
587
588   def update_modified_by_fields
589     current_time = db_current_time
590     self.created_at ||= created_at_was || current_time
591     self.updated_at = current_time
592     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
593     if !anonymous_updater
594       self.modified_by_user_uuid = current_user ? current_user.uuid : nil
595     end
596     if !timeless_updater
597       self.modified_at = current_time
598     end
599     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
600     true
601   end
602
603   def self.has_nonstring_keys? x
604     if x.is_a? Hash
605       x.each do |k,v|
606         return true if !(k.is_a?(String) || k.is_a?(Symbol)) || has_nonstring_keys?(v)
607       end
608     elsif x.is_a? Array
609       x.each do |v|
610         return true if has_nonstring_keys?(v)
611       end
612     end
613     false
614   end
615
616   def self.where_serialized(colname, value, md5: false)
617     colsql = colname.to_s
618     if md5
619       colsql = "md5(#{colsql})"
620     end
621     if value.empty?
622       # rails4 stores as null, rails3 stored as serialized [] or {}
623       sql = "#{colsql} is null or #{colsql} IN (?)"
624       sorted = value
625     else
626       sql = "#{colsql} IN (?)"
627       sorted = deep_sort_hash(value)
628     end
629     params = [sorted.to_yaml, SafeJSON.dump(sorted)]
630     if md5
631       params = params.map { |x| Digest::MD5.hexdigest(x) }
632     end
633     where(sql, params)
634   end
635
636   Serializer = {
637     Hash => HashSerializer,
638     Array => ArraySerializer,
639   }
640
641   def self.serialize(colname, type)
642     coder = Serializer[type]
643     @serialized_attributes ||= {}
644     @serialized_attributes[colname.to_s] = coder
645     super(colname, coder)
646   end
647
648   def self.serialized_attributes
649     @serialized_attributes ||= {}
650   end
651
652   def serialized_attributes
653     self.class.serialized_attributes
654   end
655
656   def foreign_key_attributes
657     attributes.keys.select { |a| a.match(/_uuid$/) }
658   end
659
660   def skip_uuid_read_permission_check
661     %w(modified_by_client_uuid)
662   end
663
664   def skip_uuid_existence_check
665     []
666   end
667
668   def normalize_collection_uuids
669     foreign_key_attributes.each do |attr|
670       attr_value = send attr
671       if attr_value.is_a? String and
672           attr_value.match(/^[0-9a-f]{32,}(\+[@\w]+)*$/)
673         begin
674           send "#{attr}=", Collection.normalize_uuid(attr_value)
675         rescue
676           # TODO: abort instead of silently accepting unnormalizable value?
677         end
678       end
679     end
680   end
681
682   @@prefixes_hash = nil
683   def self.uuid_prefixes
684     unless @@prefixes_hash
685       @@prefixes_hash = {}
686       Rails.application.eager_load!
687       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
688         if k.respond_to?(:uuid_prefix)
689           @@prefixes_hash[k.uuid_prefix] = k
690         end
691       end
692     end
693     @@prefixes_hash
694   end
695
696   def self.uuid_like_pattern
697     "#{Rails.configuration.ClusterID}-#{uuid_prefix}-_______________"
698   end
699
700   def self.uuid_regex
701     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
702   end
703
704   def ensure_valid_uuids
705     specials = [system_user_uuid]
706
707     foreign_key_attributes.each do |attr|
708       if new_record? or send (attr + "_changed?")
709         next if skip_uuid_existence_check.include? attr
710         attr_value = send attr
711         next if specials.include? attr_value
712         if attr_value
713           if (r = ArvadosModel::resource_class_for_uuid attr_value)
714             unless skip_uuid_read_permission_check.include? attr
715               r = r.readable_by(current_user)
716             end
717             if r.where(uuid: attr_value).count == 0
718               errors.add(attr, "'#{attr_value}' not found")
719             end
720           end
721         end
722       end
723     end
724   end
725
726   class Email
727     def self.kind
728       "email"
729     end
730
731     def kind
732       self.class.kind
733     end
734
735     def self.readable_by (*u)
736       self
737     end
738
739     def self.where (u)
740       [{:uuid => u[:uuid]}]
741     end
742   end
743
744   def self.resource_class_for_uuid(uuid)
745     if uuid.is_a? ArvadosModel
746       return uuid.class
747     end
748     unless uuid.is_a? String
749       return nil
750     end
751
752     uuid.match HasUuid::UUID_REGEX do |re|
753       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
754     end
755
756     if uuid.match(/.+@.+/)
757       return Email
758     end
759
760     nil
761   end
762
763   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
764   # an object in any class.
765   def self.find_by_uuid uuid
766     if self == ArvadosModel
767       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
768       # delegate to the appropriate subclass based on the given uuid.
769       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
770     else
771       super
772     end
773   end
774
775   def is_audit_logging_enabled?
776     return !(Rails.configuration.AuditLogs.MaxAge.to_i == 0 &&
777              Rails.configuration.AuditLogs.MaxDeleteBatch.to_i > 0)
778   end
779
780   def log_start_state
781     if is_audit_logging_enabled?
782       @old_attributes = Marshal.load(Marshal.dump(attributes))
783       @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
784     end
785   end
786
787   def log_change(event_type)
788     if is_audit_logging_enabled?
789       log = Log.new(event_type: event_type).fill_object(self)
790       yield log
791       log.save!
792       log_start_state
793     end
794   end
795
796   def log_create
797     if is_audit_logging_enabled?
798       log_change('create') do |log|
799         log.fill_properties('old', nil, nil)
800         log.update_to self
801       end
802     end
803   end
804
805   def log_update
806     if is_audit_logging_enabled?
807       log_change('update') do |log|
808         log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
809         log.update_to self
810       end
811     end
812   end
813
814   def log_destroy
815     if is_audit_logging_enabled?
816       log_change('delete') do |log|
817         log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
818         log.update_to nil
819       end
820     end
821   end
822 end