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