Merge branch 'master' into 14723-cwl-multiple-file-targets
[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   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   # If async is true at create or update, permission graph
45   # update is deferred allowing making multiple calls without the performance
46   # penalty.
47   attr_accessor :async_permissions_update
48
49   # Ignore listed attributes on mass assignments
50   def self.protected_attributes
51     []
52   end
53
54   class PermissionDeniedError < RequestError
55     def http_status
56       403
57     end
58   end
59
60   class AlreadyLockedError < RequestError
61     def http_status
62       422
63     end
64   end
65
66   class LockFailedError < RequestError
67     def http_status
68       422
69     end
70   end
71
72   class InvalidStateTransitionError < RequestError
73     def http_status
74       422
75     end
76   end
77
78   class UnauthorizedError < RequestError
79     def http_status
80       401
81     end
82   end
83
84   class UnresolvableContainerError < RequestError
85     def http_status
86       422
87     end
88   end
89
90   def self.kind_class(kind)
91     kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
92   end
93
94   def href
95     "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}"
96   end
97
98   def self.permit_attribute_params raw_params
99     # strong_parameters does not provide security: permissions are
100     # implemented with before_save hooks.
101     #
102     # The following permit! is necessary even with
103     # "ActionController::Parameters.permit_all_parameters = true",
104     # because permit_all does not permit nested attributes.
105     raw_params ||= {}
106
107     if raw_params
108       raw_params = raw_params.to_hash
109       raw_params.delete_if { |k, _| self.protected_attributes.include? k }
110       serialized_attributes.each do |colname, coder|
111         param = raw_params[colname.to_sym]
112         if param.nil?
113           # ok
114         elsif !param.is_a?(coder.object_class)
115           raise ArgumentError.new("#{colname} parameter must be #{coder.object_class}, not #{param.class}")
116         elsif has_nonstring_keys?(param)
117           raise ArgumentError.new("#{colname} parameter cannot have non-string hash keys")
118         end
119       end
120       # Check JSONB columns that aren't listed on serialized_attributes
121       columns.select{|c| c.type == :jsonb}.collect{|j| j.name}.each do |colname|
122         if serialized_attributes.include? colname || raw_params[colname.to_sym].nil?
123           next
124         end
125         if has_nonstring_keys?(raw_params[colname.to_sym])
126           raise ArgumentError.new("#{colname} parameter cannot have non-string hash keys")
127         end
128       end
129     end
130     ActionController::Parameters.new(raw_params).permit!
131   end
132
133   def initialize raw_params={}, *args
134     super(self.class.permit_attribute_params(raw_params), *args)
135   end
136
137   # Reload "old attributes" for logging, too.
138   def reload(*args)
139     super
140     log_start_state
141   end
142
143   def self.create raw_params={}, *args
144     super(permit_attribute_params(raw_params), *args)
145   end
146
147   def update_attributes raw_params={}, *args
148     super(self.class.permit_attribute_params(raw_params), *args)
149   end
150
151   def self.selectable_attributes(template=:user)
152     # Return an array of attribute name strings that can be selected
153     # in the given template.
154     api_accessible_attributes(template).map { |attr_spec| attr_spec.first.to_s }
155   end
156
157   def self.searchable_columns operator
158     textonly_operator = !operator.match(/[<=>]/)
159     self.columns.select do |col|
160       case col.type
161       when :string, :text
162         true
163       when :datetime, :integer, :boolean
164         !textonly_operator
165       else
166         false
167       end
168     end.map(&:name)
169   end
170
171   def self.attribute_column attr
172     self.columns.select { |col| col.name == attr.to_s }.first
173   end
174
175   def self.attributes_required_columns
176     # This method returns a hash.  Each key is the name of an API attribute,
177     # and it's mapped to a list of database columns that must be fetched
178     # to generate that attribute.
179     # This implementation generates a simple map of attributes to
180     # matching column names.  Subclasses can override this method
181     # to specify that method-backed API attributes need to fetch
182     # specific columns from the database.
183     all_columns = columns.map(&:name)
184     api_column_map = Hash.new { |hash, key| hash[key] = [] }
185     methods.grep(/^api_accessible_\w+$/).each do |method_name|
186       next if method_name == :api_accessible_attributes
187       send(method_name).each_pair do |api_attr_name, col_name|
188         col_name = col_name.to_s
189         if all_columns.include?(col_name)
190           api_column_map[api_attr_name.to_s] |= [col_name]
191         end
192       end
193     end
194     api_column_map
195   end
196
197   def self.ignored_select_attributes
198     ["href", "kind", "etag"]
199   end
200
201   def self.columns_for_attributes(select_attributes)
202     if select_attributes.empty?
203       raise ArgumentError.new("Attribute selection list cannot be empty")
204     end
205     api_column_map = attributes_required_columns
206     invalid_attrs = []
207     select_attributes.each do |s|
208       next if ignored_select_attributes.include? s
209       if not s.is_a? String or not api_column_map.include? s
210         invalid_attrs << s
211       end
212     end
213     if not invalid_attrs.empty?
214       raise ArgumentError.new("Invalid attribute(s): #{invalid_attrs.inspect}")
215     end
216     # Given an array of attribute names to select, return an array of column
217     # names that must be fetched from the database to satisfy the request.
218     select_attributes.flat_map { |attr| api_column_map[attr] }.uniq
219   end
220
221   def self.default_orders
222     ["#{table_name}.modified_at desc", "#{table_name}.uuid"]
223   end
224
225   def self.unique_columns
226     ["id", "uuid"]
227   end
228
229   def self.limit_index_columns_read
230     # This method returns a list of column names.
231     # If an index request reads that column from the database,
232     # APIs that return lists will only fetch objects until reaching
233     # max_index_database_read bytes of data from those columns.
234     []
235   end
236
237   # If current user can manage the object, return an array of uuids of
238   # users and groups that have permission to write the object. The
239   # first two elements are always [self.owner_uuid, current user's
240   # uuid].
241   #
242   # If current user can write but not manage the object, return
243   # [self.owner_uuid, current user's uuid].
244   #
245   # If current user cannot write this object, just return
246   # [self.owner_uuid].
247   def writable_by
248     return [owner_uuid] if not current_user
249     unless (owner_uuid == current_user.uuid or
250             current_user.is_admin or
251             (current_user.groups_i_can(:manage) & [uuid, owner_uuid]).any?)
252       if ((current_user.groups_i_can(:write) + [current_user.uuid]) &
253           [uuid, owner_uuid]).any?
254         return [owner_uuid, current_user.uuid]
255       else
256         return [owner_uuid]
257       end
258     end
259     [owner_uuid, current_user.uuid] + permissions.collect do |p|
260       if ['can_write', 'can_manage'].index p.name
261         p.tail_uuid
262       end
263     end.compact.uniq
264   end
265
266   # Return a query with read permissions restricted to the union of the
267   # permissions of the members of users_list, i.e. if something is readable by
268   # any user in users_list, it will be readable in the query returned by this
269   # function.
270   def self.readable_by(*users_list)
271     # Get rid of troublesome nils
272     users_list.compact!
273
274     # Load optional keyword arguments, if they exist.
275     if users_list.last.is_a? Hash
276       kwargs = users_list.pop
277     else
278       kwargs = {}
279     end
280
281     # Collect the UUIDs of the authorized users.
282     sql_table = kwargs.fetch(:table_name, table_name)
283     include_trash = kwargs.fetch(:include_trash, false)
284     include_old_versions = kwargs.fetch(:include_old_versions, false)
285
286     sql_conds = nil
287     user_uuids = users_list.map { |u| u.uuid }
288
289     exclude_trashed_records = ""
290     if !include_trash and (sql_table == "groups" or sql_table == "collections") then
291       # Only include records that are not explicitly trashed
292       exclude_trashed_records = "AND #{sql_table}.is_trashed = false"
293     end
294
295     if users_list.select { |u| u.is_admin }.any?
296       # Admin skips most permission checks, but still want to filter on trashed items.
297       if !include_trash
298         if sql_table != "api_client_authorizations"
299           # Only include records where the owner is not trashed
300           sql_conds = "#{sql_table}.owner_uuid NOT IN (SELECT target_uuid FROM #{PERMISSION_VIEW} "+
301                       "WHERE trashed = 1) #{exclude_trashed_records}"
302         end
303       end
304     else
305       trashed_check = ""
306       if !include_trash then
307         trashed_check = "AND trashed = 0"
308       end
309
310       # Note: it is possible to combine the direct_check and
311       # owner_check into a single EXISTS() clause, however it turns
312       # out query optimizer doesn't like it and forces a sequential
313       # table scan.  Constructing the query with separate EXISTS()
314       # clauses enables it to use the index.
315       #
316       # see issue 13208 for details.
317
318       # Match a direct read permission link from the user to the record uuid
319       direct_check = "#{sql_table}.uuid IN (SELECT target_uuid FROM #{PERMISSION_VIEW} "+
320                      "WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 #{trashed_check})"
321
322       # Match a read permission link from the user to the record's owner_uuid
323       owner_check = ""
324       if sql_table != "api_client_authorizations" and sql_table != "groups" then
325         owner_check = "OR #{sql_table}.owner_uuid IN (SELECT target_uuid FROM #{PERMISSION_VIEW} "+
326           "WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 #{trashed_check} AND target_owner_uuid IS NOT NULL) "
327       end
328
329       links_cond = ""
330       if sql_table == "links"
331         # Match any permission link that gives one of the authorized
332         # users some permission _or_ gives anyone else permission to
333         # view one of the authorized users.
334         links_cond = "OR (#{sql_table}.link_class IN (:permission_link_classes) AND "+
335                        "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"
336       end
337
338       sql_conds = "(#{direct_check} #{owner_check} #{links_cond}) #{exclude_trashed_records}"
339
340     end
341
342     if !include_old_versions && sql_table == "collections"
343       exclude_old_versions = "#{sql_table}.uuid = #{sql_table}.current_version_uuid"
344       if sql_conds.nil?
345         sql_conds = exclude_old_versions
346       else
347         sql_conds += " AND #{exclude_old_versions}"
348       end
349     end
350
351     self.where(sql_conds,
352                user_uuids: user_uuids,
353                permission_link_classes: ['permission', 'resources'])
354   end
355
356   def save_with_unique_name!
357     uuid_was = uuid
358     name_was = name
359     max_retries = 2
360     transaction do
361       conn = ActiveRecord::Base.connection
362       conn.exec_query 'SAVEPOINT save_with_unique_name'
363       begin
364         save!
365       rescue ActiveRecord::RecordNotUnique => rn
366         raise if max_retries == 0
367         max_retries -= 1
368
369         conn.exec_query 'ROLLBACK TO SAVEPOINT save_with_unique_name'
370
371         # Dig into the error to determine if it is specifically calling out a
372         # (owner_uuid, name) uniqueness violation.  In this specific case, and
373         # the client requested a unique name with ensure_unique_name==true,
374         # update the name field and try to save again.  Loop as necessary to
375         # discover a unique name.  It is necessary to handle name choosing at
376         # this level (as opposed to the client) to ensure that record creation
377         # never fails due to a race condition.
378         err = rn.cause
379         raise unless err.is_a?(PG::UniqueViolation)
380
381         # Unfortunately ActiveRecord doesn't abstract out any of the
382         # necessary information to figure out if this the error is actually
383         # the specific case where we want to apply the ensure_unique_name
384         # behavior, so the following code is specialized to Postgres.
385         detail = err.result.error_field(PG::Result::PG_DIAG_MESSAGE_DETAIL)
386         raise unless /^Key \(owner_uuid, name\)=\([a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}, .*?\) already exists\./.match detail
387
388         new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
389         if new_name == name
390           # If the database is fast enough to do two attempts in the
391           # same millisecond, we need to wait to ensure we try a
392           # different timestamp on each attempt.
393           sleep 0.002
394           new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
395         end
396
397         self[:name] = new_name
398         if uuid_was.nil? && !uuid.nil?
399           self[:uuid] = nil
400           if self.is_a? Collection
401             # Reset so that is assigned to the new UUID
402             self[:current_version_uuid] = nil
403           end
404         end
405         conn.exec_query 'SAVEPOINT save_with_unique_name'
406         retry
407       ensure
408         conn.exec_query 'RELEASE SAVEPOINT save_with_unique_name'
409       end
410     end
411   end
412
413   def logged_attributes
414     attributes.except(*Rails.configuration.AuditLogs.UnloggedAttributes)
415   end
416
417   def self.full_text_searchable_columns
418     self.columns.select do |col|
419       [:string, :text, :jsonb].include?(col.type)
420     end.map(&:name)
421   end
422
423   def self.full_text_tsvector
424     parts = full_text_searchable_columns.collect do |column|
425       is_jsonb = self.columns.select{|x|x.name == column}[0].type == :jsonb
426       cast = (is_jsonb || serialized_attributes[column]) ? '::text' : ''
427       "coalesce(#{column}#{cast},'')"
428     end
429     "to_tsvector('english', substr(#{parts.join(" || ' ' || ")}, 0, 8000))"
430   end
431
432   def self.apply_filters query, filters
433     ft = record_filters filters, self
434     if not ft[:cond_out].any?
435       return query
436     end
437     query.where('(' + ft[:cond_out].join(') AND (') + ')',
438                           *ft[:param_out])
439   end
440
441   protected
442
443   def self.deep_sort_hash(x)
444     if x.is_a? Hash
445       x.sort.collect do |k, v|
446         [k, deep_sort_hash(v)]
447       end.to_h
448     elsif x.is_a? Array
449       x.collect { |v| deep_sort_hash(v) }
450     else
451       x
452     end
453   end
454
455   def ensure_ownership_path_leads_to_user
456     if new_record? or owner_uuid_changed?
457       uuid_in_path = {owner_uuid => true, uuid => true}
458       x = owner_uuid
459       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
460         begin
461           if x == uuid
462             # Test for cycles with the new version, not the DB contents
463             x = owner_uuid
464           elsif !owner_class.respond_to? :find_by_uuid
465             raise ActiveRecord::RecordNotFound.new
466           else
467             x = owner_class.find_by_uuid(x).owner_uuid
468           end
469         rescue ActiveRecord::RecordNotFound => e
470           errors.add :owner_uuid, "is not owned by any user: #{e}"
471           throw(:abort)
472         end
473         if uuid_in_path[x]
474           if x == owner_uuid
475             errors.add :owner_uuid, "would create an ownership cycle"
476           else
477             errors.add :owner_uuid, "has an ownership cycle"
478           end
479           throw(:abort)
480         end
481         uuid_in_path[x] = true
482       end
483     end
484     true
485   end
486
487   def set_default_owner
488     if new_record? and current_user and respond_to? :owner_uuid=
489       self.owner_uuid ||= current_user.uuid
490     end
491   end
492
493   def ensure_owner_uuid_is_permitted
494     raise PermissionDeniedError if !current_user
495
496     if self.owner_uuid.nil?
497       errors.add :owner_uuid, "cannot be nil"
498       raise PermissionDeniedError
499     end
500
501     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
502     unless rsc_class == User or rsc_class == Group
503       errors.add :owner_uuid, "must be set to User or Group"
504       raise PermissionDeniedError
505     end
506
507     if new_record? || owner_uuid_changed?
508       # Permission on owner_uuid_was is needed to move an existing
509       # object away from its previous owner (which implies permission
510       # to modify this object itself, so we don't need to check that
511       # separately). Permission on the new owner_uuid is also needed.
512       [['old', owner_uuid_was],
513        ['new', owner_uuid]
514       ].each do |which, check_uuid|
515         if check_uuid.nil?
516           # old_owner_uuid is nil? New record, no need to check.
517         elsif !current_user.can?(write: check_uuid)
518           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}"
519           errors.add :owner_uuid, "cannot be set or changed without write permission on #{which} owner"
520           raise PermissionDeniedError
521         end
522       end
523     else
524       # If the object already existed and we're not changing
525       # owner_uuid, we only need write permission on the object
526       # itself.
527       if !current_user.can?(write: self.uuid)
528         logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} without write permission"
529         errors.add :uuid, "is not writable"
530         raise PermissionDeniedError
531       end
532     end
533
534     true
535   end
536
537   def ensure_permission_to_save
538     unless (new_record? ? permission_to_create : permission_to_update)
539       raise PermissionDeniedError
540     end
541   end
542
543   def permission_to_create
544     current_user.andand.is_active
545   end
546
547   def permission_to_update
548     if !current_user
549       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
550       return false
551     end
552     if !current_user.is_active
553       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
554       return false
555     end
556     return true if current_user.is_admin
557     if self.uuid_changed?
558       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
559       return false
560     end
561     return true
562   end
563
564   def ensure_permission_to_destroy
565     raise PermissionDeniedError unless permission_to_destroy
566   end
567
568   def permission_to_destroy
569     permission_to_update
570   end
571
572   def maybe_update_modified_by_fields
573     update_modified_by_fields if self.changed? or self.new_record?
574     true
575   end
576
577   def update_modified_by_fields
578     current_time = db_current_time
579     self.created_at ||= created_at_was || current_time
580     self.updated_at = current_time
581     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
582     if !anonymous_updater
583       self.modified_by_user_uuid = current_user ? current_user.uuid : nil
584     end
585     if !timeless_updater
586       self.modified_at = current_time
587     end
588     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
589     true
590   end
591
592   def self.has_nonstring_keys? x
593     if x.is_a? Hash
594       x.each do |k,v|
595         return true if !(k.is_a?(String) || k.is_a?(Symbol)) || has_nonstring_keys?(v)
596       end
597     elsif x.is_a? Array
598       x.each do |v|
599         return true if has_nonstring_keys?(v)
600       end
601     end
602     false
603   end
604
605   def self.has_symbols? x
606     if x.is_a? Hash
607       x.each do |k,v|
608         return true if has_symbols?(k) or has_symbols?(v)
609       end
610     elsif x.is_a? Array
611       x.each do |k|
612         return true if has_symbols?(k)
613       end
614     elsif x.is_a? Symbol
615       return true
616     elsif x.is_a? String
617       return true if x.start_with?(':') && !x.start_with?('::')
618     end
619     false
620   end
621
622   def self.recursive_stringify x
623     if x.is_a? Hash
624       Hash[x.collect do |k,v|
625              [recursive_stringify(k), recursive_stringify(v)]
626            end]
627     elsif x.is_a? Array
628       x.collect do |k|
629         recursive_stringify k
630       end
631     elsif x.is_a? Symbol
632       x.to_s
633     elsif x.is_a? String and x.start_with?(':') and !x.start_with?('::')
634       x[1..-1]
635     else
636       x
637     end
638   end
639
640   def self.where_serialized(colname, value, md5: false)
641     colsql = colname.to_s
642     if md5
643       colsql = "md5(#{colsql})"
644     end
645     if value.empty?
646       # rails4 stores as null, rails3 stored as serialized [] or {}
647       sql = "#{colsql} is null or #{colsql} IN (?)"
648       sorted = value
649     else
650       sql = "#{colsql} IN (?)"
651       sorted = deep_sort_hash(value)
652     end
653     params = [sorted.to_yaml, SafeJSON.dump(sorted)]
654     if md5
655       params = params.map { |x| Digest::MD5.hexdigest(x) }
656     end
657     where(sql, params)
658   end
659
660   Serializer = {
661     Hash => HashSerializer,
662     Array => ArraySerializer,
663   }
664
665   def self.serialize(colname, type)
666     coder = Serializer[type]
667     @serialized_attributes ||= {}
668     @serialized_attributes[colname.to_s] = coder
669     super(colname, coder)
670   end
671
672   def self.serialized_attributes
673     @serialized_attributes ||= {}
674   end
675
676   def serialized_attributes
677     self.class.serialized_attributes
678   end
679
680   def convert_serialized_symbols_to_strings
681     # ensure_serialized_attribute_type should prevent symbols from
682     # getting into the database in the first place. If someone managed
683     # to get them into the database (perhaps using an older version)
684     # we'll convert symbols to strings when loading from the
685     # database. (Otherwise, loading and saving an object with existing
686     # symbols in a serialized field will crash.)
687     jsonb_cols = self.class.columns.select{|c| c.type == :jsonb}.collect{|j| j.name}
688     (jsonb_cols + self.class.serialized_attributes.keys).uniq.each do |colname|
689       if self.class.has_symbols? attributes[colname]
690         attributes[colname] = self.class.recursive_stringify attributes[colname]
691         send(colname + '=',
692              self.class.recursive_stringify(attributes[colname]))
693       end
694     end
695   end
696
697   def foreign_key_attributes
698     attributes.keys.select { |a| a.match(/_uuid$/) }
699   end
700
701   def skip_uuid_read_permission_check
702     %w(modified_by_client_uuid)
703   end
704
705   def skip_uuid_existence_check
706     []
707   end
708
709   def normalize_collection_uuids
710     foreign_key_attributes.each do |attr|
711       attr_value = send attr
712       if attr_value.is_a? String and
713           attr_value.match(/^[0-9a-f]{32,}(\+[@\w]+)*$/)
714         begin
715           send "#{attr}=", Collection.normalize_uuid(attr_value)
716         rescue
717           # TODO: abort instead of silently accepting unnormalizable value?
718         end
719       end
720     end
721   end
722
723   @@prefixes_hash = nil
724   def self.uuid_prefixes
725     unless @@prefixes_hash
726       @@prefixes_hash = {}
727       Rails.application.eager_load!
728       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
729         if k.respond_to?(:uuid_prefix)
730           @@prefixes_hash[k.uuid_prefix] = k
731         end
732       end
733     end
734     @@prefixes_hash
735   end
736
737   def self.uuid_like_pattern
738     "#{Rails.configuration.ClusterID}-#{uuid_prefix}-_______________"
739   end
740
741   def self.uuid_regex
742     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
743   end
744
745   def ensure_valid_uuids
746     specials = [system_user_uuid]
747
748     foreign_key_attributes.each do |attr|
749       if new_record? or send (attr + "_changed?")
750         next if skip_uuid_existence_check.include? attr
751         attr_value = send attr
752         next if specials.include? attr_value
753         if attr_value
754           if (r = ArvadosModel::resource_class_for_uuid attr_value)
755             unless skip_uuid_read_permission_check.include? attr
756               r = r.readable_by(current_user)
757             end
758             if r.where(uuid: attr_value).count == 0
759               errors.add(attr, "'#{attr_value}' not found")
760             end
761           end
762         end
763       end
764     end
765   end
766
767   class Email
768     def self.kind
769       "email"
770     end
771
772     def kind
773       self.class.kind
774     end
775
776     def self.readable_by (*u)
777       self
778     end
779
780     def self.where (u)
781       [{:uuid => u[:uuid]}]
782     end
783   end
784
785   def self.resource_class_for_uuid(uuid)
786     if uuid.is_a? ArvadosModel
787       return uuid.class
788     end
789     unless uuid.is_a? String
790       return nil
791     end
792
793     uuid.match HasUuid::UUID_REGEX do |re|
794       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
795     end
796
797     if uuid.match(/.+@.+/)
798       return Email
799     end
800
801     nil
802   end
803
804   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
805   # an object in any class.
806   def self.find_by_uuid uuid
807     if self == ArvadosModel
808       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
809       # delegate to the appropriate subclass based on the given uuid.
810       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
811     else
812       super
813     end
814   end
815
816   def is_audit_logging_enabled?
817     return !(Rails.configuration.AuditLogs.MaxAge.to_i == 0 &&
818              Rails.configuration.AuditLogs.MaxDeleteBatch.to_i > 0)
819   end
820
821   def log_start_state
822     if is_audit_logging_enabled?
823       @old_attributes = Marshal.load(Marshal.dump(attributes))
824       @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
825     end
826   end
827
828   def log_change(event_type)
829     if is_audit_logging_enabled?
830       log = Log.new(event_type: event_type).fill_object(self)
831       yield log
832       log.save!
833       log_start_state
834     end
835   end
836
837   def log_create
838     if is_audit_logging_enabled?
839       log_change('create') do |log|
840         log.fill_properties('old', nil, nil)
841         log.update_to self
842       end
843     end
844   end
845
846   def log_update
847     if is_audit_logging_enabled?
848       log_change('update') do |log|
849         log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
850         log.update_to self
851       end
852     end
853   end
854
855   def log_destroy
856     if is_audit_logging_enabled?
857       log_change('delete') do |log|
858         log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
859         log.update_to nil
860       end
861     end
862   end
863 end