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