5737: Merge branch 'master' into 5737-ruby231
[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     # We prepend a space to the tsvector() argument here. Otherwise,
256     # it might start with a column that has its own (non-full-text)
257     # index, which causes Postgres to use the column index instead of
258     # the tsvector index, which causes full text queries to be just as
259     # slow as if we had no index at all.
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 ensure_serialized_attribute_type
459     # Specifying a type in the "serialize" declaration causes rails to
460     # raise an exception if a different data type is retrieved from
461     # the database during load().  The validation preventing such
462     # crash-inducing records from being inserted in the database in
463     # the first place seems to have been left as an exercise to the
464     # developer.
465     self.class.serialized_attributes.each do |colname, attr|
466       if attr.object_class
467         if self.attributes[colname].class != attr.object_class
468           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}, not a #{self.attributes[colname].class.to_s}"
469         elsif self.class.has_symbols? attributes[colname]
470           self.errors.add colname.to_sym, "must not contain symbols: #{attributes[colname].inspect}"
471         end
472       end
473     end
474   end
475
476   def convert_serialized_symbols_to_strings
477     # ensure_serialized_attribute_type should prevent symbols from
478     # getting into the database in the first place. If someone managed
479     # to get them into the database (perhaps using an older version)
480     # we'll convert symbols to strings when loading from the
481     # database. (Otherwise, loading and saving an object with existing
482     # symbols in a serialized field will crash.)
483     self.class.serialized_attributes.each do |colname, attr|
484       if self.class.has_symbols? attributes[colname]
485         attributes[colname] = self.class.recursive_stringify attributes[colname]
486         self.send(colname + '=',
487                   self.class.recursive_stringify(attributes[colname]))
488       end
489     end
490   end
491
492   def foreign_key_attributes
493     attributes.keys.select { |a| a.match(/_uuid$/) }
494   end
495
496   def skip_uuid_read_permission_check
497     %w(modified_by_client_uuid)
498   end
499
500   def skip_uuid_existence_check
501     []
502   end
503
504   def normalize_collection_uuids
505     foreign_key_attributes.each do |attr|
506       attr_value = send attr
507       if attr_value.is_a? String and
508           attr_value.match(/^[0-9a-f]{32,}(\+[@\w]+)*$/)
509         begin
510           send "#{attr}=", Collection.normalize_uuid(attr_value)
511         rescue
512           # TODO: abort instead of silently accepting unnormalizable value?
513         end
514       end
515     end
516   end
517
518   @@prefixes_hash = nil
519   def self.uuid_prefixes
520     unless @@prefixes_hash
521       @@prefixes_hash = {}
522       Rails.application.eager_load!
523       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
524         if k.respond_to?(:uuid_prefix)
525           @@prefixes_hash[k.uuid_prefix] = k
526         end
527       end
528     end
529     @@prefixes_hash
530   end
531
532   def self.uuid_like_pattern
533     "#{Rails.configuration.uuid_prefix}-#{uuid_prefix}-_______________"
534   end
535
536   def self.uuid_regex
537     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
538   end
539
540   def ensure_valid_uuids
541     specials = [system_user_uuid]
542
543     foreign_key_attributes.each do |attr|
544       if new_record? or send (attr + "_changed?")
545         next if skip_uuid_existence_check.include? attr
546         attr_value = send attr
547         next if specials.include? attr_value
548         if attr_value
549           if (r = ArvadosModel::resource_class_for_uuid attr_value)
550             unless skip_uuid_read_permission_check.include? attr
551               r = r.readable_by(current_user)
552             end
553             if r.where(uuid: attr_value).count == 0
554               errors.add(attr, "'#{attr_value}' not found")
555             end
556           end
557         end
558       end
559     end
560   end
561
562   class Email
563     def self.kind
564       "email"
565     end
566
567     def kind
568       self.class.kind
569     end
570
571     def self.readable_by (*u)
572       self
573     end
574
575     def self.where (u)
576       [{:uuid => u[:uuid]}]
577     end
578   end
579
580   def self.resource_class_for_uuid(uuid)
581     if uuid.is_a? ArvadosModel
582       return uuid.class
583     end
584     unless uuid.is_a? String
585       return nil
586     end
587
588     uuid.match HasUuid::UUID_REGEX do |re|
589       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
590     end
591
592     if uuid.match(/.+@.+/)
593       return Email
594     end
595
596     nil
597   end
598
599   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
600   # an object in any class.
601   def self.find_by_uuid uuid
602     if self == ArvadosModel
603       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
604       # delegate to the appropriate subclass based on the given uuid.
605       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
606     else
607       super
608     end
609   end
610
611   def log_start_state
612     @old_attributes = Marshal.load(Marshal.dump(attributes))
613     @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
614   end
615
616   def log_change(event_type)
617     log = Log.new(event_type: event_type).fill_object(self)
618     yield log
619     log.save!
620     log_start_state
621   end
622
623   def log_create
624     log_change('create') do |log|
625       log.fill_properties('old', nil, nil)
626       log.update_to self
627     end
628   end
629
630   def log_update
631     log_change('update') do |log|
632       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
633       log.update_to self
634     end
635   end
636
637   def log_destroy
638     log_change('delete') do |log|
639       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
640       log.update_to nil
641     end
642   end
643 end