[Console] Using Console to remote reindex with an incorrect password logs the user out of Kibana (#143440)

Fixes https://github.com/elastic/kibana/issues/140536

### Summary

Console mirrors ES response status code to the client in order to show the status of the request in the UI. However, if the status code is 403, for instance, in the case of reindexing with invalid credentials, the user is logged out of Kibana due to the [interceptor](https://github.com/elastic/kibana/blob/main/x
pack/plugins/security/public/session/unauthorized_response_http_interceptor.ts#L42) that is set up in the security plugin. This PR fixes that by setting the status code and status text as custom headers so that the client can access them in the response. This way, we can avoid logging out users if the status code is 403.

To test this, follow the steps described in
https://github.com/elastic/kibana/issues/140536
<img width="1502" alt="Screen Shot 2022-10-17 at 18 30 07"
src="https://user-images.githubusercontent.com/53621505/196190023-462cbadc-b155-4183-b9fd-47ce779a306e.png">

Co-authored-by: Muhammad Ibragimov <muhammad.ibragimov@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Muhammad Ibragimov 2023-01-27 14:41:26 +05:00 committed by GitHub
parent 482d1ced82
commit 0d613e58cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 90 additions and 56 deletions

View file

@ -8,6 +8,7 @@
import type { HttpSetup, IHttpFetchError } from '@kbn/core-http-browser';
import { XJson } from '@kbn/es-ui-shared-plugin/public';
import { KIBANA_API_PREFIX } from '../../../../common/constants';
import { extractWarningMessages } from '../../../lib/utils';
import { send } from '../../../lib/es/es';
import { BaseResponseType } from '../../../types';
@ -35,6 +36,25 @@ export interface RequestResult<V = unknown> {
const getContentType = (response: Response | undefined) =>
(response?.headers.get('Content-Type') as BaseResponseType) ?? '';
const extractStatusCodeAndText = (response: Response | undefined, path: string) => {
const isKibanaApiRequest = path.startsWith(KIBANA_API_PREFIX);
// Kibana API requests don't go through the proxy, so we can use the response status code and text.
if (isKibanaApiRequest) {
return {
statusCode: response?.status ?? 500,
statusText: response?.statusText ?? 'error',
};
}
// For ES requests, we need to extract the status code and text from the response
// headers, due to the way the proxy set up to avoid mirroring the status code which could be 401
// and trigger a login prompt. See for more details: https://github.com/elastic/kibana/issues/140536
const statusCode = parseInt(response?.headers.get('x-console-proxy-status-code') ?? '500', 10);
const statusText = response?.headers.get('x-console-proxy-status-text') ?? 'error';
return { statusCode, statusText };
};
let CURRENT_REQ_ID = 0;
export function sendRequest(args: RequestArgs): Promise<RequestResult[]> {
const requests = args.requests.slice();
@ -79,59 +99,55 @@ export function sendRequest(args: RequestArgs): Promise<RequestResult[]> {
asResponse: true,
});
const { statusCode, statusText } = extractStatusCodeAndText(response, path);
if (reqId !== CURRENT_REQ_ID) {
// Skip if previous request is not resolved yet. This can happen when issuing multiple requests at the same time and with slow networks
return;
}
if (response) {
const isSuccess =
// Things like DELETE index where the index is not there are OK.
(response.status >= 200 && response.status < 300) || response.status === 404;
if (isSuccess) {
let value;
// check if object is ArrayBuffer
if (body instanceof ArrayBuffer) {
value = body;
} else {
value = typeof body === 'string' ? body : JSON.stringify(body, null, 2);
}
const warnings = response.headers.get('warning');
if (warnings) {
const warningMessages = extractWarningMessages(warnings);
value = warningMessages.join('\n') + '\n' + value;
}
if (isMultiRequest) {
value = `# ${req.method} ${req.url} ${response.status} ${response.statusText}\n${value}`;
}
results.push({
response: {
timeMs: Date.now() - startTime,
statusCode: response.status,
statusText: response.statusText,
contentType: getContentType(response),
value,
},
request: {
data,
method,
path,
},
});
// single request terminate via sendNextRequest as well
await sendNextRequest();
let value;
// check if object is ArrayBuffer
if (body instanceof ArrayBuffer) {
value = body;
} else {
value = typeof body === 'string' ? body : JSON.stringify(body, null, 2);
}
const warnings = response.headers.get('warning');
if (warnings) {
const warningMessages = extractWarningMessages(warnings);
value = warningMessages.join('\n') + '\n' + value;
}
if (isMultiRequest) {
value = `# ${req.method} ${req.url} ${statusCode} ${statusText}\n${value}`;
}
results.push({
response: {
timeMs: Date.now() - startTime,
statusCode,
statusText,
contentType: getContentType(response),
value,
},
request: {
data,
method,
path,
},
});
// single request terminate via sendNextRequest as well
await sendNextRequest();
}
} catch (error) {
let value;
const { response, body } = error as IHttpFetchError;
const statusCode = response?.status ?? 500;
const statusText = response?.statusText ?? 'error';
const { statusCode, statusText } = extractStatusCodeAndText(response, path);
if (body) {
value = JSON.stringify(body, null, 2);

View file

@ -167,6 +167,10 @@ export const createHandler =
return response.customError({
statusCode: 502,
body: e,
headers: {
'x-console-proxy-status-code': '502',
'x-console-proxy-status-text': 'Bad Gateway',
},
});
}
// Otherwise, try the next host...
@ -179,22 +183,18 @@ export const createHandler =
headers: { warning },
} = esIncomingMessage!;
if (method.toUpperCase() !== 'HEAD') {
return response.custom({
statusCode: statusCode!,
body: esIncomingMessage!,
headers: {
warning: warning || '',
},
});
}
return response.custom({
statusCode: statusCode!,
body: `${statusCode} - ${statusMessage}`,
const isHeadRequest = method.toUpperCase() === 'HEAD';
return response.ok({
body: isHeadRequest ? `${statusCode} - ${statusMessage}` : esIncomingMessage!,
headers: {
warning: warning || '',
'Content-Type': 'text/plain',
// We need to set the status code and status text as headers so that the client can access them
// in the response. This is needed because the client is using them to show the status of the request
// in the UI. By sending them as headers we avoid logging out users if the status code is 403. E.g.
// if the user is not authorized to access the cluster, we don't want to log them out. (See https://github.com/elastic/kibana/issues/140536)
'x-console-proxy-status-code': String(statusCode) || '',
'x-console-proxy-status-text': statusMessage || '',
...(isHeadRequest && { 'Content-Type': 'text/plain' }),
},
});
};

View file

@ -96,5 +96,23 @@ describe('Console Proxy Route', () => {
expect(headers).toHaveProperty('x-elastic-product-origin');
expect(headers['x-elastic-product-origin']).toBe('kibana');
});
it('sends es status code and status text as headers', async () => {
const response = await handler(
{} as any,
{
headers: {},
query: {
method: 'POST',
path: '/api/console/proxy?path=_aliases&method=GET',
},
} as any,
kibanaResponseFactory
);
const { headers } = response.options;
expect(headers).toHaveProperty('x-console-proxy-status-code');
expect(headers).toHaveProperty('x-console-proxy-status-text');
});
});
});