]> git.arvados.org - arvados.git/blob - services/api/app/controllers/application_controller.rb
22228: Add refreshes_at field, preserve upstream expires_at.
[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                 (least(expires_at, refreshes_at) IS NULL
425                  OR least(expires_at, refreshes_at) > CURRENT_TIMESTAMP)',
426                secrets)
427         .to_a
428     end
429     @read_auths.select! { |auth| auth.scopes_allow_request? request }
430     @read_users = @read_auths.map(&:user).uniq
431   end
432
433   def require_login
434     if not current_user
435       respond_to do |format|
436         format.json { send_error("Not logged in", status: 401) }
437         format.html { redirect_to '/login' }
438       end
439       false
440     end
441   end
442
443   def admin_required
444     unless current_user and current_user.is_admin
445       send_error("Forbidden", status: 403)
446     end
447   end
448
449   def require_auth_scope
450     unless current_user && @read_auths.any? { |auth| auth.user.andand.uuid == current_user.uuid }
451       if require_login != false
452         send_error("Forbidden", status: 403)
453       end
454       false
455     end
456   end
457
458   def set_current_request_id
459     Rails.logger.tagged(request.request_id) do
460       yield
461     end
462   end
463
464   def append_info_to_payload(payload)
465     super
466     payload[:request_id] = request.request_id
467     payload[:client_ipaddr] = @remote_ip
468     payload[:client_auth] = current_api_client_authorization.andand.uuid || nil
469   end
470
471   def disable_api_methods
472     if Rails.configuration.API.DisabledAPIs[controller_name + "." + action_name]
473       send_error("Disabled", status: 404)
474     end
475   end
476
477   def set_cors_headers
478     response.headers['Access-Control-Allow-Origin'] = '*'
479     response.headers['Access-Control-Allow-Methods'] = 'GET, HEAD, PUT, POST, DELETE'
480     response.headers['Access-Control-Allow-Headers'] = 'Authorization, Content-Type'
481     response.headers['Access-Control-Max-Age'] = '86486400'
482   end
483
484   def respond_with_json_by_default
485     html_index = request.accepts.index(Mime[:html])
486     if html_index.nil? or request.accepts[0...html_index].include?(Mime[:json])
487       request.format = :json
488     end
489   end
490
491   def model_class
492     controller_name.classify.constantize
493   end
494
495   def resource_name             # params[] key used by client
496     controller_name.singularize
497   end
498
499   def table_name
500     controller_name
501   end
502
503   def find_object_for_update
504     find_object_by_uuid(with_lock: true)
505   end
506
507   def find_object_by_uuid(with_lock: false)
508     if params[:id] and params[:id].match(/\D/)
509       params[:uuid] = params.delete :id
510     end
511     @where = {}
512     # Some APIs (at least groups/contents) take an optional uuid argument.
513     # They go through this method to handle it when present but we cannot
514     # assume it is always set.
515     @where[:uuid] = params[:uuid] if params[:uuid]
516     @offset = 0
517     @limit = 1
518     @orders = []
519     @filters = []
520     @objects = nil
521
522     # This is a little hacky but sometimes the fields the user wants
523     # to selecting on are unrelated to the object being loaded here,
524     # for example groups#contents, so filter the fields that will be
525     # used in find_objects_for_index and then reset afterwards.  In
526     # some cases, code that modifies the @select list needs to set
527     # @preserve_select.
528     @preserve_select = @select
529     @select = select_for_klass(@select, self.model_class, false)
530
531     find_objects_for_index
532     if with_lock && Rails.configuration.API.LockBeforeUpdate
533       @object = @objects.lock.first
534     else
535       @object = @objects.first
536     end
537     @select = @preserve_select
538   end
539
540   def nullable_attributes
541     []
542   end
543
544   # Go code may send empty values (ie: empty string instead of NULL) that
545   # should be translated to NULL on the database.
546   def set_nullable_attrs_to_null
547     nullify_attrs(resource_attrs.to_hash).each do |k, v|
548       resource_attrs[k] = v
549     end
550   end
551
552   def nullify_attrs(a = {})
553     new_attrs = a.to_hash.symbolize_keys
554     (new_attrs.keys & nullable_attributes).each do |attr|
555       val = new_attrs[attr]
556       if (val.class == Integer && val == 0) || (val.class == String && val == "")
557         new_attrs[attr] = nil
558       end
559     end
560     return new_attrs
561   end
562
563   def reload_object_before_update
564     # This is necessary to prevent an ActiveRecord::ReadOnlyRecord
565     # error when updating an object which was retrieved using a join.
566     if @object.andand.readonly?
567       @object = model_class.find_by_uuid(@objects.first.uuid)
568     end
569   end
570
571   def load_json_value(hash, key, must_be_class=nil)
572     return if hash[key].nil?
573
574     val = hash[key]
575     if val.is_a? ActionController::Parameters
576       val = val.to_unsafe_hash
577     elsif val.is_a? String
578       val = SafeJSON.load(val)
579       hash[key] = val
580     end
581     # When assigning a Hash to an ActionController::Parameters and then
582     # retrieve it, we get another ActionController::Parameters instead of
583     # a Hash. This doesn't happen with other types. This is why 'val' is
584     # being used to do type checking below.
585     if must_be_class and !val.is_a? must_be_class
586       raise TypeError.new("parameter #{key.to_s} must be a #{must_be_class.to_s}")
587     end
588   end
589
590   def self.accept_attribute_as_json(attr, must_be_class=nil)
591     before_action lambda { accept_attribute_as_json attr, must_be_class }
592   end
593   accept_attribute_as_json :properties, Hash
594   accept_attribute_as_json :info, Hash
595   def accept_attribute_as_json(attr, must_be_class)
596     if params[resource_name] and [Hash, ActionController::Parameters].include?(resource_attrs.class)
597       if resource_attrs[attr].is_a? Hash
598         # Convert symbol keys to strings (in hashes provided by
599         # resource_attrs)
600         resource_attrs[attr] = resource_attrs[attr].
601           with_indifferent_access.to_hash
602       else
603         load_json_value(resource_attrs, attr, must_be_class)
604       end
605     end
606   end
607
608   def self.accept_param_as_json(key, must_be_class=nil)
609     prepend_before_action lambda { load_json_value(params, key, must_be_class) }
610   end
611   accept_param_as_json :reader_tokens, Array
612
613   def object_list(model_class:)
614     if @objects.respond_to?(:except)
615       limit_database_read(model_class: model_class)
616     end
617     list = {
618       :kind  => "arvados##{(@response_resource_name || resource_name).camelize(:lower)}List",
619       :etag => "",
620       :self_link => "",
621       :offset => @offset,
622       :limit => @limit,
623       :items => @objects.as_api_response(nil, {select: @select})
624     }
625     if @extra_included
626       list[:included] = @extra_included.as_api_response(nil, {select: @select})
627     end
628     case params[:count]
629     when nil, '', 'exact'
630       if @objects.respond_to? :except
631         list[:items_available] = @objects.
632           except(:limit).except(:offset).
633           count(@distinct ? :id : '*')
634       end
635     when 'none'
636     else
637       raise ArgumentError.new("count parameter must be 'exact' or 'none'")
638     end
639     list
640   end
641
642   def render_list
643     send_json object_list(model_class: self.model_class)
644   end
645
646   def remote_ip
647     # Caveat: this is highly dependent on the proxy setup. YMMV.
648     if request.headers.key?('HTTP_X_REAL_IP') then
649       # We're behind a reverse proxy
650       @remote_ip = request.headers['HTTP_X_REAL_IP']
651     else
652       # Hopefully, we are not!
653       @remote_ip = request.env['REMOTE_ADDR']
654     end
655   end
656
657   def load_required_parameters
658     (self.class.send "_#{params[:action]}_requires_parameters" rescue {}).
659       each do |key, info|
660       if info[:required] and not params.include?(key)
661         raise ArgumentError.new("#{key} parameter is required")
662       elsif info[:type] == 'boolean'
663         # Make sure params[key] is either true or false -- not a
664         # string, not nil, etc.
665         if not params.include?(key)
666           params[key] = info[:default] || false
667         elsif [false, 'false', '0', 0].include? params[key]
668           params[key] = false
669         elsif [true, 'true', '1', 1].include? params[key]
670           params[key] = true
671         else
672           raise TypeError.new("#{key} parameter must be a boolean, true or false")
673         end
674       end
675     end
676     true
677   end
678
679   def self._create_requires_parameters
680     {
681       select: {
682         type: 'array',
683         description: "An array of names of attributes to return in the response.",
684         required: false,
685       },
686       ensure_unique_name: {
687         type: "boolean",
688         description: "If the given name is already used by this owner, adjust the name to ensure uniqueness instead of returning an error.",
689         location: "query",
690         required: false,
691         default: false
692       },
693       cluster_id: {
694         type: 'string',
695         description: "Cluster ID of a federated cluster where this object should be created.",
696         location: "query",
697         required: false,
698       },
699     }
700   end
701
702   def self._update_requires_parameters
703     {
704       select: {
705         type: 'array',
706         description: "An array of names of attributes to return in the response.",
707         required: false,
708       },
709     }
710   end
711
712   def self._show_requires_parameters
713     {
714       select: {
715         type: 'array',
716         description: "An array of names of attributes to return in the response.",
717         required: false,
718       },
719     }
720   end
721
722   def self._index_requires_parameters
723     {
724       filters: {
725         type: 'array',
726         required: false,
727         description: "Filters to limit which objects are returned by their attributes.
728 Refer to the [filters reference][] for more information about how to write filters.
729
730 [filters reference]: https://doc.arvados.org/api/methods.html#filters
731 ",
732       },
733       where: {
734         type: 'object',
735         required: false,
736         description: "An object to limit which objects are returned by their attributes.
737 The keys of this object are attribute names.
738 Each value is either a single matching value or an array of matching values for that attribute.
739 The `filters` parameter is more flexible and preferred.
740 ",
741       },
742       order: {
743         type: 'array',
744         required: false,
745         description: "An array of strings to set the order in which matching objects are returned.
746 Each string has the format `<ATTRIBUTE> <DIRECTION>`.
747 `DIRECTION` can be `asc` or omitted for ascending, or `desc` for descending.
748 ",
749       },
750       select: {
751         type: 'array',
752         description: "An array of names of attributes to return from each matching object.",
753         required: false,
754       },
755       distinct: {
756         type: 'boolean',
757         required: false,
758         default: false,
759         description: "If this is true, and multiple objects have the same values
760 for the attributes that you specify in the `select` parameter, then each unique
761 set of values will only be returned once in the result set.
762 ",
763       },
764       limit: {
765         type: 'integer',
766         required: false,
767         default: DEFAULT_LIMIT,
768         description: "The maximum number of objects to return in the result.
769 Note that the API may return fewer results than this if your request hits other
770 limits set by the administrator.
771 ",
772       },
773       offset: {
774         type: 'integer',
775         required: false,
776         default: 0,
777         description: "Return matching objects starting from this index.
778 Note that result indexes may change if objects are modified in between a series
779 of list calls.
780 ",
781       },
782       count: {
783         type: 'string',
784         required: false,
785         default: 'exact',
786         description: "A string to determine result counting behavior. Supported values are:
787
788   * `\"exact\"`: The response will include an `items_available` field that
789     counts the number of objects that matched this search criteria,
790     including ones not included in `items`.
791
792   * `\"none\"`: The response will not include an `items_avaliable`
793     field. This improves performance by returning a result as soon as enough
794     `items` have been loaded for this result.
795
796 ",
797       },
798       cluster_id: {
799         type: 'string',
800         description: "Cluster ID of a federated cluster to return objects from",
801         location: "query",
802         required: false,
803       },
804       bypass_federation: {
805         type: 'boolean',
806         required: false,
807         default: false,
808         description: "If true, do not return results from other clusters in the
809 federation, only the cluster that received the request.
810 You must be an administrator to use this flag.
811 ",
812       }
813     }
814   end
815
816   def render *opts
817     if opts.first
818       response = opts.first[:json]
819       if response.is_a?(Hash) &&
820           params[:_profile] &&
821           Thread.current[:request_starttime]
822         response[:_profile] = {
823           request_time: Time.now - Thread.current[:request_starttime]
824         }
825       end
826     end
827     super(*opts)
828   end
829 end