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