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