Merge branch '9684-workflows'
[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     elsif x.is_a? Array
412       x.each do |k|
413         return true if has_symbols?(k)
414       end
415     elsif x.is_a? Symbol
416       return true
417     elsif x.is_a? String
418       return true if x.start_with?(':') && !x.start_with?('::')
419     end
420     false
421   end
422
423   def self.recursive_stringify x
424     if x.is_a? Hash
425       Hash[x.collect do |k,v|
426              [recursive_stringify(k), recursive_stringify(v)]
427            end]
428     elsif x.is_a? Array
429       x.collect do |k|
430         recursive_stringify k
431       end
432     elsif x.is_a? Symbol
433       x.to_s
434     elsif x.is_a? String and x.start_with?(':') and !x.start_with?('::')
435       x[1..-1]
436     else
437       x
438     end
439   end
440
441   def ensure_serialized_attribute_type
442     # Specifying a type in the "serialize" declaration causes rails to
443     # raise an exception if a different data type is retrieved from
444     # the database during load().  The validation preventing such
445     # crash-inducing records from being inserted in the database in
446     # the first place seems to have been left as an exercise to the
447     # developer.
448     self.class.serialized_attributes.each do |colname, attr|
449       if attr.object_class
450         if self.attributes[colname].class != attr.object_class
451           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}, not a #{self.attributes[colname].class.to_s}"
452         elsif self.class.has_symbols? attributes[colname]
453           self.errors.add colname.to_sym, "must not contain symbols: #{attributes[colname].inspect}"
454         end
455       end
456     end
457   end
458
459   def convert_serialized_symbols_to_strings
460     # ensure_serialized_attribute_type should prevent symbols from
461     # getting into the database in the first place. If someone managed
462     # to get them into the database (perhaps using an older version)
463     # we'll convert symbols to strings when loading from the
464     # database. (Otherwise, loading and saving an object with existing
465     # symbols in a serialized field will crash.)
466     self.class.serialized_attributes.each do |colname, attr|
467       if self.class.has_symbols? attributes[colname]
468         attributes[colname] = self.class.recursive_stringify attributes[colname]
469         self.send(colname + '=',
470                   self.class.recursive_stringify(attributes[colname]))
471       end
472     end
473   end
474
475   def foreign_key_attributes
476     attributes.keys.select { |a| a.match /_uuid$/ }
477   end
478
479   def skip_uuid_read_permission_check
480     %w(modified_by_client_uuid)
481   end
482
483   def skip_uuid_existence_check
484     []
485   end
486
487   def normalize_collection_uuids
488     foreign_key_attributes.each do |attr|
489       attr_value = send attr
490       if attr_value.is_a? String and
491           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
492         begin
493           send "#{attr}=", Collection.normalize_uuid(attr_value)
494         rescue
495           # TODO: abort instead of silently accepting unnormalizable value?
496         end
497       end
498     end
499   end
500
501   @@prefixes_hash = nil
502   def self.uuid_prefixes
503     unless @@prefixes_hash
504       @@prefixes_hash = {}
505       Rails.application.eager_load!
506       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
507         if k.respond_to?(:uuid_prefix)
508           @@prefixes_hash[k.uuid_prefix] = k
509         end
510       end
511     end
512     @@prefixes_hash
513   end
514
515   def self.uuid_like_pattern
516     "_____-#{uuid_prefix}-_______________"
517   end
518
519   def self.uuid_regex
520     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
521   end
522
523   def ensure_valid_uuids
524     specials = [system_user_uuid]
525
526     foreign_key_attributes.each do |attr|
527       if new_record? or send (attr + "_changed?")
528         next if skip_uuid_existence_check.include? attr
529         attr_value = send attr
530         next if specials.include? attr_value
531         if attr_value
532           if (r = ArvadosModel::resource_class_for_uuid attr_value)
533             unless skip_uuid_read_permission_check.include? attr
534               r = r.readable_by(current_user)
535             end
536             if r.where(uuid: attr_value).count == 0
537               errors.add(attr, "'#{attr_value}' not found")
538             end
539           end
540         end
541       end
542     end
543   end
544
545   class Email
546     def self.kind
547       "email"
548     end
549
550     def kind
551       self.class.kind
552     end
553
554     def self.readable_by (*u)
555       self
556     end
557
558     def self.where (u)
559       [{:uuid => u[:uuid]}]
560     end
561   end
562
563   def self.resource_class_for_uuid(uuid)
564     if uuid.is_a? ArvadosModel
565       return uuid.class
566     end
567     unless uuid.is_a? String
568       return nil
569     end
570     resource_class = nil
571
572     uuid.match HasUuid::UUID_REGEX do |re|
573       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
574     end
575
576     if uuid.match /.+@.+/
577       return Email
578     end
579
580     nil
581   end
582
583   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
584   # an object in any class.
585   def self.find_by_uuid uuid
586     if self == ArvadosModel
587       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
588       # delegate to the appropriate subclass based on the given uuid.
589       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
590     else
591       super
592     end
593   end
594
595   def log_start_state
596     @old_attributes = Marshal.load(Marshal.dump(attributes))
597     @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
598   end
599
600   def log_change(event_type)
601     log = Log.new(event_type: event_type).fill_object(self)
602     yield log
603     log.save!
604     log_start_state
605   end
606
607   def log_create
608     log_change('create') do |log|
609       log.fill_properties('old', nil, nil)
610       log.update_to self
611     end
612   end
613
614   def log_update
615     log_change('update') do |log|
616       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
617       log.update_to self
618     end
619   end
620
621   def log_destroy
622     log_change('destroy') do |log|
623       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
624       log.update_to nil
625     end
626   end
627 end