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