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