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