15720: Merge branch 'master' into 15720-fed-user-list
[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     ft[:joins].each do |t|
461       query = query.joins(t)
462     end
463     query.where('(' + ft[:cond_out].join(') AND (') + ')',
464                           *ft[:param_out])
465   end
466
467   protected
468
469   def self.deep_sort_hash(x)
470     if x.is_a? Hash
471       x.sort.collect do |k, v|
472         [k, deep_sort_hash(v)]
473       end.to_h
474     elsif x.is_a? Array
475       x.collect { |v| deep_sort_hash(v) }
476     else
477       x
478     end
479   end
480
481   def ensure_ownership_path_leads_to_user
482     if new_record? or owner_uuid_changed?
483       uuid_in_path = {owner_uuid => true, uuid => true}
484       x = owner_uuid
485       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
486         begin
487           if x == uuid
488             # Test for cycles with the new version, not the DB contents
489             x = owner_uuid
490           elsif !owner_class.respond_to? :find_by_uuid
491             raise ActiveRecord::RecordNotFound.new
492           else
493             x = owner_class.find_by_uuid(x).owner_uuid
494           end
495         rescue ActiveRecord::RecordNotFound => e
496           errors.add :owner_uuid, "is not owned by any user: #{e}"
497           throw(:abort)
498         end
499         if uuid_in_path[x]
500           if x == owner_uuid
501             errors.add :owner_uuid, "would create an ownership cycle"
502           else
503             errors.add :owner_uuid, "has an ownership cycle"
504           end
505           throw(:abort)
506         end
507         uuid_in_path[x] = true
508       end
509     end
510     true
511   end
512
513   def set_default_owner
514     if new_record? and current_user and respond_to? :owner_uuid=
515       self.owner_uuid ||= current_user.uuid
516     end
517   end
518
519   def ensure_owner_uuid_is_permitted
520     raise PermissionDeniedError if !current_user
521
522     if self.owner_uuid.nil?
523       errors.add :owner_uuid, "cannot be nil"
524       raise PermissionDeniedError
525     end
526
527     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
528     unless rsc_class == User or rsc_class == Group
529       errors.add :owner_uuid, "must be set to User or Group"
530       raise PermissionDeniedError
531     end
532
533     if new_record? || owner_uuid_changed?
534       # Permission on owner_uuid_was is needed to move an existing
535       # object away from its previous owner (which implies permission
536       # to modify this object itself, so we don't need to check that
537       # separately). Permission on the new owner_uuid is also needed.
538       [['old', owner_uuid_was],
539        ['new', owner_uuid]
540       ].each do |which, check_uuid|
541         if check_uuid.nil?
542           # old_owner_uuid is nil? New record, no need to check.
543         elsif !current_user.can?(write: check_uuid)
544           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}"
545           errors.add :owner_uuid, "cannot be set or changed without write permission on #{which} owner"
546           raise PermissionDeniedError
547         end
548       end
549     else
550       # If the object already existed and we're not changing
551       # owner_uuid, we only need write permission on the object
552       # itself.
553       if !current_user.can?(write: self.uuid)
554         logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} without write permission"
555         errors.add :uuid, "is not writable"
556         raise PermissionDeniedError
557       end
558     end
559
560     true
561   end
562
563   def ensure_permission_to_save
564     unless (new_record? ? permission_to_create : permission_to_update)
565       raise PermissionDeniedError
566     end
567   end
568
569   def permission_to_create
570     current_user.andand.is_active
571   end
572
573   def permission_to_update
574     if !current_user
575       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
576       return false
577     end
578     if !current_user.is_active
579       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
580       return false
581     end
582     return true if current_user.is_admin
583     if self.uuid_changed?
584       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
585       return false
586     end
587     return true
588   end
589
590   def ensure_permission_to_destroy
591     raise PermissionDeniedError unless permission_to_destroy
592   end
593
594   def permission_to_destroy
595     permission_to_update
596   end
597
598   def maybe_update_modified_by_fields
599     update_modified_by_fields if self.changed? or self.new_record?
600     true
601   end
602
603   def update_modified_by_fields
604     current_time = db_current_time
605     self.created_at ||= created_at_was || current_time
606     self.updated_at = current_time
607     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
608     if !anonymous_updater
609       self.modified_by_user_uuid = current_user ? current_user.uuid : nil
610     end
611     if !timeless_updater
612       self.modified_at = current_time
613     end
614     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
615     true
616   end
617
618   def self.has_nonstring_keys? x
619     if x.is_a? Hash
620       x.each do |k,v|
621         return true if !(k.is_a?(String) || k.is_a?(Symbol)) || has_nonstring_keys?(v)
622       end
623     elsif x.is_a? Array
624       x.each do |v|
625         return true if has_nonstring_keys?(v)
626       end
627     end
628     false
629   end
630
631   def self.where_serialized(colname, value, md5: false)
632     colsql = colname.to_s
633     if md5
634       colsql = "md5(#{colsql})"
635     end
636     if value.empty?
637       # rails4 stores as null, rails3 stored as serialized [] or {}
638       sql = "#{colsql} is null or #{colsql} IN (?)"
639       sorted = value
640     else
641       sql = "#{colsql} IN (?)"
642       sorted = deep_sort_hash(value)
643     end
644     params = [sorted.to_yaml, SafeJSON.dump(sorted)]
645     if md5
646       params = params.map { |x| Digest::MD5.hexdigest(x) }
647     end
648     where(sql, params)
649   end
650
651   Serializer = {
652     Hash => HashSerializer,
653     Array => ArraySerializer,
654   }
655
656   def self.serialize(colname, type)
657     coder = Serializer[type]
658     @serialized_attributes ||= {}
659     @serialized_attributes[colname.to_s] = coder
660     super(colname, coder)
661   end
662
663   def self.serialized_attributes
664     @serialized_attributes ||= {}
665   end
666
667   def serialized_attributes
668     self.class.serialized_attributes
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.ClusterID}-#{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.AuditLogs.MaxAge.to_i == 0 &&
792              Rails.configuration.AuditLogs.MaxDeleteBatch.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