mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
Add API to refresh authc headers and retry ES request when 401 is encountered (#120677)
* initial POC * remove test code * update the header holding logic * add new API to plugin context * introduce the IAuthHeadersStorage interface * fix some types, mocks and tests * export types from server entrypoint * also export error type * more doc * update generated doc * Fix ES service tests * add tests for createInternalErrorHandler * fix type in cli_setup * generated doc * add tests for configureClient * add unit tests for custom transport class * fix handler propagation to initial clients * lint * address review comments
This commit is contained in:
parent
17d2cd105f
commit
b6060544cc
45 changed files with 1714 additions and 209 deletions
|
@ -16,4 +16,5 @@ export interface ElasticsearchServiceSetup
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [legacy](./kibana-plugin-core-server.elasticsearchservicesetup.legacy.md) | { readonly config$: Observable<ElasticsearchConfig>; } | |
|
||||
| [setUnauthorizedErrorHandler](./kibana-plugin-core-server.elasticsearchservicesetup.setunauthorizederrorhandler.md) | (handler: UnauthorizedErrorHandler) => void | Register a handler that will be called when unauthorized (401) errors are returned from any API call to elasticsearch performed on behalf of a user via a [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md)<!-- -->. |
|
||||
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) > [setUnauthorizedErrorHandler](./kibana-plugin-core-server.elasticsearchservicesetup.setunauthorizederrorhandler.md)
|
||||
|
||||
## ElasticsearchServiceSetup.setUnauthorizedErrorHandler property
|
||||
|
||||
Register a handler that will be called when unauthorized (401) errors are returned from any API call to elasticsearch performed on behalf of a user via a [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md)<!-- -->.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
setUnauthorizedErrorHandler: (handler: UnauthorizedErrorHandler) => void;
|
||||
```
|
||||
|
||||
## Remarks
|
||||
|
||||
The handler will only be invoked for scoped client bound to real [request](./kibana-plugin-core-server.kibanarequest.md) instances.
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
```ts
|
||||
const handler: UnauthorizedErrorHandler = ({ request, error }, toolkit) => {
|
||||
const reauthenticationResult = await authenticator.reauthenticate(request, error);
|
||||
if (reauthenticationResult.succeeded()) {
|
||||
return toolkit.retry({
|
||||
authHeaders: reauthenticationResult.authHeaders,
|
||||
});
|
||||
}
|
||||
return toolkit.notHandled();
|
||||
}
|
||||
|
||||
coreSetup.elasticsearch.setUnauthorizedErrorHandler(handler);
|
||||
```
|
||||
|
|
@ -230,6 +230,11 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) | UiSettings parameters defined by the plugins. |
|
||||
| [UiSettingsServiceSetup](./kibana-plugin-core-server.uisettingsservicesetup.md) | |
|
||||
| [UiSettingsServiceStart](./kibana-plugin-core-server.uisettingsservicestart.md) | |
|
||||
| [UnauthorizedErrorHandlerNotHandledResult](./kibana-plugin-core-server.unauthorizederrorhandlernothandledresult.md) | |
|
||||
| [UnauthorizedErrorHandlerOptions](./kibana-plugin-core-server.unauthorizederrorhandleroptions.md) | |
|
||||
| [UnauthorizedErrorHandlerResultRetryParams](./kibana-plugin-core-server.unauthorizederrorhandlerresultretryparams.md) | |
|
||||
| [UnauthorizedErrorHandlerRetryResult](./kibana-plugin-core-server.unauthorizederrorhandlerretryresult.md) | |
|
||||
| [UnauthorizedErrorHandlerToolkit](./kibana-plugin-core-server.unauthorizederrorhandlertoolkit.md) | Toolkit passed to a [UnauthorizedErrorHandler](./kibana-plugin-core-server.unauthorizederrorhandler.md) used to generate responses from the handler |
|
||||
| [UserProvidedValues](./kibana-plugin-core-server.userprovidedvalues.md) | Describes the values explicitly set by user. |
|
||||
|
||||
## Variables
|
||||
|
@ -329,4 +334,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SharedGlobalConfig](./kibana-plugin-core-server.sharedglobalconfig.md) | |
|
||||
| [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | Allows plugins to get access to APIs available in start inside async handlers. Promise will not resolve until Core and plugin dependencies have completed <code>start</code>. This should only be used inside handlers registered during <code>setup</code> that will only be executed after <code>start</code> lifecycle. |
|
||||
| [UiSettingsType](./kibana-plugin-core-server.uisettingstype.md) | UI element type to represent the settings. |
|
||||
| [UnauthorizedError](./kibana-plugin-core-server.unauthorizederror.md) | |
|
||||
| [UnauthorizedErrorHandler](./kibana-plugin-core-server.unauthorizederrorhandler.md) | A handler used to handle unauthorized error returned by elasticsearch |
|
||||
| [UnauthorizedErrorHandlerResult](./kibana-plugin-core-server.unauthorizederrorhandlerresult.md) | |
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedError](./kibana-plugin-core-server.unauthorizederror.md)
|
||||
|
||||
## UnauthorizedError type
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type UnauthorizedError = errors.ResponseError & {
|
||||
statusCode: 401;
|
||||
};
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedErrorHandler](./kibana-plugin-core-server.unauthorizederrorhandler.md)
|
||||
|
||||
## UnauthorizedErrorHandler type
|
||||
|
||||
A handler used to handle unauthorized error returned by elasticsearch
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type UnauthorizedErrorHandler = (options: UnauthorizedErrorHandlerOptions, toolkit: UnauthorizedErrorHandlerToolkit) => MaybePromise<UnauthorizedErrorHandlerResult>;
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedErrorHandlerNotHandledResult](./kibana-plugin-core-server.unauthorizederrorhandlernothandledresult.md)
|
||||
|
||||
## UnauthorizedErrorHandlerNotHandledResult interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface UnauthorizedErrorHandlerNotHandledResult
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [type](./kibana-plugin-core-server.unauthorizederrorhandlernothandledresult.type.md) | 'notHandled' | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedErrorHandlerNotHandledResult](./kibana-plugin-core-server.unauthorizederrorhandlernothandledresult.md) > [type](./kibana-plugin-core-server.unauthorizederrorhandlernothandledresult.type.md)
|
||||
|
||||
## UnauthorizedErrorHandlerNotHandledResult.type property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
type: 'notHandled';
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedErrorHandlerOptions](./kibana-plugin-core-server.unauthorizederrorhandleroptions.md) > [error](./kibana-plugin-core-server.unauthorizederrorhandleroptions.error.md)
|
||||
|
||||
## UnauthorizedErrorHandlerOptions.error property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
error: UnauthorizedError;
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedErrorHandlerOptions](./kibana-plugin-core-server.unauthorizederrorhandleroptions.md)
|
||||
|
||||
## UnauthorizedErrorHandlerOptions interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface UnauthorizedErrorHandlerOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [error](./kibana-plugin-core-server.unauthorizederrorhandleroptions.error.md) | UnauthorizedError | |
|
||||
| [request](./kibana-plugin-core-server.unauthorizederrorhandleroptions.request.md) | KibanaRequest | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedErrorHandlerOptions](./kibana-plugin-core-server.unauthorizederrorhandleroptions.md) > [request](./kibana-plugin-core-server.unauthorizederrorhandleroptions.request.md)
|
||||
|
||||
## UnauthorizedErrorHandlerOptions.request property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
request: KibanaRequest;
|
||||
```
|
|
@ -0,0 +1,12 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedErrorHandlerResult](./kibana-plugin-core-server.unauthorizederrorhandlerresult.md)
|
||||
|
||||
## UnauthorizedErrorHandlerResult type
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type UnauthorizedErrorHandlerResult = UnauthorizedErrorHandlerRetryResult | UnauthorizedErrorHandlerNotHandledResult;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedErrorHandlerResultRetryParams](./kibana-plugin-core-server.unauthorizederrorhandlerresultretryparams.md) > [authHeaders](./kibana-plugin-core-server.unauthorizederrorhandlerresultretryparams.authheaders.md)
|
||||
|
||||
## UnauthorizedErrorHandlerResultRetryParams.authHeaders property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
authHeaders: AuthHeaders;
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedErrorHandlerResultRetryParams](./kibana-plugin-core-server.unauthorizederrorhandlerresultretryparams.md)
|
||||
|
||||
## UnauthorizedErrorHandlerResultRetryParams interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface UnauthorizedErrorHandlerResultRetryParams
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [authHeaders](./kibana-plugin-core-server.unauthorizederrorhandlerresultretryparams.authheaders.md) | AuthHeaders | |
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedErrorHandlerRetryResult](./kibana-plugin-core-server.unauthorizederrorhandlerretryresult.md)
|
||||
|
||||
## UnauthorizedErrorHandlerRetryResult interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface UnauthorizedErrorHandlerRetryResult extends UnauthorizedErrorHandlerResultRetryParams
|
||||
```
|
||||
<b>Extends:</b> UnauthorizedErrorHandlerResultRetryParams
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [type](./kibana-plugin-core-server.unauthorizederrorhandlerretryresult.type.md) | 'retry' | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedErrorHandlerRetryResult](./kibana-plugin-core-server.unauthorizederrorhandlerretryresult.md) > [type](./kibana-plugin-core-server.unauthorizederrorhandlerretryresult.type.md)
|
||||
|
||||
## UnauthorizedErrorHandlerRetryResult.type property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
type: 'retry';
|
||||
```
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedErrorHandlerToolkit](./kibana-plugin-core-server.unauthorizederrorhandlertoolkit.md)
|
||||
|
||||
## UnauthorizedErrorHandlerToolkit interface
|
||||
|
||||
Toolkit passed to a [UnauthorizedErrorHandler](./kibana-plugin-core-server.unauthorizederrorhandler.md) used to generate responses from the handler
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface UnauthorizedErrorHandlerToolkit
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [notHandled](./kibana-plugin-core-server.unauthorizederrorhandlertoolkit.nothandled.md) | () => UnauthorizedErrorHandlerNotHandledResult | The handler cannot handle the error, or was not able to authenticate. |
|
||||
| [retry](./kibana-plugin-core-server.unauthorizederrorhandlertoolkit.retry.md) | (params: UnauthorizedErrorHandlerResultRetryParams) => UnauthorizedErrorHandlerRetryResult | The handler was able to authenticate. Will retry the failed request with new auth headers |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedErrorHandlerToolkit](./kibana-plugin-core-server.unauthorizederrorhandlertoolkit.md) > [notHandled](./kibana-plugin-core-server.unauthorizederrorhandlertoolkit.nothandled.md)
|
||||
|
||||
## UnauthorizedErrorHandlerToolkit.notHandled property
|
||||
|
||||
The handler cannot handle the error, or was not able to authenticate.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
notHandled: () => UnauthorizedErrorHandlerNotHandledResult;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UnauthorizedErrorHandlerToolkit](./kibana-plugin-core-server.unauthorizederrorhandlertoolkit.md) > [retry](./kibana-plugin-core-server.unauthorizederrorhandlertoolkit.retry.md)
|
||||
|
||||
## UnauthorizedErrorHandlerToolkit.retry property
|
||||
|
||||
The handler was able to authenticate. Will retry the failed request with new auth headers
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
retry: (params: UnauthorizedErrorHandlerResultRetryParams) => UnauthorizedErrorHandlerRetryResult;
|
||||
```
|
|
@ -37,8 +37,8 @@ export const elasticsearch = new ElasticsearchService(logger, kibanaPackageJson.
|
|||
elasticsearch: {
|
||||
createClient: (type, config) => {
|
||||
const defaults = configSchema.validate({});
|
||||
return new ClusterClient(
|
||||
merge(
|
||||
return new ClusterClient({
|
||||
config: merge(
|
||||
defaults,
|
||||
{
|
||||
hosts: Array.isArray(defaults.hosts) ? defaults.hosts : [defaults.hosts],
|
||||
|
@ -46,8 +46,8 @@ export const elasticsearch = new ElasticsearchService(logger, kibanaPackageJson.
|
|||
config
|
||||
),
|
||||
logger,
|
||||
type
|
||||
);
|
||||
type,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -10,3 +10,13 @@ export const configureClientMock = jest.fn();
|
|||
jest.doMock('./configure_client', () => ({
|
||||
configureClient: configureClientMock,
|
||||
}));
|
||||
|
||||
export const createTransportMock = jest.fn();
|
||||
jest.doMock('./create_transport', () => ({
|
||||
createTransport: createTransportMock,
|
||||
}));
|
||||
|
||||
export const createInternalErrorHandlerMock = jest.fn();
|
||||
jest.doMock('./retry_unauthorized', () => ({
|
||||
createInternalErrorHandler: createInternalErrorHandlerMock,
|
||||
}));
|
||||
|
|
|
@ -6,10 +6,14 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { configureClientMock } from './cluster_client.test.mocks';
|
||||
import {
|
||||
configureClientMock,
|
||||
createTransportMock,
|
||||
createInternalErrorHandlerMock,
|
||||
} from './cluster_client.test.mocks';
|
||||
import { loggingSystemMock } from '../../logging/logging_system.mock';
|
||||
import { httpServerMock } from '../../http/http_server.mocks';
|
||||
import { GetAuthHeaders } from '../../http';
|
||||
import { httpServiceMock } from '../../http/http_service.mock';
|
||||
import { elasticsearchClientMock } from './mocks';
|
||||
import { ClusterClient } from './cluster_client';
|
||||
import { ElasticsearchClientConfig } from './client_config';
|
||||
|
@ -31,15 +35,19 @@ const createConfig = (
|
|||
|
||||
describe('ClusterClient', () => {
|
||||
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
|
||||
let getAuthHeaders: jest.MockedFunction<GetAuthHeaders>;
|
||||
let authHeaders: ReturnType<typeof httpServiceMock.createAuthHeaderStorage>;
|
||||
let internalClient: ReturnType<typeof elasticsearchClientMock.createInternalClient>;
|
||||
let scopedClient: ReturnType<typeof elasticsearchClientMock.createInternalClient>;
|
||||
|
||||
const mockTransport = { mockTransport: true };
|
||||
|
||||
beforeEach(() => {
|
||||
logger = loggingSystemMock.createLogger();
|
||||
internalClient = elasticsearchClientMock.createInternalClient();
|
||||
scopedClient = elasticsearchClientMock.createInternalClient();
|
||||
getAuthHeaders = jest.fn().mockImplementation(() => ({
|
||||
|
||||
authHeaders = httpServiceMock.createAuthHeaderStorage();
|
||||
authHeaders.get.mockImplementation(() => ({
|
||||
authorization: 'auth',
|
||||
foo: 'bar',
|
||||
}));
|
||||
|
@ -47,16 +55,26 @@ describe('ClusterClient', () => {
|
|||
configureClientMock.mockImplementation((config, { scoped = false }) => {
|
||||
return scoped ? scopedClient : internalClient;
|
||||
});
|
||||
createTransportMock.mockReturnValue(mockTransport);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
configureClientMock.mockReset();
|
||||
createTransportMock.mockReset();
|
||||
createInternalErrorHandlerMock.mockReset();
|
||||
});
|
||||
|
||||
it('creates a single internal and scoped client during initialization', () => {
|
||||
const config = createConfig();
|
||||
const getExecutionContextMock = jest.fn();
|
||||
new ClusterClient(config, logger, 'custom-type', getAuthHeaders, getExecutionContextMock);
|
||||
|
||||
new ClusterClient({
|
||||
config,
|
||||
logger,
|
||||
authHeaders,
|
||||
type: 'custom-type',
|
||||
getExecutionContext: getExecutionContextMock,
|
||||
});
|
||||
|
||||
expect(configureClientMock).toHaveBeenCalledTimes(2);
|
||||
expect(configureClientMock).toHaveBeenCalledWith(config, {
|
||||
|
@ -74,12 +92,12 @@ describe('ClusterClient', () => {
|
|||
|
||||
describe('#asInternalUser', () => {
|
||||
it('returns the internal client', () => {
|
||||
const clusterClient = new ClusterClient(
|
||||
createConfig(),
|
||||
const clusterClient = new ClusterClient({
|
||||
config: createConfig(),
|
||||
logger,
|
||||
'custom-type',
|
||||
getAuthHeaders
|
||||
);
|
||||
type: 'custom-type',
|
||||
authHeaders,
|
||||
});
|
||||
|
||||
expect(clusterClient.asInternalUser).toBe(internalClient);
|
||||
});
|
||||
|
@ -87,30 +105,90 @@ describe('ClusterClient', () => {
|
|||
|
||||
describe('#asScoped', () => {
|
||||
it('returns a scoped cluster client bound to the request', () => {
|
||||
const clusterClient = new ClusterClient(
|
||||
createConfig(),
|
||||
const clusterClient = new ClusterClient({
|
||||
config: createConfig(),
|
||||
logger,
|
||||
'custom-type',
|
||||
getAuthHeaders
|
||||
);
|
||||
type: 'custom-type',
|
||||
authHeaders,
|
||||
});
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
const scopedClusterClient = clusterClient.asScoped(request);
|
||||
|
||||
expect(scopedClient.child).toHaveBeenCalledTimes(1);
|
||||
expect(scopedClient.child).toHaveBeenCalledWith({ headers: expect.any(Object) });
|
||||
expect(scopedClient.child).toHaveBeenCalledWith({
|
||||
headers: expect.any(Object),
|
||||
Transport: mockTransport,
|
||||
});
|
||||
|
||||
expect(scopedClusterClient.asInternalUser).toBe(clusterClient.asInternalUser);
|
||||
expect(scopedClusterClient.asCurrentUser).toBe(scopedClient.child.mock.results[0].value);
|
||||
});
|
||||
|
||||
it('returns a distinct scoped cluster client on each call', () => {
|
||||
const clusterClient = new ClusterClient(
|
||||
createConfig(),
|
||||
it('calls `createTransport` with the correct parameters', () => {
|
||||
const getExecutionContext = jest.fn();
|
||||
const getUnauthorizedErrorHandler = jest.fn();
|
||||
const clusterClient = new ClusterClient({
|
||||
config: createConfig(),
|
||||
logger,
|
||||
'custom-type',
|
||||
getAuthHeaders
|
||||
);
|
||||
type: 'custom-type',
|
||||
authHeaders,
|
||||
getExecutionContext,
|
||||
getUnauthorizedErrorHandler,
|
||||
});
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
clusterClient.asScoped(request);
|
||||
|
||||
expect(createTransportMock).toHaveBeenCalledTimes(1);
|
||||
expect(createTransportMock).toHaveBeenCalledWith({
|
||||
getExecutionContext,
|
||||
getUnauthorizedErrorHandler: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('calls `createTransportcreateInternalErrorHandler` lazily', () => {
|
||||
const getExecutionContext = jest.fn();
|
||||
const getUnauthorizedErrorHandler = jest.fn();
|
||||
const clusterClient = new ClusterClient({
|
||||
config: createConfig(),
|
||||
logger,
|
||||
type: 'custom-type',
|
||||
authHeaders,
|
||||
getExecutionContext,
|
||||
getUnauthorizedErrorHandler,
|
||||
});
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
clusterClient.asScoped(request);
|
||||
|
||||
expect(createTransportMock).toHaveBeenCalledTimes(1);
|
||||
expect(createTransportMock).toHaveBeenCalledWith({
|
||||
getExecutionContext,
|
||||
getUnauthorizedErrorHandler: expect.any(Function),
|
||||
});
|
||||
|
||||
const { getUnauthorizedErrorHandler: getHandler } = createTransportMock.mock.calls[0][0];
|
||||
|
||||
expect(createInternalErrorHandlerMock).not.toHaveBeenCalled();
|
||||
|
||||
getHandler();
|
||||
|
||||
expect(createInternalErrorHandlerMock).toHaveBeenCalledTimes(1);
|
||||
expect(createInternalErrorHandlerMock).toHaveBeenCalledWith({
|
||||
request,
|
||||
getHandler: getUnauthorizedErrorHandler,
|
||||
setAuthHeaders: authHeaders.set,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a distinct scoped cluster client on each call', () => {
|
||||
const clusterClient = new ClusterClient({
|
||||
config: createConfig(),
|
||||
logger,
|
||||
type: 'custom-type',
|
||||
authHeaders,
|
||||
});
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
const scopedClusterClient1 = clusterClient.asScoped(request);
|
||||
|
@ -126,9 +204,9 @@ describe('ClusterClient', () => {
|
|||
const config = createConfig({
|
||||
requestHeadersWhitelist: ['foo'],
|
||||
});
|
||||
getAuthHeaders.mockReturnValue({});
|
||||
authHeaders.get.mockReturnValue({});
|
||||
|
||||
const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders);
|
||||
const clusterClient = new ClusterClient({ config, logger, type: 'custom-type', authHeaders });
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
headers: {
|
||||
foo: 'bar',
|
||||
|
@ -139,46 +217,50 @@ describe('ClusterClient', () => {
|
|||
clusterClient.asScoped(request);
|
||||
|
||||
expect(scopedClient.child).toHaveBeenCalledTimes(1);
|
||||
expect(scopedClient.child).toHaveBeenCalledWith({
|
||||
headers: { ...DEFAULT_HEADERS, foo: 'bar', 'x-opaque-id': expect.any(String) },
|
||||
});
|
||||
expect(scopedClient.child).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: { ...DEFAULT_HEADERS, foo: 'bar', 'x-opaque-id': expect.any(String) },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('does not filter auth headers', () => {
|
||||
const config = createConfig({
|
||||
requestHeadersWhitelist: ['authorization'],
|
||||
});
|
||||
getAuthHeaders.mockReturnValue({
|
||||
authHeaders.get.mockReturnValue({
|
||||
authorization: 'auth',
|
||||
other: 'yep',
|
||||
});
|
||||
|
||||
const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders);
|
||||
const clusterClient = new ClusterClient({ config, logger, type: 'custom-type', authHeaders });
|
||||
const request = httpServerMock.createKibanaRequest({});
|
||||
|
||||
clusterClient.asScoped(request);
|
||||
|
||||
expect(scopedClient.child).toHaveBeenCalledTimes(1);
|
||||
expect(scopedClient.child).toHaveBeenCalledWith({
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
authorization: 'auth',
|
||||
other: 'yep',
|
||||
'x-opaque-id': expect.any(String),
|
||||
},
|
||||
});
|
||||
expect(scopedClient.child).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
authorization: 'auth',
|
||||
other: 'yep',
|
||||
'x-opaque-id': expect.any(String),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('respects auth headers precedence', () => {
|
||||
const config = createConfig({
|
||||
requestHeadersWhitelist: ['authorization'],
|
||||
});
|
||||
getAuthHeaders.mockReturnValue({
|
||||
authHeaders.get.mockReturnValue({
|
||||
authorization: 'auth',
|
||||
other: 'yep',
|
||||
});
|
||||
|
||||
const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders);
|
||||
const clusterClient = new ClusterClient({ config, logger, type: 'custom-type', authHeaders });
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
headers: {
|
||||
authorization: 'override',
|
||||
|
@ -188,14 +270,16 @@ describe('ClusterClient', () => {
|
|||
clusterClient.asScoped(request);
|
||||
|
||||
expect(scopedClient.child).toHaveBeenCalledTimes(1);
|
||||
expect(scopedClient.child).toHaveBeenCalledWith({
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
authorization: 'auth',
|
||||
other: 'yep',
|
||||
'x-opaque-id': expect.any(String),
|
||||
},
|
||||
});
|
||||
expect(scopedClient.child).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
authorization: 'auth',
|
||||
other: 'yep',
|
||||
'x-opaque-id': expect.any(String),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('includes the `customHeaders` from the config without filtering them', () => {
|
||||
|
@ -206,29 +290,31 @@ describe('ClusterClient', () => {
|
|||
},
|
||||
requestHeadersWhitelist: ['authorization'],
|
||||
});
|
||||
getAuthHeaders.mockReturnValue({});
|
||||
authHeaders.get.mockReturnValue({});
|
||||
|
||||
const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders);
|
||||
const clusterClient = new ClusterClient({ config, logger, type: 'custom-type', authHeaders });
|
||||
const request = httpServerMock.createKibanaRequest({});
|
||||
|
||||
clusterClient.asScoped(request);
|
||||
|
||||
expect(scopedClient.child).toHaveBeenCalledTimes(1);
|
||||
expect(scopedClient.child).toHaveBeenCalledWith({
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
foo: 'bar',
|
||||
hello: 'dolly',
|
||||
'x-opaque-id': expect.any(String),
|
||||
},
|
||||
});
|
||||
expect(scopedClient.child).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
foo: 'bar',
|
||||
hello: 'dolly',
|
||||
'x-opaque-id': expect.any(String),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('adds the x-opaque-id header based on the request id', () => {
|
||||
const config = createConfig();
|
||||
getAuthHeaders.mockReturnValue({});
|
||||
authHeaders.get.mockReturnValue({});
|
||||
|
||||
const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders);
|
||||
const clusterClient = new ClusterClient({ config, logger, type: 'custom-type', authHeaders });
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
kibanaRequestState: { requestId: 'my-fake-id', requestUuid: 'ignore-this-id' },
|
||||
});
|
||||
|
@ -236,12 +322,14 @@ describe('ClusterClient', () => {
|
|||
clusterClient.asScoped(request);
|
||||
|
||||
expect(scopedClient.child).toHaveBeenCalledTimes(1);
|
||||
expect(scopedClient.child).toHaveBeenCalledWith({
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
'x-opaque-id': 'my-fake-id',
|
||||
},
|
||||
});
|
||||
expect(scopedClient.child).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
'x-opaque-id': 'my-fake-id',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('respect the precedence of auth headers over config headers', () => {
|
||||
|
@ -252,24 +340,26 @@ describe('ClusterClient', () => {
|
|||
},
|
||||
requestHeadersWhitelist: ['foo'],
|
||||
});
|
||||
getAuthHeaders.mockReturnValue({
|
||||
authHeaders.get.mockReturnValue({
|
||||
foo: 'auth',
|
||||
});
|
||||
|
||||
const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders);
|
||||
const clusterClient = new ClusterClient({ config, logger, type: 'custom-type', authHeaders });
|
||||
const request = httpServerMock.createKibanaRequest({});
|
||||
|
||||
clusterClient.asScoped(request);
|
||||
|
||||
expect(scopedClient.child).toHaveBeenCalledTimes(1);
|
||||
expect(scopedClient.child).toHaveBeenCalledWith({
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
foo: 'auth',
|
||||
hello: 'dolly',
|
||||
'x-opaque-id': expect.any(String),
|
||||
},
|
||||
});
|
||||
expect(scopedClient.child).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
foo: 'auth',
|
||||
hello: 'dolly',
|
||||
'x-opaque-id': expect.any(String),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('respect the precedence of request headers over config headers', () => {
|
||||
|
@ -280,9 +370,9 @@ describe('ClusterClient', () => {
|
|||
},
|
||||
requestHeadersWhitelist: ['foo'],
|
||||
});
|
||||
getAuthHeaders.mockReturnValue({});
|
||||
authHeaders.get.mockReturnValue({});
|
||||
|
||||
const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders);
|
||||
const clusterClient = new ClusterClient({ config, logger, type: 'custom-type', authHeaders });
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
headers: { foo: 'request' },
|
||||
});
|
||||
|
@ -290,14 +380,16 @@ describe('ClusterClient', () => {
|
|||
clusterClient.asScoped(request);
|
||||
|
||||
expect(scopedClient.child).toHaveBeenCalledTimes(1);
|
||||
expect(scopedClient.child).toHaveBeenCalledWith({
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
foo: 'request',
|
||||
hello: 'dolly',
|
||||
'x-opaque-id': expect.any(String),
|
||||
},
|
||||
});
|
||||
expect(scopedClient.child).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
foo: 'request',
|
||||
hello: 'dolly',
|
||||
'x-opaque-id': expect.any(String),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('respect the precedence of config headers over default headers', () => {
|
||||
|
@ -307,20 +399,22 @@ describe('ClusterClient', () => {
|
|||
[headerKey]: 'foo',
|
||||
},
|
||||
});
|
||||
getAuthHeaders.mockReturnValue({});
|
||||
authHeaders.get.mockReturnValue({});
|
||||
|
||||
const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders);
|
||||
const clusterClient = new ClusterClient({ config, logger, type: 'custom-type', authHeaders });
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
clusterClient.asScoped(request);
|
||||
|
||||
expect(scopedClient.child).toHaveBeenCalledTimes(1);
|
||||
expect(scopedClient.child).toHaveBeenCalledWith({
|
||||
headers: {
|
||||
[headerKey]: 'foo',
|
||||
'x-opaque-id': expect.any(String),
|
||||
},
|
||||
});
|
||||
expect(scopedClient.child).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
[headerKey]: 'foo',
|
||||
'x-opaque-id': expect.any(String),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('respect the precedence of request headers over default headers', () => {
|
||||
|
@ -328,9 +422,9 @@ describe('ClusterClient', () => {
|
|||
const config = createConfig({
|
||||
requestHeadersWhitelist: [headerKey],
|
||||
});
|
||||
getAuthHeaders.mockReturnValue({});
|
||||
authHeaders.get.mockReturnValue({});
|
||||
|
||||
const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders);
|
||||
const clusterClient = new ClusterClient({ config, logger, type: 'custom-type', authHeaders });
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
headers: { [headerKey]: 'foo' },
|
||||
});
|
||||
|
@ -338,12 +432,14 @@ describe('ClusterClient', () => {
|
|||
clusterClient.asScoped(request);
|
||||
|
||||
expect(scopedClient.child).toHaveBeenCalledTimes(1);
|
||||
expect(scopedClient.child).toHaveBeenCalledWith({
|
||||
headers: {
|
||||
[headerKey]: 'foo',
|
||||
'x-opaque-id': expect.any(String),
|
||||
},
|
||||
});
|
||||
expect(scopedClient.child).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
[headerKey]: 'foo',
|
||||
'x-opaque-id': expect.any(String),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('respect the precedence of x-opaque-id header over config headers', () => {
|
||||
|
@ -352,9 +448,9 @@ describe('ClusterClient', () => {
|
|||
'x-opaque-id': 'from config',
|
||||
},
|
||||
});
|
||||
getAuthHeaders.mockReturnValue({});
|
||||
authHeaders.get.mockReturnValue({});
|
||||
|
||||
const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders);
|
||||
const clusterClient = new ClusterClient({ config, logger, type: 'custom-type', authHeaders });
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
headers: { foo: 'request' },
|
||||
kibanaRequestState: { requestId: 'from request', requestUuid: 'ignore-this-id' },
|
||||
|
@ -363,21 +459,23 @@ describe('ClusterClient', () => {
|
|||
clusterClient.asScoped(request);
|
||||
|
||||
expect(scopedClient.child).toHaveBeenCalledTimes(1);
|
||||
expect(scopedClient.child).toHaveBeenCalledWith({
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
'x-opaque-id': 'from request',
|
||||
},
|
||||
});
|
||||
expect(scopedClient.child).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
'x-opaque-id': 'from request',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('filter headers when called with a `FakeRequest`', () => {
|
||||
const config = createConfig({
|
||||
requestHeadersWhitelist: ['authorization'],
|
||||
});
|
||||
getAuthHeaders.mockReturnValue({});
|
||||
authHeaders.get.mockReturnValue({});
|
||||
|
||||
const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders);
|
||||
const clusterClient = new ClusterClient({ config, logger, type: 'custom-type', authHeaders });
|
||||
const request = {
|
||||
headers: {
|
||||
authorization: 'auth',
|
||||
|
@ -388,20 +486,22 @@ describe('ClusterClient', () => {
|
|||
clusterClient.asScoped(request);
|
||||
|
||||
expect(scopedClient.child).toHaveBeenCalledTimes(1);
|
||||
expect(scopedClient.child).toHaveBeenCalledWith({
|
||||
headers: { ...DEFAULT_HEADERS, authorization: 'auth' },
|
||||
});
|
||||
expect(scopedClient.child).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: { ...DEFAULT_HEADERS, authorization: 'auth' },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('does not add auth headers when called with a `FakeRequest`', () => {
|
||||
const config = createConfig({
|
||||
requestHeadersWhitelist: ['authorization', 'foo'],
|
||||
});
|
||||
getAuthHeaders.mockReturnValue({
|
||||
authHeaders.get.mockReturnValue({
|
||||
authorization: 'auth',
|
||||
});
|
||||
|
||||
const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders);
|
||||
const clusterClient = new ClusterClient({ config, logger, type: 'custom-type', authHeaders });
|
||||
const request = {
|
||||
headers: {
|
||||
foo: 'bar',
|
||||
|
@ -412,20 +512,22 @@ describe('ClusterClient', () => {
|
|||
clusterClient.asScoped(request);
|
||||
|
||||
expect(scopedClient.child).toHaveBeenCalledTimes(1);
|
||||
expect(scopedClient.child).toHaveBeenCalledWith({
|
||||
headers: { ...DEFAULT_HEADERS, foo: 'bar' },
|
||||
});
|
||||
expect(scopedClient.child).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: { ...DEFAULT_HEADERS, foo: 'bar' },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#close', () => {
|
||||
it('closes both underlying clients', async () => {
|
||||
const clusterClient = new ClusterClient(
|
||||
createConfig(),
|
||||
const clusterClient = new ClusterClient({
|
||||
config: createConfig(),
|
||||
logger,
|
||||
'custom-type',
|
||||
getAuthHeaders
|
||||
);
|
||||
type: 'custom-type',
|
||||
authHeaders,
|
||||
});
|
||||
|
||||
await clusterClient.close();
|
||||
|
||||
|
@ -436,12 +538,12 @@ describe('ClusterClient', () => {
|
|||
it('waits for both clients to close', async (done) => {
|
||||
expect.assertions(4);
|
||||
|
||||
const clusterClient = new ClusterClient(
|
||||
createConfig(),
|
||||
const clusterClient = new ClusterClient({
|
||||
config: createConfig(),
|
||||
logger,
|
||||
'custom-type',
|
||||
getAuthHeaders
|
||||
);
|
||||
type: 'custom-type',
|
||||
authHeaders,
|
||||
});
|
||||
|
||||
let internalClientClosed = false;
|
||||
let scopedClientClosed = false;
|
||||
|
@ -479,12 +581,12 @@ describe('ClusterClient', () => {
|
|||
});
|
||||
|
||||
it('return a rejected promise is any client rejects', async () => {
|
||||
const clusterClient = new ClusterClient(
|
||||
createConfig(),
|
||||
const clusterClient = new ClusterClient({
|
||||
config: createConfig(),
|
||||
logger,
|
||||
'custom-type',
|
||||
getAuthHeaders
|
||||
);
|
||||
type: 'custom-type',
|
||||
authHeaders,
|
||||
});
|
||||
|
||||
internalClient.close.mockRejectedValue(new Error('error closing client'));
|
||||
|
||||
|
@ -494,12 +596,12 @@ describe('ClusterClient', () => {
|
|||
});
|
||||
|
||||
it('does nothing after the first call', async () => {
|
||||
const clusterClient = new ClusterClient(
|
||||
createConfig(),
|
||||
const clusterClient = new ClusterClient({
|
||||
config: createConfig(),
|
||||
logger,
|
||||
'custom-type',
|
||||
getAuthHeaders
|
||||
);
|
||||
type: 'custom-type',
|
||||
authHeaders,
|
||||
});
|
||||
|
||||
await clusterClient.close();
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import type { KibanaClient } from '@elastic/elasticsearch/lib/api/kibana';
|
||||
import { Logger } from '../../logging';
|
||||
import { GetAuthHeaders, Headers, isKibanaRequest, isRealRequest } from '../../http';
|
||||
import { IAuthHeadersStorage, Headers, isKibanaRequest, isRealRequest } from '../../http';
|
||||
import { ensureRawRequest, filterHeaders } from '../../http/router';
|
||||
import { ScopeableRequest } from '../types';
|
||||
import { ElasticsearchClient } from './types';
|
||||
|
@ -16,6 +16,12 @@ import { configureClient } from './configure_client';
|
|||
import { ElasticsearchClientConfig } from './client_config';
|
||||
import { ScopedClusterClient, IScopedClusterClient } from './scoped_cluster_client';
|
||||
import { DEFAULT_HEADERS } from '../default_headers';
|
||||
import {
|
||||
UnauthorizedErrorHandler,
|
||||
createInternalErrorHandler,
|
||||
InternalUnauthorizedErrorHandler,
|
||||
} from './retry_unauthorized';
|
||||
import { createTransport } from './create_transport';
|
||||
|
||||
const noop = () => undefined;
|
||||
|
||||
|
@ -52,17 +58,35 @@ export interface ICustomClusterClient extends IClusterClient {
|
|||
|
||||
/** @internal **/
|
||||
export class ClusterClient implements ICustomClusterClient {
|
||||
public readonly asInternalUser: KibanaClient;
|
||||
private readonly config: ElasticsearchClientConfig;
|
||||
private readonly authHeaders?: IAuthHeadersStorage;
|
||||
private readonly rootScopedClient: KibanaClient;
|
||||
private readonly getUnauthorizedErrorHandler: () => UnauthorizedErrorHandler | undefined;
|
||||
private readonly getExecutionContext: () => string | undefined;
|
||||
private isClosed = false;
|
||||
|
||||
constructor(
|
||||
private readonly config: ElasticsearchClientConfig,
|
||||
logger: Logger,
|
||||
type: string,
|
||||
private readonly getAuthHeaders: GetAuthHeaders = noop,
|
||||
getExecutionContext: () => string | undefined = noop
|
||||
) {
|
||||
public readonly asInternalUser: KibanaClient;
|
||||
|
||||
constructor({
|
||||
config,
|
||||
logger,
|
||||
type,
|
||||
authHeaders,
|
||||
getExecutionContext = noop,
|
||||
getUnauthorizedErrorHandler = noop,
|
||||
}: {
|
||||
config: ElasticsearchClientConfig;
|
||||
logger: Logger;
|
||||
type: string;
|
||||
authHeaders?: IAuthHeadersStorage;
|
||||
getExecutionContext?: () => string | undefined;
|
||||
getUnauthorizedErrorHandler?: () => UnauthorizedErrorHandler | undefined;
|
||||
}) {
|
||||
this.config = config;
|
||||
this.authHeaders = authHeaders;
|
||||
this.getExecutionContext = getExecutionContext;
|
||||
this.getUnauthorizedErrorHandler = getUnauthorizedErrorHandler;
|
||||
|
||||
this.asInternalUser = configureClient(config, { logger, type, getExecutionContext });
|
||||
this.rootScopedClient = configureClient(config, {
|
||||
logger,
|
||||
|
@ -74,8 +98,15 @@ export class ClusterClient implements ICustomClusterClient {
|
|||
|
||||
asScoped(request: ScopeableRequest) {
|
||||
const scopedHeaders = this.getScopedHeaders(request);
|
||||
|
||||
const transportClass = createTransport({
|
||||
getExecutionContext: this.getExecutionContext,
|
||||
getUnauthorizedErrorHandler: this.createInternalErrorHandlerAccessor(request),
|
||||
});
|
||||
|
||||
const scopedClient = this.rootScopedClient.child({
|
||||
headers: scopedHeaders,
|
||||
Transport: transportClass,
|
||||
});
|
||||
return new ScopedClusterClient(this.asInternalUser, scopedClient);
|
||||
}
|
||||
|
@ -88,12 +119,26 @@ export class ClusterClient implements ICustomClusterClient {
|
|||
await Promise.all([this.asInternalUser.close(), this.rootScopedClient.close()]);
|
||||
}
|
||||
|
||||
private createInternalErrorHandlerAccessor = (
|
||||
request: ScopeableRequest
|
||||
): (() => InternalUnauthorizedErrorHandler) | undefined => {
|
||||
if (!this.authHeaders) {
|
||||
return undefined;
|
||||
}
|
||||
return () =>
|
||||
createInternalErrorHandler({
|
||||
request,
|
||||
getHandler: this.getUnauthorizedErrorHandler,
|
||||
setAuthHeaders: this.authHeaders!.set,
|
||||
});
|
||||
};
|
||||
|
||||
private getScopedHeaders(request: ScopeableRequest): Headers {
|
||||
let scopedHeaders: Headers;
|
||||
if (isRealRequest(request)) {
|
||||
const requestHeaders = ensureRawRequest(request).headers ?? {};
|
||||
const requestIdHeaders = isKibanaRequest(request) ? { 'x-opaque-id': request.id } : {};
|
||||
const authHeaders = this.getAuthHeaders(request) ?? {};
|
||||
const authHeaders = this.authHeaders ? this.authHeaders.get(request) : {};
|
||||
|
||||
scopedHeaders = {
|
||||
...filterHeaders(requestHeaders, this.config.requestHeadersWhitelist),
|
||||
|
|
|
@ -11,6 +11,11 @@ jest.doMock('./client_config', () => ({
|
|||
parseClientOptions: parseClientOptionsMock,
|
||||
}));
|
||||
|
||||
export const createTransportMock = jest.fn();
|
||||
jest.doMock('./create_transport', () => ({
|
||||
createTransport: createTransportMock,
|
||||
}));
|
||||
|
||||
export const ClientMock = jest.fn();
|
||||
jest.doMock('@elastic/elasticsearch', () => {
|
||||
const actual = jest.requireActual('@elastic/elasticsearch');
|
||||
|
|
|
@ -11,7 +11,11 @@ jest.mock('./log_query_and_deprecation.ts', () => ({
|
|||
instrumentEsQueryAndDeprecationLogger: jest.fn(),
|
||||
}));
|
||||
|
||||
import { parseClientOptionsMock, ClientMock } from './configure_client.test.mocks';
|
||||
import {
|
||||
parseClientOptionsMock,
|
||||
createTransportMock,
|
||||
ClientMock,
|
||||
} from './configure_client.test.mocks';
|
||||
import { loggingSystemMock } from '../../logging/logging_system.mock';
|
||||
import type { ElasticsearchClientConfig } from './client_config';
|
||||
import { configureClient } from './configure_client';
|
||||
|
@ -78,6 +82,36 @@ describe('configureClient', () => {
|
|||
expect(client).toBe(ClientMock.mock.results[0].value);
|
||||
});
|
||||
|
||||
it('calls `createTransport` with the correct parameters', () => {
|
||||
const getExecutionContext = jest.fn();
|
||||
configureClient(config, { logger, type: 'test', scoped: false, getExecutionContext });
|
||||
|
||||
expect(createTransportMock).toHaveBeenCalledTimes(1);
|
||||
expect(createTransportMock).toHaveBeenCalledWith({ getExecutionContext });
|
||||
|
||||
createTransportMock.mockClear();
|
||||
|
||||
configureClient(config, { logger, type: 'test', scoped: true, getExecutionContext });
|
||||
|
||||
expect(createTransportMock).toHaveBeenCalledTimes(1);
|
||||
expect(createTransportMock).toHaveBeenCalledWith({ getExecutionContext });
|
||||
});
|
||||
|
||||
it('constructs a client using the Transport returned by `createTransport`', () => {
|
||||
const mockedTransport = { mockTransport: true };
|
||||
createTransportMock.mockReturnValue(mockedTransport);
|
||||
|
||||
const client = configureClient(config, { logger, type: 'test', scoped: false });
|
||||
|
||||
expect(ClientMock).toHaveBeenCalledTimes(1);
|
||||
expect(ClientMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
Transport: mockedTransport,
|
||||
})
|
||||
);
|
||||
expect(client).toBe(ClientMock.mock.results[0].value);
|
||||
});
|
||||
|
||||
it('calls instrumentEsQueryAndDeprecationLogger', () => {
|
||||
const client = configureClient(config, { logger, type: 'test', scoped: false });
|
||||
|
||||
|
|
|
@ -6,17 +6,12 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Client, Transport, HttpConnection } from '@elastic/elasticsearch';
|
||||
import { Client, HttpConnection } from '@elastic/elasticsearch';
|
||||
import type { KibanaClient } from '@elastic/elasticsearch/lib/api/kibana';
|
||||
import type {
|
||||
TransportRequestParams,
|
||||
TransportRequestOptions,
|
||||
TransportResult,
|
||||
} from '@elastic/elasticsearch';
|
||||
|
||||
import { Logger } from '../../logging';
|
||||
import { parseClientOptions, ElasticsearchClientConfig } from './client_config';
|
||||
import { instrumentEsQueryAndDeprecationLogger } from './log_query_and_deprecation';
|
||||
import { createTransport } from './create_transport';
|
||||
|
||||
const noop = () => undefined;
|
||||
|
||||
|
@ -35,22 +30,7 @@ export const configureClient = (
|
|||
}
|
||||
): KibanaClient => {
|
||||
const clientOptions = parseClientOptions(config, scoped);
|
||||
class KibanaTransport extends Transport {
|
||||
request(params: TransportRequestParams, options?: TransportRequestOptions) {
|
||||
const opts: TransportRequestOptions = options || {};
|
||||
const opaqueId = getExecutionContext();
|
||||
if (opaqueId && !opts.opaqueId) {
|
||||
// rewrites headers['x-opaque-id'] if it presents
|
||||
opts.opaqueId = opaqueId;
|
||||
}
|
||||
// Enforce the client to return TransportResult.
|
||||
// It's required for bwc with responses in 7.x version.
|
||||
if (opts.meta === undefined) {
|
||||
opts.meta = true;
|
||||
}
|
||||
return super.request(params, opts) as Promise<TransportResult<any, any>>;
|
||||
}
|
||||
}
|
||||
const KibanaTransport = createTransport({ getExecutionContext });
|
||||
|
||||
const client = new Client({
|
||||
...clientOptions,
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { TransportRequestParams, TransportRequestOptions } from '@elastic/transport';
|
||||
import type { TransportOptions } from '@elastic/transport/lib/Transport';
|
||||
|
||||
export const transportConstructorMock: jest.MockedFunction<(options: TransportOptions) => void> =
|
||||
jest.fn();
|
||||
export const transportRequestMock = jest.fn();
|
||||
|
||||
class TransportMock {
|
||||
constructor(options: TransportOptions) {
|
||||
transportConstructorMock(options);
|
||||
}
|
||||
|
||||
request(params: TransportRequestParams, options?: TransportRequestOptions) {
|
||||
return transportRequestMock(params, options);
|
||||
}
|
||||
}
|
||||
|
||||
jest.doMock('@elastic/elasticsearch', () => {
|
||||
const realModule = jest.requireActual('@elastic/elasticsearch');
|
||||
return {
|
||||
...realModule,
|
||||
Transport: TransportMock,
|
||||
};
|
||||
});
|
469
src/core/server/elasticsearch/client/create_transport.test.ts
Normal file
469
src/core/server/elasticsearch/client/create_transport.test.ts
Normal file
|
@ -0,0 +1,469 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { transportConstructorMock, transportRequestMock } from './create_transport.test.mocks';
|
||||
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
import type { BaseConnectionPool } from '@elastic/elasticsearch';
|
||||
import type { InternalUnauthorizedErrorHandler } from './retry_unauthorized';
|
||||
import { createTransport, ErrorHandlerAccessor } from './create_transport';
|
||||
|
||||
const createConnectionPool = () => {
|
||||
return { _connectionPool: 'mocked' } as unknown as BaseConnectionPool;
|
||||
};
|
||||
|
||||
const baseConstructorParams = {
|
||||
connectionPool: createConnectionPool(),
|
||||
};
|
||||
|
||||
const createUnauthorizedError = () => {
|
||||
return new errors.ResponseError({
|
||||
statusCode: 401,
|
||||
warnings: [],
|
||||
meta: {} as any,
|
||||
});
|
||||
};
|
||||
|
||||
describe('createTransport', () => {
|
||||
let getUnauthorizedErrorHandler: jest.MockedFunction<ErrorHandlerAccessor>;
|
||||
let getExecutionContext: jest.MockedFunction<() => string | undefined>;
|
||||
|
||||
beforeEach(() => {
|
||||
getUnauthorizedErrorHandler = jest.fn();
|
||||
getExecutionContext = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
transportConstructorMock.mockReset();
|
||||
transportRequestMock.mockReset();
|
||||
});
|
||||
|
||||
const createTransportClass = () => {
|
||||
return createTransport({
|
||||
getUnauthorizedErrorHandler,
|
||||
getExecutionContext,
|
||||
});
|
||||
};
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('calls the parent constructor with the passed options', () => {
|
||||
const transportClass = createTransportClass();
|
||||
|
||||
const options = {
|
||||
connectionPool: createConnectionPool(),
|
||||
maxRetries: 42,
|
||||
};
|
||||
|
||||
new transportClass(options);
|
||||
|
||||
expect(transportConstructorMock).toHaveBeenCalledTimes(1);
|
||||
expect(transportConstructorMock).toHaveBeenCalledWith(options);
|
||||
});
|
||||
|
||||
it('omits the headers when calling the parent constructor', () => {
|
||||
const transportClass = createTransportClass();
|
||||
|
||||
const options = {
|
||||
connectionPool: createConnectionPool(),
|
||||
maxRetries: 42,
|
||||
headers: {
|
||||
foo: 'bar',
|
||||
},
|
||||
};
|
||||
|
||||
new transportClass(options);
|
||||
|
||||
const { headers, ...optionsWithoutHeaders } = options;
|
||||
|
||||
expect(transportConstructorMock).toHaveBeenCalledTimes(1);
|
||||
expect(transportConstructorMock).toHaveBeenCalledWith(optionsWithoutHeaders);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#request', () => {
|
||||
it('calls `super.request`', async () => {
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass(baseConstructorParams);
|
||||
const requestOptions = { method: 'GET', path: '/' };
|
||||
|
||||
await transport.request(requestOptions);
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not mutate the arguments', async () => {
|
||||
const transportClass = createTransportClass();
|
||||
const constructorHeaders = { over: '9000', shared: 'from-constructor' };
|
||||
const transport = new transportClass({
|
||||
...baseConstructorParams,
|
||||
headers: constructorHeaders,
|
||||
});
|
||||
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
const options = {
|
||||
headers: { hello: 'dolly', shared: 'from-options' },
|
||||
};
|
||||
|
||||
await transport.request(requestParams, options);
|
||||
|
||||
expect(requestParams).toEqual({ method: 'GET', path: '/' });
|
||||
expect(options).toEqual({ headers: { hello: 'dolly', shared: 'from-options' } });
|
||||
});
|
||||
|
||||
describe('`meta` option', () => {
|
||||
it('adds `meta: true` to the options when not provided', async () => {
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass(baseConstructorParams);
|
||||
const requestOptions = { method: 'GET', path: '/' };
|
||||
|
||||
await transport.request(requestOptions, {});
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(1);
|
||||
expect(transportRequestMock).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
meta: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('does not add `meta: true` to the options when provided', async () => {
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass(baseConstructorParams);
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
|
||||
await transport.request(requestParams, { meta: false });
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(1);
|
||||
expect(transportRequestMock).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
meta: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('`opaqueId` option', () => {
|
||||
it('uses the value from the options when provided', async () => {
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass(baseConstructorParams);
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
|
||||
await transport.request(requestParams, { opaqueId: 'some-opaque-id' });
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(1);
|
||||
expect(transportRequestMock).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
opaqueId: 'some-opaque-id',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the value from getExecutionContext when provided', async () => {
|
||||
getExecutionContext.mockReturnValue('opaque-id-from-exec-context');
|
||||
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass(baseConstructorParams);
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
|
||||
await transport.request(requestParams, {});
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(1);
|
||||
expect(transportRequestMock).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
opaqueId: 'opaque-id-from-exec-context',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the value from the options when provided both by the options and execution context', async () => {
|
||||
getExecutionContext.mockReturnValue('opaque-id-from-exec-context');
|
||||
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass(baseConstructorParams);
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
|
||||
await transport.request(requestParams, { opaqueId: 'opaque-id-from-options' });
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(1);
|
||||
expect(transportRequestMock).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
opaqueId: 'opaque-id-from-options',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('`headers` option', () => {
|
||||
it('uses the headers from the options when provided', async () => {
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass(baseConstructorParams);
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
|
||||
const headers = { foo: 'bar', hello: 'dolly' };
|
||||
|
||||
await transport.request(requestParams, { headers });
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(1);
|
||||
expect(transportRequestMock).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
headers,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the headers passed to the constructor when provided', async () => {
|
||||
const transportClass = createTransportClass();
|
||||
const headers = { over: '9000', because: 'we can' };
|
||||
const transport = new transportClass({ ...baseConstructorParams, headers });
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
|
||||
await transport.request(requestParams, {});
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(1);
|
||||
expect(transportRequestMock).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
headers,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('merges the headers from the constructor and from the options', async () => {
|
||||
const transportClass = createTransportClass();
|
||||
const constructorHeaders = { over: '9000', shared: 'from-constructor' };
|
||||
const transport = new transportClass({
|
||||
...baseConstructorParams,
|
||||
headers: constructorHeaders,
|
||||
});
|
||||
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
const requestHeaders = { hello: 'dolly', shared: 'from-options' };
|
||||
|
||||
await transport.request(requestParams, { headers: requestHeaders });
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(1);
|
||||
expect(transportRequestMock).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
over: '9000',
|
||||
hello: 'dolly',
|
||||
shared: 'from-options',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unauthorized error handler', () => {
|
||||
it('does not call the handler if the error is not an `unauthorized`', async () => {
|
||||
const handler: jest.MockedFunction<InternalUnauthorizedErrorHandler> = jest.fn();
|
||||
handler.mockReturnValue({ type: 'notHandled' });
|
||||
|
||||
getUnauthorizedErrorHandler.mockReturnValue(handler);
|
||||
|
||||
transportRequestMock.mockImplementation(() => {
|
||||
throw new Error('woups');
|
||||
});
|
||||
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass(baseConstructorParams);
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
|
||||
await expect(transport.request(requestParams, {})).rejects.toThrowError('woups');
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(1);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not attempt to retry the call if no handler is provided', async () => {
|
||||
transportRequestMock.mockImplementation(() => {
|
||||
throw new Error('woups');
|
||||
});
|
||||
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass(baseConstructorParams);
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
|
||||
await expect(transport.request(requestParams, {})).rejects.toThrowError('woups');
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls the handler if the error is an `unauthorized`', async () => {
|
||||
const handler: jest.MockedFunction<InternalUnauthorizedErrorHandler> = jest.fn();
|
||||
handler.mockReturnValue({ type: 'notHandled' });
|
||||
|
||||
getUnauthorizedErrorHandler.mockReturnValue(handler);
|
||||
|
||||
const error = createUnauthorizedError();
|
||||
|
||||
transportRequestMock.mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass(baseConstructorParams);
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
|
||||
await expect(transport.request(requestParams, {})).rejects.toThrowError(error);
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
it('does not retry the call if the handler returns `notHandled`', async () => {
|
||||
const handler: jest.MockedFunction<InternalUnauthorizedErrorHandler> = jest.fn();
|
||||
handler.mockReturnValue({ type: 'notHandled' });
|
||||
|
||||
getUnauthorizedErrorHandler.mockReturnValue(handler);
|
||||
|
||||
const error = createUnauthorizedError();
|
||||
|
||||
transportRequestMock.mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass(baseConstructorParams);
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
|
||||
await expect(transport.request(requestParams, {})).rejects.toThrowError(error);
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('retries the call if the handler returns `retry` and return result from the retry', async () => {
|
||||
const handler: jest.MockedFunction<InternalUnauthorizedErrorHandler> = jest.fn();
|
||||
handler.mockReturnValue({ type: 'retry', authHeaders: {} });
|
||||
|
||||
getUnauthorizedErrorHandler.mockReturnValue(handler);
|
||||
|
||||
const error = createUnauthorizedError();
|
||||
|
||||
const retryResult = { body: 'some dummy content' };
|
||||
|
||||
transportRequestMock
|
||||
.mockImplementationOnce(() => {
|
||||
throw error;
|
||||
})
|
||||
.mockResolvedValueOnce(retryResult);
|
||||
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass(baseConstructorParams);
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
|
||||
await expect(transport.request(requestParams, {})).resolves.toEqual(retryResult);
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('does not retry more than once even in case of unauthorized errors', async () => {
|
||||
const handler: jest.MockedFunction<InternalUnauthorizedErrorHandler> = jest.fn();
|
||||
handler.mockReturnValue({ type: 'retry', authHeaders: {} });
|
||||
|
||||
getUnauthorizedErrorHandler.mockReturnValue(handler);
|
||||
|
||||
const error = createUnauthorizedError();
|
||||
|
||||
transportRequestMock.mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass(baseConstructorParams);
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
|
||||
await expect(transport.request(requestParams, {})).rejects.toThrowError(error);
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('updates the headers for the second internal call in case of `retry`', async () => {
|
||||
const handler: jest.MockedFunction<InternalUnauthorizedErrorHandler> = jest.fn();
|
||||
handler.mockReturnValue({ type: 'retry', authHeaders: { authorization: 'retry' } });
|
||||
|
||||
getUnauthorizedErrorHandler.mockReturnValue(handler);
|
||||
|
||||
const error = createUnauthorizedError();
|
||||
|
||||
const retryResult = { body: 'some dummy content' };
|
||||
|
||||
transportRequestMock
|
||||
.mockImplementationOnce(() => {
|
||||
throw error;
|
||||
})
|
||||
.mockResolvedValueOnce(retryResult);
|
||||
|
||||
const initialHeaders = { authorization: 'initial', foo: 'bar' };
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass({ ...baseConstructorParams, headers: initialHeaders });
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
|
||||
await expect(transport.request(requestParams, {})).resolves.toEqual(retryResult);
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(2);
|
||||
expect(transportRequestMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
requestParams,
|
||||
expect.objectContaining({
|
||||
headers: initialHeaders,
|
||||
})
|
||||
);
|
||||
expect(transportRequestMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
requestParams,
|
||||
expect.objectContaining({
|
||||
headers: { authorization: 'retry', foo: 'bar' },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('updates the headers for next requests in case of `retry`', async () => {
|
||||
const handler: jest.MockedFunction<InternalUnauthorizedErrorHandler> = jest.fn();
|
||||
handler.mockReturnValue({ type: 'retry', authHeaders: { authorization: 'retry' } });
|
||||
|
||||
getUnauthorizedErrorHandler.mockReturnValue(handler);
|
||||
|
||||
const error = createUnauthorizedError();
|
||||
|
||||
const retryResult = { body: 'some dummy content' };
|
||||
|
||||
transportRequestMock
|
||||
.mockImplementationOnce(() => {
|
||||
throw error;
|
||||
})
|
||||
.mockResolvedValue(retryResult);
|
||||
|
||||
const initialHeaders = { authorization: 'initial', foo: 'bar' };
|
||||
const transportClass = createTransportClass();
|
||||
const transport = new transportClass({ ...baseConstructorParams, headers: initialHeaders });
|
||||
const requestParams = { method: 'GET', path: '/' };
|
||||
|
||||
await expect(transport.request(requestParams, {})).resolves.toEqual(retryResult);
|
||||
await expect(transport.request(requestParams, {})).resolves.toEqual(retryResult);
|
||||
|
||||
expect(transportRequestMock).toHaveBeenCalledTimes(3);
|
||||
expect(transportRequestMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
requestParams,
|
||||
expect.objectContaining({
|
||||
headers: { authorization: 'retry', foo: 'bar' },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
90
src/core/server/elasticsearch/client/create_transport.ts
Normal file
90
src/core/server/elasticsearch/client/create_transport.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { IncomingHttpHeaders } from 'http';
|
||||
import type {
|
||||
TransportRequestParams,
|
||||
TransportRequestOptions,
|
||||
TransportResult,
|
||||
} from '@elastic/transport';
|
||||
import type { TransportOptions } from '@elastic/transport/lib/Transport';
|
||||
import { Transport } from '@elastic/elasticsearch';
|
||||
import { isUnauthorizedError } from './errors';
|
||||
import { InternalUnauthorizedErrorHandler, isRetryResult } from './retry_unauthorized';
|
||||
|
||||
type TransportClass = typeof Transport;
|
||||
|
||||
export type ErrorHandlerAccessor = () => InternalUnauthorizedErrorHandler;
|
||||
|
||||
const noop = () => undefined;
|
||||
|
||||
export const createTransport = ({
|
||||
getExecutionContext = noop,
|
||||
getUnauthorizedErrorHandler,
|
||||
}: {
|
||||
getExecutionContext?: () => string | undefined;
|
||||
getUnauthorizedErrorHandler?: ErrorHandlerAccessor;
|
||||
}): TransportClass => {
|
||||
class KibanaTransport extends Transport {
|
||||
private headers: IncomingHttpHeaders = {};
|
||||
|
||||
constructor(options: TransportOptions) {
|
||||
const { headers = {}, ...otherOptions } = options;
|
||||
super(otherOptions);
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
async request(params: TransportRequestParams, options?: TransportRequestOptions) {
|
||||
const opts: TransportRequestOptions = options ? { ...options } : {};
|
||||
const opaqueId = getExecutionContext();
|
||||
if (opaqueId && !opts.opaqueId) {
|
||||
// rewrites headers['x-opaque-id'] if it presents
|
||||
opts.opaqueId = opaqueId;
|
||||
}
|
||||
// Enforce the client to return TransportResult.
|
||||
// It's required for bwc with responses in 7.x version.
|
||||
if (opts.meta === undefined) {
|
||||
opts.meta = true;
|
||||
}
|
||||
|
||||
// add stored headers to the options
|
||||
opts.headers = {
|
||||
...this.headers,
|
||||
...options?.headers,
|
||||
};
|
||||
|
||||
try {
|
||||
return (await super.request(params, opts)) as TransportResult<any, any>;
|
||||
} catch (e) {
|
||||
if (isUnauthorizedError(e)) {
|
||||
const unauthorizedErrorHandler = getUnauthorizedErrorHandler
|
||||
? getUnauthorizedErrorHandler()
|
||||
: undefined;
|
||||
if (unauthorizedErrorHandler) {
|
||||
const result = await unauthorizedErrorHandler(e);
|
||||
if (isRetryResult(result)) {
|
||||
this.headers = {
|
||||
...this.headers,
|
||||
...result.authHeaders,
|
||||
};
|
||||
const retryOpts = { ...opts };
|
||||
retryOpts.headers = {
|
||||
...this.headers,
|
||||
...options?.headers,
|
||||
};
|
||||
return (await super.request(params, retryOpts)) as TransportResult<any, any>;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return KibanaTransport;
|
||||
};
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
|
||||
/** @public */
|
||||
export type UnauthorizedError = errors.ResponseError & {
|
||||
statusCode: 401;
|
||||
};
|
||||
|
|
|
@ -24,3 +24,13 @@ export type { IClusterClient, ICustomClusterClient } from './cluster_client';
|
|||
export { configureClient } from './configure_client';
|
||||
export { getRequestDebugMeta, getErrorMessage } from './log_query_and_deprecation';
|
||||
export { retryCallCluster, migrationRetryCallCluster } from './retry_call_cluster';
|
||||
export type {
|
||||
UnauthorizedErrorHandlerOptions,
|
||||
UnauthorizedErrorHandlerResultRetryParams,
|
||||
UnauthorizedErrorHandlerRetryResult,
|
||||
UnauthorizedErrorHandlerNotHandledResult,
|
||||
UnauthorizedErrorHandlerResult,
|
||||
UnauthorizedErrorHandlerToolkit,
|
||||
UnauthorizedErrorHandler,
|
||||
} from './retry_unauthorized';
|
||||
export type { UnauthorizedError } from './errors';
|
||||
|
|
197
src/core/server/elasticsearch/client/retry_unauthorized.test.ts
Normal file
197
src/core/server/elasticsearch/client/retry_unauthorized.test.ts
Normal file
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { SetAuthHeaders } from '../../http';
|
||||
import { httpServerMock } from '../../http/http_server.mocks';
|
||||
import type { UnauthorizedError } from './errors';
|
||||
import {
|
||||
createInternalErrorHandler,
|
||||
isRetryResult,
|
||||
isNotHandledResult,
|
||||
toolkit,
|
||||
} from './retry_unauthorized';
|
||||
|
||||
const createUnauthorizedError = (): UnauthorizedError => {
|
||||
return { statusCode: 401 } as UnauthorizedError;
|
||||
};
|
||||
|
||||
describe('createInternalErrorHandler', () => {
|
||||
let setAuthHeaders: jest.MockedFunction<SetAuthHeaders>;
|
||||
|
||||
beforeEach(() => {
|
||||
setAuthHeaders = jest.fn();
|
||||
});
|
||||
|
||||
it('calls and returns the result from the provided handler', async () => {
|
||||
const handlerResponse = toolkit.retry({ authHeaders: { foo: 'bar' } });
|
||||
const handler = jest.fn().mockReturnValue(handlerResponse);
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
const internalHandler = createInternalErrorHandler({
|
||||
getHandler: () => handler,
|
||||
request,
|
||||
setAuthHeaders,
|
||||
});
|
||||
|
||||
const error = createUnauthorizedError();
|
||||
const result = await internalHandler(error);
|
||||
|
||||
expect(result).toEqual(handlerResponse);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith({ request, error }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls `setAuthHeaders` when the handler returns `retry`', async () => {
|
||||
const handlerResponse = toolkit.retry({ authHeaders: { foo: 'bar' } });
|
||||
const handler = jest.fn().mockReturnValue(handlerResponse);
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
const internalHandler = createInternalErrorHandler({
|
||||
getHandler: () => handler,
|
||||
request,
|
||||
setAuthHeaders,
|
||||
});
|
||||
|
||||
const error = createUnauthorizedError();
|
||||
await internalHandler(error);
|
||||
|
||||
expect(setAuthHeaders).toHaveBeenCalledTimes(1);
|
||||
expect(setAuthHeaders).toHaveBeenCalledWith(request, handlerResponse.authHeaders);
|
||||
});
|
||||
|
||||
it('does not call `setAuthHeaders` when the handler returns `notHandled`', async () => {
|
||||
const handlerResponse = toolkit.notHandled();
|
||||
const handler = jest.fn().mockReturnValue(handlerResponse);
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
const internalHandler = createInternalErrorHandler({
|
||||
getHandler: () => handler,
|
||||
request,
|
||||
setAuthHeaders,
|
||||
});
|
||||
|
||||
const error = createUnauthorizedError();
|
||||
await internalHandler(error);
|
||||
|
||||
expect(setAuthHeaders).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns `notHandled` if the handler throws', async () => {
|
||||
const handler = jest.fn().mockImplementation(() => {
|
||||
throw new Error('woups');
|
||||
});
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
const internalHandler = createInternalErrorHandler({
|
||||
getHandler: () => handler,
|
||||
request,
|
||||
setAuthHeaders,
|
||||
});
|
||||
|
||||
const error = createUnauthorizedError();
|
||||
const result = await internalHandler(error);
|
||||
|
||||
expect(isNotHandledResult(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('handles asynchronous handlers', async () => {
|
||||
const handlerResponse = toolkit.retry({ authHeaders: { foo: 'bar' } });
|
||||
const handler = jest.fn().mockResolvedValue(handlerResponse);
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
const internalHandler = createInternalErrorHandler({
|
||||
getHandler: () => handler,
|
||||
request,
|
||||
setAuthHeaders,
|
||||
});
|
||||
|
||||
const error = createUnauthorizedError();
|
||||
const result = await internalHandler(error);
|
||||
|
||||
expect(result).toEqual(handlerResponse);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith({ request, error }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('returns `notHandled` without calling the provided handler for fake requests', async () => {
|
||||
const handler = jest.fn();
|
||||
const fakeRequest = {
|
||||
headers: {
|
||||
authorization: 'foobar',
|
||||
},
|
||||
};
|
||||
|
||||
const internalHandler = createInternalErrorHandler({
|
||||
getHandler: () => handler,
|
||||
request: fakeRequest,
|
||||
setAuthHeaders,
|
||||
});
|
||||
|
||||
const result = await internalHandler(createUnauthorizedError());
|
||||
|
||||
expect(isNotHandledResult(result)).toBe(true);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('checks the presence of a registered handler for each error', async () => {
|
||||
const handlerResponse = toolkit.retry({ authHeaders: { foo: 'bar' } });
|
||||
const handler = jest.fn().mockResolvedValue(handlerResponse);
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
const getHandler = jest.fn().mockReturnValueOnce(undefined).mockReturnValueOnce(handler);
|
||||
|
||||
const internalHandler = createInternalErrorHandler({
|
||||
getHandler,
|
||||
request,
|
||||
setAuthHeaders,
|
||||
});
|
||||
|
||||
const error = createUnauthorizedError();
|
||||
let result = await internalHandler(error);
|
||||
|
||||
expect(isNotHandledResult(result)).toBe(true);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
|
||||
result = await internalHandler(error);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(isRetryResult(result)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRetryResult', () => {
|
||||
it('returns `true` for a `retry` result', () => {
|
||||
expect(
|
||||
isRetryResult(
|
||||
toolkit.retry({
|
||||
authHeaders: { foo: 'bar' },
|
||||
})
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns `false` for a `notHandled` result', () => {
|
||||
expect(isRetryResult(toolkit.notHandled())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNotHandledResult', () => {
|
||||
it('returns `false` for a `retry` result', () => {
|
||||
expect(
|
||||
isNotHandledResult(
|
||||
toolkit.retry({
|
||||
authHeaders: { foo: 'bar' },
|
||||
})
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns `true` for a `notHandled` result', () => {
|
||||
expect(isNotHandledResult(toolkit.notHandled())).toBe(true);
|
||||
});
|
||||
});
|
137
src/core/server/elasticsearch/client/retry_unauthorized.ts
Normal file
137
src/core/server/elasticsearch/client/retry_unauthorized.ts
Normal file
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { MaybePromise } from '@kbn/utility-types';
|
||||
import { AuthHeaders, KibanaRequest, SetAuthHeaders, isRealRequest } from '../../http';
|
||||
import { ScopeableRequest } from '../types';
|
||||
import { UnauthorizedError } from './errors';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface UnauthorizedErrorHandlerOptions {
|
||||
error: UnauthorizedError;
|
||||
request: KibanaRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface UnauthorizedErrorHandlerResultRetryParams {
|
||||
authHeaders: AuthHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface UnauthorizedErrorHandlerRetryResult
|
||||
extends UnauthorizedErrorHandlerResultRetryParams {
|
||||
type: 'retry';
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface UnauthorizedErrorHandlerNotHandledResult {
|
||||
type: 'notHandled';
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type UnauthorizedErrorHandlerResult =
|
||||
| UnauthorizedErrorHandlerRetryResult
|
||||
| UnauthorizedErrorHandlerNotHandledResult;
|
||||
|
||||
/**
|
||||
* Toolkit passed to a {@link UnauthorizedErrorHandler} used to generate responses from the handler
|
||||
* @public
|
||||
*/
|
||||
export interface UnauthorizedErrorHandlerToolkit {
|
||||
/**
|
||||
* The handler cannot handle the error, or was not able to authenticate.
|
||||
*/
|
||||
notHandled: () => UnauthorizedErrorHandlerNotHandledResult;
|
||||
/**
|
||||
* The handler was able to authenticate. Will retry the failed request with new auth headers
|
||||
*/
|
||||
retry: (params: UnauthorizedErrorHandlerResultRetryParams) => UnauthorizedErrorHandlerRetryResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* A handler used to handle unauthorized error returned by elasticsearch
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type UnauthorizedErrorHandler = (
|
||||
options: UnauthorizedErrorHandlerOptions,
|
||||
toolkit: UnauthorizedErrorHandlerToolkit
|
||||
) => MaybePromise<UnauthorizedErrorHandlerResult>;
|
||||
|
||||
/** @internal */
|
||||
export type InternalUnauthorizedErrorHandler = (
|
||||
error: UnauthorizedError
|
||||
) => MaybePromise<UnauthorizedErrorHandlerResult>;
|
||||
|
||||
/** @internal */
|
||||
export const toolkit: UnauthorizedErrorHandlerToolkit = {
|
||||
notHandled: () => ({ type: 'notHandled' }),
|
||||
retry: ({ authHeaders }) => ({
|
||||
type: 'retry',
|
||||
authHeaders,
|
||||
}),
|
||||
};
|
||||
|
||||
const notHandledInternalErrorHandler: InternalUnauthorizedErrorHandler = () => toolkit.notHandled();
|
||||
|
||||
/**
|
||||
* Converts the public version of `UnauthorizedErrorHandler` to the internal one used by the ES client
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const createInternalErrorHandler = ({
|
||||
getHandler,
|
||||
request,
|
||||
setAuthHeaders,
|
||||
}: {
|
||||
getHandler: () => UnauthorizedErrorHandler | undefined;
|
||||
request: ScopeableRequest;
|
||||
setAuthHeaders: SetAuthHeaders;
|
||||
}): InternalUnauthorizedErrorHandler => {
|
||||
// we don't want to support 401 retry for fake requests
|
||||
if (!isRealRequest(request)) {
|
||||
return notHandledInternalErrorHandler;
|
||||
}
|
||||
return async (error) => {
|
||||
try {
|
||||
const handler = getHandler();
|
||||
if (!handler) {
|
||||
return toolkit.notHandled();
|
||||
}
|
||||
const result = await handler({ request, error }, toolkit);
|
||||
if (isRetryResult(result)) {
|
||||
setAuthHeaders(request, result.authHeaders);
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
return toolkit.notHandled();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const isRetryResult = (
|
||||
result: UnauthorizedErrorHandlerResult
|
||||
): result is UnauthorizedErrorHandlerRetryResult => {
|
||||
return result.type === 'retry';
|
||||
};
|
||||
|
||||
export const isNotHandledResult = (
|
||||
result: UnauthorizedErrorHandlerResult
|
||||
): result is UnauthorizedErrorHandlerNotHandledResult => {
|
||||
return result.type === 'notHandled';
|
||||
};
|
|
@ -19,6 +19,7 @@ import { ElasticsearchConfig } from './elasticsearch_config';
|
|||
import { ElasticsearchService } from './elasticsearch_service';
|
||||
import {
|
||||
InternalElasticsearchServiceSetup,
|
||||
ElasticsearchServiceSetup,
|
||||
ElasticsearchStatusMeta,
|
||||
ElasticsearchServicePreboot,
|
||||
} from './types';
|
||||
|
@ -27,18 +28,23 @@ import { ServiceStatus, ServiceStatusLevels } from '../status';
|
|||
|
||||
type MockedElasticSearchServicePreboot = jest.Mocked<ElasticsearchServicePreboot>;
|
||||
|
||||
export interface MockedElasticSearchServiceSetup {
|
||||
export type MockedElasticSearchServiceSetup = jest.Mocked<
|
||||
Omit<ElasticsearchServiceSetup, 'legacy'>
|
||||
> & {
|
||||
legacy: {
|
||||
config$: BehaviorSubject<ElasticsearchConfig>;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
type MockedElasticSearchServiceStart = MockedElasticSearchServiceSetup & {
|
||||
export interface MockedElasticSearchServiceStart {
|
||||
legacy: {
|
||||
config$: BehaviorSubject<ElasticsearchConfig>;
|
||||
};
|
||||
client: ClusterClientMock;
|
||||
createClient: jest.MockedFunction<
|
||||
(name: string, config?: Partial<ElasticsearchClientConfig>) => CustomClusterClientMock
|
||||
>;
|
||||
};
|
||||
}
|
||||
|
||||
const createPrebootContractMock = () => {
|
||||
const prebootContract: MockedElasticSearchServicePreboot = {
|
||||
|
@ -53,6 +59,7 @@ const createPrebootContractMock = () => {
|
|||
|
||||
const createSetupContractMock = () => {
|
||||
const setupContract: MockedElasticSearchServiceSetup = {
|
||||
setUnauthorizedErrorHandler: jest.fn(),
|
||||
legacy: {
|
||||
config$: new BehaviorSubject({} as ElasticsearchConfig),
|
||||
},
|
||||
|
@ -79,7 +86,9 @@ const createInternalPrebootContractMock = createPrebootContractMock;
|
|||
|
||||
type MockedInternalElasticSearchServiceSetup = jest.Mocked<InternalElasticsearchServiceSetup>;
|
||||
const createInternalSetupContractMock = () => {
|
||||
const setupContract: MockedInternalElasticSearchServiceSetup = {
|
||||
const setupContract = createSetupContractMock();
|
||||
const internalSetupContract: MockedInternalElasticSearchServiceSetup = {
|
||||
...setupContract,
|
||||
esNodesCompatibility$: new BehaviorSubject<NodesVersionCompatibility>({
|
||||
isCompatible: true,
|
||||
incompatibleNodes: [],
|
||||
|
@ -90,11 +99,8 @@ const createInternalSetupContractMock = () => {
|
|||
level: ServiceStatusLevels.available,
|
||||
summary: 'Elasticsearch is available',
|
||||
}),
|
||||
legacy: {
|
||||
...createSetupContractMock().legacy,
|
||||
},
|
||||
};
|
||||
return setupContract;
|
||||
return internalSetupContract;
|
||||
};
|
||||
|
||||
const createInternalStartContractMock = createStartContractMock;
|
||||
|
|
|
@ -127,7 +127,9 @@ describe('#preboot', () => {
|
|||
expect(clusterClient).toBe(mockClusterClientInstance);
|
||||
|
||||
expect(MockClusterClient).toHaveBeenCalledTimes(1);
|
||||
expect(MockClusterClient.mock.calls[0][0]).toEqual(expect.objectContaining(customConfig));
|
||||
expect(MockClusterClient.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({ config: expect.objectContaining(customConfig) })
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a new client on each call', async () => {
|
||||
|
@ -151,7 +153,7 @@ describe('#preboot', () => {
|
|||
};
|
||||
|
||||
prebootContract.createClient('some-custom-type', customConfig);
|
||||
const config = MockClusterClient.mock.calls[0][0];
|
||||
const config = MockClusterClient.mock.calls[0][0].config;
|
||||
|
||||
expect(config).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -334,7 +336,9 @@ describe('#start', () => {
|
|||
expect(clusterClient).toBe(mockClusterClientInstance);
|
||||
|
||||
expect(MockClusterClient).toHaveBeenCalledTimes(1);
|
||||
expect(MockClusterClient.mock.calls[0][0]).toEqual(expect.objectContaining(customConfig));
|
||||
expect(MockClusterClient.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({ config: expect.objectContaining(customConfig) })
|
||||
);
|
||||
});
|
||||
it('creates a new client on each call', async () => {
|
||||
await elasticsearchService.setup(setupDeps);
|
||||
|
@ -365,7 +369,7 @@ describe('#start', () => {
|
|||
};
|
||||
|
||||
startContract.createClient('some-custom-type', customConfig);
|
||||
const config = MockClusterClient.mock.calls[0][0];
|
||||
const config = MockClusterClient.mock.calls[0][0].config;
|
||||
|
||||
expect(config).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Logger } from '../logging';
|
|||
|
||||
import { ClusterClient, ElasticsearchClientConfig } from './client';
|
||||
import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config';
|
||||
import type { InternalHttpServiceSetup, GetAuthHeaders } from '../http';
|
||||
import type { InternalHttpServiceSetup, IAuthHeadersStorage } from '../http';
|
||||
import type { InternalExecutionContextSetup, IExecutionContext } from '../execution_context';
|
||||
import {
|
||||
InternalElasticsearchServicePreboot,
|
||||
|
@ -28,6 +28,7 @@ import { pollEsNodesVersion } from './version_check/ensure_es_version';
|
|||
import { calculateStatus$ } from './status';
|
||||
import { isValidConnection } from './is_valid_connection';
|
||||
import { isInlineScriptingEnabled } from './is_scripting_enabled';
|
||||
import type { UnauthorizedErrorHandler } from './client/retry_unauthorized';
|
||||
|
||||
export interface SetupDeps {
|
||||
http: InternalHttpServiceSetup;
|
||||
|
@ -42,10 +43,11 @@ export class ElasticsearchService
|
|||
private readonly config$: Observable<ElasticsearchConfig>;
|
||||
private stop$ = new Subject();
|
||||
private kibanaVersion: string;
|
||||
private getAuthHeaders?: GetAuthHeaders;
|
||||
private authHeaders?: IAuthHeadersStorage;
|
||||
private executionContextClient?: IExecutionContext;
|
||||
private esNodesCompatibility$?: Observable<NodesVersionCompatibility>;
|
||||
private client?: ClusterClient;
|
||||
private unauthorizedErrorHandler?: UnauthorizedErrorHandler;
|
||||
|
||||
constructor(private readonly coreContext: CoreContext) {
|
||||
this.kibanaVersion = coreContext.env.packageInfo.version;
|
||||
|
@ -76,7 +78,7 @@ export class ElasticsearchService
|
|||
|
||||
const config = await this.config$.pipe(first()).toPromise();
|
||||
|
||||
this.getAuthHeaders = deps.http.getAuthHeaders;
|
||||
this.authHeaders = deps.http.authRequestHeaders;
|
||||
this.executionContextClient = deps.executionContext;
|
||||
this.client = this.createClusterClient('data', config);
|
||||
|
||||
|
@ -96,6 +98,12 @@ export class ElasticsearchService
|
|||
},
|
||||
esNodesCompatibility$,
|
||||
status$: calculateStatus$(esNodesCompatibility$),
|
||||
setUnauthorizedErrorHandler: (handler) => {
|
||||
if (this.unauthorizedErrorHandler) {
|
||||
throw new Error('setUnauthorizedErrorHandler can only be called once.');
|
||||
}
|
||||
this.unauthorizedErrorHandler = handler;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -153,12 +161,13 @@ export class ElasticsearchService
|
|||
clientConfig?: Partial<ElasticsearchClientConfig>
|
||||
) {
|
||||
const config = clientConfig ? merge({}, baseConfig, clientConfig) : baseConfig;
|
||||
return new ClusterClient(
|
||||
return new ClusterClient({
|
||||
config,
|
||||
this.coreContext.logger.get('elasticsearch'),
|
||||
logger: this.coreContext.logger.get('elasticsearch'),
|
||||
type,
|
||||
this.getAuthHeaders,
|
||||
() => this.executionContextClient?.getAsHeader()
|
||||
);
|
||||
authHeaders: this.authHeaders,
|
||||
getExecutionContext: () => this.executionContextClient?.getAsHeader(),
|
||||
getUnauthorizedErrorHandler: () => this.unauthorizedErrorHandler,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,15 @@ export type {
|
|||
GetResponse,
|
||||
DeleteDocumentResponse,
|
||||
ElasticsearchErrorDetails,
|
||||
// unauthorized error handler
|
||||
UnauthorizedErrorHandlerOptions,
|
||||
UnauthorizedErrorHandlerResultRetryParams,
|
||||
UnauthorizedErrorHandlerRetryResult,
|
||||
UnauthorizedErrorHandlerNotHandledResult,
|
||||
UnauthorizedErrorHandlerResult,
|
||||
UnauthorizedErrorHandlerToolkit,
|
||||
UnauthorizedErrorHandler,
|
||||
UnauthorizedError,
|
||||
} from './client';
|
||||
export { getRequestDebugMeta, getErrorMessage } from './client';
|
||||
export { pollEsNodesVersion } from './version_check/ensure_es_version';
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ElasticsearchConfig } from './elasticsearch_config';
|
|||
import { IClusterClient, ICustomClusterClient, ElasticsearchClientConfig } from './client';
|
||||
import { NodesVersionCompatibility } from './version_check/ensure_es_version';
|
||||
import { ServiceStatus } from '../status';
|
||||
import type { UnauthorizedErrorHandler } from './client/retry_unauthorized';
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -55,6 +56,29 @@ export interface ElasticsearchServicePreboot {
|
|||
* @public
|
||||
*/
|
||||
export interface ElasticsearchServiceSetup {
|
||||
/**
|
||||
* Register a handler that will be called when unauthorized (401) errors are returned from any API
|
||||
* call to elasticsearch performed on behalf of a user via a {@link IScopedClusterClient | scoped cluster client}.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const handler: UnauthorizedErrorHandler = ({ request, error }, toolkit) => {
|
||||
* const reauthenticationResult = await authenticator.reauthenticate(request, error);
|
||||
* if (reauthenticationResult.succeeded()) {
|
||||
* return toolkit.retry({
|
||||
* authHeaders: reauthenticationResult.authHeaders,
|
||||
* });
|
||||
* }
|
||||
* return toolkit.notHandled();
|
||||
* }
|
||||
*
|
||||
* coreSetup.elasticsearch.setUnauthorizedErrorHandler(handler);
|
||||
* ```
|
||||
*
|
||||
* @remarks The handler will only be invoked for scoped client bound to real {@link KibanaRequest | request} instances.
|
||||
*/
|
||||
setUnauthorizedErrorHandler: (handler: UnauthorizedErrorHandler) => void;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Use {@link ElasticsearchServiceStart.legacy} instead.
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Request } from '@hapi/hapi';
|
||||
import { KibanaRequest, ensureRawRequest } from './router';
|
||||
import { AuthHeaders } from './lifecycle/auth';
|
||||
|
@ -18,11 +19,22 @@ import { AuthHeaders } from './lifecycle/auth';
|
|||
export type GetAuthHeaders = (request: KibanaRequest) => AuthHeaders | undefined;
|
||||
|
||||
/** @internal */
|
||||
export class AuthHeadersStorage {
|
||||
export type SetAuthHeaders = (request: KibanaRequest, headers: AuthHeaders) => void;
|
||||
|
||||
/** @internal */
|
||||
export interface IAuthHeadersStorage {
|
||||
set: SetAuthHeaders;
|
||||
get: GetAuthHeaders;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class AuthHeadersStorage implements IAuthHeadersStorage {
|
||||
private authHeadersCache = new WeakMap<Request, AuthHeaders>();
|
||||
|
||||
public set = (request: KibanaRequest | Request, headers: AuthHeaders) => {
|
||||
this.authHeadersCache.set(ensureRawRequest(request), headers);
|
||||
};
|
||||
|
||||
public get: GetAuthHeaders = (request) => {
|
||||
return this.authHeadersCache.get(ensureRawRequest(request));
|
||||
};
|
||||
|
|
|
@ -42,7 +42,7 @@ import {
|
|||
createCookieSessionStorageFactory,
|
||||
} from './cookie_session_storage';
|
||||
import { AuthStateStorage } from './auth_state_storage';
|
||||
import { AuthHeadersStorage, GetAuthHeaders } from './auth_headers_storage';
|
||||
import { AuthHeadersStorage, IAuthHeadersStorage } from './auth_headers_storage';
|
||||
import { BasePath } from './base_path_service';
|
||||
import { getEcsResponseLog } from './logging';
|
||||
import { HttpServiceSetup, HttpServerInfo, HttpAuth } from './types';
|
||||
|
@ -71,7 +71,7 @@ export interface HttpServerSetup {
|
|||
registerAuth: HttpServiceSetup['registerAuth'];
|
||||
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
|
||||
registerOnPreResponse: HttpServiceSetup['registerOnPreResponse'];
|
||||
getAuthHeaders: GetAuthHeaders;
|
||||
authRequestHeaders: IAuthHeadersStorage;
|
||||
auth: HttpAuth;
|
||||
getServerInfo: () => HttpServerInfo;
|
||||
}
|
||||
|
@ -171,7 +171,7 @@ export class HttpServer {
|
|||
get: this.authState.get,
|
||||
isAuthenticated: this.authState.isAuthenticated,
|
||||
},
|
||||
getAuthHeaders: this.authRequestHeaders.get,
|
||||
authRequestHeaders: this.authRequestHeaders,
|
||||
getServerInfo: () => ({
|
||||
name: config.name,
|
||||
hostname: config.host,
|
||||
|
|
|
@ -29,6 +29,7 @@ import { OnPreAuthToolkit } from './lifecycle/on_pre_auth';
|
|||
import { OnPreResponseToolkit } from './lifecycle/on_pre_response';
|
||||
import { configMock } from '../config/mocks';
|
||||
import { ExternalUrlConfig } from '../external_url';
|
||||
import type { IAuthHeadersStorage } from './auth_headers_storage';
|
||||
|
||||
type BasePathMocked = jest.Mocked<InternalHttpServiceSetup['basePath']>;
|
||||
type AuthMocked = jest.Mocked<InternalHttpServiceSetup['auth']>;
|
||||
|
@ -44,10 +45,11 @@ export type HttpServiceSetupMock = jest.Mocked<
|
|||
createRouter: jest.MockedFunction<() => RouterMock>;
|
||||
};
|
||||
export type InternalHttpServiceSetupMock = jest.Mocked<
|
||||
Omit<InternalHttpServiceSetup, 'basePath' | 'createRouter'>
|
||||
Omit<InternalHttpServiceSetup, 'basePath' | 'createRouter' | 'authRequestHeaders'>
|
||||
> & {
|
||||
basePath: BasePathMocked;
|
||||
createRouter: jest.MockedFunction<(path: string) => RouterMock>;
|
||||
authRequestHeaders: jest.Mocked<IAuthHeadersStorage>;
|
||||
};
|
||||
export type HttpServiceStartMock = jest.Mocked<HttpServiceStart> & {
|
||||
basePath: BasePathMocked;
|
||||
|
@ -78,6 +80,14 @@ const createAuthMock = () => {
|
|||
return mock;
|
||||
};
|
||||
|
||||
const createAuthHeaderStorageMock = () => {
|
||||
const mock: jest.Mocked<IAuthHeadersStorage> = {
|
||||
set: jest.fn(),
|
||||
get: jest.fn(),
|
||||
};
|
||||
return mock;
|
||||
};
|
||||
|
||||
const createInternalPrebootContractMock = () => {
|
||||
const mock: InternalHttpServicePrebootMock = {
|
||||
registerRoutes: jest.fn(),
|
||||
|
@ -138,14 +148,14 @@ const createInternalSetupContractMock = () => {
|
|||
csp: CspConfig.DEFAULT,
|
||||
externalUrl: ExternalUrlConfig.DEFAULT,
|
||||
auth: createAuthMock(),
|
||||
getAuthHeaders: jest.fn(),
|
||||
authRequestHeaders: createAuthHeaderStorageMock(),
|
||||
getServerInfo: jest.fn(),
|
||||
registerPrebootRoutes: jest.fn(),
|
||||
registerRouterAfterListening: jest.fn(),
|
||||
};
|
||||
mock.createCookieSessionStorageFactory.mockResolvedValue(sessionStorageMock.createFactory());
|
||||
mock.createRouter.mockImplementation(() => mockRouter.create());
|
||||
mock.getAuthHeaders.mockReturnValue({ authorization: 'authorization-header' });
|
||||
mock.authRequestHeaders.get.mockReturnValue({ authorization: 'authorization-header' });
|
||||
mock.getServerInfo.mockReturnValue({
|
||||
hostname: 'localhost',
|
||||
name: 'kibana',
|
||||
|
@ -258,5 +268,6 @@ export const httpServiceMock = {
|
|||
createOnPreResponseToolkit: createOnPreResponseToolkitMock,
|
||||
createOnPreRoutingToolkit: createOnPreRoutingToolkitMock,
|
||||
createAuthToolkit: createAuthToolkitMock,
|
||||
createAuthHeaderStorage: createAuthHeaderStorageMock,
|
||||
createRouter: mockRouter.create,
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
export { config, HttpConfig } from './http_config';
|
||||
export type { HttpConfigType } from './http_config';
|
||||
export { HttpService } from './http_service';
|
||||
export type { GetAuthHeaders } from './auth_headers_storage';
|
||||
export type { GetAuthHeaders, SetAuthHeaders, IAuthHeadersStorage } from './auth_headers_storage';
|
||||
export type { AuthStatus, GetAuthState, IsAuthenticated } from './auth_state_storage';
|
||||
export {
|
||||
isKibanaRequest,
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { IContextProvider, IContextContainer } from '../context';
|
||||
import { ICspConfig } from '../csp';
|
||||
import { GetAuthState, IsAuthenticated } from './auth_state_storage';
|
||||
import { GetAuthHeaders } from './auth_headers_storage';
|
||||
import { IAuthHeadersStorage } from './auth_headers_storage';
|
||||
import { IRouter } from './router';
|
||||
import { HttpServerSetup } from './http_server';
|
||||
import { SessionStorageCookieOptions } from './cookie_session_storage';
|
||||
|
@ -398,7 +398,7 @@ export interface InternalHttpServiceSetup
|
|||
) => IRouter<Context>;
|
||||
registerRouterAfterListening: (router: IRouter) => void;
|
||||
registerStaticDir: (path: string, dirPath: string) => void;
|
||||
getAuthHeaders: GetAuthHeaders;
|
||||
authRequestHeaders: IAuthHeadersStorage;
|
||||
registerRouteHandlerContext: <
|
||||
Context extends RequestHandlerContext,
|
||||
ContextName extends keyof Context
|
||||
|
@ -407,6 +407,7 @@ export interface InternalHttpServiceSetup
|
|||
contextName: ContextName,
|
||||
provider: RequestHandlerContextProvider<Context, ContextName>
|
||||
) => RequestHandlerContextContainer;
|
||||
|
||||
registerPrebootRoutes(path: string, callback: (router: IRouter) => void): void;
|
||||
}
|
||||
|
||||
|
|
|
@ -138,6 +138,14 @@ export type {
|
|||
ElasticsearchConfigPreboot,
|
||||
ElasticsearchErrorDetails,
|
||||
PollEsNodesVersionOptions,
|
||||
UnauthorizedErrorHandlerOptions,
|
||||
UnauthorizedErrorHandlerResultRetryParams,
|
||||
UnauthorizedErrorHandlerRetryResult,
|
||||
UnauthorizedErrorHandlerNotHandledResult,
|
||||
UnauthorizedErrorHandlerResult,
|
||||
UnauthorizedErrorHandlerToolkit,
|
||||
UnauthorizedErrorHandler,
|
||||
UnauthorizedError,
|
||||
} from './elasticsearch';
|
||||
|
||||
export type { IExternalUrlConfig, IExternalUrlPolicy } from './external_url';
|
||||
|
|
|
@ -156,6 +156,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
|
|||
},
|
||||
elasticsearch: {
|
||||
legacy: deps.elasticsearch.legacy,
|
||||
setUnauthorizedErrorHandler: deps.elasticsearch.setUnauthorizedErrorHandler,
|
||||
},
|
||||
executionContext: {
|
||||
withContext: deps.executionContext.withContext,
|
||||
|
|
|
@ -27,6 +27,7 @@ import { EcsEventKind } from '@kbn/logging';
|
|||
import { EcsEventOutcome } from '@kbn/logging';
|
||||
import { EcsEventType } from '@kbn/logging';
|
||||
import { EnvironmentMode } from '@kbn/config';
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
import type { KibanaClient } from '@elastic/elasticsearch/lib/api/kibana';
|
||||
|
@ -35,7 +36,7 @@ import { LoggerFactory } from '@kbn/logging';
|
|||
import { LogLevel as LogLevel_2 } from '@kbn/logging';
|
||||
import { LogMeta } from '@kbn/logging';
|
||||
import { LogRecord } from '@kbn/logging';
|
||||
import type { MaybePromise } from '@kbn/utility-types';
|
||||
import { MaybePromise } from '@kbn/utility-types';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
import { Observable } from 'rxjs';
|
||||
import { PackageInfo } from '@kbn/config';
|
||||
|
@ -939,6 +940,7 @@ export interface ElasticsearchServiceSetup {
|
|||
legacy: {
|
||||
readonly config$: Observable<ElasticsearchConfig>;
|
||||
};
|
||||
setUnauthorizedErrorHandler: (handler: UnauthorizedErrorHandler) => void;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -3088,6 +3090,49 @@ export interface UiSettingsServiceStart {
|
|||
// @public
|
||||
export type UiSettingsType = 'undefined' | 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string' | 'array' | 'image' | 'color';
|
||||
|
||||
// @public (undocumented)
|
||||
export type UnauthorizedError = errors.ResponseError & {
|
||||
statusCode: 401;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type UnauthorizedErrorHandler = (options: UnauthorizedErrorHandlerOptions, toolkit: UnauthorizedErrorHandlerToolkit) => MaybePromise<UnauthorizedErrorHandlerResult>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface UnauthorizedErrorHandlerNotHandledResult {
|
||||
// (undocumented)
|
||||
type: 'notHandled';
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface UnauthorizedErrorHandlerOptions {
|
||||
// (undocumented)
|
||||
error: UnauthorizedError;
|
||||
// (undocumented)
|
||||
request: KibanaRequest;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type UnauthorizedErrorHandlerResult = UnauthorizedErrorHandlerRetryResult | UnauthorizedErrorHandlerNotHandledResult;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface UnauthorizedErrorHandlerResultRetryParams {
|
||||
// (undocumented)
|
||||
authHeaders: AuthHeaders;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface UnauthorizedErrorHandlerRetryResult extends UnauthorizedErrorHandlerResultRetryParams {
|
||||
// (undocumented)
|
||||
type: 'retry';
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface UnauthorizedErrorHandlerToolkit {
|
||||
notHandled: () => UnauthorizedErrorHandlerNotHandledResult;
|
||||
retry: (params: UnauthorizedErrorHandlerResultRetryParams) => UnauthorizedErrorHandlerRetryResult;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface UserProvidedValues<T = any> {
|
||||
// (undocumented)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue