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