Merge branch 'master' of git.curoverse.com:arvados into 11876-r-sdk
[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       [:string, :text, :jsonb].include?(col.type)
361     end.map(&:name)
362   end
363
364   def self.full_text_tsvector
365     parts = full_text_searchable_columns.collect do |column|
366       cast = serialized_attributes[column] ? '::text' : ''
367       "coalesce(#{column}#{cast},'')"
368     end
369     "to_tsvector('english', #{parts.join(" || ' ' || ")})"
370   end
371
372   def self.apply_filters query, filters
373     ft = record_filters filters, self
374     if not ft[:cond_out].any?
375       return query
376     end
377     query.where('(' + ft[:cond_out].join(') AND (') + ')',
378                           *ft[:param_out])
379   end
380
381   protected
382
383   def self.deep_sort_hash(x)
384     if x.is_a? Hash
385       x.sort.collect do |k, v|
386         [k, deep_sort_hash(v)]
387       end.to_h
388     elsif x.is_a? Array
389       x.collect { |v| deep_sort_hash(v) }
390     else
391       x
392     end
393   end
394
395   def ensure_ownership_path_leads_to_user
396     if new_record? or owner_uuid_changed?
397       uuid_in_path = {owner_uuid => true, uuid => true}
398       x = owner_uuid
399       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
400         begin
401           if x == uuid
402             # Test for cycles with the new version, not the DB contents
403             x = owner_uuid
404           elsif !owner_class.respond_to? :find_by_uuid
405             raise ActiveRecord::RecordNotFound.new
406           else
407             x = owner_class.find_by_uuid(x).owner_uuid
408           end
409         rescue ActiveRecord::RecordNotFound => e
410           errors.add :owner_uuid, "is not owned by any user: #{e}"
411           return false
412         end
413         if uuid_in_path[x]
414           if x == owner_uuid
415             errors.add :owner_uuid, "would create an ownership cycle"
416           else
417             errors.add :owner_uuid, "has an ownership cycle"
418           end
419           return false
420         end
421         uuid_in_path[x] = true
422       end
423     end
424     true
425   end
426
427   def set_default_owner
428     if new_record? and current_user and respond_to? :owner_uuid=
429       self.owner_uuid ||= current_user.uuid
430     end
431   end
432
433   def ensure_owner_uuid_is_permitted
434     raise PermissionDeniedError if !current_user
435
436     if self.owner_uuid.nil?
437       errors.add :owner_uuid, "cannot be nil"
438       raise PermissionDeniedError
439     end
440
441     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
442     unless rsc_class == User or rsc_class == Group
443       errors.add :owner_uuid, "must be set to User or Group"
444       raise PermissionDeniedError
445     end
446
447     if new_record? || owner_uuid_changed?
448       # Permission on owner_uuid_was is needed to move an existing
449       # object away from its previous owner (which implies permission
450       # to modify this object itself, so we don't need to check that
451       # separately). Permission on the new owner_uuid is also needed.
452       [['old', owner_uuid_was],
453        ['new', owner_uuid]
454       ].each do |which, check_uuid|
455         if check_uuid.nil?
456           # old_owner_uuid is nil? New record, no need to check.
457         elsif !current_user.can?(write: check_uuid)
458           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}"
459           errors.add :owner_uuid, "cannot be set or changed without write permission on #{which} owner"
460           raise PermissionDeniedError
461         end
462       end
463     else
464       # If the object already existed and we're not changing
465       # owner_uuid, we only need write permission on the object
466       # itself.
467       if !current_user.can?(write: self.uuid)
468         logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} without write permission"
469         errors.add :uuid, "is not writable"
470         raise PermissionDeniedError
471       end
472     end
473
474     true
475   end
476
477   def ensure_permission_to_save
478     unless (new_record? ? permission_to_create : permission_to_update)
479       raise PermissionDeniedError
480     end
481   end
482
483   def permission_to_create
484     current_user.andand.is_active
485   end
486
487   def permission_to_update
488     if !current_user
489       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
490       return false
491     end
492     if !current_user.is_active
493       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
494       return false
495     end
496     return true if current_user.is_admin
497     if self.uuid_changed?
498       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
499       return false
500     end
501     return true
502   end
503
504   def ensure_permission_to_destroy
505     raise PermissionDeniedError unless permission_to_destroy
506   end
507
508   def permission_to_destroy
509     permission_to_update
510   end
511
512   def maybe_update_modified_by_fields
513     update_modified_by_fields if self.changed? or self.new_record?
514     true
515   end
516
517   def update_modified_by_fields
518     current_time = db_current_time
519     self.created_at = created_at_was || current_time
520     self.updated_at = current_time
521     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
522     self.modified_at = current_time
523     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
524     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
525     true
526   end
527
528   def self.has_nonstring_keys? x
529     if x.is_a? Hash
530       x.each do |k,v|
531         return true if !(k.is_a?(String) || k.is_a?(Symbol)) || has_nonstring_keys?(v)
532       end
533     elsif x.is_a? Array
534       x.each do |v|
535         return true if has_nonstring_keys?(v)
536       end
537     end
538     false
539   end
540
541   def self.has_symbols? x
542     if x.is_a? Hash
543       x.each do |k,v|
544         return true if has_symbols?(k) or has_symbols?(v)
545       end
546     elsif x.is_a? Array
547       x.each do |k|
548         return true if has_symbols?(k)
549       end
550     elsif x.is_a? Symbol
551       return true
552     elsif x.is_a? String
553       return true if x.start_with?(':') && !x.start_with?('::')
554     end
555     false
556   end
557
558   def self.recursive_stringify x
559     if x.is_a? Hash
560       Hash[x.collect do |k,v|
561              [recursive_stringify(k), recursive_stringify(v)]
562            end]
563     elsif x.is_a? Array
564       x.collect do |k|
565         recursive_stringify k
566       end
567     elsif x.is_a? Symbol
568       x.to_s
569     elsif x.is_a? String and x.start_with?(':') and !x.start_with?('::')
570       x[1..-1]
571     else
572       x
573     end
574   end
575
576   def self.where_serialized(colname, value)
577     if value.empty?
578       # rails4 stores as null, rails3 stored as serialized [] or {}
579       sql = "#{colname.to_s} is null or #{colname.to_s} IN (?)"
580       sorted = value
581     else
582       sql = "#{colname.to_s} IN (?)"
583       sorted = deep_sort_hash(value)
584     end
585     where(sql, [sorted.to_yaml, SafeJSON.dump(sorted)])
586   end
587
588   Serializer = {
589     Hash => HashSerializer,
590     Array => ArraySerializer,
591   }
592
593   def self.serialize(colname, type)
594     coder = Serializer[type]
595     @serialized_attributes ||= {}
596     @serialized_attributes[colname.to_s] = coder
597     super(colname, coder)
598   end
599
600   def self.serialized_attributes
601     @serialized_attributes ||= {}
602   end
603
604   def serialized_attributes
605     self.class.serialized_attributes
606   end
607
608   def convert_serialized_symbols_to_strings
609     # ensure_serialized_attribute_type should prevent symbols from
610     # getting into the database in the first place. If someone managed
611     # to get them into the database (perhaps using an older version)
612     # we'll convert symbols to strings when loading from the
613     # database. (Otherwise, loading and saving an object with existing
614     # symbols in a serialized field will crash.)
615     self.class.serialized_attributes.each do |colname, attr|
616       if self.class.has_symbols? attributes[colname]
617         attributes[colname] = self.class.recursive_stringify attributes[colname]
618         send(colname + '=',
619              self.class.recursive_stringify(attributes[colname]))
620       end
621     end
622   end
623
624   def foreign_key_attributes
625     attributes.keys.select { |a| a.match(/_uuid$/) }
626   end
627
628   def skip_uuid_read_permission_check
629     %w(modified_by_client_uuid)
630   end
631
632   def skip_uuid_existence_check
633     []
634   end
635
636   def normalize_collection_uuids
637     foreign_key_attributes.each do |attr|
638       attr_value = send attr
639       if attr_value.is_a? String and
640           attr_value.match(/^[0-9a-f]{32,}(\+[@\w]+)*$/)
641         begin
642           send "#{attr}=", Collection.normalize_uuid(attr_value)
643         rescue
644           # TODO: abort instead of silently accepting unnormalizable value?
645         end
646       end
647     end
648   end
649
650   @@prefixes_hash = nil
651   def self.uuid_prefixes
652     unless @@prefixes_hash
653       @@prefixes_hash = {}
654       Rails.application.eager_load!
655       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
656         if k.respond_to?(:uuid_prefix)
657           @@prefixes_hash[k.uuid_prefix] = k
658         end
659       end
660     end
661     @@prefixes_hash
662   end
663
664   def self.uuid_like_pattern
665     "#{Rails.configuration.uuid_prefix}-#{uuid_prefix}-_______________"
666   end
667
668   def self.uuid_regex
669     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
670   end
671
672   def ensure_valid_uuids
673     specials = [system_user_uuid]
674
675     foreign_key_attributes.each do |attr|
676       if new_record? or send (attr + "_changed?")
677         next if skip_uuid_existence_check.include? attr
678         attr_value = send attr
679         next if specials.include? attr_value
680         if attr_value
681           if (r = ArvadosModel::resource_class_for_uuid attr_value)
682             unless skip_uuid_read_permission_check.include? attr
683               r = r.readable_by(current_user)
684             end
685             if r.where(uuid: attr_value).count == 0
686               errors.add(attr, "'#{attr_value}' not found")
687             end
688           end
689         end
690       end
691     end
692   end
693
694   class Email
695     def self.kind
696       "email"
697     end
698
699     def kind
700       self.class.kind
701     end
702
703     def self.readable_by (*u)
704       self
705     end
706
707     def self.where (u)
708       [{:uuid => u[:uuid]}]
709     end
710   end
711
712   def self.resource_class_for_uuid(uuid)
713     if uuid.is_a? ArvadosModel
714       return uuid.class
715     end
716     unless uuid.is_a? String
717       return nil
718     end
719
720     uuid.match HasUuid::UUID_REGEX do |re|
721       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
722     end
723
724     if uuid.match(/.+@.+/)
725       return Email
726     end
727
728     nil
729   end
730
731   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
732   # an object in any class.
733   def self.find_by_uuid uuid
734     if self == ArvadosModel
735       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
736       # delegate to the appropriate subclass based on the given uuid.
737       self.resource_class_for_uuid(uuid).unscoped.find_by_uuid(uuid)
738     else
739       super
740     end
741   end
742
743   def log_start_state
744     @old_attributes = Marshal.load(Marshal.dump(attributes))
745     @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
746   end
747
748   def log_change(event_type)
749     log = Log.new(event_type: event_type).fill_object(self)
750     yield log
751     log.save!
752     log_start_state
753   end
754
755   def log_create
756     log_change('create') do |log|
757       log.fill_properties('old', nil, nil)
758       log.update_to self
759     end
760   end
761
762   def log_update
763     log_change('update') do |log|
764       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
765       log.update_to self
766     end
767   end
768
769   def log_destroy
770     log_change('delete') do |log|
771       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
772       log.update_to nil
773     end
774   end
775 end