11139: Merge branch 'master' into 11139-nodemanager-mem-scale-factor
[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 logged_attributes
247     attributes.except(*Rails.configuration.unlogged_attributes)
248   end
249
250   def self.full_text_searchable_columns
251     self.columns.select do |col|
252       col.type == :string or col.type == :text
253     end.map(&:name)
254   end
255
256   def self.full_text_tsvector
257     parts = full_text_searchable_columns.collect do |column|
258       "coalesce(#{column},'')"
259     end
260     "to_tsvector('english', #{parts.join(" || ' ' || ")})"
261   end
262
263   def self.apply_filters query, filters
264     ft = record_filters filters, self
265     if not ft[:cond_out].any?
266       return query
267     end
268     query.where('(' + ft[:cond_out].join(') AND (') + ')',
269                           *ft[:param_out])
270   end
271
272   protected
273
274   def self.deep_sort_hash(x)
275     if x.is_a? Hash
276       x.sort.collect do |k, v|
277         [k, deep_sort_hash(v)]
278       end.to_h
279     elsif x.is_a? Array
280       x.collect { |v| deep_sort_hash(v) }
281     else
282       x
283     end
284   end
285
286   def ensure_ownership_path_leads_to_user
287     if new_record? or owner_uuid_changed?
288       uuid_in_path = {owner_uuid => true, uuid => true}
289       x = owner_uuid
290       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
291         begin
292           if x == uuid
293             # Test for cycles with the new version, not the DB contents
294             x = owner_uuid
295           elsif !owner_class.respond_to? :find_by_uuid
296             raise ActiveRecord::RecordNotFound.new
297           else
298             x = owner_class.find_by_uuid(x).owner_uuid
299           end
300         rescue ActiveRecord::RecordNotFound => e
301           errors.add :owner_uuid, "is not owned by any user: #{e}"
302           return false
303         end
304         if uuid_in_path[x]
305           if x == owner_uuid
306             errors.add :owner_uuid, "would create an ownership cycle"
307           else
308             errors.add :owner_uuid, "has an ownership cycle"
309           end
310           return false
311         end
312         uuid_in_path[x] = true
313       end
314     end
315     true
316   end
317
318   def set_default_owner
319     if new_record? and current_user and respond_to? :owner_uuid=
320       self.owner_uuid ||= current_user.uuid
321     end
322   end
323
324   def ensure_owner_uuid_is_permitted
325     raise PermissionDeniedError if !current_user
326
327     if self.owner_uuid.nil?
328       errors.add :owner_uuid, "cannot be nil"
329       raise PermissionDeniedError
330     end
331
332     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
333     unless rsc_class == User or rsc_class == Group
334       errors.add :owner_uuid, "must be set to User or Group"
335       raise PermissionDeniedError
336     end
337
338     # Verify "write" permission on old owner
339     # default fail unless one of:
340     # owner_uuid did not change
341     # previous owner_uuid is nil
342     # current user is the old owner
343     # current user is this object
344     # current user can_write old owner
345     unless !owner_uuid_changed? or
346         owner_uuid_was.nil? or
347         current_user.uuid == self.owner_uuid_was or
348         current_user.uuid == self.uuid or
349         current_user.can? write: self.owner_uuid_was
350       logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{uuid} but does not have permission to write old owner_uuid #{owner_uuid_was}"
351       errors.add :owner_uuid, "cannot be changed without write permission on old owner"
352       raise PermissionDeniedError
353     end
354
355     # Verify "write" permission on new owner
356     # default fail unless one of:
357     # current_user is this object
358     # current user can_write new owner, or this object if owner unchanged
359     if new_record? or owner_uuid_changed? or is_a?(ApiClientAuthorization)
360       write_target = owner_uuid
361     else
362       write_target = uuid
363     end
364     unless current_user == self or current_user.can? write: write_target
365       logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{uuid} but does not have permission to write new owner_uuid #{owner_uuid}"
366       errors.add :owner_uuid, "cannot be changed without write permission on new owner"
367       raise PermissionDeniedError
368     end
369
370     true
371   end
372
373   def ensure_permission_to_save
374     unless (new_record? ? permission_to_create : permission_to_update)
375       raise PermissionDeniedError
376     end
377   end
378
379   def permission_to_create
380     current_user.andand.is_active
381   end
382
383   def permission_to_update
384     if !current_user
385       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
386       return false
387     end
388     if !current_user.is_active
389       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
390       return false
391     end
392     return true if current_user.is_admin
393     if self.uuid_changed?
394       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
395       return false
396     end
397     return true
398   end
399
400   def ensure_permission_to_destroy
401     raise PermissionDeniedError unless permission_to_destroy
402   end
403
404   def permission_to_destroy
405     permission_to_update
406   end
407
408   def maybe_update_modified_by_fields
409     update_modified_by_fields if self.changed? or self.new_record?
410     true
411   end
412
413   def update_modified_by_fields
414     current_time = db_current_time
415     self.updated_at = current_time
416     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
417     self.modified_at = current_time
418     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
419     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
420     true
421   end
422
423   def self.has_symbols? x
424     if x.is_a? Hash
425       x.each do |k,v|
426         return true if has_symbols?(k) or has_symbols?(v)
427       end
428     elsif x.is_a? Array
429       x.each do |k|
430         return true if has_symbols?(k)
431       end
432     elsif x.is_a? Symbol
433       return true
434     elsif x.is_a? String
435       return true if x.start_with?(':') && !x.start_with?('::')
436     end
437     false
438   end
439
440   def self.recursive_stringify x
441     if x.is_a? Hash
442       Hash[x.collect do |k,v|
443              [recursive_stringify(k), recursive_stringify(v)]
444            end]
445     elsif x.is_a? Array
446       x.collect do |k|
447         recursive_stringify k
448       end
449     elsif x.is_a? Symbol
450       x.to_s
451     elsif x.is_a? String and x.start_with?(':') and !x.start_with?('::')
452       x[1..-1]
453     else
454       x
455     end
456   end
457
458   def self.where_serialized(colname, value)
459     sorted = deep_sort_hash(value)
460     where("#{colname.to_s} IN (?)", [sorted.to_yaml, SafeJSON.dump(sorted)])
461   end
462
463   Serializer = {
464     Hash => HashSerializer,
465     Array => ArraySerializer,
466   }
467
468   def self.serialize(colname, type)
469     super(colname, Serializer[type])
470   end
471
472   def ensure_serialized_attribute_type
473     # Specifying a type in the "serialize" declaration causes rails to
474     # raise an exception if a different data type is retrieved from
475     # the database during load().  The validation preventing such
476     # crash-inducing records from being inserted in the database in
477     # the first place seems to have been left as an exercise to the
478     # developer.
479     self.class.serialized_attributes.each do |colname, attr|
480       if attr.object_class
481         if self.attributes[colname].class != attr.object_class
482           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}, not a #{self.attributes[colname].class.to_s}"
483         elsif self.class.has_symbols? attributes[colname]
484           self.errors.add colname.to_sym, "must not contain symbols: #{attributes[colname].inspect}"
485         end
486       end
487     end
488   end
489
490   def convert_serialized_symbols_to_strings
491     # ensure_serialized_attribute_type should prevent symbols from
492     # getting into the database in the first place. If someone managed
493     # to get them into the database (perhaps using an older version)
494     # we'll convert symbols to strings when loading from the
495     # database. (Otherwise, loading and saving an object with existing
496     # symbols in a serialized field will crash.)
497     self.class.serialized_attributes.each do |colname, attr|
498       if self.class.has_symbols? attributes[colname]
499         attributes[colname] = self.class.recursive_stringify attributes[colname]
500         self.send(colname + '=',
501                   self.class.recursive_stringify(attributes[colname]))
502       end
503     end
504   end
505
506   def foreign_key_attributes
507     attributes.keys.select { |a| a.match(/_uuid$/) }
508   end
509
510   def skip_uuid_read_permission_check
511     %w(modified_by_client_uuid)
512   end
513
514   def skip_uuid_existence_check
515     []
516   end
517
518   def normalize_collection_uuids
519     foreign_key_attributes.each do |attr|
520       attr_value = send attr
521       if attr_value.is_a? String and
522           attr_value.match(/^[0-9a-f]{32,}(\+[@\w]+)*$/)
523         begin
524           send "#{attr}=", Collection.normalize_uuid(attr_value)
525         rescue
526           # TODO: abort instead of silently accepting unnormalizable value?
527         end
528       end
529     end
530   end
531
532   @@prefixes_hash = nil
533   def self.uuid_prefixes
534     unless @@prefixes_hash
535       @@prefixes_hash = {}
536       Rails.application.eager_load!
537       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
538         if k.respond_to?(:uuid_prefix)
539           @@prefixes_hash[k.uuid_prefix] = k
540         end
541       end
542     end
543     @@prefixes_hash
544   end
545
546   def self.uuid_like_pattern
547     "#{Rails.configuration.uuid_prefix}-#{uuid_prefix}-_______________"
548   end
549
550   def self.uuid_regex
551     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
552   end
553
554   def ensure_valid_uuids
555     specials = [system_user_uuid]
556
557     foreign_key_attributes.each do |attr|
558       if new_record? or send (attr + "_changed?")
559         next if skip_uuid_existence_check.include? attr
560         attr_value = send attr
561         next if specials.include? attr_value
562         if attr_value
563           if (r = ArvadosModel::resource_class_for_uuid attr_value)
564             unless skip_uuid_read_permission_check.include? attr
565               r = r.readable_by(current_user)
566             end
567             if r.where(uuid: attr_value).count == 0
568               errors.add(attr, "'#{attr_value}' not found")
569             end
570           end
571         end
572       end
573     end
574   end
575
576   class Email
577     def self.kind
578       "email"
579     end
580
581     def kind
582       self.class.kind
583     end
584
585     def self.readable_by (*u)
586       self
587     end
588
589     def self.where (u)
590       [{:uuid => u[:uuid]}]
591     end
592   end
593
594   def self.resource_class_for_uuid(uuid)
595     if uuid.is_a? ArvadosModel
596       return uuid.class
597     end
598     unless uuid.is_a? String
599       return nil
600     end
601
602     uuid.match HasUuid::UUID_REGEX do |re|
603       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
604     end
605
606     if uuid.match(/.+@.+/)
607       return Email
608     end
609
610     nil
611   end
612
613   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
614   # an object in any class.
615   def self.find_by_uuid uuid
616     if self == ArvadosModel
617       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
618       # delegate to the appropriate subclass based on the given uuid.
619       self.resource_class_for_uuid(uuid).unscoped.find_by_uuid(uuid)
620     else
621       super
622     end
623   end
624
625   def log_start_state
626     @old_attributes = Marshal.load(Marshal.dump(attributes))
627     @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
628   end
629
630   def log_change(event_type)
631     log = Log.new(event_type: event_type).fill_object(self)
632     yield log
633     log.save!
634     log_start_state
635   end
636
637   def log_create
638     log_change('create') do |log|
639       log.fill_properties('old', nil, nil)
640       log.update_to self
641     end
642   end
643
644   def log_update
645     log_change('update') do |log|
646       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
647       log.update_to self
648     end
649   end
650
651   def log_destroy
652     log_change('delete') do |log|
653       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
654       log.update_to nil
655     end
656   end
657 end