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