]> git.arvados.org - arvados.git/blob - services/api/app/controllers/application_controller.rb
Merge branch '22754-process-panel-slowness' into main. Closes #22754
[arvados.git] / services / api / app / controllers / application_controller.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'safe_json'
6 require 'request_error'
7
8 module ApiTemplateOverride
9   def allowed_to_render?(fieldset, field, model, options)
10     return false if !super
11     if options[:select]
12       options[:select].include? field.to_s
13     else
14       true
15     end
16   end
17 end
18
19 class ActsAsApi::ApiTemplate
20   prepend ApiTemplateOverride
21 end
22
23 require 'load_param'
24
25 class ApplicationController < ActionController::Base
26   include CurrentApiClient
27   include LoadParam
28   include DbCurrentTime
29
30   respond_to :json
31
32   # Although CSRF protection is already enabled by default, this is
33   # still needed to reposition CSRF checks later in callback order.
34   protect_from_forgery
35
36   ERROR_ACTIONS = [:render_error, :render_not_found]
37
38   around_action :set_current_request_id
39   before_action :disable_api_methods
40   before_action :set_cors_headers
41   before_action :respond_with_json_by_default
42   before_action :remote_ip
43   before_action :load_read_auths
44   before_action :require_auth_scope, except: ERROR_ACTIONS
45
46   before_action :catch_redirect_hint
47   before_action :load_required_parameters
48   before_action :load_limit_offset_order_params, only: [:index, :contents]
49   before_action :load_select_param
50   before_action(:find_object_by_uuid,
51                 except: [:index, :create, :update] + ERROR_ACTIONS)
52   before_action :find_object_for_update, only: [:update]
53   before_action :load_where_param, only: [:index, :contents]
54   before_action :load_filters_param, only: [:index, :contents]
55   before_action :find_objects_for_index, :only => :index
56   before_action(:set_nullable_attrs_to_null, only: [:update, :create])
57   before_action :reload_object_before_update, :only => :update
58   before_action(:render_404_if_no_object,
59                 except: [:index, :create] + ERROR_ACTIONS)
60   before_action :only_admin_can_bypass_federation
61
62   attr_writer :resource_attrs
63
64   begin
65     rescue_from(Exception,
66                 ArvadosModel::PermissionDeniedError,
67                 :with => :render_error)
68     rescue_from(ActiveRecord::RecordNotFound,
69                 ActionController::RoutingError,
70                 AbstractController::ActionNotFound,
71                 :with => :render_not_found)
72   end
73
74   def initialize *args
75     super
76     @object = nil
77     @objects = nil
78     @offset = nil
79     @limit = nil
80     @select = nil
81     @distinct = nil
82     @response_resource_name = nil
83     @attrs = nil
84     @extra_included = nil
85   end
86
87   def default_url_options
88     options = {}
89     if Rails.configuration.Services.Controller.ExternalURL != URI("")
90       exturl = Rails.configuration.Services.Controller.ExternalURL
91       options[:host] = exturl.host
92       options[:port] = exturl.port
93       options[:protocol] = exturl.scheme
94     end
95     options
96   end
97
98   def index
99     if params[:eager] and params[:eager] != '0' and params[:eager] != 0 and params[:eager] != ''
100       @objects.each(&:eager_load_associations)
101     end
102     render_list
103   end
104
105   def show
106     send_json @object.as_api_response(nil, select: select_for_klass(@select, model_class))
107   end
108
109   def create
110     @object = model_class.new resource_attrs
111
112     if @object.respond_to?(:name) && params[:ensure_unique_name]
113       @object.save_with_unique_name!
114     else
115       @object.save!
116     end
117
118     show
119   end
120
121   def update
122     attrs_to_update = resource_attrs.reject { |k,v|
123       [:kind, :etag, :href].index k
124     }
125     @object.update! attrs_to_update
126     show
127   end
128
129   def destroy
130     @object.destroy
131     show
132   end
133
134   def catch_redirect_hint
135     if !current_user
136       if params.has_key?('redirect_to') then
137         session[:redirect_to] = params[:redirect_to]
138       end
139     end
140   end
141
142   def render_404_if_no_object
143     render_not_found "Object not found" if !@object
144   end
145
146   def only_admin_can_bypass_federation
147     unless !params[:bypass_federation] || current_user.andand.is_admin
148       send_error("The bypass_federation parameter is only permitted when current user is admin", status: 403)
149     end
150   end
151
152   def render_error(e)
153     logger.error e.inspect
154     if e.respond_to? :backtrace and e.backtrace
155       # This will be cleared by lograge after adding it to the log.
156       # Usually lograge would get the exceptions, but in our case we're catching
157       # all of them with exception handlers that cannot re-raise them because they
158       # don't get propagated.
159       Thread.current[:exception] = e.inspect
160       Thread.current[:backtrace] = e.backtrace.collect { |x| x + "\n" }.join('')
161     end
162     if (@object.respond_to? :errors and
163         @object.errors.andand.full_messages.andand.any?)
164       errors = @object.errors.full_messages
165       logger.error errors.inspect
166     else
167       errors = [e.inspect]
168     end
169
170     case e
171     when ActiveRecord::Deadlocked,
172          ActiveRecord::ConnectionNotEstablished,
173          ActiveRecord::LockWaitTimeout,
174          ActiveRecord::QueryAborted
175       status = 500
176     else
177       status = e.respond_to?(:http_status) ? e.http_status : 422
178     end
179
180     send_error(*errors, status: status)
181   end
182
183   def render_not_found(e=ActionController::RoutingError.new("Path not found"))
184     logger.error e.inspect
185     send_error("Path not found", status: 404)
186   end
187
188   def render_accepted
189     send_json ({accepted: true}), status: 202
190   end
191
192   protected
193
194   def bool_param(pname)
195     if params.include?(pname)
196       if params[pname].is_a?(Boolean)
197         return params[pname]
198       else
199         logger.warn "Warning: received non-boolean value #{params[pname].inspect} for boolean parameter #{pname} on #{self.class.inspect}, treating as false."
200       end
201     end
202     false
203   end
204
205   def send_error(*args)
206     if args.last.is_a? Hash
207       err = args.pop
208     else
209       err = {}
210     end
211     err[:errors] ||= args
212     err[:errors].map! do |err|
213       err += " (#{request.request_id})"
214     end
215     err[:error_token] = [Time.now.utc.to_i, "%08x" % rand(16 ** 8)].join("+")
216     status = err.delete(:status) || 422
217     logger.error "Error #{err[:error_token]}: #{status}"
218     send_json err, status: status
219   end
220
221   def send_json response, opts={}
222     # The obvious render(json: ...) forces a slow JSON encoder. See
223     # #3021 and commit logs. Might be fixed in Rails 4.1.
224     render({
225              plain: SafeJSON.dump(response).html_safe,
226              content_type: 'application/json'
227            }.merge opts)
228   end
229
230   def find_objects_for_index
231     @objects ||= model_class.readable_by(*@read_users, {
232       :include_trash => ((self.class._index_requires_parameters[:include_trash] && bool_param(:include_trash)) || 'untrash' == action_name),
233       :include_old_versions => self.class._index_requires_parameters[:include_old_versions] && bool_param(:include_old_versions)
234     })
235     apply_where_limit_order_params
236   end
237
238   def apply_filters model_class=nil
239     model_class ||= self.model_class
240     @objects = model_class.apply_filters(@objects, @filters)
241   end
242
243   def select_for_klass sel, model_class, raise_unknown=true
244     return nil if sel.nil?
245     # Filter the select fields to only the ones that apply to the
246     # given class.
247     sel.map do |column|
248       sp = column.split(".")
249       if sp.length == 2 && sp[0] == model_class.table_name && model_class.selectable_attributes.include?(sp[1])
250         sp[1]
251       elsif model_class.selectable_attributes.include? column
252         column
253       elsif raise_unknown
254         raise ArgumentError.new("Invalid attribute '#{column}' of #{model_class.name} in select parameter")
255       else
256         nil
257       end
258     end.compact
259   end
260
261   def apply_where_limit_order_params model_class=nil
262     model_class ||= self.model_class
263     apply_filters model_class
264
265     ar_table_name = @objects.table_name
266     if @where.is_a? Hash and @where.any?
267       conditions = ['1=1']
268       @where.each do |attr,value|
269         if attr.to_s == 'any'
270           if value.is_a?(Array) and
271               value.length == 2 and
272               value[0] == 'contains' then
273             ilikes = []
274             model_class.any_searchable_columns('ilike').each do |column|
275               # Including owner_uuid in an "any column" search will
276               # probably just return a lot of false positives.
277               next if column == 'owner_uuid'
278               ilikes << "#{ar_table_name}.#{column} ilike ?"
279               conditions << "%#{value[1]}%"
280             end
281             if ilikes.any?
282               conditions[0] << ' and (' + ilikes.join(' or ') + ')'
283             end
284           else
285             equals = []
286             model_class.any_searchable_columns('=').each do |column|
287               equals << "#{ar_table_name}.#{column} = ?"
288               conditions << value
289             end
290             conditions[0] << ' and (' + equals.join(' or ') + ')'
291           end
292         elsif attr.to_s.match(/^[a-z][_a-z0-9]+$/) and
293             model_class.columns.collect(&:name).index(attr.to_s)
294           if value.nil?
295             conditions[0] << " and #{ar_table_name}.#{attr} is ?"
296             conditions << nil
297           elsif value.is_a? Array
298             if value[0] == 'contains' and value.length == 2
299               conditions[0] << " and #{ar_table_name}.#{attr} like ?"
300               conditions << "%#{value[1]}%"
301             else
302               conditions[0] << " and #{ar_table_name}.#{attr} in (?)"
303               conditions << value
304             end
305           elsif value.is_a? String or value.is_a? Integer or value == true or value == false
306             conditions[0] << " and #{ar_table_name}.#{attr}=?"
307             conditions << value
308           elsif value.is_a? Hash
309             # Not quite the same thing as "equal?" but better than nothing?
310             value.each do |k,v|
311               if v.is_a? String
312                 conditions[0] << " and #{ar_table_name}.#{attr} ilike ?"
313                 conditions << "%#{k}%#{v}%"
314               end
315             end
316           end
317         end
318       end
319       if conditions.length > 1
320         conditions[0].sub!(/^1=1 and /, '')
321         @objects = @objects.
322           where(*conditions)
323       end
324     end
325
326     if @select
327       unless action_name.in? %w(create update destroy)
328         # Map attribute names in @select to real column names, resolve
329         # those to fully-qualified SQL column names, and pass the
330         # resulting string to the select method.
331         columns_list = model_class.columns_for_attributes(select_for_klass @select, model_class).
332           map { |s| "#{ar_table_name}.#{ActiveRecord::Base.connection.quote_column_name s}" }
333         @objects = @objects.select(columns_list.join(", "))
334       end
335
336       # This information helps clients understand what they're seeing
337       # (Workbench always expects it), but they can't select it explicitly
338       # because it's not an SQL column.  Always add it.
339       # (This is harmless, given that clients can deduce what they're
340       # looking at by the returned UUID anyway.)
341       @select |= ["kind"]
342     end
343     @objects = @objects.order(@orders.join ", ") if @orders.any?
344     @objects = @objects.limit(@limit)
345     @objects = @objects.offset(@offset)
346     @objects = @objects.distinct() if @distinct
347   end
348
349   # limit_database_read ensures @objects (which must be an
350   # ActiveRelation) does not return too many results to fit in memory,
351   # by previewing the results and calling @objects.limit() if
352   # necessary.
353   def limit_database_read(model_class:)
354     return if @limit == 0 || @limit == 1
355     model_class ||= self.model_class
356     limit_columns = model_class.limit_index_columns_read
357     limit_columns &= model_class.columns_for_attributes(select_for_klass @select, model_class) if @select
358     return if limit_columns.empty?
359     model_class.transaction do
360       # This query does not use `pg_column_size()` because the returned value
361       # can be smaller than the apparent length thanks to compression.
362       # `octet_length(::text)` better reflects how expensive it will be for
363       # Rails to process the data.
364       limit_query = @objects.
365         except(:select, :distinct).
366         select("(%s) as read_length" %
367                limit_columns.map { |s| "coalesce(octet_length(#{model_class.table_name}.#{s}::text),0)" }.join(" + "))
368       new_limit = 0
369       read_total = 0
370       limit_query.each do |record|
371         new_limit += 1
372         read_total += record.read_length.to_i
373         if read_total >= Rails.configuration.API.MaxIndexDatabaseRead
374           new_limit -= 1 if new_limit > 1
375           @limit = new_limit
376           break
377         elsif new_limit >= @limit
378           break
379         end
380       end
381       @objects = @objects.limit(@limit)
382       # Force @objects to run its query inside this transaction.
383       @objects.each { |_| break }
384     end
385   end
386
387   def resource_attrs
388     return @attrs if @attrs
389     @attrs = params[resource_name]
390     if @attrs.nil?
391       @attrs = {}
392     elsif @attrs.is_a? String
393       @attrs = Oj.strict_load @attrs, symbol_keys: true
394     end
395     unless [Hash, ActionController::Parameters].include? @attrs.class
396       message = "No #{resource_name}"
397       if resource_name.index('_')
398         message << " (or #{resource_name.camelcase(:lower)})"
399       end
400       message << " hash provided with request"
401       raise ArgumentError.new(message)
402     end
403     %w(created_at modified_by_client_uuid modified_by_user_uuid modified_at).each do |x|
404       @attrs.delete x.to_sym
405     end
406     @attrs = @attrs.symbolize_keys if @attrs.is_a? ActiveSupport::HashWithIndifferentAccess
407     @attrs
408   end
409
410   # Authentication
411   def load_read_auths
412     @read_auths = []
413     if current_api_client_authorization
414       @read_auths << current_api_client_authorization
415     end
416     # Load reader tokens if this is a read request.
417     # If there are too many reader tokens, assume the request is malicious
418     # and ignore it.
419     if request.get? and params[:reader_tokens] and
420       params[:reader_tokens].size < 100
421       secrets = params[:reader_tokens].map { |t|
422         if t.is_a? String and t.starts_with? "v2/"
423           t.split("/")[2]
424         else
425           t
426         end
427       }
428       @read_auths += ApiClientAuthorization
429         .includes(:user)
430         .where('api_token IN (?) AND
431                 (least(expires_at, refreshes_at) IS NULL
432                  OR least(expires_at, refreshes_at) > CURRENT_TIMESTAMP)',
433                secrets)
434         .to_a
435     end
436     @read_auths.select! { |auth| auth.scopes_allow_request? request }
437     @read_users = @read_auths.map(&:user).uniq
438   end
439
440   def require_login
441     if not current_user
442       respond_to do |format|
443         format.json { send_error("Not logged in", status: 401) }
444         format.html { redirect_to '/login' }
445       end
446       false
447     end
448   end
449
450   def admin_required
451     unless current_user and current_user.is_admin
452       send_error("Forbidden", status: 403)
453     end
454   end
455
456   def require_auth_scope
457     unless current_user && @read_auths.any? { |auth| auth.user.andand.uuid == current_user.uuid }
458       if require_login != false
459         send_error("Forbidden", status: 403)
460       end
461       false
462     end
463   end
464
465   def set_current_request_id
466     Rails.logger.tagged(request.request_id) do
467       yield
468     end
469   end
470
471   def append_info_to_payload(payload)
472     super
473     payload[:request_id] = request.request_id
474     payload[:client_ipaddr] = @remote_ip
475     payload[:client_auth] = current_api_client_authorization.andand.uuid || nil
476   end
477
478   def disable_api_methods
479     if Rails.configuration.API.DisabledAPIs[controller_name + "." + action_name]
480       send_error("Disabled", status: 404)
481     end
482   end
483
484   def set_cors_headers
485     response.headers['Access-Control-Allow-Origin'] = '*'
486     response.headers['Access-Control-Allow-Methods'] = 'GET, HEAD, PUT, POST, DELETE'
487     response.headers['Access-Control-Allow-Headers'] = 'Authorization, Content-Type'
488     response.headers['Access-Control-Max-Age'] = '86486400'
489   end
490
491   def respond_with_json_by_default
492     html_index = request.accepts.index(Mime[:html])
493     if html_index.nil? or request.accepts[0...html_index].include?(Mime[:json])
494       request.format = :json
495     end
496   end
497
498   def model_class
499     controller_name.classify.constantize
500   end
501
502   def resource_name             # params[] key used by client
503     controller_name.singularize
504   end
505
506   def table_name
507     controller_name
508   end
509
510   def find_object_for_update
511     find_object_by_uuid(with_lock: true)
512   end
513
514   def find_object_by_uuid(with_lock: false)
515     if params[:id] and params[:id].match(/\D/)
516       params[:uuid] = params.delete :id
517     end
518     @where = {}
519     # Some APIs (at least groups/contents) take an optional uuid argument.
520     # They go through this method to handle it when present but we cannot
521     # assume it is always set.
522     @where[:uuid] = params[:uuid] if params[:uuid]
523     @offset = 0
524     @limit = 1
525     @orders = []
526     @filters = []
527     @objects = nil
528
529     # This is a little hacky but sometimes the fields the user wants
530     # to selecting on are unrelated to the object being loaded here,
531     # for example groups#contents, so filter the fields that will be
532     # used in find_objects_for_index and then reset afterwards.  In
533     # some cases, code that modifies the @select list needs to set
534     # @preserve_select.
535     @preserve_select = @select
536     @select = select_for_klass(@select, self.model_class, false)
537
538     find_objects_for_index
539     if with_lock && Rails.configuration.API.LockBeforeUpdate
540       @object = @objects.lock.first
541     else
542       @object = @objects.first
543     end
544     @select = @preserve_select
545   end
546
547   def nullable_attributes
548     []
549   end
550
551   # Go code may send empty values (ie: empty string instead of NULL) that
552   # should be translated to NULL on the database.
553   def set_nullable_attrs_to_null
554     nullify_attrs(resource_attrs.to_hash).each do |k, v|
555       resource_attrs[k] = v
556     end
557   end
558
559   def nullify_attrs(a = {})
560     new_attrs = a.to_hash.symbolize_keys
561     (new_attrs.keys & nullable_attributes).each do |attr|
562       val = new_attrs[attr]
563       if (val.class == Integer && val == 0) || (val.class == String && val == "")
564         new_attrs[attr] = nil
565       end
566     end
567     return new_attrs
568   end
569
570   def reload_object_before_update
571     # This is necessary to prevent an ActiveRecord::ReadOnlyRecord
572     # error when updating an object which was retrieved using a join.
573     if @object.andand.readonly?
574       @object = model_class.find_by_uuid(@objects.first.uuid)
575     end
576   end
577
578   def load_json_value(hash, key, must_be_class=nil)
579     return if hash[key].nil?
580
581     val = hash[key]
582     if val.is_a? ActionController::Parameters
583       val = val.to_unsafe_hash
584     elsif val.is_a? String
585       val = SafeJSON.load(val)
586       hash[key] = val
587     end
588     # When assigning a Hash to an ActionController::Parameters and then
589     # retrieve it, we get another ActionController::Parameters instead of
590     # a Hash. This doesn't happen with other types. This is why 'val' is
591     # being used to do type checking below.
592     if must_be_class and !val.is_a? must_be_class
593       raise TypeError.new("parameter #{key.to_s} must be a #{must_be_class.to_s}")
594     end
595   end
596
597   def self.accept_attribute_as_json(attr, must_be_class=nil)
598     before_action lambda { accept_attribute_as_json attr, must_be_class }
599   end
600   accept_attribute_as_json :properties, Hash
601   accept_attribute_as_json :info, Hash
602   def accept_attribute_as_json(attr, must_be_class)
603     if params[resource_name] and [Hash, ActionController::Parameters].include?(resource_attrs.class)
604       if resource_attrs[attr].is_a? Hash
605         # Convert symbol keys to strings (in hashes provided by
606         # resource_attrs)
607         resource_attrs[attr] = resource_attrs[attr].
608           with_indifferent_access.to_hash
609       else
610         load_json_value(resource_attrs, attr, must_be_class)
611       end
612     end
613   end
614
615   def self.accept_param_as_json(key, must_be_class=nil)
616     prepend_before_action lambda { load_json_value(params, key, must_be_class) }
617   end
618   accept_param_as_json :reader_tokens, Array
619
620   def object_list(model_class:)
621     if @objects.respond_to?(:except)
622       limit_database_read(model_class: model_class)
623     end
624     list = {
625       :kind  => "arvados##{(@response_resource_name || resource_name).camelize(:lower)}List",
626       :etag => "",
627       :self_link => "",
628       :offset => @offset,
629       :limit => @limit,
630       :items => @objects.as_api_response(nil, {select: @select})
631     }
632     if @extra_included
633       list[:included] = @extra_included.as_api_response(nil, {select: @select})
634     end
635     case params[:count]
636     when nil, '', 'exact'
637       if @objects.respond_to? :except
638         list[:items_available] = @objects.
639           except(:limit).except(:offset).
640           count(@distinct ? :id : '*')
641       end
642     when 'none'
643     else
644       raise ArgumentError.new("count parameter must be 'exact' or 'none'")
645     end
646     list
647   end
648
649   def render_list
650     send_json object_list(model_class: self.model_class)
651   end
652
653   def remote_ip
654     # Caveat: this is highly dependent on the proxy setup. YMMV.
655     if request.headers.key?('HTTP_X_REAL_IP') then
656       # We're behind a reverse proxy
657       @remote_ip = request.headers['HTTP_X_REAL_IP']
658     else
659       # Hopefully, we are not!
660       @remote_ip = request.env['REMOTE_ADDR']
661     end
662   end
663
664   def load_required_parameters
665     (self.class.send "_#{params[:action]}_requires_parameters" rescue {}).
666       each do |key, info|
667       if info[:required] and not params.include?(key)
668         raise ArgumentError.new("#{key} parameter is required")
669       elsif info[:type] == 'boolean'
670         # Make sure params[key] is either true or false -- not a
671         # string, not nil, etc.
672         if not params.include?(key)
673           params[key] = info[:default] || false
674         elsif [false, 'false', '0', 0].include? params[key]
675           params[key] = false
676         elsif [true, 'true', '1', 1].include? params[key]
677           params[key] = true
678         else
679           raise TypeError.new("#{key} parameter must be a boolean, true or false")
680         end
681       end
682     end
683     true
684   end
685
686   def self._create_requires_parameters
687     {
688       select: {
689         type: 'array',
690         description: "An array of names of attributes to return in the response.",
691         required: false,
692       },
693       ensure_unique_name: {
694         type: "boolean",
695         description: "If the given name is already used by this owner, adjust the name to ensure uniqueness instead of returning an error.",
696         location: "query",
697         required: false,
698         default: false
699       },
700       cluster_id: {
701         type: 'string',
702         description: "Cluster ID of a federated cluster where this object should be created.",
703         location: "query",
704         required: false,
705       },
706     }
707   end
708
709   def self._update_requires_parameters
710     {
711       select: {
712         type: 'array',
713         description: "An array of names of attributes to return in the response.",
714         required: false,
715       },
716     }
717   end
718
719   def self._show_requires_parameters
720     {
721       select: {
722         type: 'array',
723         description: "An array of names of attributes to return in the response.",
724         required: false,
725       },
726     }
727   end
728
729   def self._index_requires_parameters
730     {
731       filters: {
732         type: 'array',
733         required: false,
734         description: "Filters to limit which objects are returned by their attributes.
735 Refer to the [filters reference][] for more information about how to write filters.
736
737 [filters reference]: https://doc.arvados.org/api/methods.html#filters
738 ",
739       },
740       where: {
741         type: 'object',
742         required: false,
743         description: "An object to limit which objects are returned by their attributes.
744 The keys of this object are attribute names.
745 Each value is either a single matching value or an array of matching values for that attribute.
746 The `filters` parameter is more flexible and preferred.
747 ",
748       },
749       order: {
750         type: 'array',
751         required: false,
752         description: "An array of strings to set the order in which matching objects are returned.
753 Each string has the format `<ATTRIBUTE> <DIRECTION>`.
754 `DIRECTION` can be `asc` or omitted for ascending, or `desc` for descending.
755 ",
756       },
757       select: {
758         type: 'array',
759         description: "An array of names of attributes to return from each matching object.",
760         required: false,
761       },
762       distinct: {
763         type: 'boolean',
764         required: false,
765         default: false,
766         description: "If this is true, and multiple objects have the same values
767 for the attributes that you specify in the `select` parameter, then each unique
768 set of values will only be returned once in the result set.
769 ",
770       },
771       limit: {
772         type: 'integer',
773         required: false,
774         default: DEFAULT_LIMIT,
775         description: "The maximum number of objects to return in the result.
776 Note that the API may return fewer results than this if your request hits other
777 limits set by the administrator.
778 ",
779       },
780       offset: {
781         type: 'integer',
782         required: false,
783         default: 0,
784         description: "Return matching objects starting from this index.
785 Note that result indexes may change if objects are modified in between a series
786 of list calls.
787 ",
788       },
789       count: {
790         type: 'string',
791         required: false,
792         default: 'exact',
793         description: "A string to determine result counting behavior. Supported values are:
794
795   * `\"exact\"`: The response will include an `items_available` field that
796     counts the number of objects that matched this search criteria,
797     including ones not included in `items`.
798
799   * `\"none\"`: The response will not include an `items_avaliable`
800     field. This improves performance by returning a result as soon as enough
801     `items` have been loaded for this result.
802
803 ",
804       },
805       cluster_id: {
806         type: 'string',
807         description: "Cluster ID of a federated cluster to return objects from",
808         location: "query",
809         required: false,
810       },
811       bypass_federation: {
812         type: 'boolean',
813         required: false,
814         default: false,
815         description: "If true, do not return results from other clusters in the
816 federation, only the cluster that received the request.
817 You must be an administrator to use this flag.
818 ",
819       }
820     }
821   end
822
823   def render *opts
824     if opts.first
825       response = opts.first[:json]
826       if response.is_a?(Hash) &&
827           params[:_profile] &&
828           Thread.current[:request_starttime]
829         response[:_profile] = {
830           request_time: Time.now - Thread.current[:request_starttime]
831         }
832       end
833     end
834     super(*opts)
835   end
836 end