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