13561: Exclude past versions regardless of user & include_trash setting.
[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 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     include_old_versions = kwargs.fetch(:include_old_versions, false)
262
263     sql_conds = nil
264     user_uuids = users_list.map { |u| u.uuid }
265
266     exclude_trashed_records = ""
267     if !include_trash and (sql_table == "groups" or sql_table == "collections") then
268       # Only include records that are not explicitly trashed
269       exclude_trashed_records = "AND #{sql_table}.is_trashed = false"
270     end
271
272     exclude_old_versions = ""
273     if !include_old_versions && sql_table == "collections"
274       exclude_old_versions = "AND #{sql_table}.uuid = #{sql_table}.current_version_uuid"
275     end
276
277     if users_list.select { |u| u.is_admin }.any?
278       # Admin skips most permission checks, but still want to filter on trashed items.
279       if !include_trash
280         if sql_table != "api_client_authorizations"
281           # Only include records where the owner is not trashed
282           sql_conds = "NOT EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
283                       "WHERE trashed = 1 AND "+
284                       "(#{sql_table}.owner_uuid = target_uuid)) #{exclude_trashed_records}"
285         end
286       end
287     else
288       trashed_check = ""
289       if !include_trash then
290         trashed_check = "AND trashed = 0"
291       end
292
293       # Note: it is possible to combine the direct_check and
294       # owner_check into a single EXISTS() clause, however it turns
295       # out query optimizer doesn't like it and forces a sequential
296       # table scan.  Constructing the query with separate EXISTS()
297       # clauses enables it to use the index.
298       #
299       # see issue 13208 for details.
300
301       # Match a direct read permission link from the user to the record uuid
302       direct_check = "EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
303                      "WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 #{trashed_check} AND target_uuid = #{sql_table}.uuid)"
304
305       # Match a read permission link from the user to the record's owner_uuid
306       owner_check = ""
307       if sql_table != "api_client_authorizations" and sql_table != "groups" then
308         owner_check = "OR EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
309           "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) "
310       end
311
312       links_cond = ""
313       if sql_table == "links"
314         # Match any permission link that gives one of the authorized
315         # users some permission _or_ gives anyone else permission to
316         # view one of the authorized users.
317         links_cond = "OR (#{sql_table}.link_class IN (:permission_link_classes) AND "+
318                        "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"
319       end
320
321       sql_conds = "(#{direct_check} #{owner_check} #{links_cond}) #{exclude_trashed_records}"
322
323     end
324
325     if exclude_old_versions != ""
326       if sql_conds.nil?
327         sql_conds = exclude_old_versions[4..-1] # Remove "AND "
328       else
329         sql_conds += " #{exclude_old_versions}"
330       end
331     end
332
333     self.where(sql_conds,
334                user_uuids: user_uuids,
335                permission_link_classes: ['permission', 'resources'])
336   end
337
338   def save_with_unique_name!
339     uuid_was = uuid
340     name_was = name
341     max_retries = 2
342     transaction do
343       conn = ActiveRecord::Base.connection
344       conn.exec_query 'SAVEPOINT save_with_unique_name'
345       begin
346         save!
347       rescue ActiveRecord::RecordNotUnique => rn
348         raise if max_retries == 0
349         max_retries -= 1
350
351         conn.exec_query 'ROLLBACK TO SAVEPOINT save_with_unique_name'
352
353         # Dig into the error to determine if it is specifically calling out a
354         # (owner_uuid, name) uniqueness violation.  In this specific case, and
355         # the client requested a unique name with ensure_unique_name==true,
356         # update the name field and try to save again.  Loop as necessary to
357         # discover a unique name.  It is necessary to handle name choosing at
358         # this level (as opposed to the client) to ensure that record creation
359         # never fails due to a race condition.
360         err = rn.original_exception
361         raise unless err.is_a?(PG::UniqueViolation)
362
363         # Unfortunately ActiveRecord doesn't abstract out any of the
364         # necessary information to figure out if this the error is actually
365         # the specific case where we want to apply the ensure_unique_name
366         # behavior, so the following code is specialized to Postgres.
367         detail = err.result.error_field(PG::Result::PG_DIAG_MESSAGE_DETAIL)
368         raise unless /^Key \(owner_uuid, name\)=\([a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}, .*?\) already exists\./.match detail
369
370         new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
371         if new_name == name
372           # If the database is fast enough to do two attempts in the
373           # same millisecond, we need to wait to ensure we try a
374           # different timestamp on each attempt.
375           sleep 0.002
376           new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
377         end
378
379         self[:name] = new_name
380         if uuid_was.nil? && !uuid.nil?
381           self[:uuid] = nil
382           if self.is_a? Collection
383             # Reset so that is assigned to the new UUID
384             self[:current_version_uuid] = nil
385           end
386         end
387         conn.exec_query 'SAVEPOINT save_with_unique_name'
388         retry
389       ensure
390         conn.exec_query 'RELEASE SAVEPOINT save_with_unique_name'
391       end
392     end
393   end
394
395   def logged_attributes
396     attributes.except(*Rails.configuration.unlogged_attributes)
397   end
398
399   def self.full_text_searchable_columns
400     self.columns.select do |col|
401       [:string, :text, :jsonb].include?(col.type)
402     end.map(&:name)
403   end
404
405   def self.full_text_tsvector
406     parts = full_text_searchable_columns.collect do |column|
407       cast = serialized_attributes[column] ? '::text' : ''
408       "coalesce(#{column}#{cast},'')"
409     end
410     "to_tsvector('english', #{parts.join(" || ' ' || ")})"
411   end
412
413   def self.apply_filters query, filters
414     ft = record_filters filters, self
415     if not ft[:cond_out].any?
416       return query
417     end
418     query.where('(' + ft[:cond_out].join(') AND (') + ')',
419                           *ft[:param_out])
420   end
421
422   protected
423
424   def self.deep_sort_hash(x)
425     if x.is_a? Hash
426       x.sort.collect do |k, v|
427         [k, deep_sort_hash(v)]
428       end.to_h
429     elsif x.is_a? Array
430       x.collect { |v| deep_sort_hash(v) }
431     else
432       x
433     end
434   end
435
436   def ensure_ownership_path_leads_to_user
437     if new_record? or owner_uuid_changed?
438       uuid_in_path = {owner_uuid => true, uuid => true}
439       x = owner_uuid
440       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
441         begin
442           if x == uuid
443             # Test for cycles with the new version, not the DB contents
444             x = owner_uuid
445           elsif !owner_class.respond_to? :find_by_uuid
446             raise ActiveRecord::RecordNotFound.new
447           else
448             x = owner_class.find_by_uuid(x).owner_uuid
449           end
450         rescue ActiveRecord::RecordNotFound => e
451           errors.add :owner_uuid, "is not owned by any user: #{e}"
452           return false
453         end
454         if uuid_in_path[x]
455           if x == owner_uuid
456             errors.add :owner_uuid, "would create an ownership cycle"
457           else
458             errors.add :owner_uuid, "has an ownership cycle"
459           end
460           return false
461         end
462         uuid_in_path[x] = true
463       end
464     end
465     true
466   end
467
468   def set_default_owner
469     if new_record? and current_user and respond_to? :owner_uuid=
470       self.owner_uuid ||= current_user.uuid
471     end
472   end
473
474   def ensure_owner_uuid_is_permitted
475     raise PermissionDeniedError if !current_user
476
477     if self.owner_uuid.nil?
478       errors.add :owner_uuid, "cannot be nil"
479       raise PermissionDeniedError
480     end
481
482     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
483     unless rsc_class == User or rsc_class == Group
484       errors.add :owner_uuid, "must be set to User or Group"
485       raise PermissionDeniedError
486     end
487
488     if new_record? || owner_uuid_changed?
489       # Permission on owner_uuid_was is needed to move an existing
490       # object away from its previous owner (which implies permission
491       # to modify this object itself, so we don't need to check that
492       # separately). Permission on the new owner_uuid is also needed.
493       [['old', owner_uuid_was],
494        ['new', owner_uuid]
495       ].each do |which, check_uuid|
496         if check_uuid.nil?
497           # old_owner_uuid is nil? New record, no need to check.
498         elsif !current_user.can?(write: check_uuid)
499           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}"
500           errors.add :owner_uuid, "cannot be set or changed without write permission on #{which} owner"
501           raise PermissionDeniedError
502         end
503       end
504     else
505       # If the object already existed and we're not changing
506       # owner_uuid, we only need write permission on the object
507       # itself.
508       if !current_user.can?(write: self.uuid)
509         logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} without write permission"
510         errors.add :uuid, "is not writable"
511         raise PermissionDeniedError
512       end
513     end
514
515     true
516   end
517
518   def ensure_permission_to_save
519     unless (new_record? ? permission_to_create : permission_to_update)
520       raise PermissionDeniedError
521     end
522   end
523
524   def permission_to_create
525     current_user.andand.is_active
526   end
527
528   def permission_to_update
529     if !current_user
530       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
531       return false
532     end
533     if !current_user.is_active
534       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
535       return false
536     end
537     return true if current_user.is_admin
538     if self.uuid_changed?
539       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
540       return false
541     end
542     return true
543   end
544
545   def ensure_permission_to_destroy
546     raise PermissionDeniedError unless permission_to_destroy
547   end
548
549   def permission_to_destroy
550     permission_to_update
551   end
552
553   def maybe_update_modified_by_fields
554     update_modified_by_fields if self.changed? or self.new_record?
555     true
556   end
557
558   def update_modified_by_fields
559     current_time = db_current_time
560     self.created_at ||= created_at_was || current_time
561     self.updated_at = current_time
562     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
563     if !anonymous_updater
564       self.modified_by_user_uuid = current_user ? current_user.uuid : nil
565       self.modified_at = current_time
566     end
567     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
568     true
569   end
570
571   def self.has_nonstring_keys? x
572     if x.is_a? Hash
573       x.each do |k,v|
574         return true if !(k.is_a?(String) || k.is_a?(Symbol)) || has_nonstring_keys?(v)
575       end
576     elsif x.is_a? Array
577       x.each do |v|
578         return true if has_nonstring_keys?(v)
579       end
580     end
581     false
582   end
583
584   def self.has_symbols? x
585     if x.is_a? Hash
586       x.each do |k,v|
587         return true if has_symbols?(k) or has_symbols?(v)
588       end
589     elsif x.is_a? Array
590       x.each do |k|
591         return true if has_symbols?(k)
592       end
593     elsif x.is_a? Symbol
594       return true
595     elsif x.is_a? String
596       return true if x.start_with?(':') && !x.start_with?('::')
597     end
598     false
599   end
600
601   def self.recursive_stringify x
602     if x.is_a? Hash
603       Hash[x.collect do |k,v|
604              [recursive_stringify(k), recursive_stringify(v)]
605            end]
606     elsif x.is_a? Array
607       x.collect do |k|
608         recursive_stringify k
609       end
610     elsif x.is_a? Symbol
611       x.to_s
612     elsif x.is_a? String and x.start_with?(':') and !x.start_with?('::')
613       x[1..-1]
614     else
615       x
616     end
617   end
618
619   def self.where_serialized(colname, value, md5: false)
620     colsql = colname.to_s
621     if md5
622       colsql = "md5(#{colsql})"
623     end
624     if value.empty?
625       # rails4 stores as null, rails3 stored as serialized [] or {}
626       sql = "#{colsql} is null or #{colsql} IN (?)"
627       sorted = value
628     else
629       sql = "#{colsql} IN (?)"
630       sorted = deep_sort_hash(value)
631     end
632     params = [sorted.to_yaml, SafeJSON.dump(sorted)]
633     if md5
634       params = params.map { |x| Digest::MD5.hexdigest(x) }
635     end
636     where(sql, params)
637   end
638
639   Serializer = {
640     Hash => HashSerializer,
641     Array => ArraySerializer,
642   }
643
644   def self.serialize(colname, type)
645     coder = Serializer[type]
646     @serialized_attributes ||= {}
647     @serialized_attributes[colname.to_s] = coder
648     super(colname, coder)
649   end
650
651   def self.serialized_attributes
652     @serialized_attributes ||= {}
653   end
654
655   def serialized_attributes
656     self.class.serialized_attributes
657   end
658
659   def convert_serialized_symbols_to_strings
660     # ensure_serialized_attribute_type should prevent symbols from
661     # getting into the database in the first place. If someone managed
662     # to get them into the database (perhaps using an older version)
663     # we'll convert symbols to strings when loading from the
664     # database. (Otherwise, loading and saving an object with existing
665     # symbols in a serialized field will crash.)
666     self.class.serialized_attributes.each do |colname, attr|
667       if self.class.has_symbols? attributes[colname]
668         attributes[colname] = self.class.recursive_stringify attributes[colname]
669         send(colname + '=',
670              self.class.recursive_stringify(attributes[colname]))
671       end
672     end
673   end
674
675   def foreign_key_attributes
676     attributes.keys.select { |a| a.match(/_uuid$/) }
677   end
678
679   def skip_uuid_read_permission_check
680     %w(modified_by_client_uuid)
681   end
682
683   def skip_uuid_existence_check
684     []
685   end
686
687   def normalize_collection_uuids
688     foreign_key_attributes.each do |attr|
689       attr_value = send attr
690       if attr_value.is_a? String and
691           attr_value.match(/^[0-9a-f]{32,}(\+[@\w]+)*$/)
692         begin
693           send "#{attr}=", Collection.normalize_uuid(attr_value)
694         rescue
695           # TODO: abort instead of silently accepting unnormalizable value?
696         end
697       end
698     end
699   end
700
701   @@prefixes_hash = nil
702   def self.uuid_prefixes
703     unless @@prefixes_hash
704       @@prefixes_hash = {}
705       Rails.application.eager_load!
706       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
707         if k.respond_to?(:uuid_prefix)
708           @@prefixes_hash[k.uuid_prefix] = k
709         end
710       end
711     end
712     @@prefixes_hash
713   end
714
715   def self.uuid_like_pattern
716     "#{Rails.configuration.uuid_prefix}-#{uuid_prefix}-_______________"
717   end
718
719   def self.uuid_regex
720     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
721   end
722
723   def ensure_valid_uuids
724     specials = [system_user_uuid]
725
726     foreign_key_attributes.each do |attr|
727       if new_record? or send (attr + "_changed?")
728         next if skip_uuid_existence_check.include? attr
729         attr_value = send attr
730         next if specials.include? attr_value
731         if attr_value
732           if (r = ArvadosModel::resource_class_for_uuid attr_value)
733             unless skip_uuid_read_permission_check.include? attr
734               r = r.readable_by(current_user)
735             end
736             if r.where(uuid: attr_value).count == 0
737               errors.add(attr, "'#{attr_value}' not found")
738             end
739           end
740         end
741       end
742     end
743   end
744
745   class Email
746     def self.kind
747       "email"
748     end
749
750     def kind
751       self.class.kind
752     end
753
754     def self.readable_by (*u)
755       self
756     end
757
758     def self.where (u)
759       [{:uuid => u[:uuid]}]
760     end
761   end
762
763   def self.resource_class_for_uuid(uuid)
764     if uuid.is_a? ArvadosModel
765       return uuid.class
766     end
767     unless uuid.is_a? String
768       return nil
769     end
770
771     uuid.match HasUuid::UUID_REGEX do |re|
772       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
773     end
774
775     if uuid.match(/.+@.+/)
776       return Email
777     end
778
779     nil
780   end
781
782   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
783   # an object in any class.
784   def self.find_by_uuid uuid
785     if self == ArvadosModel
786       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
787       # delegate to the appropriate subclass based on the given uuid.
788       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
789     else
790       super
791     end
792   end
793
794   def is_audit_logging_enabled?
795     return !(Rails.configuration.max_audit_log_age.to_i == 0 &&
796              Rails.configuration.max_audit_log_delete_batch.to_i > 0)
797   end
798
799   def log_start_state
800     if is_audit_logging_enabled?
801       @old_attributes = Marshal.load(Marshal.dump(attributes))
802       @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
803     end
804   end
805
806   def log_change(event_type)
807     if is_audit_logging_enabled?
808       log = Log.new(event_type: event_type).fill_object(self)
809       yield log
810       log.save!
811       log_start_state
812     end
813   end
814
815   def log_create
816     if is_audit_logging_enabled?
817       log_change('create') do |log|
818         log.fill_properties('old', nil, nil)
819         log.update_to self
820       end
821     end
822   end
823
824   def log_update
825     if is_audit_logging_enabled?
826       log_change('update') do |log|
827         log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
828         log.update_to self
829       end
830     end
831   end
832
833   def log_destroy
834     if is_audit_logging_enabled?
835       log_change('delete') do |log|
836         log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
837         log.update_to nil
838       end
839     end
840   end
841 end