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