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