Throw error during startup if scripting is disable in ES (#113068)

This commit is contained in:
Pierre Gayvallet 2021-09-27 11:35:25 +02:00 committed by GitHub
parent 48be0ca3b5
commit d2bd7f8487
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 97 additions and 210 deletions

View file

@ -1,18 +0,0 @@
/*
* 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 { RegisterDeprecationsConfig } from '../../deprecations';
import { getScriptingDisabledDeprecations } from './scripting_disabled_deprecation';
export const getElasticsearchDeprecationsProvider = (): RegisterDeprecationsConfig => {
return {
getDeprecations: async (context) => {
return [...(await getScriptingDisabledDeprecations({ esClient: context.esClient }))];
},
};
};

View file

@ -1,9 +0,0 @@
/*
* 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.
*/
export { getElasticsearchDeprecationsProvider } from './deprecation_provider';

View file

@ -1,12 +0,0 @@
/*
* 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.
*/
export const isInlineScriptingDisabledMock = jest.fn();
jest.doMock('./is_scripting_disabled', () => ({
isInlineScriptingDisabled: isInlineScriptingDisabledMock,
}));

View file

@ -1,63 +0,0 @@
/*
* 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 { isInlineScriptingDisabledMock } from './scripting_disabled_deprecation.test.mocks';
import { elasticsearchServiceMock } from '../../elasticsearch/elasticsearch_service.mock';
import { getScriptingDisabledDeprecations } from './scripting_disabled_deprecation';
describe('getScriptingDisabledDeprecations', () => {
let esClient: ReturnType<typeof elasticsearchServiceMock.createScopedClusterClient>;
beforeEach(() => {
esClient = elasticsearchServiceMock.createScopedClusterClient();
});
afterEach(() => {
isInlineScriptingDisabledMock.mockReset();
});
it('calls `isInlineScriptingDisabled` with the correct arguments', async () => {
await getScriptingDisabledDeprecations({
esClient,
});
expect(isInlineScriptingDisabledMock).toHaveBeenCalledTimes(1);
expect(isInlineScriptingDisabledMock).toHaveBeenCalledWith({
client: esClient.asInternalUser,
});
});
it('returns no deprecations if scripting is not disabled', async () => {
isInlineScriptingDisabledMock.mockResolvedValue(false);
const deprecations = await getScriptingDisabledDeprecations({
esClient,
});
expect(deprecations).toHaveLength(0);
});
it('returns a deprecation if scripting is disabled', async () => {
isInlineScriptingDisabledMock.mockResolvedValue(true);
const deprecations = await getScriptingDisabledDeprecations({
esClient,
});
expect(deprecations).toHaveLength(1);
expect(deprecations[0]).toEqual({
title: expect.any(String),
message: expect.any(String),
level: 'critical',
requireRestart: false,
correctiveActions: {
manualSteps: expect.any(Array),
},
});
});
});

View file

@ -1,44 +0,0 @@
/*
* 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 { i18n } from '@kbn/i18n';
import type { DeprecationsDetails } from '../../deprecations';
import { IScopedClusterClient } from '../../elasticsearch';
import { isInlineScriptingDisabled } from './is_scripting_disabled';
interface GetScriptingDisabledDeprecations {
esClient: IScopedClusterClient;
}
export const getScriptingDisabledDeprecations = async ({
esClient,
}: GetScriptingDisabledDeprecations): Promise<DeprecationsDetails[]> => {
const deprecations: DeprecationsDetails[] = [];
if (await isInlineScriptingDisabled({ client: esClient.asInternalUser })) {
deprecations.push({
title: i18n.translate('core.elasticsearch.deprecations.scriptingDisabled.title', {
defaultMessage: 'Inline scripting is disabled on elasticsearch',
}),
message: i18n.translate('core.elasticsearch.deprecations.scriptingDisabled.message', {
defaultMessage:
'Starting in 8.0, Kibana will require inline scripting to be enabled,' +
'and will fail to start otherwise.',
}),
level: 'critical',
requireRestart: false,
correctiveActions: {
manualSteps: [
i18n.translate('core.elasticsearch.deprecations.scriptingDisabled.manualSteps.1', {
defaultMessage: 'Set `script.allowed_types=inline` in your elasticsearch config ',
}),
],
},
});
}
return deprecations;
};

View file

@ -8,3 +8,8 @@
export const MockClusterClient = jest.fn();
jest.mock('./client/cluster_client', () => ({ ClusterClient: MockClusterClient }));
export const isScriptingEnabledMock = jest.fn();
jest.doMock('./is_scripting_enabled', () => ({
isInlineScriptingEnabled: isScriptingEnabledMock,
}));

View file

@ -16,8 +16,9 @@ jest.mock('./version_check/ensure_es_version', () => ({
pollEsNodesVersion: jest.fn(),
}));
import { MockClusterClient, isScriptingEnabledMock } from './elasticsearch_service.test.mocks';
import type { NodesVersionCompatibility } from './version_check/ensure_es_version';
import { MockClusterClient } from './elasticsearch_service.test.mocks';
import { BehaviorSubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { REPO_ROOT } from '@kbn/dev-utils';
@ -30,7 +31,6 @@ import { executionContextServiceMock } from '../execution_context/execution_cont
import { configSchema, ElasticsearchConfig } from './elasticsearch_config';
import { ElasticsearchService, SetupDeps } from './elasticsearch_service';
import { elasticsearchClientMock } from './client/mocks';
import { deprecationsServiceMock } from '../deprecations/deprecations_service.mock';
import { duration } from 'moment';
import { isValidConnection as isValidConnectionMock } from './is_valid_connection';
import { pollEsNodesVersion as pollEsNodesVersionMocked } from './version_check/ensure_es_version';
@ -50,15 +50,11 @@ let coreContext: CoreContext;
let mockClusterClientInstance: ReturnType<typeof elasticsearchClientMock.createCustomClusterClient>;
let mockConfig$: BehaviorSubject<any>;
let setupDeps: SetupDeps;
let deprecationsSetup: ReturnType<typeof deprecationsServiceMock.createInternalSetupContract>;
beforeEach(() => {
deprecationsSetup = deprecationsServiceMock.createInternalSetupContract();
setupDeps = {
http: httpServiceMock.createInternalSetupContract(),
executionContext: executionContextServiceMock.createInternalSetupContract(),
deprecations: deprecationsSetup,
};
env = Env.createDefault(REPO_ROOT, getEnvOptions());
@ -78,15 +74,20 @@ beforeEach(() => {
coreContext = { coreId: Symbol(), env, logger, configService: configService as any };
elasticsearchService = new ElasticsearchService(coreContext);
MockClusterClient.mockClear();
mockClusterClientInstance = elasticsearchClientMock.createCustomClusterClient();
MockClusterClient.mockImplementation(() => mockClusterClientInstance);
isScriptingEnabledMock.mockResolvedValue(true);
// @ts-expect-error TS does not get that `pollEsNodesVersion` is mocked
pollEsNodesVersionMocked.mockImplementation(pollEsNodesVersionActual);
});
afterEach(() => jest.clearAllMocks());
afterEach(() => {
jest.clearAllMocks();
MockClusterClient.mockClear();
isScriptingEnabledMock.mockReset();
});
describe('#preboot', () => {
describe('#config', () => {
@ -181,22 +182,6 @@ describe('#setup', () => {
);
});
it('registers its deprecation provider', async () => {
const registry = deprecationsServiceMock.createSetupContract();
deprecationsSetup.getRegistry.mockReturnValue(registry);
await elasticsearchService.setup(setupDeps);
expect(deprecationsSetup.getRegistry).toHaveBeenCalledTimes(1);
expect(deprecationsSetup.getRegistry).toHaveBeenCalledWith('elasticsearch');
expect(registry.registerDeprecations).toHaveBeenCalledTimes(1);
expect(registry.registerDeprecations).toHaveBeenCalledWith({
getDeprecations: expect.any(Function),
});
});
it('esNodeVersionCompatibility$ only starts polling when subscribed to', async (done) => {
const mockedClient = mockClusterClientInstance.asInternalUser;
mockedClient.nodes.info.mockImplementation(() =>
@ -302,6 +287,39 @@ describe('#start', () => {
});
});
describe('isInlineScriptingEnabled', () => {
it('does not throw error when scripting is enabled', async () => {
isScriptingEnabledMock.mockResolvedValue(true);
await elasticsearchService.setup(setupDeps);
expect(isScriptingEnabledMock).not.toHaveBeenCalled();
await expect(elasticsearchService.start()).resolves.toBeDefined();
expect(isScriptingEnabledMock).toHaveBeenCalledTimes(1);
});
it('throws an error if scripting is disabled', async () => {
isScriptingEnabledMock.mockResolvedValue(false);
await elasticsearchService.setup(setupDeps);
await expect(elasticsearchService.start()).rejects.toThrowError(
'Inline scripting is disabled'
);
});
it('does not throw error when `skipStartupConnectionCheck` is true', async () => {
isScriptingEnabledMock.mockResolvedValue(false);
mockConfig$.next({
...(await mockConfig$.pipe(first()).toPromise()),
skipStartupConnectionCheck: true,
});
await elasticsearchService.setup(setupDeps);
await expect(elasticsearchService.start()).resolves.toBeDefined();
});
});
describe('#createClient', () => {
it('allows to specify config properties', async () => {
await elasticsearchService.setup(setupDeps);

View file

@ -18,7 +18,6 @@ import { ClusterClient, ElasticsearchClientConfig } from './client';
import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config';
import type { InternalHttpServiceSetup, GetAuthHeaders } from '../http';
import type { InternalExecutionContextSetup, IExecutionContext } from '../execution_context';
import type { InternalDeprecationsServiceSetup } from '../deprecations';
import {
InternalElasticsearchServicePreboot,
InternalElasticsearchServiceSetup,
@ -28,11 +27,10 @@ import type { NodesVersionCompatibility } from './version_check/ensure_es_versio
import { pollEsNodesVersion } from './version_check/ensure_es_version';
import { calculateStatus$ } from './status';
import { isValidConnection } from './is_valid_connection';
import { getElasticsearchDeprecationsProvider } from './deprecations';
import { isInlineScriptingEnabled } from './is_scripting_enabled';
export interface SetupDeps {
http: InternalHttpServiceSetup;
deprecations: InternalDeprecationsServiceSetup;
executionContext: InternalExecutionContextSetup;
}
@ -82,10 +80,6 @@ export class ElasticsearchService
this.executionContextClient = deps.executionContext;
this.client = this.createClusterClient('data', config);
deps.deprecations
.getRegistry('elasticsearch')
.registerDeprecations(getElasticsearchDeprecationsProvider());
const esNodesCompatibility$ = pollEsNodesVersion({
internalClient: this.client.asInternalUser,
log: this.log,
@ -122,6 +116,18 @@ export class ElasticsearchService
if (!config.skipStartupConnectionCheck) {
// Ensure that the connection is established and the product is valid before moving on
await isValidConnection(this.esNodesCompatibility$);
// Ensure inline scripting is enabled on the ES cluster
const scriptingEnabled = await isInlineScriptingEnabled({
client: this.client.asInternalUser,
});
if (!scriptingEnabled) {
throw new Error(
'Inline scripting is disabled on the Elasticsearch cluster, and is mandatory for Kibana to function. ' +
'Please enabled inline scripting, then restart Kibana. ' +
'Refer to https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-scripting-security.html for more info.'
);
}
}
return {

View file

@ -11,9 +11,9 @@ import {
TestElasticsearchUtils,
TestKibanaUtils,
} from '../../../test_helpers/kbn_server';
import { isInlineScriptingDisabled } from '../deprecations/is_scripting_disabled';
import { isInlineScriptingEnabled } from '../is_scripting_enabled';
describe('isInlineScriptingDisabled', () => {
describe('isInlineScriptingEnabled', () => {
let esServer: TestElasticsearchUtils;
let kibanaServer: TestKibanaUtils;
@ -33,6 +33,13 @@ describe('isInlineScriptingDisabled', () => {
es: {
esArgs,
},
kbn: {
elasticsearch: {
// required for the server to start without throwing
// as inline scripting is disabled in some tests
skipStartupConnectionCheck: true,
},
},
},
});
@ -40,43 +47,43 @@ describe('isInlineScriptingDisabled', () => {
kibanaServer = await startKibana();
};
it('returns false when `script.allowed_types` is unset', async () => {
it('returns true when `script.allowed_types` is unset', async () => {
await startServers({ esArgs: [] });
const disabled = await isInlineScriptingDisabled({
const enabled = await isInlineScriptingEnabled({
client: kibanaServer.coreStart.elasticsearch.client.asInternalUser,
});
expect(disabled).toEqual(false);
expect(enabled).toEqual(true);
});
it('returns false when `script.allowed_types` is `inline`', async () => {
it('returns true when `script.allowed_types` is `inline`', async () => {
await startServers({ esArgs: ['script.allowed_types=inline'] });
const disabled = await isInlineScriptingDisabled({
const enabled = await isInlineScriptingEnabled({
client: kibanaServer.coreStart.elasticsearch.client.asInternalUser,
});
expect(disabled).toEqual(false);
expect(enabled).toEqual(true);
});
it('returns true when `script.allowed_types` is `stored`', async () => {
it('returns false when `script.allowed_types` is `stored`', async () => {
await startServers({ esArgs: ['script.allowed_types=stored'] });
const disabled = await isInlineScriptingDisabled({
const enabled = await isInlineScriptingEnabled({
client: kibanaServer.coreStart.elasticsearch.client.asInternalUser,
});
expect(disabled).toEqual(true);
expect(enabled).toEqual(false);
});
it('returns true when `script.allowed_types` is `none', async () => {
it('returns false when `script.allowed_types` is `none', async () => {
await startServers({ esArgs: ['script.allowed_types=none'] });
const disabled = await isInlineScriptingDisabled({
const enabled = await isInlineScriptingEnabled({
client: kibanaServer.coreStart.elasticsearch.client.asInternalUser,
});
expect(disabled).toEqual(true);
expect(enabled).toEqual(false);
});
});

View file

@ -7,10 +7,10 @@
*/
import { estypes } from '@elastic/elasticsearch';
import { elasticsearchServiceMock } from '../../elasticsearch/elasticsearch_service.mock';
import { isInlineScriptingDisabled } from './is_scripting_disabled';
import { elasticsearchServiceMock } from './elasticsearch_service.mock';
import { isInlineScriptingEnabled } from './is_scripting_enabled';
describe('isInlineScriptingDisabled', () => {
describe('isInlineScriptingEnabled', () => {
let client: ReturnType<typeof elasticsearchServiceMock.createElasticsearchClient>;
beforeEach(() => {
@ -23,17 +23,17 @@ describe('isInlineScriptingDisabled', () => {
);
};
it('returns `false` if all settings are empty', async () => {
it('returns `true` if all settings are empty', async () => {
mockSettingsValue({
transient: {},
persistent: {},
defaults: {},
});
expect(await isInlineScriptingDisabled({ client })).toEqual(false);
expect(await isInlineScriptingEnabled({ client })).toEqual(true);
});
it('returns `false` if `defaults.script.allowed_types` is `inline`', async () => {
it('returns `true` if `defaults.script.allowed_types` is `inline`', async () => {
mockSettingsValue({
transient: {},
persistent: {},
@ -42,10 +42,10 @@ describe('isInlineScriptingDisabled', () => {
},
});
expect(await isInlineScriptingDisabled({ client })).toEqual(false);
expect(await isInlineScriptingEnabled({ client })).toEqual(true);
});
it('returns `true` if `defaults.script.allowed_types` is `none`', async () => {
it('returns `false` if `defaults.script.allowed_types` is `none`', async () => {
mockSettingsValue({
transient: {},
persistent: {},
@ -54,10 +54,10 @@ describe('isInlineScriptingDisabled', () => {
},
});
expect(await isInlineScriptingDisabled({ client })).toEqual(true);
expect(await isInlineScriptingEnabled({ client })).toEqual(false);
});
it('returns `true` if `defaults.script.allowed_types` is `stored`', async () => {
it('returns `false` if `defaults.script.allowed_types` is `stored`', async () => {
mockSettingsValue({
transient: {},
persistent: {},
@ -66,7 +66,7 @@ describe('isInlineScriptingDisabled', () => {
},
});
expect(await isInlineScriptingDisabled({ client })).toEqual(true);
expect(await isInlineScriptingEnabled({ client })).toEqual(false);
});
it('respect the persistent->defaults priority', async () => {
@ -80,7 +80,7 @@ describe('isInlineScriptingDisabled', () => {
},
});
expect(await isInlineScriptingDisabled({ client })).toEqual(false);
expect(await isInlineScriptingEnabled({ client })).toEqual(true);
});
it('respect the transient->persistent priority', async () => {
@ -94,6 +94,6 @@ describe('isInlineScriptingDisabled', () => {
defaults: {},
});
expect(await isInlineScriptingDisabled({ client })).toEqual(true);
expect(await isInlineScriptingEnabled({ client })).toEqual(false);
});
});

View file

@ -6,11 +6,11 @@
* Side Public License, v 1.
*/
import { ElasticsearchClient } from '../../elasticsearch';
import { ElasticsearchClient } from './client';
const scriptAllowedTypesKey = 'script.allowed_types';
export const isInlineScriptingDisabled = async ({
export const isInlineScriptingEnabled = async ({
client,
}: {
client: ElasticsearchClient;
@ -28,7 +28,5 @@ export const isInlineScriptingDisabled = async ({
[];
// when unspecified, the setting as a default `[]` value that means that both scriptings are allowed.
const scriptAllowed = scriptAllowedTypes.length === 0 || scriptAllowedTypes.includes('inline');
return !scriptAllowed;
return scriptAllowedTypes.length === 0 || scriptAllowedTypes.includes('inline');
};

View file

@ -218,7 +218,6 @@ export class Server {
const elasticsearchServiceSetup = await this.elasticsearch.setup({
http: httpSetup,
executionContext: executionContextSetup,
deprecations: deprecationsSetup,
});
const metricsSetup = await this.metrics.setup({ http: httpSetup });