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