Merge branch '12683-keepstore-buffers'
[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     query_on = kwargs.fetch(:query_on, self)
259
260     sql_conds = []
261     user_uuids = users_list.map { |u| u.uuid }
262
263     if users_list.select { |u| u.is_admin }.any?
264       if !include_trash
265         # exclude rows that are explicitly trashed.
266         if sql_table != "api_client_authorizations"
267           sql_conds.push "NOT EXISTS(SELECT 1
268                   FROM #{PERMISSION_VIEW}
269                   WHERE trashed = 1 AND
270                   (#{sql_table}.uuid = target_uuid OR #{sql_table}.owner_uuid = target_uuid))"
271         end
272       end
273     else
274       if include_trash
275         trashed_check = ""
276       else
277         trashed_check = "AND trashed = 0"
278       end
279
280       if sql_table != "api_client_authorizations" and sql_table != "groups"
281         owner_check = "OR (target_uuid = #{sql_table}.owner_uuid AND target_owner_uuid IS NOT NULL)"
282       else
283         owner_check = ""
284       end
285
286       sql_conds.push "EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
287                      "WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 #{trashed_check} AND (target_uuid = #{sql_table}.uuid #{owner_check}))"
288
289       if sql_table == "links"
290         # Match any permission link that gives one of the authorized
291         # users some permission _or_ gives anyone else permission to
292         # view one of the authorized users.
293         sql_conds.push "(#{sql_table}.link_class IN (:permission_link_classes) AND "+
294                        "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"
295       end
296     end
297
298     query_on.where(sql_conds.join(' OR '),
299                     user_uuids: user_uuids,
300                     permission_link_classes: ['permission', 'resources'])
301   end
302
303   def save_with_unique_name!
304     uuid_was = uuid
305     name_was = name
306     max_retries = 2
307     transaction do
308       conn = ActiveRecord::Base.connection
309       conn.exec_query 'SAVEPOINT save_with_unique_name'
310       begin
311         save!
312       rescue ActiveRecord::RecordNotUnique => rn
313         raise if max_retries == 0
314         max_retries -= 1
315
316         conn.exec_query 'ROLLBACK TO SAVEPOINT save_with_unique_name'
317
318         # Dig into the error to determine if it is specifically calling out a
319         # (owner_uuid, name) uniqueness violation.  In this specific case, and
320         # the client requested a unique name with ensure_unique_name==true,
321         # update the name field and try to save again.  Loop as necessary to
322         # discover a unique name.  It is necessary to handle name choosing at
323         # this level (as opposed to the client) to ensure that record creation
324         # never fails due to a race condition.
325         err = rn.original_exception
326         raise unless err.is_a?(PG::UniqueViolation)
327
328         # Unfortunately ActiveRecord doesn't abstract out any of the
329         # necessary information to figure out if this the error is actually
330         # the specific case where we want to apply the ensure_unique_name
331         # behavior, so the following code is specialized to Postgres.
332         detail = err.result.error_field(PG::Result::PG_DIAG_MESSAGE_DETAIL)
333         raise unless /^Key \(owner_uuid, name\)=\([a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}, .*?\) already exists\./.match detail
334
335         new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
336         if new_name == name
337           # If the database is fast enough to do two attempts in the
338           # same millisecond, we need to wait to ensure we try a
339           # different timestamp on each attempt.
340           sleep 0.002
341           new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
342         end
343
344         self[:name] = new_name
345         self[:uuid] = nil if uuid_was.nil? && !uuid.nil?
346         conn.exec_query 'SAVEPOINT save_with_unique_name'
347         retry
348       ensure
349         conn.exec_query 'RELEASE SAVEPOINT save_with_unique_name'
350       end
351     end
352   end
353
354   def logged_attributes
355     attributes.except(*Rails.configuration.unlogged_attributes)
356   end
357
358   def self.full_text_searchable_columns
359     self.columns.select do |col|
360       col.type == :string or col.type == :text
361     end.map(&:name)
362   end
363
364   def self.full_text_tsvector
365     parts = full_text_searchable_columns.collect do |column|
366       "coalesce(#{column},'')"
367     end
368     "to_tsvector('english', #{parts.join(" || ' ' || ")})"
369   end
370
371   def self.apply_filters query, filters
372     ft = record_filters filters, self
373     if not ft[:cond_out].any?
374       return query
375     end
376     query.where('(' + ft[:cond_out].join(') AND (') + ')',
377                           *ft[:param_out])
378   end
379
380   protected
381
382   def self.deep_sort_hash(x)
383     if x.is_a? Hash
384       x.sort.collect do |k, v|
385         [k, deep_sort_hash(v)]
386       end.to_h
387     elsif x.is_a? Array
388       x.collect { |v| deep_sort_hash(v) }
389     else
390       x
391     end
392   end
393
394   def ensure_ownership_path_leads_to_user
395     if new_record? or owner_uuid_changed?
396       uuid_in_path = {owner_uuid => true, uuid => true}
397       x = owner_uuid
398       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
399         begin
400           if x == uuid
401             # Test for cycles with the new version, not the DB contents
402             x = owner_uuid
403           elsif !owner_class.respond_to? :find_by_uuid
404             raise ActiveRecord::RecordNotFound.new
405           else
406             x = owner_class.find_by_uuid(x).owner_uuid
407           end
408         rescue ActiveRecord::RecordNotFound => e
409           errors.add :owner_uuid, "is not owned by any user: #{e}"
410           return false
411         end
412         if uuid_in_path[x]
413           if x == owner_uuid
414             errors.add :owner_uuid, "would create an ownership cycle"
415           else
416             errors.add :owner_uuid, "has an ownership cycle"
417           end
418           return false
419         end
420         uuid_in_path[x] = true
421       end
422     end
423     true
424   end
425
426   def set_default_owner
427     if new_record? and current_user and respond_to? :owner_uuid=
428       self.owner_uuid ||= current_user.uuid
429     end
430   end
431
432   def ensure_owner_uuid_is_permitted
433     raise PermissionDeniedError if !current_user
434
435     if self.owner_uuid.nil?
436       errors.add :owner_uuid, "cannot be nil"
437       raise PermissionDeniedError
438     end
439
440     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
441     unless rsc_class == User or rsc_class == Group
442       errors.add :owner_uuid, "must be set to User or Group"
443       raise PermissionDeniedError
444     end
445
446     if new_record? || owner_uuid_changed?
447       # Permission on owner_uuid_was is needed to move an existing
448       # object away from its previous owner (which implies permission
449       # to modify this object itself, so we don't need to check that
450       # separately). Permission on the new owner_uuid is also needed.
451       [['old', owner_uuid_was],
452        ['new', owner_uuid]
453       ].each do |which, check_uuid|
454         if check_uuid.nil?
455           # old_owner_uuid is nil? New record, no need to check.
456         elsif !current_user.can?(write: check_uuid)
457           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}"
458           errors.add :owner_uuid, "cannot be set or changed without write permission on #{which} owner"
459           raise PermissionDeniedError
460         end
461       end
462     else
463       # If the object already existed and we're not changing
464       # owner_uuid, we only need write permission on the object
465       # itself.
466       if !current_user.can?(write: self.uuid)
467         logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} without write permission"
468         errors.add :uuid, "is not writable"
469         raise PermissionDeniedError
470       end
471     end
472
473     true
474   end
475
476   def ensure_permission_to_save
477     unless (new_record? ? permission_to_create : permission_to_update)
478       raise PermissionDeniedError
479     end
480   end
481
482   def permission_to_create
483     current_user.andand.is_active
484   end
485
486   def permission_to_update
487     if !current_user
488       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
489       return false
490     end
491     if !current_user.is_active
492       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
493       return false
494     end
495     return true if current_user.is_admin
496     if self.uuid_changed?
497       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
498       return false
499     end
500     return true
501   end
502
503   def ensure_permission_to_destroy
504     raise PermissionDeniedError unless permission_to_destroy
505   end
506
507   def permission_to_destroy
508     permission_to_update
509   end
510
511   def maybe_update_modified_by_fields
512     update_modified_by_fields if self.changed? or self.new_record?
513     true
514   end
515
516   def update_modified_by_fields
517     current_time = db_current_time
518     self.created_at = created_at_was || current_time
519     self.updated_at = current_time
520     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
521     self.modified_at = current_time
522     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
523     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
524     true
525   end
526
527   def self.has_nonstring_keys? x
528     if x.is_a? Hash
529       x.each do |k,v|
530         return true if !(k.is_a?(String) || k.is_a?(Symbol)) || has_nonstring_keys?(v)
531       end
532     elsif x.is_a? Array
533       x.each do |v|
534         return true if has_nonstring_keys?(v)
535       end
536     end
537     false
538   end
539
540   def self.has_symbols? x
541     if x.is_a? Hash
542       x.each do |k,v|
543         return true if has_symbols?(k) or has_symbols?(v)
544       end
545     elsif x.is_a? Array
546       x.each do |k|
547         return true if has_symbols?(k)
548       end
549     elsif x.is_a? Symbol
550       return true
551     elsif x.is_a? String
552       return true if x.start_with?(':') && !x.start_with?('::')
553     end
554     false
555   end
556
557   def self.recursive_stringify x
558     if x.is_a? Hash
559       Hash[x.collect do |k,v|
560              [recursive_stringify(k), recursive_stringify(v)]
561            end]
562     elsif x.is_a? Array
563       x.collect do |k|
564         recursive_stringify k
565       end
566     elsif x.is_a? Symbol
567       x.to_s
568     elsif x.is_a? String and x.start_with?(':') and !x.start_with?('::')
569       x[1..-1]
570     else
571       x
572     end
573   end
574
575   def self.where_serialized(colname, value)
576     if value.empty?
577       # rails4 stores as null, rails3 stored as serialized [] or {}
578       sql = "#{colname.to_s} is null or #{colname.to_s} IN (?)"
579       sorted = value
580     else
581       sql = "#{colname.to_s} IN (?)"
582       sorted = deep_sort_hash(value)
583     end
584     where(sql, [sorted.to_yaml, SafeJSON.dump(sorted)])
585   end
586
587   Serializer = {
588     Hash => HashSerializer,
589     Array => ArraySerializer,
590   }
591
592   def self.serialize(colname, type)
593     coder = Serializer[type]
594     @serialized_attributes ||= {}
595     @serialized_attributes[colname.to_s] = coder
596     super(colname, coder)
597   end
598
599   def self.serialized_attributes
600     @serialized_attributes ||= {}
601   end
602
603   def serialized_attributes
604     self.class.serialized_attributes
605   end
606
607   def convert_serialized_symbols_to_strings
608     # ensure_serialized_attribute_type should prevent symbols from
609     # getting into the database in the first place. If someone managed
610     # to get them into the database (perhaps using an older version)
611     # we'll convert symbols to strings when loading from the
612     # database. (Otherwise, loading and saving an object with existing
613     # symbols in a serialized field will crash.)
614     self.class.serialized_attributes.each do |colname, attr|
615       if self.class.has_symbols? attributes[colname]
616         attributes[colname] = self.class.recursive_stringify attributes[colname]
617         send(colname + '=',
618              self.class.recursive_stringify(attributes[colname]))
619       end
620     end
621   end
622
623   def foreign_key_attributes
624     attributes.keys.select { |a| a.match(/_uuid$/) }
625   end
626
627   def skip_uuid_read_permission_check
628     %w(modified_by_client_uuid)
629   end
630
631   def skip_uuid_existence_check
632     []
633   end
634
635   def normalize_collection_uuids
636     foreign_key_attributes.each do |attr|
637       attr_value = send attr
638       if attr_value.is_a? String and
639           attr_value.match(/^[0-9a-f]{32,}(\+[@\w]+)*$/)
640         begin
641           send "#{attr}=", Collection.normalize_uuid(attr_value)
642         rescue
643           # TODO: abort instead of silently accepting unnormalizable value?
644         end
645       end
646     end
647   end
648
649   @@prefixes_hash = nil
650   def self.uuid_prefixes
651     unless @@prefixes_hash
652       @@prefixes_hash = {}
653       Rails.application.eager_load!
654       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
655         if k.respond_to?(:uuid_prefix)
656           @@prefixes_hash[k.uuid_prefix] = k
657         end
658       end
659     end
660     @@prefixes_hash
661   end
662
663   def self.uuid_like_pattern
664     "#{Rails.configuration.uuid_prefix}-#{uuid_prefix}-_______________"
665   end
666
667   def self.uuid_regex
668     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
669   end
670
671   def ensure_valid_uuids
672     specials = [system_user_uuid]
673
674     foreign_key_attributes.each do |attr|
675       if new_record? or send (attr + "_changed?")
676         next if skip_uuid_existence_check.include? attr
677         attr_value = send attr
678         next if specials.include? attr_value
679         if attr_value
680           if (r = ArvadosModel::resource_class_for_uuid attr_value)
681             unless skip_uuid_read_permission_check.include? attr
682               r = r.readable_by(current_user)
683             end
684             if r.where(uuid: attr_value).count == 0
685               errors.add(attr, "'#{attr_value}' not found")
686             end
687           end
688         end
689       end
690     end
691   end
692
693   class Email
694     def self.kind
695       "email"
696     end
697
698     def kind
699       self.class.kind
700     end
701
702     def self.readable_by (*u)
703       self
704     end
705
706     def self.where (u)
707       [{:uuid => u[:uuid]}]
708     end
709   end
710
711   def self.resource_class_for_uuid(uuid)
712     if uuid.is_a? ArvadosModel
713       return uuid.class
714     end
715     unless uuid.is_a? String
716       return nil
717     end
718
719     uuid.match HasUuid::UUID_REGEX do |re|
720       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
721     end
722
723     if uuid.match(/.+@.+/)
724       return Email
725     end
726
727     nil
728   end
729
730   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
731   # an object in any class.
732   def self.find_by_uuid uuid
733     if self == ArvadosModel
734       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
735       # delegate to the appropriate subclass based on the given uuid.
736       self.resource_class_for_uuid(uuid).unscoped.find_by_uuid(uuid)
737     else
738       super
739     end
740   end
741
742   def log_start_state
743     @old_attributes = Marshal.load(Marshal.dump(attributes))
744     @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
745   end
746
747   def log_change(event_type)
748     log = Log.new(event_type: event_type).fill_object(self)
749     yield log
750     log.save!
751     log_start_state
752   end
753
754   def log_create
755     log_change('create') do |log|
756       log.fill_properties('old', nil, nil)
757       log.update_to self
758     end
759   end
760
761   def log_update
762     log_change('update') do |log|
763       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
764       log.update_to self
765     end
766   end
767
768   def log_destroy
769     log_change('delete') do |log|
770       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
771       log.update_to nil
772     end
773   end
774 end