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