3036: API server unit tests pass again after adding uniqueness constraints.
[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   before_validation :normalize_collection_uuids
25   validate :ensure_serialized_attribute_type
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       # Link head points to this row, or to the owner of this row (the thing to be read)
150       #
151       # Link tail originates from this user, or a group that is readable by this
152       # user (the identity with authorization to read)
153       #
154       # Link class is 'permission' ('write' and 'manage' implicitly include 'read')
155       where(sql_conds.join(' OR '), *sql_params)
156     else
157       # At least one user is admin, so don't bother to apply any restrictions.
158       self
159     end
160   end
161
162   def logged_attributes
163     attributes
164   end
165
166   def has_permission? perm_type, target_uuid
167     Link.where(link_class: "permission",
168                name: perm_type,
169                tail_uuid: uuid,
170                head_uuid: target_uuid).any?
171   end
172
173   protected
174
175   def ensure_ownership_path_leads_to_user
176     if new_record? or owner_uuid_changed?
177       uuid_in_path = {owner_uuid => true, uuid => true}
178       x = owner_uuid
179       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
180         begin
181           if x == uuid
182             # Test for cycles with the new version, not the DB contents
183             x = owner_uuid
184           elsif !owner_class.respond_to? :find_by_uuid
185             raise ActiveRecord::RecordNotFound.new
186           else
187             x = owner_class.find_by_uuid(x).owner_uuid
188           end
189         rescue ActiveRecord::RecordNotFound => e
190           errors.add :owner_uuid, "is not owned by any user: #{e}"
191           return false
192         end
193         if uuid_in_path[x]
194           if x == owner_uuid
195             errors.add :owner_uuid, "would create an ownership cycle"
196           else
197             errors.add :owner_uuid, "has an ownership cycle"
198           end
199           return false
200         end
201         uuid_in_path[x] = true
202       end
203     end
204     true
205   end
206
207   def ensure_owner_uuid_is_permitted
208     raise PermissionDeniedError if !current_user
209
210     if new_record? and respond_to? :owner_uuid=
211       self.owner_uuid ||= current_user.uuid
212     end
213
214     if self.owner_uuid.nil?
215       errors.add :owner_uuid, "cannot be nil"
216       raise PermissionDeniedError
217     end
218
219     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
220     unless rsc_class == User or rsc_class == Group
221       errors.add :owner_uuid, "can only be set to User or Group"
222       raise PermissionDeniedError
223     end
224
225     # Verify "write" permission on old owner
226     # default fail unless one of:
227     # owner_uuid did not change
228     # previous owner_uuid is nil
229     # current user is the old owner
230     # current user is this object
231     # current user can_write old owner
232     unless !owner_uuid_changed? or
233         owner_uuid_was.nil? or
234         current_user.uuid == self.owner_uuid_was or
235         current_user.uuid == self.uuid or
236         current_user.can? write: self.owner_uuid_was
237       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}"
238       errors.add :owner_uuid, "cannot be changed without write permission on old owner"
239       raise PermissionDeniedError
240     end
241
242     # Verify "write" permission on new owner
243     # default fail unless one of:
244     # current_user is this object
245     # current user can_write new owner
246     unless current_user == self or current_user.can? write: owner_uuid
247       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}"
248       errors.add :owner_uuid, "cannot be changed without write permission on new owner"
249       raise PermissionDeniedError
250     end
251
252     true
253   end
254
255   def ensure_permission_to_save
256     unless (new_record? ? permission_to_create : permission_to_update)
257       raise PermissionDeniedError
258     end
259   end
260
261   def permission_to_create
262     current_user.andand.is_active
263   end
264
265   def permission_to_update
266     if !current_user
267       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
268       return false
269     end
270     if !current_user.is_active
271       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
272       return false
273     end
274     return true if current_user.is_admin
275     if self.uuid_changed?
276       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
277       return false
278     end
279     return true
280   end
281
282   def ensure_permission_to_destroy
283     raise PermissionDeniedError unless permission_to_destroy
284   end
285
286   def permission_to_destroy
287     permission_to_update
288   end
289
290   def maybe_update_modified_by_fields
291     update_modified_by_fields if self.changed? or self.new_record?
292     true
293   end
294
295   def update_modified_by_fields
296     self.updated_at = Time.now
297     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
298     self.modified_at = Time.now
299     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
300     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
301     true
302   end
303
304   def self.has_symbols? x
305     if x.is_a? Hash
306       x.each do |k,v|
307         return true if has_symbols?(k) or has_symbols?(v)
308       end
309       false
310     elsif x.is_a? Array
311       x.each do |k|
312         return true if has_symbols?(k)
313       end
314       false
315     else
316       (x.class == Symbol)
317     end
318   end
319
320   def self.recursive_stringify x
321     if x.is_a? Hash
322       Hash[x.collect do |k,v|
323              [recursive_stringify(k), recursive_stringify(v)]
324            end]
325     elsif x.is_a? Array
326       x.collect do |k|
327         recursive_stringify k
328       end
329     elsif x.is_a? Symbol
330       x.to_s
331     else
332       x
333     end
334   end
335
336   def ensure_serialized_attribute_type
337     # Specifying a type in the "serialize" declaration causes rails to
338     # raise an exception if a different data type is retrieved from
339     # the database during load().  The validation preventing such
340     # crash-inducing records from being inserted in the database in
341     # the first place seems to have been left as an exercise to the
342     # developer.
343     self.class.serialized_attributes.each do |colname, attr|
344       if attr.object_class
345         if self.attributes[colname].class != attr.object_class
346           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}, not a #{self.attributes[colname].class.to_s}"
347         elsif self.class.has_symbols? attributes[colname]
348           self.errors.add colname.to_sym, "must not contain symbols: #{attributes[colname].inspect}"
349         end
350       end
351     end
352   end
353
354   def convert_serialized_symbols_to_strings
355     # ensure_serialized_attribute_type should prevent symbols from
356     # getting into the database in the first place. If someone managed
357     # to get them into the database (perhaps using an older version)
358     # we'll convert symbols to strings when loading from the
359     # database. (Otherwise, loading and saving an object with existing
360     # symbols in a serialized field will crash.)
361     self.class.serialized_attributes.each do |colname, attr|
362       if self.class.has_symbols? attributes[colname]
363         attributes[colname] = self.class.recursive_stringify attributes[colname]
364         self.send(colname + '=',
365                   self.class.recursive_stringify(attributes[colname]))
366       end
367     end
368   end
369
370   def foreign_key_attributes
371     attributes.keys.select { |a| a.match /_uuid$/ }
372   end
373
374   def skip_uuid_read_permission_check
375     %w(modified_by_client_uuid)
376   end
377
378   def skip_uuid_existence_check
379     []
380   end
381
382   def normalize_collection_uuids
383     foreign_key_attributes.each do |attr|
384       attr_value = send attr
385       if attr_value.is_a? String and
386           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
387         begin
388           send "#{attr}=", Collection.normalize_uuid(attr_value)
389         rescue
390           # TODO: abort instead of silently accepting unnormalizable value?
391         end
392       end
393     end
394   end
395
396   @@prefixes_hash = nil
397   def self.uuid_prefixes
398     unless @@prefixes_hash
399       @@prefixes_hash = {}
400       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
401         if k.respond_to?(:uuid_prefix)
402           @@prefixes_hash[k.uuid_prefix] = k
403         end
404       end
405     end
406     @@prefixes_hash
407   end
408
409   def self.uuid_like_pattern
410     "_____-#{uuid_prefix}-_______________"
411   end
412
413   def ensure_valid_uuids
414     specials = [system_user_uuid]
415
416     foreign_key_attributes.each do |attr|
417       if new_record? or send (attr + "_changed?")
418         next if skip_uuid_existence_check.include? attr
419         attr_value = send attr
420         next if specials.include? attr_value
421         if attr_value
422           if (r = ArvadosModel::resource_class_for_uuid attr_value)
423             unless skip_uuid_read_permission_check.include? attr
424               r = r.readable_by(current_user)
425             end
426             if r.where(uuid: attr_value).count == 0
427               errors.add(attr, "'#{attr_value}' not found")
428             end
429           end
430         end
431       end
432     end
433   end
434
435   class Email
436     def self.kind
437       "email"
438     end
439
440     def kind
441       self.class.kind
442     end
443
444     def self.readable_by (*u)
445       self
446     end
447
448     def self.where (u)
449       [{:uuid => u[:uuid]}]
450     end
451   end
452
453   def self.resource_class_for_uuid(uuid)
454     if uuid.is_a? ArvadosModel
455       return uuid.class
456     end
457     unless uuid.is_a? String
458       return nil
459     end
460     resource_class = nil
461
462     Rails.application.eager_load!
463     uuid.match HasUuid::UUID_REGEX do |re|
464       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
465     end
466
467     if uuid.match /.+@.+/
468       return Email
469     end
470
471     nil
472   end
473
474   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
475   # an object in any class.
476   def self.find_by_uuid uuid
477     if self == ArvadosModel
478       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
479       # delegate to the appropriate subclass based on the given uuid.
480       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
481     else
482       super
483     end
484   end
485
486   def log_start_state
487     @old_etag = etag
488     @old_attributes = logged_attributes
489   end
490
491   def log_change(event_type)
492     log = Log.new(event_type: event_type).fill_object(self)
493     yield log
494     log.save!
495     connection.execute "NOTIFY logs, '#{log.id}'"
496     log_start_state
497   end
498
499   def log_create
500     log_change('create') do |log|
501       log.fill_properties('old', nil, nil)
502       log.update_to self
503     end
504   end
505
506   def log_update
507     log_change('update') do |log|
508       log.fill_properties('old', @old_etag, @old_attributes)
509       log.update_to self
510     end
511   end
512
513   def log_destroy
514     log_change('destroy') do |log|
515       log.fill_properties('old', @old_etag, @old_attributes)
516       log.update_to nil
517     end
518   end
519 end