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