]> git.arvados.org - arvados.git/blob - services/api/app/controllers/application_controller.rb
Merge branch '22608-upgrade-rails'
[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.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           end
285         elsif attr.to_s.match(/^[a-z][_a-z0-9]+$/) and
286             model_class.columns.collect(&:name).index(attr.to_s)
287           if value.nil?
288             conditions[0] << " and #{ar_table_name}.#{attr} is ?"
289             conditions << nil
290           elsif value.is_a? Array
291             if value[0] == 'contains' and value.length == 2
292               conditions[0] << " and #{ar_table_name}.#{attr} like ?"
293               conditions << "%#{value[1]}%"
294             else
295               conditions[0] << " and #{ar_table_name}.#{attr} in (?)"
296               conditions << value
297             end
298           elsif value.is_a? String or value.is_a? Integer or value == true or value == false
299             conditions[0] << " and #{ar_table_name}.#{attr}=?"
300             conditions << value
301           elsif value.is_a? Hash
302             # Not quite the same thing as "equal?" but better than nothing?
303             value.each do |k,v|
304               if v.is_a? String
305                 conditions[0] << " and #{ar_table_name}.#{attr} ilike ?"
306                 conditions << "%#{k}%#{v}%"
307               end
308             end
309           end
310         end
311       end
312       if conditions.length > 1
313         conditions[0].sub!(/^1=1 and /, '')
314         @objects = @objects.
315           where(*conditions)
316       end
317     end
318
319     if @select
320       unless action_name.in? %w(create update destroy)
321         # Map attribute names in @select to real column names, resolve
322         # those to fully-qualified SQL column names, and pass the
323         # resulting string to the select method.
324         columns_list = model_class.columns_for_attributes(select_for_klass @select, model_class).
325           map { |s| "#{ar_table_name}.#{ActiveRecord::Base.connection.quote_column_name s}" }
326         @objects = @objects.select(columns_list.join(", "))
327       end
328
329       # This information helps clients understand what they're seeing
330       # (Workbench always expects it), but they can't select it explicitly
331       # because it's not an SQL column.  Always add it.
332       # (This is harmless, given that clients can deduce what they're
333       # looking at by the returned UUID anyway.)
334       @select |= ["kind"]
335     end
336     @objects = @objects.order(@orders.join ", ") if @orders.any?
337     @objects = @objects.limit(@limit)
338     @objects = @objects.offset(@offset)
339     @objects = @objects.distinct() if @distinct
340   end
341
342   # limit_database_read ensures @objects (which must be an
343   # ActiveRelation) does not return too many results to fit in memory,
344   # by previewing the results and calling @objects.limit() if
345   # necessary.
346   def limit_database_read(model_class:)
347     return if @limit == 0 || @limit == 1
348     model_class ||= self.model_class
349     limit_columns = model_class.limit_index_columns_read
350     limit_columns &= model_class.columns_for_attributes(select_for_klass @select, model_class) if @select
351     return if limit_columns.empty?
352     model_class.transaction do
353       # This query does not use `pg_column_size()` because the returned value
354       # can be smaller than the apparent length thanks to compression.
355       # `octet_length(::text)` better reflects how expensive it will be for
356       # Rails to process the data.
357       limit_query = @objects.
358         except(:select, :distinct).
359         select("(%s) as read_length" %
360                limit_columns.map { |s| "octet_length(#{model_class.table_name}.#{s}::text)" }.join(" + "))
361       new_limit = 0
362       read_total = 0
363       limit_query.each do |record|
364         new_limit += 1
365         read_total += record.read_length.to_i
366         if read_total >= Rails.configuration.API.MaxIndexDatabaseRead
367           new_limit -= 1 if new_limit > 1
368           @limit = new_limit
369           break
370         elsif new_limit >= @limit
371           break
372         end
373       end
374       @objects = @objects.limit(@limit)
375       # Force @objects to run its query inside this transaction.
376       @objects.each { |_| break }
377     end
378   end
379
380   def resource_attrs
381     return @attrs if @attrs
382     @attrs = params[resource_name]
383     if @attrs.nil?
384       @attrs = {}
385     elsif @attrs.is_a? String
386       @attrs = Oj.strict_load @attrs, symbol_keys: true
387     end
388     unless [Hash, ActionController::Parameters].include? @attrs.class
389       message = "No #{resource_name}"
390       if resource_name.index('_')
391         message << " (or #{resource_name.camelcase(:lower)})"
392       end
393       message << " hash provided with request"
394       raise ArgumentError.new(message)
395     end
396     %w(created_at modified_by_client_uuid modified_by_user_uuid modified_at).each do |x|
397       @attrs.delete x.to_sym
398     end
399     @attrs = @attrs.symbolize_keys if @attrs.is_a? ActiveSupport::HashWithIndifferentAccess
400     @attrs
401   end
402
403   # Authentication
404   def load_read_auths
405     @read_auths = []
406     if current_api_client_authorization
407       @read_auths << current_api_client_authorization
408     end
409     # Load reader tokens if this is a read request.
410     # If there are too many reader tokens, assume the request is malicious
411     # and ignore it.
412     if request.get? and params[:reader_tokens] and
413       params[:reader_tokens].size < 100
414       secrets = params[:reader_tokens].map { |t|
415         if t.is_a? String and t.starts_with? "v2/"
416           t.split("/")[2]
417         else
418           t
419         end
420       }
421       @read_auths += ApiClientAuthorization
422         .includes(:user)
423         .where('api_token IN (?) AND
424                 (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)',
425                secrets)
426         .to_a
427     end
428     @read_auths.select! { |auth| auth.scopes_allow_request? request }
429     @read_users = @read_auths.map(&:user).uniq
430   end
431
432   def require_login
433     if not current_user
434       respond_to do |format|
435         format.json { send_error("Not logged in", status: 401) }
436         format.html { redirect_to '/login' }
437       end
438       false
439     end
440   end
441
442   def admin_required
443     unless current_user and current_user.is_admin
444       send_error("Forbidden", status: 403)
445     end
446   end
447
448   def require_auth_scope
449     unless current_user && @read_auths.any? { |auth| auth.user.andand.uuid == current_user.uuid }
450       if require_login != false
451         send_error("Forbidden", status: 403)
452       end
453       false
454     end
455   end
456
457   def set_current_request_id
458     Rails.logger.tagged(request.request_id) do
459       yield
460     end
461   end
462
463   def append_info_to_payload(payload)
464     super
465     payload[:request_id] = request.request_id
466     payload[:client_ipaddr] = @remote_ip
467     payload[:client_auth] = current_api_client_authorization.andand.uuid || nil
468   end
469
470   def disable_api_methods
471     if Rails.configuration.API.DisabledAPIs[controller_name + "." + action_name]
472       send_error("Disabled", status: 404)
473     end
474   end
475
476   def set_cors_headers
477     response.headers['Access-Control-Allow-Origin'] = '*'
478     response.headers['Access-Control-Allow-Methods'] = 'GET, HEAD, PUT, POST, DELETE'
479     response.headers['Access-Control-Allow-Headers'] = 'Authorization, Content-Type'
480     response.headers['Access-Control-Max-Age'] = '86486400'
481   end
482
483   def respond_with_json_by_default
484     html_index = request.accepts.index(Mime[:html])
485     if html_index.nil? or request.accepts[0...html_index].include?(Mime[:json])
486       request.format = :json
487     end
488   end
489
490   def model_class
491     controller_name.classify.constantize
492   end
493
494   def resource_name             # params[] key used by client
495     controller_name.singularize
496   end
497
498   def table_name
499     controller_name
500   end
501
502   def find_object_for_update
503     find_object_by_uuid(with_lock: true)
504   end
505
506   def find_object_by_uuid(with_lock: false)
507     if params[:id] and params[:id].match(/\D/)
508       params[:uuid] = params.delete :id
509     end
510     @where = {}
511     # Some APIs (at least groups/contents) take an optional uuid argument.
512     # They go through this method to handle it when present but we cannot
513     # assume it is always set.
514     @where[:uuid] = params[:uuid] if params[:uuid]
515     @offset = 0
516     @limit = 1
517     @orders = []
518     @filters = []
519     @objects = nil
520
521     # This is a little hacky but sometimes the fields the user wants
522     # to selecting on are unrelated to the object being loaded here,
523     # for example groups#contents, so filter the fields that will be
524     # used in find_objects_for_index and then reset afterwards.  In
525     # some cases, code that modifies the @select list needs to set
526     # @preserve_select.
527     @preserve_select = @select
528     @select = select_for_klass(@select, self.model_class, false)
529
530     find_objects_for_index
531     if with_lock && Rails.configuration.API.LockBeforeUpdate
532       @object = @objects.lock.first
533     else
534       @object = @objects.first
535     end
536     @select = @preserve_select
537   end
538
539   def nullable_attributes
540     []
541   end
542
543   # Go code may send empty values (ie: empty string instead of NULL) that
544   # should be translated to NULL on the database.
545   def set_nullable_attrs_to_null
546     nullify_attrs(resource_attrs.to_hash).each do |k, v|
547       resource_attrs[k] = v
548     end
549   end
550
551   def nullify_attrs(a = {})
552     new_attrs = a.to_hash.symbolize_keys
553     (new_attrs.keys & nullable_attributes).each do |attr|
554       val = new_attrs[attr]
555       if (val.class == Integer && val == 0) || (val.class == String && val == "")
556         new_attrs[attr] = nil
557       end
558     end
559     return new_attrs
560   end
561
562   def reload_object_before_update
563     # This is necessary to prevent an ActiveRecord::ReadOnlyRecord
564     # error when updating an object which was retrieved using a join.
565     if @object.andand.readonly?
566       @object = model_class.find_by_uuid(@objects.first.uuid)
567     end
568   end
569
570   def load_json_value(hash, key, must_be_class=nil)
571     return if hash[key].nil?
572
573     val = hash[key]
574     if val.is_a? ActionController::Parameters
575       val = val.to_unsafe_hash
576     elsif val.is_a? String
577       val = SafeJSON.load(val)
578       hash[key] = val
579     end
580     # When assigning a Hash to an ActionController::Parameters and then
581     # retrieve it, we get another ActionController::Parameters instead of
582     # a Hash. This doesn't happen with other types. This is why 'val' is
583     # being used to do type checking below.
584     if must_be_class and !val.is_a? must_be_class
585       raise TypeError.new("parameter #{key.to_s} must be a #{must_be_class.to_s}")
586     end
587   end
588
589   def self.accept_attribute_as_json(attr, must_be_class=nil)
590     before_action lambda { accept_attribute_as_json attr, must_be_class }
591   end
592   accept_attribute_as_json :properties, Hash
593   accept_attribute_as_json :info, Hash
594   def accept_attribute_as_json(attr, must_be_class)
595     if params[resource_name] and [Hash, ActionController::Parameters].include?(resource_attrs.class)
596       if resource_attrs[attr].is_a? Hash
597         # Convert symbol keys to strings (in hashes provided by
598         # resource_attrs)
599         resource_attrs[attr] = resource_attrs[attr].
600           with_indifferent_access.to_hash
601       else
602         load_json_value(resource_attrs, attr, must_be_class)
603       end
604     end
605   end
606
607   def self.accept_param_as_json(key, must_be_class=nil)
608     prepend_before_action lambda { load_json_value(params, key, must_be_class) }
609   end
610   accept_param_as_json :reader_tokens, Array
611
612   def object_list(model_class:)
613     if @objects.respond_to?(:except)
614       limit_database_read(model_class: model_class)
615     end
616     list = {
617       :kind  => "arvados##{(@response_resource_name || resource_name).camelize(:lower)}List",
618       :etag => "",
619       :self_link => "",
620       :offset => @offset,
621       :limit => @limit,
622       :items => @objects.as_api_response(nil, {select: @select})
623     }
624     if @extra_included
625       list[:included] = @extra_included.as_api_response(nil, {select: @select})
626     end
627     case params[:count]
628     when nil, '', 'exact'
629       if @objects.respond_to? :except
630         list[:items_available] = @objects.
631           except(:limit).except(:offset).
632           count(@distinct ? :id : '*')
633       end
634     when 'none'
635     else
636       raise ArgumentError.new("count parameter must be 'exact' or 'none'")
637     end
638     list
639   end
640
641   def render_list
642     send_json object_list(model_class: self.model_class)
643   end
644
645   def remote_ip
646     # Caveat: this is highly dependent on the proxy setup. YMMV.
647     if request.headers.key?('HTTP_X_REAL_IP') then
648       # We're behind a reverse proxy
649       @remote_ip = request.headers['HTTP_X_REAL_IP']
650     else
651       # Hopefully, we are not!
652       @remote_ip = request.env['REMOTE_ADDR']
653     end
654   end
655
656   def load_required_parameters
657     (self.class.send "_#{params[:action]}_requires_parameters" rescue {}).
658       each do |key, info|
659       if info[:required] and not params.include?(key)
660         raise ArgumentError.new("#{key} parameter is required")
661       elsif info[:type] == 'boolean'
662         # Make sure params[key] is either true or false -- not a
663         # string, not nil, etc.
664         if not params.include?(key)
665           params[key] = info[:default] || false
666         elsif [false, 'false', '0', 0].include? params[key]
667           params[key] = false
668         elsif [true, 'true', '1', 1].include? params[key]
669           params[key] = true
670         else
671           raise TypeError.new("#{key} parameter must be a boolean, true or false")
672         end
673       end
674     end
675     true
676   end
677
678   def self._create_requires_parameters
679     {
680       select: {
681         type: 'array',
682         description: "An array of names of attributes to return in the response.",
683         required: false,
684       },
685       ensure_unique_name: {
686         type: "boolean",
687         description: "If the given name is already used by this owner, adjust the name to ensure uniqueness instead of returning an error.",
688         location: "query",
689         required: false,
690         default: false
691       },
692       cluster_id: {
693         type: 'string',
694         description: "Cluster ID of a federated cluster where this object should be created.",
695         location: "query",
696         required: false,
697       },
698     }
699   end
700
701   def self._update_requires_parameters
702     {
703       select: {
704         type: 'array',
705         description: "An array of names of attributes to return in the response.",
706         required: false,
707       },
708     }
709   end
710
711   def self._show_requires_parameters
712     {
713       select: {
714         type: 'array',
715         description: "An array of names of attributes to return in the response.",
716         required: false,
717       },
718     }
719   end
720
721   def self._index_requires_parameters
722     {
723       filters: {
724         type: 'array',
725         required: false,
726         description: "Filters to limit which objects are returned by their attributes.
727 Refer to the [filters reference][] for more information about how to write filters.
728
729 [filters reference]: https://doc.arvados.org/api/methods.html#filters
730 ",
731       },
732       where: {
733         type: 'object',
734         required: false,
735         description: "An object to limit which objects are returned by their attributes.
736 The keys of this object are attribute names.
737 Each value is either a single matching value or an array of matching values for that attribute.
738 The `filters` parameter is more flexible and preferred.
739 ",
740       },
741       order: {
742         type: 'array',
743         required: false,
744         description: "An array of strings to set the order in which matching objects are returned.
745 Each string has the format `<ATTRIBUTE> <DIRECTION>`.
746 `DIRECTION` can be `asc` or omitted for ascending, or `desc` for descending.
747 ",
748       },
749       select: {
750         type: 'array',
751         description: "An array of names of attributes to return from each matching object.",
752         required: false,
753       },
754       distinct: {
755         type: 'boolean',
756         required: false,
757         default: false,
758         description: "If this is true, and multiple objects have the same values
759 for the attributes that you specify in the `select` parameter, then each unique
760 set of values will only be returned once in the result set.
761 ",
762       },
763       limit: {
764         type: 'integer',
765         required: false,
766         default: DEFAULT_LIMIT,
767         description: "The maximum number of objects to return in the result.
768 Note that the API may return fewer results than this if your request hits other
769 limits set by the administrator.
770 ",
771       },
772       offset: {
773         type: 'integer',
774         required: false,
775         default: 0,
776         description: "Return matching objects starting from this index.
777 Note that result indexes may change if objects are modified in between a series
778 of list calls.
779 ",
780       },
781       count: {
782         type: 'string',
783         required: false,
784         default: 'exact',
785         description: "A string to determine result counting behavior. Supported values are:
786
787   * `\"exact\"`: The response will include an `items_available` field that
788     counts the number of objects that matched this search criteria,
789     including ones not included in `items`.
790
791   * `\"none\"`: The response will not include an `items_avaliable`
792     field. This improves performance by returning a result as soon as enough
793     `items` have been loaded for this result.
794
795 ",
796       },
797       cluster_id: {
798         type: 'string',
799         description: "Cluster ID of a federated cluster to return objects from",
800         location: "query",
801         required: false,
802       },
803       bypass_federation: {
804         type: 'boolean',
805         required: false,
806         default: false,
807         description: "If true, do not return results from other clusters in the
808 federation, only the cluster that received the request.
809 You must be an administrator to use this flag.
810 ",
811       }
812     }
813   end
814
815   def render *opts
816     if opts.first
817       response = opts.first[:json]
818       if response.is_a?(Hash) &&
819           params[:_profile] &&
820           Thread.current[:request_starttime]
821         response[:_profile] = {
822           request_time: Time.now - Thread.current[:request_starttime]
823         }
824       end
825     end
826     super(*opts)
827   end
828 end