9799: Remove duplicate uuids from db queries.
[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 of the authorized users.
195     user_uuids = users_list.map { |u| u.uuid }
196
197     # Collect the UUIDs of all groups readable by any of the
198     # authorized users. If one of these (or the UUID of one of the
199     # authorized users themselves) is an object's owner_uuid, that
200     # object is readable.
201     owner_uuids = user_uuids + users_list.flat_map { |u| u.groups_i_can(:read) }
202     owner_uuids.uniq!
203
204     sql_conds = []
205     sql_table = kwargs.fetch(:table_name, table_name)
206
207     # Match any object (evidently a group or user) whose UUID is
208     # listed explicitly in owner_uuids.
209     sql_conds += ["#{sql_table}.uuid in (:owner_uuids)"]
210
211     # Match any object whose owner is listed explicitly in
212     # owner_uuids.
213     sql_conds += ["#{sql_table}.owner_uuid IN (:owner_uuids)"]
214
215     # Match the head of any permission link whose tail is listed
216     # explicitly in owner_uuids.
217     sql_conds += ["#{sql_table}.uuid IN (SELECT head_uuid FROM links WHERE link_class='permission' AND tail_uuid IN (:owner_uuids))"]
218
219     if sql_table == "links"
220       # Match any permission link that gives one of the authorized
221       # users some permission _or_ gives anyone else permission to
222       # view one of the authorized users.
223       sql_conds += ["(#{sql_table}.link_class in (:permission_link_classes) AND "+
224                     "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"]
225     end
226
227     where(sql_conds.join(' OR '),
228           owner_uuids: owner_uuids,
229           user_uuids: user_uuids,
230           permission_link_classes: ['permission', 'resources'])
231   end
232
233   def logged_attributes
234     attributes.except *Rails.configuration.unlogged_attributes
235   end
236
237   def self.full_text_searchable_columns
238     self.columns.select do |col|
239       col.type == :string or col.type == :text
240     end.map(&:name)
241   end
242
243   def self.full_text_tsvector
244     parts = full_text_searchable_columns.collect do |column|
245       "coalesce(#{column},'')"
246     end
247     # We prepend a space to the tsvector() argument here. Otherwise,
248     # it might start with a column that has its own (non-full-text)
249     # index, which causes Postgres to use the column index instead of
250     # the tsvector index, which causes full text queries to be just as
251     # slow as if we had no index at all.
252     "to_tsvector('english', ' ' || #{parts.join(" || ' ' || ")})"
253   end
254
255   protected
256
257   def ensure_ownership_path_leads_to_user
258     if new_record? or owner_uuid_changed?
259       uuid_in_path = {owner_uuid => true, uuid => true}
260       x = owner_uuid
261       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
262         begin
263           if x == uuid
264             # Test for cycles with the new version, not the DB contents
265             x = owner_uuid
266           elsif !owner_class.respond_to? :find_by_uuid
267             raise ActiveRecord::RecordNotFound.new
268           else
269             x = owner_class.find_by_uuid(x).owner_uuid
270           end
271         rescue ActiveRecord::RecordNotFound => e
272           errors.add :owner_uuid, "is not owned by any user: #{e}"
273           return false
274         end
275         if uuid_in_path[x]
276           if x == owner_uuid
277             errors.add :owner_uuid, "would create an ownership cycle"
278           else
279             errors.add :owner_uuid, "has an ownership cycle"
280           end
281           return false
282         end
283         uuid_in_path[x] = true
284       end
285     end
286     true
287   end
288
289   def set_default_owner
290     if new_record? and current_user and respond_to? :owner_uuid=
291       self.owner_uuid ||= current_user.uuid
292     end
293   end
294
295   def ensure_owner_uuid_is_permitted
296     raise PermissionDeniedError if !current_user
297
298     if self.owner_uuid.nil?
299       errors.add :owner_uuid, "cannot be nil"
300       raise PermissionDeniedError
301     end
302
303     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
304     unless rsc_class == User or rsc_class == Group
305       errors.add :owner_uuid, "must be set to User or Group"
306       raise PermissionDeniedError
307     end
308
309     # Verify "write" permission on old owner
310     # default fail unless one of:
311     # owner_uuid did not change
312     # previous owner_uuid is nil
313     # current user is the old owner
314     # current user is this object
315     # current user can_write old owner
316     unless !owner_uuid_changed? or
317         owner_uuid_was.nil? or
318         current_user.uuid == self.owner_uuid_was or
319         current_user.uuid == self.uuid or
320         current_user.can? write: self.owner_uuid_was
321       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}"
322       errors.add :owner_uuid, "cannot be changed without write permission on old owner"
323       raise PermissionDeniedError
324     end
325
326     # Verify "write" permission on new owner
327     # default fail unless one of:
328     # current_user is this object
329     # current user can_write new owner, or this object if owner unchanged
330     if new_record? or owner_uuid_changed? or is_a?(ApiClientAuthorization)
331       write_target = owner_uuid
332     else
333       write_target = uuid
334     end
335     unless current_user == self or current_user.can? write: write_target
336       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}"
337       errors.add :owner_uuid, "cannot be changed without write permission on new owner"
338       raise PermissionDeniedError
339     end
340
341     true
342   end
343
344   def ensure_permission_to_save
345     unless (new_record? ? permission_to_create : permission_to_update)
346       raise PermissionDeniedError
347     end
348   end
349
350   def permission_to_create
351     current_user.andand.is_active
352   end
353
354   def permission_to_update
355     if !current_user
356       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
357       return false
358     end
359     if !current_user.is_active
360       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
361       return false
362     end
363     return true if current_user.is_admin
364     if self.uuid_changed?
365       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
366       return false
367     end
368     return true
369   end
370
371   def ensure_permission_to_destroy
372     raise PermissionDeniedError unless permission_to_destroy
373   end
374
375   def permission_to_destroy
376     permission_to_update
377   end
378
379   def maybe_update_modified_by_fields
380     update_modified_by_fields if self.changed? or self.new_record?
381     true
382   end
383
384   def update_modified_by_fields
385     current_time = db_current_time
386     self.updated_at = current_time
387     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
388     self.modified_at = current_time
389     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
390     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
391     true
392   end
393
394   def self.has_symbols? x
395     if x.is_a? Hash
396       x.each do |k,v|
397         return true if has_symbols?(k) or has_symbols?(v)
398       end
399     elsif x.is_a? Array
400       x.each do |k|
401         return true if has_symbols?(k)
402       end
403     elsif x.is_a? Symbol
404       return true
405     elsif x.is_a? String
406       return true if x.start_with?(':') && !x.start_with?('::')
407     end
408     false
409   end
410
411   def self.recursive_stringify x
412     if x.is_a? Hash
413       Hash[x.collect do |k,v|
414              [recursive_stringify(k), recursive_stringify(v)]
415            end]
416     elsif x.is_a? Array
417       x.collect do |k|
418         recursive_stringify k
419       end
420     elsif x.is_a? Symbol
421       x.to_s
422     elsif x.is_a? String and x.start_with?(':') and !x.start_with?('::')
423       x[1..-1]
424     else
425       x
426     end
427   end
428
429   def ensure_serialized_attribute_type
430     # Specifying a type in the "serialize" declaration causes rails to
431     # raise an exception if a different data type is retrieved from
432     # the database during load().  The validation preventing such
433     # crash-inducing records from being inserted in the database in
434     # the first place seems to have been left as an exercise to the
435     # developer.
436     self.class.serialized_attributes.each do |colname, attr|
437       if attr.object_class
438         if self.attributes[colname].class != attr.object_class
439           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}, not a #{self.attributes[colname].class.to_s}"
440         elsif self.class.has_symbols? attributes[colname]
441           self.errors.add colname.to_sym, "must not contain symbols: #{attributes[colname].inspect}"
442         end
443       end
444     end
445   end
446
447   def convert_serialized_symbols_to_strings
448     # ensure_serialized_attribute_type should prevent symbols from
449     # getting into the database in the first place. If someone managed
450     # to get them into the database (perhaps using an older version)
451     # we'll convert symbols to strings when loading from the
452     # database. (Otherwise, loading and saving an object with existing
453     # symbols in a serialized field will crash.)
454     self.class.serialized_attributes.each do |colname, attr|
455       if self.class.has_symbols? attributes[colname]
456         attributes[colname] = self.class.recursive_stringify attributes[colname]
457         self.send(colname + '=',
458                   self.class.recursive_stringify(attributes[colname]))
459       end
460     end
461   end
462
463   def foreign_key_attributes
464     attributes.keys.select { |a| a.match /_uuid$/ }
465   end
466
467   def skip_uuid_read_permission_check
468     %w(modified_by_client_uuid)
469   end
470
471   def skip_uuid_existence_check
472     []
473   end
474
475   def normalize_collection_uuids
476     foreign_key_attributes.each do |attr|
477       attr_value = send attr
478       if attr_value.is_a? String and
479           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
480         begin
481           send "#{attr}=", Collection.normalize_uuid(attr_value)
482         rescue
483           # TODO: abort instead of silently accepting unnormalizable value?
484         end
485       end
486     end
487   end
488
489   @@prefixes_hash = nil
490   def self.uuid_prefixes
491     unless @@prefixes_hash
492       @@prefixes_hash = {}
493       Rails.application.eager_load!
494       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
495         if k.respond_to?(:uuid_prefix)
496           @@prefixes_hash[k.uuid_prefix] = k
497         end
498       end
499     end
500     @@prefixes_hash
501   end
502
503   def self.uuid_like_pattern
504     "_____-#{uuid_prefix}-_______________"
505   end
506
507   def self.uuid_regex
508     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
509   end
510
511   def ensure_valid_uuids
512     specials = [system_user_uuid]
513
514     foreign_key_attributes.each do |attr|
515       if new_record? or send (attr + "_changed?")
516         next if skip_uuid_existence_check.include? attr
517         attr_value = send attr
518         next if specials.include? attr_value
519         if attr_value
520           if (r = ArvadosModel::resource_class_for_uuid attr_value)
521             unless skip_uuid_read_permission_check.include? attr
522               r = r.readable_by(current_user)
523             end
524             if r.where(uuid: attr_value).count == 0
525               errors.add(attr, "'#{attr_value}' not found")
526             end
527           end
528         end
529       end
530     end
531   end
532
533   class Email
534     def self.kind
535       "email"
536     end
537
538     def kind
539       self.class.kind
540     end
541
542     def self.readable_by (*u)
543       self
544     end
545
546     def self.where (u)
547       [{:uuid => u[:uuid]}]
548     end
549   end
550
551   def self.resource_class_for_uuid(uuid)
552     if uuid.is_a? ArvadosModel
553       return uuid.class
554     end
555     unless uuid.is_a? String
556       return nil
557     end
558     resource_class = nil
559
560     uuid.match HasUuid::UUID_REGEX do |re|
561       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
562     end
563
564     if uuid.match /.+@.+/
565       return Email
566     end
567
568     nil
569   end
570
571   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
572   # an object in any class.
573   def self.find_by_uuid uuid
574     if self == ArvadosModel
575       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
576       # delegate to the appropriate subclass based on the given uuid.
577       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
578     else
579       super
580     end
581   end
582
583   def log_start_state
584     @old_attributes = Marshal.load(Marshal.dump(attributes))
585     @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
586   end
587
588   def log_change(event_type)
589     log = Log.new(event_type: event_type).fill_object(self)
590     yield log
591     log.save!
592     log_start_state
593   end
594
595   def log_create
596     log_change('create') do |log|
597       log.fill_properties('old', nil, nil)
598       log.update_to self
599     end
600   end
601
602   def log_update
603     log_change('update') do |log|
604       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
605       log.update_to self
606     end
607   end
608
609   def log_destroy
610     log_change('destroy') do |log|
611       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
612       log.update_to nil
613     end
614   end
615 end