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