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