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