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