11720: Merge branch 'master' into 11720-govendor
[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   after_initialize :log_start_state
13   before_save :ensure_permission_to_save
14   before_save :ensure_owner_uuid_is_permitted
15   before_save :ensure_ownership_path_leads_to_user
16   before_destroy :ensure_owner_uuid_is_permitted
17   before_destroy :ensure_permission_to_destroy
18   before_create :update_modified_by_fields
19   before_update :maybe_update_modified_by_fields
20   after_create :log_create
21   after_update :log_update
22   after_destroy :log_destroy
23   after_find :convert_serialized_symbols_to_strings
24   before_validation :normalize_collection_uuids
25   before_validation :set_default_owner
26   validate :ensure_valid_uuids
27
28   # Note: This only returns permission links. It does not account for
29   # permissions obtained via user.is_admin or
30   # user.uuid==object.owner_uuid.
31   has_many(:permissions,
32            ->{where(link_class: 'permission')},
33            foreign_key: :head_uuid,
34            class_name: 'Link',
35            primary_key: :uuid)
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 LockFailedError < StandardError
50     def http_status
51       422
52     end
53   end
54
55   class InvalidStateTransitionError < StandardError
56     def http_status
57       422
58     end
59   end
60
61   class UnauthorizedError < StandardError
62     def http_status
63       401
64     end
65   end
66
67   class UnresolvableContainerError < StandardError
68     def http_status
69       422
70     end
71   end
72
73   def self.kind_class(kind)
74     kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
75   end
76
77   def href
78     "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}"
79   end
80
81   def self.permit_attribute_params raw_params
82     # strong_parameters does not provide security: permissions are
83     # implemented with before_save hooks.
84     #
85     # The following permit! is necessary even with
86     # "ActionController::Parameters.permit_all_parameters = true",
87     # because permit_all does not permit nested attributes.
88     if raw_params
89       serialized_attributes.each do |colname, coder|
90         param = raw_params[colname.to_sym]
91         if param.nil?
92           # ok
93         elsif !param.is_a?(coder.object_class)
94           raise ArgumentError.new("#{colname} parameter must be #{coder.object_class}, not #{param.class}")
95         elsif has_nonstring_keys?(param)
96           raise ArgumentError.new("#{colname} parameter cannot have non-string hash keys")
97         end
98       end
99     end
100     ActionController::Parameters.new(raw_params).permit!
101   end
102
103   def initialize raw_params={}, *args
104     super(self.class.permit_attribute_params(raw_params), *args)
105   end
106
107   # Reload "old attributes" for logging, too.
108   def reload(*args)
109     super
110     log_start_state
111   end
112
113   def self.create raw_params={}, *args
114     super(permit_attribute_params(raw_params), *args)
115   end
116
117   def update_attributes raw_params={}, *args
118     super(self.class.permit_attribute_params(raw_params), *args)
119   end
120
121   def self.selectable_attributes(template=:user)
122     # Return an array of attribute name strings that can be selected
123     # in the given template.
124     api_accessible_attributes(template).map { |attr_spec| attr_spec.first.to_s }
125   end
126
127   def self.searchable_columns operator
128     textonly_operator = !operator.match(/[<=>]/)
129     self.columns.select do |col|
130       case col.type
131       when :string, :text
132         true
133       when :datetime, :integer, :boolean
134         !textonly_operator
135       else
136         false
137       end
138     end.map(&:name)
139   end
140
141   def self.attribute_column attr
142     self.columns.select { |col| col.name == attr.to_s }.first
143   end
144
145   def self.attributes_required_columns
146     # This method returns a hash.  Each key is the name of an API attribute,
147     # and it's mapped to a list of database columns that must be fetched
148     # to generate that attribute.
149     # This implementation generates a simple map of attributes to
150     # matching column names.  Subclasses can override this method
151     # to specify that method-backed API attributes need to fetch
152     # specific columns from the database.
153     all_columns = columns.map(&:name)
154     api_column_map = Hash.new { |hash, key| hash[key] = [] }
155     methods.grep(/^api_accessible_\w+$/).each do |method_name|
156       next if method_name == :api_accessible_attributes
157       send(method_name).each_pair do |api_attr_name, col_name|
158         col_name = col_name.to_s
159         if all_columns.include?(col_name)
160           api_column_map[api_attr_name.to_s] |= [col_name]
161         end
162       end
163     end
164     api_column_map
165   end
166
167   def self.ignored_select_attributes
168     ["href", "kind", "etag"]
169   end
170
171   def self.columns_for_attributes(select_attributes)
172     if select_attributes.empty?
173       raise ArgumentError.new("Attribute selection list cannot be empty")
174     end
175     api_column_map = attributes_required_columns
176     invalid_attrs = []
177     select_attributes.each do |s|
178       next if ignored_select_attributes.include? s
179       if not s.is_a? String or not api_column_map.include? s
180         invalid_attrs << s
181       end
182     end
183     if not invalid_attrs.empty?
184       raise ArgumentError.new("Invalid attribute(s): #{invalid_attrs.inspect}")
185     end
186     # Given an array of attribute names to select, return an array of column
187     # names that must be fetched from the database to satisfy the request.
188     select_attributes.flat_map { |attr| api_column_map[attr] }.uniq
189   end
190
191   def self.default_orders
192     ["#{table_name}.modified_at desc", "#{table_name}.uuid"]
193   end
194
195   def self.unique_columns
196     ["id", "uuid"]
197   end
198
199   def self.limit_index_columns_read
200     # This method returns a list of column names.
201     # If an index request reads that column from the database,
202     # APIs that return lists will only fetch objects until reaching
203     # max_index_database_read bytes of data from those columns.
204     []
205   end
206
207   # If current user can manage the object, return an array of uuids of
208   # users and groups that have permission to write the object. The
209   # first two elements are always [self.owner_uuid, current user's
210   # uuid].
211   #
212   # If current user can write but not manage the object, return
213   # [self.owner_uuid, current user's uuid].
214   #
215   # If current user cannot write this object, just return
216   # [self.owner_uuid].
217   def writable_by
218     return [owner_uuid] if not current_user
219     unless (owner_uuid == current_user.uuid or
220             current_user.is_admin or
221             (current_user.groups_i_can(:manage) & [uuid, owner_uuid]).any?)
222       if ((current_user.groups_i_can(:write) + [current_user.uuid]) &
223           [uuid, owner_uuid]).any?
224         return [owner_uuid, current_user.uuid]
225       else
226         return [owner_uuid]
227       end
228     end
229     [owner_uuid, current_user.uuid] + permissions.collect do |p|
230       if ['can_write', 'can_manage'].index p.name
231         p.tail_uuid
232       end
233     end.compact.uniq
234   end
235
236   # Return a query with read permissions restricted to the union of of the
237   # permissions of the members of users_list, i.e. if something is readable by
238   # any user in users_list, it will be readable in the query returned by this
239   # function.
240   def self.readable_by(*users_list)
241     # Get rid of troublesome nils
242     users_list.compact!
243
244     # Load optional keyword arguments, if they exist.
245     if users_list.last.is_a? Hash
246       kwargs = users_list.pop
247     else
248       kwargs = {}
249     end
250
251     # Check if any of the users are admin.  If so, we're done.
252     if users_list.select { |u| u.is_admin }.any?
253       return self
254     end
255
256     # Collect the UUIDs of the authorized users.
257     user_uuids = users_list.map { |u| u.uuid }
258
259     # Collect the UUIDs of all groups readable by any of the
260     # authorized users. If one of these (or the UUID of one of the
261     # authorized users themselves) is an object's owner_uuid, that
262     # object is readable.
263     owner_uuids = user_uuids + users_list.flat_map { |u| u.groups_i_can(:read) }
264     owner_uuids.uniq!
265
266     sql_conds = []
267     sql_table = kwargs.fetch(:table_name, table_name)
268
269     # Match any object (evidently a group or user) whose UUID is
270     # listed explicitly in owner_uuids.
271     sql_conds += ["#{sql_table}.uuid in (:owner_uuids)"]
272
273     # Match any object whose owner is listed explicitly in
274     # owner_uuids.
275     sql_conds += ["#{sql_table}.owner_uuid IN (:owner_uuids)"]
276
277     # Match the head of any permission link whose tail is listed
278     # explicitly in owner_uuids.
279     sql_conds += ["#{sql_table}.uuid IN (SELECT head_uuid FROM links WHERE link_class='permission' AND tail_uuid IN (:owner_uuids))"]
280
281     if sql_table == "links"
282       # Match any permission link that gives one of the authorized
283       # users some permission _or_ gives anyone else permission to
284       # view one of the authorized users.
285       sql_conds += ["(#{sql_table}.link_class in (:permission_link_classes) AND "+
286                     "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"]
287     end
288
289     where(sql_conds.join(' OR '),
290           owner_uuids: owner_uuids,
291           user_uuids: user_uuids,
292           permission_link_classes: ['permission', 'resources'])
293   end
294
295   def save_with_unique_name!
296     uuid_was = uuid
297     name_was = name
298     max_retries = 2
299     transaction do
300       conn = ActiveRecord::Base.connection
301       conn.exec_query 'SAVEPOINT save_with_unique_name'
302       begin
303         save!
304       rescue ActiveRecord::RecordNotUnique => rn
305         raise if max_retries == 0
306         max_retries -= 1
307
308         conn.exec_query 'ROLLBACK TO SAVEPOINT save_with_unique_name'
309
310         # Dig into the error to determine if it is specifically calling out a
311         # (owner_uuid, name) uniqueness violation.  In this specific case, and
312         # the client requested a unique name with ensure_unique_name==true,
313         # update the name field and try to save again.  Loop as necessary to
314         # discover a unique name.  It is necessary to handle name choosing at
315         # this level (as opposed to the client) to ensure that record creation
316         # never fails due to a race condition.
317         err = rn.original_exception
318         raise unless err.is_a?(PG::UniqueViolation)
319
320         # Unfortunately ActiveRecord doesn't abstract out any of the
321         # necessary information to figure out if this the error is actually
322         # the specific case where we want to apply the ensure_unique_name
323         # behavior, so the following code is specialized to Postgres.
324         detail = err.result.error_field(PG::Result::PG_DIAG_MESSAGE_DETAIL)
325         raise unless /^Key \(owner_uuid, name\)=\([a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}, .*?\) already exists\./.match detail
326
327         new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
328         if new_name == name
329           # If the database is fast enough to do two attempts in the
330           # same millisecond, we need to wait to ensure we try a
331           # different timestamp on each attempt.
332           sleep 0.002
333           new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
334         end
335
336         self[:name] = new_name
337         self[:uuid] = nil if uuid_was.nil? && !uuid.nil?
338         conn.exec_query 'SAVEPOINT save_with_unique_name'
339         retry
340       ensure
341         conn.exec_query 'RELEASE SAVEPOINT save_with_unique_name'
342       end
343     end
344   end
345
346   def logged_attributes
347     attributes.except(*Rails.configuration.unlogged_attributes)
348   end
349
350   def self.full_text_searchable_columns
351     self.columns.select do |col|
352       col.type == :string or col.type == :text
353     end.map(&:name)
354   end
355
356   def self.full_text_tsvector
357     parts = full_text_searchable_columns.collect do |column|
358       "coalesce(#{column},'')"
359     end
360     "to_tsvector('english', #{parts.join(" || ' ' || ")})"
361   end
362
363   def self.apply_filters query, filters
364     ft = record_filters filters, self
365     if not ft[:cond_out].any?
366       return query
367     end
368     query.where('(' + ft[:cond_out].join(') AND (') + ')',
369                           *ft[:param_out])
370   end
371
372   protected
373
374   def self.deep_sort_hash(x)
375     if x.is_a? Hash
376       x.sort.collect do |k, v|
377         [k, deep_sort_hash(v)]
378       end.to_h
379     elsif x.is_a? Array
380       x.collect { |v| deep_sort_hash(v) }
381     else
382       x
383     end
384   end
385
386   def ensure_ownership_path_leads_to_user
387     if new_record? or owner_uuid_changed?
388       uuid_in_path = {owner_uuid => true, uuid => true}
389       x = owner_uuid
390       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
391         begin
392           if x == uuid
393             # Test for cycles with the new version, not the DB contents
394             x = owner_uuid
395           elsif !owner_class.respond_to? :find_by_uuid
396             raise ActiveRecord::RecordNotFound.new
397           else
398             x = owner_class.find_by_uuid(x).owner_uuid
399           end
400         rescue ActiveRecord::RecordNotFound => e
401           errors.add :owner_uuid, "is not owned by any user: #{e}"
402           return false
403         end
404         if uuid_in_path[x]
405           if x == owner_uuid
406             errors.add :owner_uuid, "would create an ownership cycle"
407           else
408             errors.add :owner_uuid, "has an ownership cycle"
409           end
410           return false
411         end
412         uuid_in_path[x] = true
413       end
414     end
415     true
416   end
417
418   def set_default_owner
419     if new_record? and current_user and respond_to? :owner_uuid=
420       self.owner_uuid ||= current_user.uuid
421     end
422   end
423
424   def ensure_owner_uuid_is_permitted
425     raise PermissionDeniedError if !current_user
426
427     if self.owner_uuid.nil?
428       errors.add :owner_uuid, "cannot be nil"
429       raise PermissionDeniedError
430     end
431
432     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
433     unless rsc_class == User or rsc_class == Group
434       errors.add :owner_uuid, "must be set to User or Group"
435       raise PermissionDeniedError
436     end
437
438     if new_record? || owner_uuid_changed?
439       # Permission on owner_uuid_was is needed to move an existing
440       # object away from its previous owner (which implies permission
441       # to modify this object itself, so we don't need to check that
442       # separately). Permission on the new owner_uuid is also needed.
443       [['old', owner_uuid_was],
444        ['new', owner_uuid]
445       ].each do |which, check_uuid|
446         if check_uuid.nil?
447           # old_owner_uuid is nil? New record, no need to check.
448         elsif !current_user.can?(write: check_uuid)
449           logger.warn "User #{current_user.uuid} tried to set ownership of #{self.class.to_s} #{self.uuid} but does not have permission to write #{which} owner_uuid #{check_uuid}"
450           errors.add :owner_uuid, "cannot be set or changed without write permission on #{which} owner"
451           raise PermissionDeniedError
452         end
453       end
454     else
455       # If the object already existed and we're not changing
456       # owner_uuid, we only need write permission on the object
457       # itself.
458       if !current_user.can?(write: self.uuid)
459         logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} without write permission"
460         errors.add :uuid, "is not writable"
461         raise PermissionDeniedError
462       end
463     end
464
465     true
466   end
467
468   def ensure_permission_to_save
469     unless (new_record? ? permission_to_create : permission_to_update)
470       raise PermissionDeniedError
471     end
472   end
473
474   def permission_to_create
475     current_user.andand.is_active
476   end
477
478   def permission_to_update
479     if !current_user
480       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
481       return false
482     end
483     if !current_user.is_active
484       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
485       return false
486     end
487     return true if current_user.is_admin
488     if self.uuid_changed?
489       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
490       return false
491     end
492     return true
493   end
494
495   def ensure_permission_to_destroy
496     raise PermissionDeniedError unless permission_to_destroy
497   end
498
499   def permission_to_destroy
500     permission_to_update
501   end
502
503   def maybe_update_modified_by_fields
504     update_modified_by_fields if self.changed? or self.new_record?
505     true
506   end
507
508   def update_modified_by_fields
509     current_time = db_current_time
510     self.created_at = created_at_was || current_time
511     self.updated_at = current_time
512     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
513     self.modified_at = current_time
514     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
515     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
516     true
517   end
518
519   def self.has_nonstring_keys? x
520     if x.is_a? Hash
521       x.each do |k,v|
522         return true if !(k.is_a?(String) || k.is_a?(Symbol)) || has_nonstring_keys?(v)
523       end
524     elsif x.is_a? Array
525       x.each do |v|
526         return true if has_nonstring_keys?(v)
527       end
528     end
529     false
530   end
531
532   def self.has_symbols? x
533     if x.is_a? Hash
534       x.each do |k,v|
535         return true if has_symbols?(k) or has_symbols?(v)
536       end
537     elsif x.is_a? Array
538       x.each do |k|
539         return true if has_symbols?(k)
540       end
541     elsif x.is_a? Symbol
542       return true
543     elsif x.is_a? String
544       return true if x.start_with?(':') && !x.start_with?('::')
545     end
546     false
547   end
548
549   def self.recursive_stringify x
550     if x.is_a? Hash
551       Hash[x.collect do |k,v|
552              [recursive_stringify(k), recursive_stringify(v)]
553            end]
554     elsif x.is_a? Array
555       x.collect do |k|
556         recursive_stringify k
557       end
558     elsif x.is_a? Symbol
559       x.to_s
560     elsif x.is_a? String and x.start_with?(':') and !x.start_with?('::')
561       x[1..-1]
562     else
563       x
564     end
565   end
566
567   def self.where_serialized(colname, value)
568     if value.empty?
569       # rails4 stores as null, rails3 stored as serialized [] or {}
570       sql = "#{colname.to_s} is null or #{colname.to_s} IN (?)"
571       sorted = value
572     else
573       sql = "#{colname.to_s} IN (?)"
574       sorted = deep_sort_hash(value)
575     end
576     where(sql, [sorted.to_yaml, SafeJSON.dump(sorted)])
577   end
578
579   Serializer = {
580     Hash => HashSerializer,
581     Array => ArraySerializer,
582   }
583
584   def self.serialize(colname, type)
585     coder = Serializer[type]
586     @serialized_attributes ||= {}
587     @serialized_attributes[colname.to_s] = coder
588     super(colname, coder)
589   end
590
591   def self.serialized_attributes
592     @serialized_attributes ||= {}
593   end
594
595   def serialized_attributes
596     self.class.serialized_attributes
597   end
598
599   def convert_serialized_symbols_to_strings
600     # ensure_serialized_attribute_type should prevent symbols from
601     # getting into the database in the first place. If someone managed
602     # to get them into the database (perhaps using an older version)
603     # we'll convert symbols to strings when loading from the
604     # database. (Otherwise, loading and saving an object with existing
605     # symbols in a serialized field will crash.)
606     self.class.serialized_attributes.each do |colname, attr|
607       if self.class.has_symbols? attributes[colname]
608         attributes[colname] = self.class.recursive_stringify attributes[colname]
609         send(colname + '=',
610              self.class.recursive_stringify(attributes[colname]))
611       end
612     end
613   end
614
615   def foreign_key_attributes
616     attributes.keys.select { |a| a.match(/_uuid$/) }
617   end
618
619   def skip_uuid_read_permission_check
620     %w(modified_by_client_uuid)
621   end
622
623   def skip_uuid_existence_check
624     []
625   end
626
627   def normalize_collection_uuids
628     foreign_key_attributes.each do |attr|
629       attr_value = send attr
630       if attr_value.is_a? String and
631           attr_value.match(/^[0-9a-f]{32,}(\+[@\w]+)*$/)
632         begin
633           send "#{attr}=", Collection.normalize_uuid(attr_value)
634         rescue
635           # TODO: abort instead of silently accepting unnormalizable value?
636         end
637       end
638     end
639   end
640
641   @@prefixes_hash = nil
642   def self.uuid_prefixes
643     unless @@prefixes_hash
644       @@prefixes_hash = {}
645       Rails.application.eager_load!
646       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
647         if k.respond_to?(:uuid_prefix)
648           @@prefixes_hash[k.uuid_prefix] = k
649         end
650       end
651     end
652     @@prefixes_hash
653   end
654
655   def self.uuid_like_pattern
656     "#{Rails.configuration.uuid_prefix}-#{uuid_prefix}-_______________"
657   end
658
659   def self.uuid_regex
660     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
661   end
662
663   def ensure_valid_uuids
664     specials = [system_user_uuid]
665
666     foreign_key_attributes.each do |attr|
667       if new_record? or send (attr + "_changed?")
668         next if skip_uuid_existence_check.include? attr
669         attr_value = send attr
670         next if specials.include? attr_value
671         if attr_value
672           if (r = ArvadosModel::resource_class_for_uuid attr_value)
673             unless skip_uuid_read_permission_check.include? attr
674               r = r.readable_by(current_user)
675             end
676             if r.where(uuid: attr_value).count == 0
677               errors.add(attr, "'#{attr_value}' not found")
678             end
679           end
680         end
681       end
682     end
683   end
684
685   class Email
686     def self.kind
687       "email"
688     end
689
690     def kind
691       self.class.kind
692     end
693
694     def self.readable_by (*u)
695       self
696     end
697
698     def self.where (u)
699       [{:uuid => u[:uuid]}]
700     end
701   end
702
703   def self.resource_class_for_uuid(uuid)
704     if uuid.is_a? ArvadosModel
705       return uuid.class
706     end
707     unless uuid.is_a? String
708       return nil
709     end
710
711     uuid.match HasUuid::UUID_REGEX do |re|
712       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
713     end
714
715     if uuid.match(/.+@.+/)
716       return Email
717     end
718
719     nil
720   end
721
722   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
723   # an object in any class.
724   def self.find_by_uuid uuid
725     if self == ArvadosModel
726       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
727       # delegate to the appropriate subclass based on the given uuid.
728       self.resource_class_for_uuid(uuid).unscoped.find_by_uuid(uuid)
729     else
730       super
731     end
732   end
733
734   def log_start_state
735     @old_attributes = Marshal.load(Marshal.dump(attributes))
736     @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
737   end
738
739   def log_change(event_type)
740     log = Log.new(event_type: event_type).fill_object(self)
741     yield log
742     log.save!
743     log_start_state
744   end
745
746   def log_create
747     log_change('create') do |log|
748       log.fill_properties('old', nil, nil)
749       log.update_to self
750     end
751   end
752
753   def log_update
754     log_change('update') do |log|
755       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
756       log.update_to self
757     end
758   end
759
760   def log_destroy
761     log_change('delete') do |log|
762       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
763       log.update_to nil
764     end
765   end
766 end