3551: When invoking run_test_server.py during tests, pass 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
8   attr_protected :created_at
9   attr_protected :modified_by_user_uuid
10   attr_protected :modified_by_client_uuid
11   attr_protected :modified_at
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   validate :ensure_serialized_attribute_type
25   validate :normalize_collection_uuids
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, :foreign_key => :head_uuid, :class_name => 'Link', :primary_key => :uuid, :conditions => "link_class = 'permission'"
32
33   class PermissionDeniedError < StandardError
34     def http_status
35       403
36     end
37   end
38
39   class UnauthorizedError < StandardError
40     def http_status
41       401
42     end
43   end
44
45   def self.kind_class(kind)
46     kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
47   end
48
49   def href
50     "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}"
51   end
52
53   def self.searchable_columns operator
54     textonly_operator = !operator.match(/[<=>]/)
55     self.columns.select do |col|
56       case col.type
57       when :string, :text
58         true
59       when :datetime, :integer, :boolean
60         !textonly_operator
61       else
62         false
63       end
64     end.map(&:name)
65   end
66
67   def self.attribute_column attr
68     self.columns.select { |col| col.name == attr.to_s }.first
69   end
70
71   # Return nil if current user is not allowed to see the list of
72   # writers. Otherwise, return a list of user_ and group_uuids with
73   # write permission. (If not returning nil, current_user is always in
74   # the list because can_manage permission is needed to see the list
75   # of writers.)
76   def writable_by
77     unless (owner_uuid == current_user.uuid or
78             current_user.is_admin or
79             current_user.groups_i_can(:manage).index(owner_uuid))
80       return nil
81     end
82     [owner_uuid, current_user.uuid] + permissions.collect do |p|
83       if ['can_write', 'can_manage'].index p.name
84         p.tail_uuid
85       end
86     end.compact.uniq
87   end
88
89   # Return a query with read permissions restricted to the union of of the
90   # permissions of the members of users_list, i.e. if something is readable by
91   # any user in users_list, it will be readable in the query returned by this
92   # function.
93   def self.readable_by(*users_list)
94     # Get rid of troublesome nils
95     users_list.compact!
96
97     # Load optional keyword arguments, if they exist.
98     if users_list.last.is_a? Hash
99       kwargs = users_list.pop
100     else
101       kwargs = {}
102     end
103
104     # Check if any of the users are admin.  If so, we're done.
105     if users_list.select { |u| u.is_admin }.empty?
106
107       # Collect the uuids for each user and any groups readable by each user.
108       user_uuids = users_list.map { |u| u.uuid }
109       uuid_list = user_uuids + users_list.flat_map { |u| u.groups_i_can(:read) }
110       sanitized_uuid_list = uuid_list.
111         collect { |uuid| sanitize(uuid) }.join(', ')
112       sql_conds = []
113       sql_params = []
114       sql_table = kwargs.fetch(:table_name, table_name)
115       or_object_uuid = ''
116
117       # This row is owned by a member of users_list, or owned by a group
118       # readable by a member of users_list
119       # or
120       # This row uuid is the uuid of a member of users_list
121       # or
122       # A permission link exists ('write' and 'manage' implicitly include
123       # 'read') from a member of users_list, or a group readable by users_list,
124       # to this row, or to the owner of this row (see join() below).
125       permitted_uuids = "(SELECT head_uuid FROM links WHERE link_class='permission' AND tail_uuid IN (#{sanitized_uuid_list}))"
126
127       sql_conds += ["#{sql_table}.owner_uuid in (?)",
128                     "#{sql_table}.uuid in (?)",
129                     "#{sql_table}.uuid IN #{permitted_uuids}"]
130       sql_params += [uuid_list, user_uuids]
131
132       if sql_table == "links" and users_list.any?
133         # This row is a 'permission' or 'resources' link class
134         # The uuid for a member of users_list is referenced in either the head
135         # or tail of the link
136         sql_conds += ["(#{sql_table}.link_class in (#{sanitize 'permission'}, #{sanitize 'resources'}) AND (#{sql_table}.head_uuid IN (?) OR #{sql_table}.tail_uuid IN (?)))"]
137         sql_params += [user_uuids, user_uuids]
138       end
139
140       if sql_table == "logs" and users_list.any?
141         # Link head points to the object described by this row
142         sql_conds += ["#{sql_table}.object_uuid IN #{permitted_uuids}"]
143
144         # This object described by this row is owned by this user, or owned by a group readable by this user
145         sql_conds += ["#{sql_table}.object_owner_uuid in (?)"]
146         sql_params += [uuid_list]
147       end
148
149       if sql_table == "collections" and users_list.any?
150         # There is a 'name' link going from a readable group to the collection.
151         name_links = "(SELECT head_uuid FROM links WHERE link_class='name' AND tail_uuid IN (#{sanitized_uuid_list}))"
152         sql_conds += ["#{sql_table}.uuid IN #{name_links}"]
153       end
154
155       # Link head points to this row, or to the owner of this row (the thing to be read)
156       #
157       # Link tail originates from this user, or a group that is readable by this
158       # user (the identity with authorization to read)
159       #
160       # Link class is 'permission' ('write' and 'manage' implicitly include 'read')
161       where(sql_conds.join(' OR '), *sql_params)
162     else
163       # At least one user is admin, so don't bother to apply any restrictions.
164       self
165     end
166   end
167
168   def logged_attributes
169     attributes
170   end
171
172   def has_permission? perm_type, target_uuid
173     Link.where(link_class: "permission",
174                name: perm_type,
175                tail_uuid: uuid,
176                head_uuid: target_uuid).any?
177   end
178
179   protected
180
181   def ensure_ownership_path_leads_to_user
182     if new_record? or owner_uuid_changed?
183       uuid_in_path = {owner_uuid => true, uuid => true}
184       x = owner_uuid
185       while (owner_class = self.class.resource_class_for_uuid(x)) != User
186         begin
187           if x == uuid
188             # Test for cycles with the new version, not the DB contents
189             x = owner_uuid
190           elsif !owner_class.respond_to? :find_by_uuid
191             raise ActiveRecord::RecordNotFound.new
192           else
193             x = owner_class.find_by_uuid(x).owner_uuid
194           end
195         rescue ActiveRecord::RecordNotFound => e
196           errors.add :owner_uuid, "is not owned by any user: #{e}"
197           return false
198         end
199         if uuid_in_path[x]
200           if x == owner_uuid
201             errors.add :owner_uuid, "would create an ownership cycle"
202           else
203             errors.add :owner_uuid, "has an ownership cycle"
204           end
205           return false
206         end
207         uuid_in_path[x] = true
208       end
209     end
210     true
211   end
212
213   def ensure_owner_uuid_is_permitted
214     raise PermissionDeniedError if !current_user
215     if new_record? and respond_to? :owner_uuid=
216       self.owner_uuid ||= current_user.uuid
217     end
218     # Verify permission to write to old owner (unless owner_uuid was
219     # nil -- or hasn't changed, in which case the following
220     # "permission to write to new owner" block will take care of us)
221     unless !owner_uuid_changed? or
222         owner_uuid_was.nil? or
223         current_user.uuid == self.owner_uuid_was or
224         current_user.uuid == self.uuid or
225         current_user.can? write: self.owner_uuid_was
226       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}"
227       errors.add :owner_uuid, "cannot be changed without write permission on old owner"
228       raise PermissionDeniedError
229     end
230     # Verify permission to write to new owner
231     unless current_user == self or current_user.can? write: owner_uuid
232       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}"
233       errors.add :owner_uuid, "cannot be changed without write permission on new owner"
234       raise PermissionDeniedError
235     end
236   end
237
238   def ensure_permission_to_save
239     unless (new_record? ? permission_to_create : permission_to_update)
240       raise PermissionDeniedError
241     end
242   end
243
244   def permission_to_create
245     current_user.andand.is_active
246   end
247
248   def permission_to_update
249     if !current_user
250       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
251       return false
252     end
253     if !current_user.is_active
254       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
255       return false
256     end
257     return true if current_user.is_admin
258     if self.uuid_changed?
259       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
260       return false
261     end
262     return true
263   end
264
265   def ensure_permission_to_destroy
266     raise PermissionDeniedError unless permission_to_destroy
267   end
268
269   def permission_to_destroy
270     permission_to_update
271   end
272
273   def maybe_update_modified_by_fields
274     update_modified_by_fields if self.changed? or self.new_record?
275     true
276   end
277
278   def update_modified_by_fields
279     self.updated_at = Time.now
280     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
281     self.modified_at = Time.now
282     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
283     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
284     true
285   end
286
287   def self.has_symbols? x
288     if x.is_a? Hash
289       x.each do |k,v|
290         return true if has_symbols?(k) or has_symbols?(v)
291       end
292       false
293     elsif x.is_a? Array
294       x.each do |k|
295         return true if has_symbols?(k)
296       end
297       false
298     else
299       (x.class == Symbol)
300     end
301   end
302
303   def self.recursive_stringify x
304     if x.is_a? Hash
305       Hash[x.collect do |k,v|
306              [recursive_stringify(k), recursive_stringify(v)]
307            end]
308     elsif x.is_a? Array
309       x.collect do |k|
310         recursive_stringify k
311       end
312     elsif x.is_a? Symbol
313       x.to_s
314     else
315       x
316     end
317   end
318
319   def ensure_serialized_attribute_type
320     # Specifying a type in the "serialize" declaration causes rails to
321     # raise an exception if a different data type is retrieved from
322     # the database during load().  The validation preventing such
323     # crash-inducing records from being inserted in the database in
324     # the first place seems to have been left as an exercise to the
325     # developer.
326     self.class.serialized_attributes.each do |colname, attr|
327       if attr.object_class
328         if self.attributes[colname].class != attr.object_class
329           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}, not a #{self.attributes[colname].class.to_s}"
330         elsif self.class.has_symbols? attributes[colname]
331           self.errors.add colname.to_sym, "must not contain symbols: #{attributes[colname].inspect}"
332         end
333       end
334     end
335   end
336
337   def convert_serialized_symbols_to_strings
338     # ensure_serialized_attribute_type should prevent symbols from
339     # getting into the database in the first place. If someone managed
340     # to get them into the database (perhaps using an older version)
341     # we'll convert symbols to strings when loading from the
342     # database. (Otherwise, loading and saving an object with existing
343     # symbols in a serialized field will crash.)
344     self.class.serialized_attributes.each do |colname, attr|
345       if self.class.has_symbols? attributes[colname]
346         attributes[colname] = self.class.recursive_stringify attributes[colname]
347         self.send(colname + '=',
348                   self.class.recursive_stringify(attributes[colname]))
349       end
350     end
351   end
352
353   def foreign_key_attributes
354     attributes.keys.select { |a| a.match /_uuid$/ }
355   end
356
357   def skip_uuid_read_permission_check
358     %w(modified_by_client_uuid)
359   end
360
361   def skip_uuid_existence_check
362     []
363   end
364
365   def normalize_collection_uuids
366     foreign_key_attributes.each do |attr|
367       attr_value = send attr
368       if attr_value.is_a? String and
369           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
370         begin
371           send "#{attr}=", Collection.normalize_uuid(attr_value)
372         rescue
373           # TODO: abort instead of silently accepting unnormalizable value?
374         end
375       end
376     end
377   end
378
379   @@UUID_REGEX = /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/
380
381   @@prefixes_hash = nil
382   def self.uuid_prefixes
383     unless @@prefixes_hash
384       @@prefixes_hash = {}
385       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
386         if k.respond_to?(:uuid_prefix)
387           @@prefixes_hash[k.uuid_prefix] = k
388         end
389       end
390     end
391     @@prefixes_hash
392   end
393
394   def self.uuid_like_pattern
395     "_____-#{uuid_prefix}-_______________"
396   end
397
398   def ensure_valid_uuids
399     specials = [system_user_uuid, 'd41d8cd98f00b204e9800998ecf8427e+0']
400
401     foreign_key_attributes.each do |attr|
402       if new_record? or send (attr + "_changed?")
403         next if skip_uuid_existence_check.include? attr
404         attr_value = send attr
405         next if specials.include? attr_value
406         if attr_value
407           if (r = ArvadosModel::resource_class_for_uuid attr_value)
408             unless skip_uuid_read_permission_check.include? attr
409               r = r.readable_by(current_user)
410             end
411             if r.where(uuid: attr_value).count == 0
412               errors.add(attr, "'#{attr_value}' not found")
413             end
414           end
415         end
416       end
417     end
418   end
419
420   class Email
421     def self.kind
422       "email"
423     end
424
425     def kind
426       self.class.kind
427     end
428
429     def self.readable_by (*u)
430       self
431     end
432
433     def self.where (u)
434       [{:uuid => u[:uuid]}]
435     end
436   end
437
438   def self.resource_class_for_uuid(uuid)
439     if uuid.is_a? ArvadosModel
440       return uuid.class
441     end
442     unless uuid.is_a? String
443       return nil
444     end
445     if uuid.match /^[0-9a-f]{32}(\+[^,]+)*(,[0-9a-f]{32}(\+[^,]+)*)*$/
446       return Collection
447     end
448     resource_class = nil
449
450     Rails.application.eager_load!
451     uuid.match @@UUID_REGEX do |re|
452       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
453     end
454
455     if uuid.match /.+@.+/
456       return Email
457     end
458
459     nil
460   end
461
462   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
463   # an object in any class.
464   def self.find_by_uuid uuid
465     if self == ArvadosModel
466       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
467       # delegate to the appropriate subclass based on the given uuid.
468       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
469     else
470       super
471     end
472   end
473
474   def log_start_state
475     @old_etag = etag
476     @old_attributes = logged_attributes
477   end
478
479   def log_change(event_type)
480     log = Log.new(event_type: event_type).fill_object(self)
481     yield log
482     log.save!
483     connection.execute "NOTIFY logs, '#{log.id}'"
484     log_start_state
485   end
486
487   def log_create
488     log_change('create') do |log|
489       log.fill_properties('old', nil, nil)
490       log.update_to self
491     end
492   end
493
494   def log_update
495     log_change('update') do |log|
496       log.fill_properties('old', @old_etag, @old_attributes)
497       log.update_to self
498     end
499   end
500
501   def log_destroy
502     log_change('destroy') do |log|
503       log.fill_properties('old', @old_etag, @old_attributes)
504       log.update_to nil
505     end
506   end
507 end