15106: Adds trigram index migration
[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_coalesce
424     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   end
430
431   def self.full_text_trgm
432     "(#{full_text_coalesce.join(" || ' ' || ")})"
433   end
434
435   def self.full_text_tsvector
436     parts = full_text_searchable_columns.collect do |column|
437       is_jsonb = self.columns.select{|x|x.name == column}[0].type == :jsonb
438       cast = (is_jsonb || serialized_attributes[column]) ? '::text' : ''
439       "coalesce(#{column}#{cast},'')"
440     end
441     "to_tsvector('english', substr(#{parts.join(" || ' ' || ")}, 0, 8000))"
442   end
443
444   def self.apply_filters query, filters
445     ft = record_filters filters, self
446     if not ft[:cond_out].any?
447       return query
448     end
449     query.where('(' + ft[:cond_out].join(') AND (') + ')',
450                           *ft[:param_out])
451   end
452
453   protected
454
455   def self.deep_sort_hash(x)
456     if x.is_a? Hash
457       x.sort.collect do |k, v|
458         [k, deep_sort_hash(v)]
459       end.to_h
460     elsif x.is_a? Array
461       x.collect { |v| deep_sort_hash(v) }
462     else
463       x
464     end
465   end
466
467   def ensure_ownership_path_leads_to_user
468     if new_record? or owner_uuid_changed?
469       uuid_in_path = {owner_uuid => true, uuid => true}
470       x = owner_uuid
471       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
472         begin
473           if x == uuid
474             # Test for cycles with the new version, not the DB contents
475             x = owner_uuid
476           elsif !owner_class.respond_to? :find_by_uuid
477             raise ActiveRecord::RecordNotFound.new
478           else
479             x = owner_class.find_by_uuid(x).owner_uuid
480           end
481         rescue ActiveRecord::RecordNotFound => e
482           errors.add :owner_uuid, "is not owned by any user: #{e}"
483           throw(:abort)
484         end
485         if uuid_in_path[x]
486           if x == owner_uuid
487             errors.add :owner_uuid, "would create an ownership cycle"
488           else
489             errors.add :owner_uuid, "has an ownership cycle"
490           end
491           throw(:abort)
492         end
493         uuid_in_path[x] = true
494       end
495     end
496     true
497   end
498
499   def set_default_owner
500     if new_record? and current_user and respond_to? :owner_uuid=
501       self.owner_uuid ||= current_user.uuid
502     end
503   end
504
505   def ensure_owner_uuid_is_permitted
506     raise PermissionDeniedError if !current_user
507
508     if self.owner_uuid.nil?
509       errors.add :owner_uuid, "cannot be nil"
510       raise PermissionDeniedError
511     end
512
513     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
514     unless rsc_class == User or rsc_class == Group
515       errors.add :owner_uuid, "must be set to User or Group"
516       raise PermissionDeniedError
517     end
518
519     if new_record? || owner_uuid_changed?
520       # Permission on owner_uuid_was is needed to move an existing
521       # object away from its previous owner (which implies permission
522       # to modify this object itself, so we don't need to check that
523       # separately). Permission on the new owner_uuid is also needed.
524       [['old', owner_uuid_was],
525        ['new', owner_uuid]
526       ].each do |which, check_uuid|
527         if check_uuid.nil?
528           # old_owner_uuid is nil? New record, no need to check.
529         elsif !current_user.can?(write: check_uuid)
530           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}"
531           errors.add :owner_uuid, "cannot be set or changed without write permission on #{which} owner"
532           raise PermissionDeniedError
533         end
534       end
535     else
536       # If the object already existed and we're not changing
537       # owner_uuid, we only need write permission on the object
538       # itself.
539       if !current_user.can?(write: self.uuid)
540         logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} without write permission"
541         errors.add :uuid, "is not writable"
542         raise PermissionDeniedError
543       end
544     end
545
546     true
547   end
548
549   def ensure_permission_to_save
550     unless (new_record? ? permission_to_create : permission_to_update)
551       raise PermissionDeniedError
552     end
553   end
554
555   def permission_to_create
556     current_user.andand.is_active
557   end
558
559   def permission_to_update
560     if !current_user
561       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
562       return false
563     end
564     if !current_user.is_active
565       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
566       return false
567     end
568     return true if current_user.is_admin
569     if self.uuid_changed?
570       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
571       return false
572     end
573     return true
574   end
575
576   def ensure_permission_to_destroy
577     raise PermissionDeniedError unless permission_to_destroy
578   end
579
580   def permission_to_destroy
581     permission_to_update
582   end
583
584   def maybe_update_modified_by_fields
585     update_modified_by_fields if self.changed? or self.new_record?
586     true
587   end
588
589   def update_modified_by_fields
590     current_time = db_current_time
591     self.created_at ||= created_at_was || current_time
592     self.updated_at = current_time
593     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
594     if !anonymous_updater
595       self.modified_by_user_uuid = current_user ? current_user.uuid : nil
596     end
597     if !timeless_updater
598       self.modified_at = current_time
599     end
600     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
601     true
602   end
603
604   def self.has_nonstring_keys? x
605     if x.is_a? Hash
606       x.each do |k,v|
607         return true if !(k.is_a?(String) || k.is_a?(Symbol)) || has_nonstring_keys?(v)
608       end
609     elsif x.is_a? Array
610       x.each do |v|
611         return true if has_nonstring_keys?(v)
612       end
613     end
614     false
615   end
616
617   def self.has_symbols? x
618     if x.is_a? Hash
619       x.each do |k,v|
620         return true if has_symbols?(k) or has_symbols?(v)
621       end
622     elsif x.is_a? Array
623       x.each do |k|
624         return true if has_symbols?(k)
625       end
626     elsif x.is_a? Symbol
627       return true
628     elsif x.is_a? String
629       return true if x.start_with?(':') && !x.start_with?('::')
630     end
631     false
632   end
633
634   def self.recursive_stringify x
635     if x.is_a? Hash
636       Hash[x.collect do |k,v|
637              [recursive_stringify(k), recursive_stringify(v)]
638            end]
639     elsif x.is_a? Array
640       x.collect do |k|
641         recursive_stringify k
642       end
643     elsif x.is_a? Symbol
644       x.to_s
645     elsif x.is_a? String and x.start_with?(':') and !x.start_with?('::')
646       x[1..-1]
647     else
648       x
649     end
650   end
651
652   def self.where_serialized(colname, value, md5: false)
653     colsql = colname.to_s
654     if md5
655       colsql = "md5(#{colsql})"
656     end
657     if value.empty?
658       # rails4 stores as null, rails3 stored as serialized [] or {}
659       sql = "#{colsql} is null or #{colsql} IN (?)"
660       sorted = value
661     else
662       sql = "#{colsql} IN (?)"
663       sorted = deep_sort_hash(value)
664     end
665     params = [sorted.to_yaml, SafeJSON.dump(sorted)]
666     if md5
667       params = params.map { |x| Digest::MD5.hexdigest(x) }
668     end
669     where(sql, params)
670   end
671
672   Serializer = {
673     Hash => HashSerializer,
674     Array => ArraySerializer,
675   }
676
677   def self.serialize(colname, type)
678     coder = Serializer[type]
679     @serialized_attributes ||= {}
680     @serialized_attributes[colname.to_s] = coder
681     super(colname, coder)
682   end
683
684   def self.serialized_attributes
685     @serialized_attributes ||= {}
686   end
687
688   def serialized_attributes
689     self.class.serialized_attributes
690   end
691
692   def convert_serialized_symbols_to_strings
693     # ensure_serialized_attribute_type should prevent symbols from
694     # getting into the database in the first place. If someone managed
695     # to get them into the database (perhaps using an older version)
696     # we'll convert symbols to strings when loading from the
697     # database. (Otherwise, loading and saving an object with existing
698     # symbols in a serialized field will crash.)
699     jsonb_cols = self.class.columns.select{|c| c.type == :jsonb}.collect{|j| j.name}
700     (jsonb_cols + self.class.serialized_attributes.keys).uniq.each do |colname|
701       if self.class.has_symbols? attributes[colname]
702         attributes[colname] = self.class.recursive_stringify attributes[colname]
703         send(colname + '=',
704              self.class.recursive_stringify(attributes[colname]))
705       end
706     end
707   end
708
709   def foreign_key_attributes
710     attributes.keys.select { |a| a.match(/_uuid$/) }
711   end
712
713   def skip_uuid_read_permission_check
714     %w(modified_by_client_uuid)
715   end
716
717   def skip_uuid_existence_check
718     []
719   end
720
721   def normalize_collection_uuids
722     foreign_key_attributes.each do |attr|
723       attr_value = send attr
724       if attr_value.is_a? String and
725           attr_value.match(/^[0-9a-f]{32,}(\+[@\w]+)*$/)
726         begin
727           send "#{attr}=", Collection.normalize_uuid(attr_value)
728         rescue
729           # TODO: abort instead of silently accepting unnormalizable value?
730         end
731       end
732     end
733   end
734
735   @@prefixes_hash = nil
736   def self.uuid_prefixes
737     unless @@prefixes_hash
738       @@prefixes_hash = {}
739       Rails.application.eager_load!
740       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
741         if k.respond_to?(:uuid_prefix)
742           @@prefixes_hash[k.uuid_prefix] = k
743         end
744       end
745     end
746     @@prefixes_hash
747   end
748
749   def self.uuid_like_pattern
750     "#{Rails.configuration.ClusterID}-#{uuid_prefix}-_______________"
751   end
752
753   def self.uuid_regex
754     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
755   end
756
757   def ensure_valid_uuids
758     specials = [system_user_uuid]
759
760     foreign_key_attributes.each do |attr|
761       if new_record? or send (attr + "_changed?")
762         next if skip_uuid_existence_check.include? attr
763         attr_value = send attr
764         next if specials.include? attr_value
765         if attr_value
766           if (r = ArvadosModel::resource_class_for_uuid attr_value)
767             unless skip_uuid_read_permission_check.include? attr
768               r = r.readable_by(current_user)
769             end
770             if r.where(uuid: attr_value).count == 0
771               errors.add(attr, "'#{attr_value}' not found")
772             end
773           end
774         end
775       end
776     end
777   end
778
779   class Email
780     def self.kind
781       "email"
782     end
783
784     def kind
785       self.class.kind
786     end
787
788     def self.readable_by (*u)
789       self
790     end
791
792     def self.where (u)
793       [{:uuid => u[:uuid]}]
794     end
795   end
796
797   def self.resource_class_for_uuid(uuid)
798     if uuid.is_a? ArvadosModel
799       return uuid.class
800     end
801     unless uuid.is_a? String
802       return nil
803     end
804
805     uuid.match HasUuid::UUID_REGEX do |re|
806       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
807     end
808
809     if uuid.match(/.+@.+/)
810       return Email
811     end
812
813     nil
814   end
815
816   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
817   # an object in any class.
818   def self.find_by_uuid uuid
819     if self == ArvadosModel
820       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
821       # delegate to the appropriate subclass based on the given uuid.
822       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
823     else
824       super
825     end
826   end
827
828   def is_audit_logging_enabled?
829     return !(Rails.configuration.AuditLogs.MaxAge.to_i == 0 &&
830              Rails.configuration.AuditLogs.MaxDeleteBatch.to_i > 0)
831   end
832
833   def log_start_state
834     if is_audit_logging_enabled?
835       @old_attributes = Marshal.load(Marshal.dump(attributes))
836       @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
837     end
838   end
839
840   def log_change(event_type)
841     if is_audit_logging_enabled?
842       log = Log.new(event_type: event_type).fill_object(self)
843       yield log
844       log.save!
845       log_start_state
846     end
847   end
848
849   def log_create
850     if is_audit_logging_enabled?
851       log_change('create') do |log|
852         log.fill_properties('old', nil, nil)
853         log.update_to self
854       end
855     end
856   end
857
858   def log_update
859     if is_audit_logging_enabled?
860       log_change('update') do |log|
861         log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
862         log.update_to self
863       end
864     end
865   end
866
867   def log_destroy
868     if is_audit_logging_enabled?
869       log_change('delete') do |log|
870         log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
871         log.update_to nil
872       end
873     end
874   end
875 end