12032: Rename permission_view and remove Rails permission cache.
[arvados.git] / services / api / app / models / arvados_model.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'has_uuid'
6 require 'record_filters'
7 require 'serializers'
8
9 class ArvadosModel < ActiveRecord::Base
10   self.abstract_class = true
11
12   include CurrentApiClient      # current_user, current_api_client, etc.
13   include DbCurrentTime
14   extend RecordFilters
15
16   after_initialize :log_start_state
17   before_save :ensure_permission_to_save
18   before_save :ensure_owner_uuid_is_permitted
19   before_save :ensure_ownership_path_leads_to_user
20   before_destroy :ensure_owner_uuid_is_permitted
21   before_destroy :ensure_permission_to_destroy
22   before_create :update_modified_by_fields
23   before_update :maybe_update_modified_by_fields
24   after_create :log_create
25   after_update :log_update
26   after_destroy :log_destroy
27   after_find :convert_serialized_symbols_to_strings
28   before_validation :normalize_collection_uuids
29   before_validation :set_default_owner
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            ->{where(link_class: 'permission')},
37            foreign_key: :head_uuid,
38            class_name: 'Link',
39            primary_key: :uuid)
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 LockFailedError < StandardError
54     def http_status
55       422
56     end
57   end
58
59   class InvalidStateTransitionError < StandardError
60     def http_status
61       422
62     end
63   end
64
65   class UnauthorizedError < StandardError
66     def http_status
67       401
68     end
69   end
70
71   class UnresolvableContainerError < StandardError
72     def http_status
73       422
74     end
75   end
76
77   def self.kind_class(kind)
78     kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
79   end
80
81   def href
82     "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}"
83   end
84
85   def self.permit_attribute_params raw_params
86     # strong_parameters does not provide security: permissions are
87     # implemented with before_save hooks.
88     #
89     # The following permit! is necessary even with
90     # "ActionController::Parameters.permit_all_parameters = true",
91     # because permit_all does not permit nested attributes.
92     if raw_params
93       serialized_attributes.each do |colname, coder|
94         param = raw_params[colname.to_sym]
95         if param.nil?
96           # ok
97         elsif !param.is_a?(coder.object_class)
98           raise ArgumentError.new("#{colname} parameter must be #{coder.object_class}, not #{param.class}")
99         elsif has_nonstring_keys?(param)
100           raise ArgumentError.new("#{colname} parameter cannot have non-string hash keys")
101         end
102       end
103     end
104     ActionController::Parameters.new(raw_params).permit!
105   end
106
107   def initialize raw_params={}, *args
108     super(self.class.permit_attribute_params(raw_params), *args)
109   end
110
111   # Reload "old attributes" for logging, too.
112   def reload(*args)
113     super
114     log_start_state
115   end
116
117   def self.create raw_params={}, *args
118     super(permit_attribute_params(raw_params), *args)
119   end
120
121   def update_attributes raw_params={}, *args
122     super(self.class.permit_attribute_params(raw_params), *args)
123   end
124
125   def self.selectable_attributes(template=:user)
126     # Return an array of attribute name strings that can be selected
127     # in the given template.
128     api_accessible_attributes(template).map { |attr_spec| attr_spec.first.to_s }
129   end
130
131   def self.searchable_columns operator
132     textonly_operator = !operator.match(/[<=>]/)
133     self.columns.select do |col|
134       case col.type
135       when :string, :text
136         true
137       when :datetime, :integer, :boolean
138         !textonly_operator
139       else
140         false
141       end
142     end.map(&:name)
143   end
144
145   def self.attribute_column attr
146     self.columns.select { |col| col.name == attr.to_s }.first
147   end
148
149   def self.attributes_required_columns
150     # This method returns a hash.  Each key is the name of an API attribute,
151     # and it's mapped to a list of database columns that must be fetched
152     # to generate that attribute.
153     # This implementation generates a simple map of attributes to
154     # matching column names.  Subclasses can override this method
155     # to specify that method-backed API attributes need to fetch
156     # specific columns from the database.
157     all_columns = columns.map(&:name)
158     api_column_map = Hash.new { |hash, key| hash[key] = [] }
159     methods.grep(/^api_accessible_\w+$/).each do |method_name|
160       next if method_name == :api_accessible_attributes
161       send(method_name).each_pair do |api_attr_name, col_name|
162         col_name = col_name.to_s
163         if all_columns.include?(col_name)
164           api_column_map[api_attr_name.to_s] |= [col_name]
165         end
166       end
167     end
168     api_column_map
169   end
170
171   def self.ignored_select_attributes
172     ["href", "kind", "etag"]
173   end
174
175   def self.columns_for_attributes(select_attributes)
176     if select_attributes.empty?
177       raise ArgumentError.new("Attribute selection list cannot be empty")
178     end
179     api_column_map = attributes_required_columns
180     invalid_attrs = []
181     select_attributes.each do |s|
182       next if ignored_select_attributes.include? s
183       if not s.is_a? String or not api_column_map.include? s
184         invalid_attrs << s
185       end
186     end
187     if not invalid_attrs.empty?
188       raise ArgumentError.new("Invalid attribute(s): #{invalid_attrs.inspect}")
189     end
190     # Given an array of attribute names to select, return an array of column
191     # names that must be fetched from the database to satisfy the request.
192     select_attributes.flat_map { |attr| api_column_map[attr] }.uniq
193   end
194
195   def self.default_orders
196     ["#{table_name}.modified_at desc", "#{table_name}.uuid"]
197   end
198
199   def self.unique_columns
200     ["id", "uuid"]
201   end
202
203   def self.limit_index_columns_read
204     # This method returns a list of column names.
205     # If an index request reads that column from the database,
206     # APIs that return lists will only fetch objects until reaching
207     # max_index_database_read bytes of data from those columns.
208     []
209   end
210
211   # If current user can manage the object, return an array of uuids of
212   # users and groups that have permission to write the object. The
213   # first two elements are always [self.owner_uuid, current user's
214   # uuid].
215   #
216   # If current user can write but not manage the object, return
217   # [self.owner_uuid, current user's uuid].
218   #
219   # If current user cannot write this object, just return
220   # [self.owner_uuid].
221   def writable_by
222     return [owner_uuid] if not current_user
223     unless (owner_uuid == current_user.uuid or
224             current_user.is_admin or
225             (current_user.groups_i_can(:manage) & [uuid, owner_uuid]).any?)
226       if ((current_user.groups_i_can(:write) + [current_user.uuid]) &
227           [uuid, owner_uuid]).any?
228         return [owner_uuid, current_user.uuid]
229       else
230         return [owner_uuid]
231       end
232     end
233     [owner_uuid, current_user.uuid] + permissions.collect do |p|
234       if ['can_write', 'can_manage'].index p.name
235         p.tail_uuid
236       end
237     end.compact.uniq
238   end
239
240   # Return a query with read permissions restricted to the union of of the
241   # permissions of the members of users_list, i.e. if something is readable by
242   # any user in users_list, it will be readable in the query returned by this
243   # function.
244   def self.readable_by(*users_list)
245     # Get rid of troublesome nils
246     users_list.compact!
247
248     # Load optional keyword arguments, if they exist.
249     if users_list.last.is_a? Hash
250       kwargs = users_list.pop
251     else
252       kwargs = {}
253     end
254
255     # Collect the UUIDs of the authorized users.
256     sql_table = kwargs.fetch(:table_name, table_name)
257     include_trash = kwargs.fetch(:include_trash, false)
258     query_on = kwargs.fetch(:query_on, self)
259
260     sql_conds = []
261     user_uuids = users_list.map { |u| u.uuid }
262
263     if users_list.select { |u| u.is_admin }.any?
264       if !include_trash
265         # exclude rows that are explicitly trashed.
266         if sql_table != "api_client_authorizations"
267           sql_conds.push "NOT EXISTS(SELECT 1
268                   FROM #{PERMISSION_VIEW}
269                   WHERE trashed = 1 AND
270                   (#{sql_table}.uuid = target_uuid OR #{sql_table}.owner_uuid = target_uuid))"
271         end
272       end
273     else
274       # Can read object (evidently a group or user) whose UUID is listed
275       # explicitly in user_uuids.
276       sql_conds.push "#{sql_table}.uuid IN (:user_uuids)"
277
278       if include_trash
279         trashed_check = "true"
280       else
281         trashed_check = "trashed = 0"
282       end
283
284       if sql_table != "api_client_authorizations" and sql_table != "groups"
285         owner_check = "OR (target_uuid = #{sql_table}.owner_uuid AND target_owner_uuid IS NOT NULL)"
286         sql_conds.push "#{sql_table}.owner_uuid IN (:user_uuids)"
287       else
288         owner_check = ""
289       end
290
291       sql_conds.push "EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
292                      "WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 AND #{trashed_check} AND (target_uuid = #{sql_table}.uuid #{owner_check}))"
293
294       if sql_table == "links"
295         # Match any permission link that gives one of the authorized
296         # users some permission _or_ gives anyone else permission to
297         # view one of the authorized users.
298         sql_conds.push "(#{sql_table}.link_class IN (:permission_link_classes) AND "+
299                        "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"
300       end
301     end
302
303     query_on.where(sql_conds.join(' OR '),
304                     user_uuids: user_uuids,
305                     permission_link_classes: ['permission', 'resources'])
306   end
307
308   def save_with_unique_name!
309     uuid_was = uuid
310     name_was = name
311     max_retries = 2
312     transaction do
313       conn = ActiveRecord::Base.connection
314       conn.exec_query 'SAVEPOINT save_with_unique_name'
315       begin
316         save!
317       rescue ActiveRecord::RecordNotUnique => rn
318         raise if max_retries == 0
319         max_retries -= 1
320
321         conn.exec_query 'ROLLBACK TO SAVEPOINT save_with_unique_name'
322
323         # Dig into the error to determine if it is specifically calling out a
324         # (owner_uuid, name) uniqueness violation.  In this specific case, and
325         # the client requested a unique name with ensure_unique_name==true,
326         # update the name field and try to save again.  Loop as necessary to
327         # discover a unique name.  It is necessary to handle name choosing at
328         # this level (as opposed to the client) to ensure that record creation
329         # never fails due to a race condition.
330         err = rn.original_exception
331         raise unless err.is_a?(PG::UniqueViolation)
332
333         # Unfortunately ActiveRecord doesn't abstract out any of the
334         # necessary information to figure out if this the error is actually
335         # the specific case where we want to apply the ensure_unique_name
336         # behavior, so the following code is specialized to Postgres.
337         detail = err.result.error_field(PG::Result::PG_DIAG_MESSAGE_DETAIL)
338         raise unless /^Key \(owner_uuid, name\)=\([a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}, .*?\) already exists\./.match detail
339
340         new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
341         if new_name == name
342           # If the database is fast enough to do two attempts in the
343           # same millisecond, we need to wait to ensure we try a
344           # different timestamp on each attempt.
345           sleep 0.002
346           new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
347         end
348
349         self[:name] = new_name
350         self[:uuid] = nil if uuid_was.nil? && !uuid.nil?
351         conn.exec_query 'SAVEPOINT save_with_unique_name'
352         retry
353       ensure
354         conn.exec_query 'RELEASE SAVEPOINT save_with_unique_name'
355       end
356     end
357   end
358
359   def logged_attributes
360     attributes.except(*Rails.configuration.unlogged_attributes)
361   end
362
363   def self.full_text_searchable_columns
364     self.columns.select do |col|
365       col.type == :string or col.type == :text
366     end.map(&:name)
367   end
368
369   def self.full_text_tsvector
370     parts = full_text_searchable_columns.collect do |column|
371       "coalesce(#{column},'')"
372     end
373     "to_tsvector('english', #{parts.join(" || ' ' || ")})"
374   end
375
376   def self.apply_filters query, filters
377     ft = record_filters filters, self
378     if not ft[:cond_out].any?
379       return query
380     end
381     query.where('(' + ft[:cond_out].join(') AND (') + ')',
382                           *ft[:param_out])
383   end
384
385   protected
386
387   def self.deep_sort_hash(x)
388     if x.is_a? Hash
389       x.sort.collect do |k, v|
390         [k, deep_sort_hash(v)]
391       end.to_h
392     elsif x.is_a? Array
393       x.collect { |v| deep_sort_hash(v) }
394     else
395       x
396     end
397   end
398
399   def ensure_ownership_path_leads_to_user
400     if new_record? or owner_uuid_changed?
401       uuid_in_path = {owner_uuid => true, uuid => true}
402       x = owner_uuid
403       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
404         begin
405           if x == uuid
406             # Test for cycles with the new version, not the DB contents
407             x = owner_uuid
408           elsif !owner_class.respond_to? :find_by_uuid
409             raise ActiveRecord::RecordNotFound.new
410           else
411             x = owner_class.find_by_uuid(x).owner_uuid
412           end
413         rescue ActiveRecord::RecordNotFound => e
414           errors.add :owner_uuid, "is not owned by any user: #{e}"
415           return false
416         end
417         if uuid_in_path[x]
418           if x == owner_uuid
419             errors.add :owner_uuid, "would create an ownership cycle"
420           else
421             errors.add :owner_uuid, "has an ownership cycle"
422           end
423           return false
424         end
425         uuid_in_path[x] = true
426       end
427     end
428     true
429   end
430
431   def set_default_owner
432     if new_record? and current_user and respond_to? :owner_uuid=
433       self.owner_uuid ||= current_user.uuid
434     end
435   end
436
437   def ensure_owner_uuid_is_permitted
438     raise PermissionDeniedError if !current_user
439
440     if self.owner_uuid.nil?
441       errors.add :owner_uuid, "cannot be nil"
442       raise PermissionDeniedError
443     end
444
445     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
446     unless rsc_class == User or rsc_class == Group
447       errors.add :owner_uuid, "must be set to User or Group"
448       raise PermissionDeniedError
449     end
450
451     if new_record? || owner_uuid_changed?
452       # Permission on owner_uuid_was is needed to move an existing
453       # object away from its previous owner (which implies permission
454       # to modify this object itself, so we don't need to check that
455       # separately). Permission on the new owner_uuid is also needed.
456       [['old', owner_uuid_was],
457        ['new', owner_uuid]
458       ].each do |which, check_uuid|
459         if check_uuid.nil?
460           # old_owner_uuid is nil? New record, no need to check.
461         elsif !current_user.can?(write: check_uuid)
462           logger.warn "User #{current_user.uuid} tried to set ownership of #{self.class.to_s} #{self.uuid} but does not have permission to write #{which} owner_uuid #{check_uuid}"
463           errors.add :owner_uuid, "cannot be set or changed without write permission on #{which} owner"
464           raise PermissionDeniedError
465         end
466       end
467     else
468       # If the object already existed and we're not changing
469       # owner_uuid, we only need write permission on the object
470       # itself.
471       if !current_user.can?(write: self.uuid)
472         logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} without write permission"
473         errors.add :uuid, "is not writable"
474         raise PermissionDeniedError
475       end
476     end
477
478     true
479   end
480
481   def ensure_permission_to_save
482     unless (new_record? ? permission_to_create : permission_to_update)
483       raise PermissionDeniedError
484     end
485   end
486
487   def permission_to_create
488     current_user.andand.is_active
489   end
490
491   def permission_to_update
492     if !current_user
493       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
494       return false
495     end
496     if !current_user.is_active
497       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
498       return false
499     end
500     return true if current_user.is_admin
501     if self.uuid_changed?
502       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
503       return false
504     end
505     return true
506   end
507
508   def ensure_permission_to_destroy
509     raise PermissionDeniedError unless permission_to_destroy
510   end
511
512   def permission_to_destroy
513     permission_to_update
514   end
515
516   def maybe_update_modified_by_fields
517     update_modified_by_fields if self.changed? or self.new_record?
518     true
519   end
520
521   def update_modified_by_fields
522     current_time = db_current_time
523     self.created_at = created_at_was || current_time
524     self.updated_at = current_time
525     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
526     self.modified_at = current_time
527     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
528     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
529     true
530   end
531
532   def self.has_nonstring_keys? x
533     if x.is_a? Hash
534       x.each do |k,v|
535         return true if !(k.is_a?(String) || k.is_a?(Symbol)) || has_nonstring_keys?(v)
536       end
537     elsif x.is_a? Array
538       x.each do |v|
539         return true if has_nonstring_keys?(v)
540       end
541     end
542     false
543   end
544
545   def self.has_symbols? x
546     if x.is_a? Hash
547       x.each do |k,v|
548         return true if has_symbols?(k) or has_symbols?(v)
549       end
550     elsif x.is_a? Array
551       x.each do |k|
552         return true if has_symbols?(k)
553       end
554     elsif x.is_a? Symbol
555       return true
556     elsif x.is_a? String
557       return true if x.start_with?(':') && !x.start_with?('::')
558     end
559     false
560   end
561
562   def self.recursive_stringify x
563     if x.is_a? Hash
564       Hash[x.collect do |k,v|
565              [recursive_stringify(k), recursive_stringify(v)]
566            end]
567     elsif x.is_a? Array
568       x.collect do |k|
569         recursive_stringify k
570       end
571     elsif x.is_a? Symbol
572       x.to_s
573     elsif x.is_a? String and x.start_with?(':') and !x.start_with?('::')
574       x[1..-1]
575     else
576       x
577     end
578   end
579
580   def self.where_serialized(colname, value)
581     if value.empty?
582       # rails4 stores as null, rails3 stored as serialized [] or {}
583       sql = "#{colname.to_s} is null or #{colname.to_s} IN (?)"
584       sorted = value
585     else
586       sql = "#{colname.to_s} IN (?)"
587       sorted = deep_sort_hash(value)
588     end
589     where(sql, [sorted.to_yaml, SafeJSON.dump(sorted)])
590   end
591
592   Serializer = {
593     Hash => HashSerializer,
594     Array => ArraySerializer,
595   }
596
597   def self.serialize(colname, type)
598     coder = Serializer[type]
599     @serialized_attributes ||= {}
600     @serialized_attributes[colname.to_s] = coder
601     super(colname, coder)
602   end
603
604   def self.serialized_attributes
605     @serialized_attributes ||= {}
606   end
607
608   def serialized_attributes
609     self.class.serialized_attributes
610   end
611
612   def convert_serialized_symbols_to_strings
613     # ensure_serialized_attribute_type should prevent symbols from
614     # getting into the database in the first place. If someone managed
615     # to get them into the database (perhaps using an older version)
616     # we'll convert symbols to strings when loading from the
617     # database. (Otherwise, loading and saving an object with existing
618     # symbols in a serialized field will crash.)
619     self.class.serialized_attributes.each do |colname, attr|
620       if self.class.has_symbols? attributes[colname]
621         attributes[colname] = self.class.recursive_stringify attributes[colname]
622         send(colname + '=',
623              self.class.recursive_stringify(attributes[colname]))
624       end
625     end
626   end
627
628   def foreign_key_attributes
629     attributes.keys.select { |a| a.match(/_uuid$/) }
630   end
631
632   def skip_uuid_read_permission_check
633     %w(modified_by_client_uuid)
634   end
635
636   def skip_uuid_existence_check
637     []
638   end
639
640   def normalize_collection_uuids
641     foreign_key_attributes.each do |attr|
642       attr_value = send attr
643       if attr_value.is_a? String and
644           attr_value.match(/^[0-9a-f]{32,}(\+[@\w]+)*$/)
645         begin
646           send "#{attr}=", Collection.normalize_uuid(attr_value)
647         rescue
648           # TODO: abort instead of silently accepting unnormalizable value?
649         end
650       end
651     end
652   end
653
654   @@prefixes_hash = nil
655   def self.uuid_prefixes
656     unless @@prefixes_hash
657       @@prefixes_hash = {}
658       Rails.application.eager_load!
659       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
660         if k.respond_to?(:uuid_prefix)
661           @@prefixes_hash[k.uuid_prefix] = k
662         end
663       end
664     end
665     @@prefixes_hash
666   end
667
668   def self.uuid_like_pattern
669     "#{Rails.configuration.uuid_prefix}-#{uuid_prefix}-_______________"
670   end
671
672   def self.uuid_regex
673     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
674   end
675
676   def ensure_valid_uuids
677     specials = [system_user_uuid]
678
679     foreign_key_attributes.each do |attr|
680       if new_record? or send (attr + "_changed?")
681         next if skip_uuid_existence_check.include? attr
682         attr_value = send attr
683         next if specials.include? attr_value
684         if attr_value
685           if (r = ArvadosModel::resource_class_for_uuid attr_value)
686             unless skip_uuid_read_permission_check.include? attr
687               r = r.readable_by(current_user)
688             end
689             if r.where(uuid: attr_value).count == 0
690               errors.add(attr, "'#{attr_value}' not found")
691             end
692           end
693         end
694       end
695     end
696   end
697
698   class Email
699     def self.kind
700       "email"
701     end
702
703     def kind
704       self.class.kind
705     end
706
707     def self.readable_by (*u)
708       self
709     end
710
711     def self.where (u)
712       [{:uuid => u[:uuid]}]
713     end
714   end
715
716   def self.resource_class_for_uuid(uuid)
717     if uuid.is_a? ArvadosModel
718       return uuid.class
719     end
720     unless uuid.is_a? String
721       return nil
722     end
723
724     uuid.match HasUuid::UUID_REGEX do |re|
725       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
726     end
727
728     if uuid.match(/.+@.+/)
729       return Email
730     end
731
732     nil
733   end
734
735   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
736   # an object in any class.
737   def self.find_by_uuid uuid
738     if self == ArvadosModel
739       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
740       # delegate to the appropriate subclass based on the given uuid.
741       self.resource_class_for_uuid(uuid).unscoped.find_by_uuid(uuid)
742     else
743       super
744     end
745   end
746
747   def log_start_state
748     @old_attributes = Marshal.load(Marshal.dump(attributes))
749     @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
750   end
751
752   def log_change(event_type)
753     log = Log.new(event_type: event_type).fill_object(self)
754     yield log
755     log.save!
756     log_start_state
757   end
758
759   def log_create
760     log_change('create') do |log|
761       log.fill_properties('old', nil, nil)
762       log.update_to self
763     end
764   end
765
766   def log_update
767     log_change('update') do |log|
768       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
769       log.update_to self
770     end
771   end
772
773   def log_destroy
774     log_change('delete') do |log|
775       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
776       log.update_to nil
777     end
778   end
779 end