diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 4db1c2dd3b5eb..d21d6ad81a0c6 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -255,6 +255,8 @@
# Security
/src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform
+/src/plugins/security_oss/ @elastic/kibana-security
+/test/security_functional/ @elastic/kibana-security
/x-pack/plugins/spaces/ @elastic/kibana-security
/x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security
/x-pack/plugins/security/ @elastic/kibana-security
diff --git a/.i18nrc.json b/.i18nrc.json
index 153a5a6cafece..7ecaa89f66604 100644
--- a/.i18nrc.json
+++ b/.i18nrc.json
@@ -37,6 +37,7 @@
"regionMap": "src/plugins/region_map",
"savedObjects": "src/plugins/saved_objects",
"savedObjectsManagement": "src/plugins/saved_objects_management",
+ "security": "src/plugins/security_oss",
"server": "src/legacy/server",
"statusPage": "src/legacy/core_plugins/status_page",
"telemetry": [
diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc
index 67b7aa8e6a011..052ab95e1a18b 100644
--- a/docs/developer/plugin-list.asciidoc
+++ b/docs/developer/plugin-list.asciidoc
@@ -155,6 +155,11 @@ It also provides a stateful version of it on the start contract.
|WARNING: Missing README.
+|{kib-repo}blob/{branch}/src/plugins/security_oss/README.md[securityOss]
+|securityOss is responsible for educating users about Elastic's free security features,
+so they can properly protect the data within their clusters.
+
+
|{kib-repo}blob/{branch}/src/plugins/share/README.md[share]
|Replaces the legacy ui/share module for registering share context menus.
diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js
index 4facbe1ffbb07..2b338b1c054aa 100644
--- a/scripts/functional_tests.js
+++ b/scripts/functional_tests.js
@@ -23,6 +23,7 @@ const alwaysImportedTests = [
require.resolve('../test/plugin_functional/config.ts'),
require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'),
require.resolve('../test/new_visualize_flow/config.js'),
+ require.resolve('../test/security_functional/config.ts'),
];
// eslint-disable-next-line no-restricted-syntax
const onlyNotInCoverageTests = [
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
index 959e1f8dc3e72..0039debe383bd 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
@@ -93,6 +93,7 @@ kibana_vars=(
path.data
pid.file
regionmap
+ security.showInsecureClusterWarning
server.basePath
server.customResponseHeaders
server.compression.enabled
diff --git a/src/plugins/security_oss/README.md b/src/plugins/security_oss/README.md
new file mode 100644
index 0000000000000..6143149fec384
--- /dev/null
+++ b/src/plugins/security_oss/README.md
@@ -0,0 +1,4 @@
+# `securityOss` plugin
+
+`securityOss` is responsible for educating users about Elastic's free security features,
+so they can properly protect the data within their clusters.
diff --git a/src/plugins/security_oss/kibana.json b/src/plugins/security_oss/kibana.json
new file mode 100644
index 0000000000000..70e37d586f1db
--- /dev/null
+++ b/src/plugins/security_oss/kibana.json
@@ -0,0 +1,10 @@
+{
+ "id": "securityOss",
+ "version": "8.0.0",
+ "kibanaVersion": "kibana",
+ "configPath": ["security"],
+ "ui": true,
+ "server": true,
+ "requiredPlugins": [],
+ "requiredBundles": []
+}
diff --git a/src/plugins/security_oss/public/config.ts b/src/plugins/security_oss/public/config.ts
new file mode 100644
index 0000000000000..17f6b5a53eb6c
--- /dev/null
+++ b/src/plugins/security_oss/public/config.ts
@@ -0,0 +1,22 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface ConfigType {
+ showInsecureClusterWarning: boolean;
+}
diff --git a/src/plugins/security_oss/public/index.ts b/src/plugins/security_oss/public/index.ts
new file mode 100644
index 0000000000000..2e63a9316c99b
--- /dev/null
+++ b/src/plugins/security_oss/public/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { PluginInitializerContext } from 'kibana/public';
+
+import { SecurityOssPlugin } from './plugin';
+
+export { SecurityOssPluginSetup, SecurityOssPluginStart } from './plugin';
+export const plugin = (initializerContext: PluginInitializerContext) =>
+ new SecurityOssPlugin(initializerContext);
diff --git a/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.test.tsx b/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.test.tsx
new file mode 100644
index 0000000000000..b414ab78cdfdb
--- /dev/null
+++ b/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.test.tsx
@@ -0,0 +1,37 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { defaultAlertText } from './default_alert';
+
+describe('defaultAlertText', () => {
+ it('creates a valid MountPoint that can cleanup correctly', () => {
+ const mountPoint = defaultAlertText(jest.fn());
+
+ const el = document.createElement('div');
+ const unmount = mountPoint(el);
+
+ expect(el.querySelectorAll('[data-test-subj="insecureClusterDefaultAlertText"]')).toHaveLength(
+ 1
+ );
+
+ unmount();
+
+ expect(el).toMatchInlineSnapshot(`
`);
+ });
+});
diff --git a/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx b/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx
new file mode 100644
index 0000000000000..f2eeedb5b7372
--- /dev/null
+++ b/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx
@@ -0,0 +1,97 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+ EuiButton,
+ EuiCheckbox,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiText,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { I18nProvider, FormattedMessage } from '@kbn/i18n/react';
+import { MountPoint } from 'kibana/public';
+import React, { useState } from 'react';
+import { render, unmountComponentAtNode } from 'react-dom';
+
+export const defaultAlertTitle = i18n.translate('security.checkup.insecureClusterTitle', {
+ defaultMessage: 'Please secure your installation',
+});
+
+export const defaultAlertText: (onDismiss: (persist: boolean) => void) => MountPoint = (
+ onDismiss
+) => (e) => {
+ const AlertText = () => {
+ const [persist, setPersist] = useState(false);
+
+ return (
+
+
+
+
+
+
+ setPersist(changeEvent.target.checked)}
+ label={i18n.translate('security.checkup.dontShowAgain', {
+ defaultMessage: `Don't show again`,
+ })}
+ />
+
+
+
+
+ {i18n.translate('security.checkup.learnMoreButtonText', {
+ defaultMessage: `Learn more`,
+ })}
+
+
+
+ onDismiss(persist)}
+ data-test-subj="defaultDismissAlertButton"
+ >
+ {i18n.translate('security.checkup.dismissButtonText', {
+ defaultMessage: `Dismiss`,
+ })}
+
+
+
+
+
+ );
+ };
+
+ render(, e);
+
+ return () => unmountComponentAtNode(e);
+};
diff --git a/src/plugins/security_oss/public/insecure_cluster_service/components/index.ts b/src/plugins/security_oss/public/insecure_cluster_service/components/index.ts
new file mode 100644
index 0000000000000..9334dad2b8193
--- /dev/null
+++ b/src/plugins/security_oss/public/insecure_cluster_service/components/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { defaultAlertTitle, defaultAlertText } from './default_alert';
diff --git a/src/plugins/security_oss/public/insecure_cluster_service/index.ts b/src/plugins/security_oss/public/insecure_cluster_service/index.ts
new file mode 100644
index 0000000000000..7817dc383c168
--- /dev/null
+++ b/src/plugins/security_oss/public/insecure_cluster_service/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export {
+ InsecureClusterService,
+ InsecureClusterServiceSetup,
+ InsecureClusterServiceStart,
+} from './insecure_cluster_service';
diff --git a/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.mock.tsx b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.mock.tsx
new file mode 100644
index 0000000000000..630becb49dd4c
--- /dev/null
+++ b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.mock.tsx
@@ -0,0 +1,37 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+ InsecureClusterServiceSetup,
+ InsecureClusterServiceStart,
+} from './insecure_cluster_service';
+
+export const mockInsecureClusterService = {
+ createSetup: () => {
+ return {
+ setAlertTitle: jest.fn(),
+ setAlertText: jest.fn(),
+ } as InsecureClusterServiceSetup;
+ },
+ createStart: () => {
+ return {
+ hideAlert: jest.fn(),
+ } as InsecureClusterServiceStart;
+ },
+};
diff --git a/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.test.tsx b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.test.tsx
new file mode 100644
index 0000000000000..a81f361689743
--- /dev/null
+++ b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.test.tsx
@@ -0,0 +1,336 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { InsecureClusterService } from './insecure_cluster_service';
+import { ConfigType } from '../config';
+import { coreMock } from '../../../../core/public/mocks';
+import { nextTick } from 'test_utils/enzyme_helpers';
+
+let mockOnDismissCallback: (persist: boolean) => void = jest.fn().mockImplementation(() => {
+ throw new Error('expected callback to be replaced!');
+});
+
+jest.mock('./components', () => {
+ return {
+ defaultAlertTitle: 'mocked default alert title',
+ defaultAlertText: (onDismiss: any) => {
+ mockOnDismissCallback = onDismiss;
+ return 'mocked default alert text';
+ },
+ };
+});
+
+interface InitOpts {
+ displayAlert?: boolean;
+ isAnonymousPath?: boolean;
+ tenant?: string;
+}
+
+function initCore({
+ displayAlert = true,
+ isAnonymousPath = false,
+ tenant = '/server-base-path',
+}: InitOpts = {}) {
+ const coreSetup = coreMock.createSetup();
+ (coreSetup.http.basePath.serverBasePath as string) = tenant;
+
+ const coreStart = coreMock.createStart();
+ coreStart.http.get.mockImplementation(async (url: unknown) => {
+ if (url === '/internal/security_oss/display_insecure_cluster_alert') {
+ return { displayAlert };
+ }
+ throw new Error(`unexpected call to http.get: ${url}`);
+ });
+ coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(isAnonymousPath);
+
+ coreStart.notifications.toasts.addWarning.mockReturnValue({ id: 'mock_alert_id' });
+ return { coreSetup, coreStart };
+}
+
+describe('InsecureClusterService', () => {
+ describe('display scenarios', () => {
+ it('does not display an alert when the warning is explicitly disabled via config', async () => {
+ const config: ConfigType = { showInsecureClusterWarning: false };
+ const { coreSetup, coreStart } = initCore({ displayAlert: true });
+ const storage = coreMock.createStorage();
+
+ const service = new InsecureClusterService(config, storage);
+ service.setup({ core: coreSetup });
+ service.start({ core: coreStart });
+
+ await nextTick();
+
+ expect(coreStart.http.get).not.toHaveBeenCalled();
+ expect(coreStart.notifications.toasts.addWarning).not.toHaveBeenCalled();
+
+ expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled();
+ expect(storage.setItem).not.toHaveBeenCalled();
+ });
+
+ it('does not display an alert when the endpoint check returns false', async () => {
+ const config: ConfigType = { showInsecureClusterWarning: true };
+ const { coreSetup, coreStart } = initCore({ displayAlert: false });
+ const storage = coreMock.createStorage();
+
+ const service = new InsecureClusterService(config, storage);
+ service.setup({ core: coreSetup });
+ service.start({ core: coreStart });
+
+ await nextTick();
+
+ expect(coreStart.http.get).toHaveBeenCalledTimes(1);
+ expect(coreStart.notifications.toasts.addWarning).not.toHaveBeenCalled();
+
+ expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled();
+ expect(storage.setItem).not.toHaveBeenCalled();
+ });
+
+ it('does not display an alert when on an anonymous path', async () => {
+ const config: ConfigType = { showInsecureClusterWarning: true };
+ const { coreSetup, coreStart } = initCore({ displayAlert: true, isAnonymousPath: true });
+ const storage = coreMock.createStorage();
+
+ const service = new InsecureClusterService(config, storage);
+ service.setup({ core: coreSetup });
+ service.start({ core: coreStart });
+
+ await nextTick();
+
+ expect(coreStart.http.get).not.toHaveBeenCalled();
+ expect(coreStart.notifications.toasts.addWarning).not.toHaveBeenCalled();
+
+ expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled();
+ expect(storage.setItem).not.toHaveBeenCalled();
+ });
+
+ it('only reads storage information from the current tenant', async () => {
+ const config: ConfigType = { showInsecureClusterWarning: true };
+ const { coreSetup, coreStart } = initCore({
+ displayAlert: true,
+ tenant: '/my-specific-tenant',
+ });
+
+ const storage = coreMock.createStorage();
+ storage.getItem.mockReturnValue(JSON.stringify({ show: false }));
+
+ const service = new InsecureClusterService(config, storage);
+ service.setup({ core: coreSetup });
+ service.start({ core: coreStart });
+
+ await nextTick();
+
+ expect(storage.getItem).toHaveBeenCalledTimes(1);
+ expect(storage.getItem).toHaveBeenCalledWith(
+ 'insecureClusterWarningVisibility/my-specific-tenant'
+ );
+ });
+
+ it('does not display an alert when hidden via storage', async () => {
+ const config: ConfigType = { showInsecureClusterWarning: true };
+ const { coreSetup, coreStart } = initCore({ displayAlert: true });
+
+ const storage = coreMock.createStorage();
+ storage.getItem.mockReturnValue(JSON.stringify({ show: false }));
+
+ const service = new InsecureClusterService(config, storage);
+ service.setup({ core: coreSetup });
+ service.start({ core: coreStart });
+
+ await nextTick();
+
+ expect(coreStart.http.get).not.toHaveBeenCalled();
+ expect(coreStart.notifications.toasts.addWarning).not.toHaveBeenCalled();
+
+ expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled();
+ expect(storage.setItem).not.toHaveBeenCalled();
+ });
+
+ it('displays an alert when persisted preference is corrupted', async () => {
+ const config: ConfigType = { showInsecureClusterWarning: true };
+ const { coreSetup, coreStart } = initCore({ displayAlert: true });
+
+ const storage = coreMock.createStorage();
+ storage.getItem.mockReturnValue('{ this is a string of invalid JSON');
+
+ const service = new InsecureClusterService(config, storage);
+ service.setup({ core: coreSetup });
+ service.start({ core: coreStart });
+
+ await nextTick();
+
+ expect(coreStart.http.get).toHaveBeenCalledTimes(1);
+ expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1);
+
+ expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled();
+ expect(storage.setItem).not.toHaveBeenCalled();
+ });
+
+ it('displays an alert when enabled via config and endpoint checks', async () => {
+ const config: ConfigType = { showInsecureClusterWarning: true };
+ const { coreSetup, coreStart } = initCore({ displayAlert: true });
+ const storage = coreMock.createStorage();
+
+ const service = new InsecureClusterService(config, storage);
+ service.setup({ core: coreSetup });
+ service.start({ core: coreStart });
+
+ await nextTick();
+
+ expect(coreStart.http.get).toHaveBeenCalledTimes(1);
+ expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1);
+ expect(coreStart.notifications.toasts.addWarning.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "iconType": "alert",
+ "text": "mocked default alert text",
+ "title": "mocked default alert title",
+ },
+ Object {
+ "toastLifeTimeMs": 864000000,
+ },
+ ]
+ `);
+
+ expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled();
+ expect(storage.setItem).not.toHaveBeenCalled();
+ });
+
+ it('dismisses the alert when requested, and remembers this preference', async () => {
+ const config: ConfigType = { showInsecureClusterWarning: true };
+ const { coreSetup, coreStart } = initCore({ displayAlert: true });
+ const storage = coreMock.createStorage();
+
+ const service = new InsecureClusterService(config, storage);
+ service.setup({ core: coreSetup });
+ service.start({ core: coreStart });
+
+ await nextTick();
+
+ expect(coreStart.http.get).toHaveBeenCalledTimes(1);
+ expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1);
+
+ mockOnDismissCallback(true);
+
+ expect(coreStart.notifications.toasts.remove).toHaveBeenCalledTimes(1);
+ expect(storage.setItem).toHaveBeenCalledWith(
+ 'insecureClusterWarningVisibility/server-base-path',
+ JSON.stringify({ show: false })
+ );
+ });
+ });
+
+ describe('#setup', () => {
+ it('allows the alert title and text to be replaced exactly once', async () => {
+ const config: ConfigType = { showInsecureClusterWarning: true };
+ const storage = coreMock.createStorage();
+
+ const { coreSetup } = initCore();
+
+ const service = new InsecureClusterService(config, storage);
+ const { setAlertTitle, setAlertText } = service.setup({ core: coreSetup });
+ setAlertTitle('some new title');
+ setAlertText('some new alert text');
+
+ expect(() => setAlertTitle('')).toThrowErrorMatchingInlineSnapshot(
+ `"alert title has already been set"`
+ );
+ expect(() => setAlertText('')).toThrowErrorMatchingInlineSnapshot(
+ `"alert text has already been set"`
+ );
+ });
+
+ it('allows the alert title and text to be replaced', async () => {
+ const config: ConfigType = { showInsecureClusterWarning: true };
+ const { coreSetup, coreStart } = initCore({ displayAlert: true });
+ const storage = coreMock.createStorage();
+
+ const service = new InsecureClusterService(config, storage);
+ const { setAlertTitle, setAlertText } = service.setup({ core: coreSetup });
+ setAlertTitle('some new title');
+ setAlertText('some new alert text');
+
+ service.start({ core: coreStart });
+
+ await nextTick();
+
+ expect(coreStart.http.get).toHaveBeenCalledTimes(1);
+ expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1);
+ expect(coreStart.notifications.toasts.addWarning.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "iconType": "alert",
+ "text": "some new alert text",
+ "title": "some new title",
+ },
+ Object {
+ "toastLifeTimeMs": 864000000,
+ },
+ ]
+ `);
+
+ expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled();
+ expect(storage.setItem).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('#start', () => {
+ it('allows the alert to be hidden via start contract, and remembers this preference', async () => {
+ const config: ConfigType = { showInsecureClusterWarning: true };
+ const { coreSetup, coreStart } = initCore({ displayAlert: true });
+ const storage = coreMock.createStorage();
+
+ const service = new InsecureClusterService(config, storage);
+ service.setup({ core: coreSetup });
+ const { hideAlert } = service.start({ core: coreStart });
+
+ await nextTick();
+
+ expect(coreStart.http.get).toHaveBeenCalledTimes(1);
+ expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1);
+
+ hideAlert(true);
+
+ expect(coreStart.notifications.toasts.remove).toHaveBeenCalledTimes(1);
+ expect(storage.setItem).toHaveBeenCalledWith(
+ 'insecureClusterWarningVisibility/server-base-path',
+ JSON.stringify({ show: false })
+ );
+ });
+
+ it('allows the alert to be hidden via start contract, and does not remember the preference', async () => {
+ const config: ConfigType = { showInsecureClusterWarning: true };
+ const { coreSetup, coreStart } = initCore({ displayAlert: true });
+ const storage = coreMock.createStorage();
+
+ const service = new InsecureClusterService(config, storage);
+ service.setup({ core: coreSetup });
+ const { hideAlert } = service.start({ core: coreStart });
+
+ await nextTick();
+
+ expect(coreStart.http.get).toHaveBeenCalledTimes(1);
+ expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1);
+
+ hideAlert(false);
+
+ expect(coreStart.notifications.toasts.remove).toHaveBeenCalledTimes(1);
+ expect(storage.setItem).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.tsx b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.tsx
new file mode 100644
index 0000000000000..e6255233354b7
--- /dev/null
+++ b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.tsx
@@ -0,0 +1,164 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { CoreSetup, CoreStart, MountPoint, Toast } from 'kibana/public';
+
+import { BehaviorSubject, combineLatest, from } from 'rxjs';
+import { distinctUntilChanged, map } from 'rxjs/operators';
+import { ConfigType } from '../config';
+import { defaultAlertText, defaultAlertTitle } from './components';
+
+interface SetupDeps {
+ core: Pick;
+}
+
+interface StartDeps {
+ core: Pick;
+}
+
+export interface InsecureClusterServiceSetup {
+ setAlertTitle: (alertTitle: string | MountPoint) => void;
+ setAlertText: (alertText: string | MountPoint) => void;
+}
+
+export interface InsecureClusterServiceStart {
+ hideAlert: (persist: boolean) => void;
+}
+
+export class InsecureClusterService {
+ private enabled: boolean;
+
+ private alertVisibility$: BehaviorSubject;
+
+ private storage: Storage;
+
+ private alertToast?: Toast;
+
+ private alertTitle?: string | MountPoint;
+
+ private alertText?: string | MountPoint;
+
+ private storageKey?: string;
+
+ constructor(config: Pick, storage: Storage) {
+ this.storage = storage;
+ this.enabled = config.showInsecureClusterWarning;
+ this.alertVisibility$ = new BehaviorSubject(this.enabled);
+ }
+
+ public setup({ core }: SetupDeps): InsecureClusterServiceSetup {
+ const tenant = core.http.basePath.serverBasePath;
+ this.storageKey = `insecureClusterWarningVisibility${tenant}`;
+ this.enabled = this.enabled && this.getPersistedVisibilityPreference();
+ this.alertVisibility$.next(this.enabled);
+
+ return {
+ setAlertTitle: (alertTitle: string | MountPoint) => {
+ if (this.alertTitle) {
+ throw new Error('alert title has already been set');
+ }
+ this.alertTitle = alertTitle;
+ },
+ setAlertText: (alertText: string | MountPoint) => {
+ if (this.alertText) {
+ throw new Error('alert text has already been set');
+ }
+ this.alertText = alertText;
+ },
+ };
+ }
+
+ public start({ core }: StartDeps): InsecureClusterServiceStart {
+ const shouldInitialize =
+ this.enabled && !core.http.anonymousPaths.isAnonymous(window.location.pathname);
+
+ if (shouldInitialize) {
+ this.initializeAlert(core);
+ }
+
+ return {
+ hideAlert: (persist: boolean) => this.setAlertVisibility(false, persist),
+ };
+ }
+
+ private initializeAlert(core: StartDeps['core']) {
+ const displayAlert$ = from(
+ core.http
+ .get<{ displayAlert: boolean }>('/internal/security_oss/display_insecure_cluster_alert')
+ .catch((e) => {
+ // in the event we can't make this call, assume we shouldn't display this alert.
+ return { displayAlert: false };
+ })
+ );
+
+ // 10 days is reasonably long enough to call "forever" for a page load.
+ // Can't go too much longer than this. See https://github.com/elastic/kibana/issues/64264#issuecomment-618400354
+ const oneMinute = 60000;
+ const tenDays = oneMinute * 60 * 24 * 10;
+
+ combineLatest([displayAlert$, this.alertVisibility$])
+ .pipe(
+ map(([{ displayAlert }, isAlertVisible]) => displayAlert && isAlertVisible),
+ distinctUntilChanged()
+ )
+ .subscribe((showAlert) => {
+ if (showAlert && !this.alertToast) {
+ this.alertToast = core.notifications.toasts.addWarning(
+ {
+ title: this.alertTitle ?? defaultAlertTitle,
+ text:
+ this.alertText ??
+ defaultAlertText((persist: boolean) => this.setAlertVisibility(false, persist)),
+ iconType: 'alert',
+ },
+ {
+ toastLifeTimeMs: tenDays,
+ }
+ );
+ } else if (!showAlert && this.alertToast) {
+ core.notifications.toasts.remove(this.alertToast);
+ this.alertToast = undefined;
+ }
+ });
+ }
+
+ private setAlertVisibility(show: boolean, persist: boolean) {
+ if (!this.enabled) {
+ return;
+ }
+ this.alertVisibility$.next(show);
+ if (persist) {
+ this.setPersistedVisibilityPreference(show);
+ }
+ }
+
+ private getPersistedVisibilityPreference() {
+ const entry = this.storage.getItem(this.storageKey!) ?? '{}';
+ try {
+ const { show = true } = JSON.parse(entry);
+ return show;
+ } catch (e) {
+ return true;
+ }
+ }
+
+ private setPersistedVisibilityPreference(show: boolean) {
+ this.storage.setItem(this.storageKey!, JSON.stringify({ show }));
+ }
+}
diff --git a/src/plugins/security_oss/public/mocks.ts b/src/plugins/security_oss/public/mocks.ts
new file mode 100644
index 0000000000000..f4913d2de671b
--- /dev/null
+++ b/src/plugins/security_oss/public/mocks.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { mockSecurityOssPlugin } from './plugin.mock';
diff --git a/src/plugins/security_oss/public/plugin.mock.ts b/src/plugins/security_oss/public/plugin.mock.ts
new file mode 100644
index 0000000000000..c513d241dccbb
--- /dev/null
+++ b/src/plugins/security_oss/public/plugin.mock.ts
@@ -0,0 +1,34 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { mockInsecureClusterService } from './insecure_cluster_service/insecure_cluster_service.mock';
+import { SecurityOssPluginSetup, SecurityOssPluginStart } from './plugin';
+
+export const mockSecurityOssPlugin = {
+ createSetup: () => {
+ return {
+ insecureCluster: mockInsecureClusterService.createSetup(),
+ } as DeeplyMockedKeys;
+ },
+ createStart: () => {
+ return {
+ insecureCluster: mockInsecureClusterService.createStart(),
+ } as DeeplyMockedKeys;
+ },
+};
diff --git a/src/plugins/security_oss/public/plugin.ts b/src/plugins/security_oss/public/plugin.ts
new file mode 100644
index 0000000000000..2f3eed0bde5eb
--- /dev/null
+++ b/src/plugins/security_oss/public/plugin.ts
@@ -0,0 +1,58 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
+import { ConfigType } from './config';
+import {
+ InsecureClusterService,
+ InsecureClusterServiceSetup,
+ InsecureClusterServiceStart,
+} from './insecure_cluster_service';
+
+export interface SecurityOssPluginSetup {
+ insecureCluster: InsecureClusterServiceSetup;
+}
+
+export interface SecurityOssPluginStart {
+ insecureCluster: InsecureClusterServiceStart;
+}
+
+export class SecurityOssPlugin
+ implements Plugin {
+ private readonly config: ConfigType;
+
+ private insecureClusterService: InsecureClusterService;
+
+ constructor(private readonly initializerContext: PluginInitializerContext) {
+ this.config = this.initializerContext.config.get();
+ this.insecureClusterService = new InsecureClusterService(this.config, localStorage);
+ }
+
+ public setup(core: CoreSetup) {
+ return {
+ insecureCluster: this.insecureClusterService.setup({ core }),
+ };
+ }
+
+ public start(core: CoreStart) {
+ return {
+ insecureCluster: this.insecureClusterService.start({ core }),
+ };
+ }
+}
diff --git a/src/plugins/security_oss/server/check_cluster_data.test.ts b/src/plugins/security_oss/server/check_cluster_data.test.ts
new file mode 100644
index 0000000000000..a8245931daf04
--- /dev/null
+++ b/src/plugins/security_oss/server/check_cluster_data.test.ts
@@ -0,0 +1,148 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { elasticsearchServiceMock, loggingSystemMock } from '../../../core/server/mocks';
+import { createClusterDataCheck } from './check_cluster_data';
+
+describe('checkClusterForUserData', () => {
+ it('returns false if no data is found', async () => {
+ const esClient = elasticsearchServiceMock.createElasticsearchClient();
+ esClient.cat.indices.mockResolvedValue(
+ elasticsearchServiceMock.createApiResponse({ body: [] })
+ );
+
+ const log = loggingSystemMock.createLogger();
+
+ const response = await createClusterDataCheck()(esClient, log);
+ expect(response).toEqual(false);
+ expect(esClient.cat.indices).toHaveBeenCalledTimes(1);
+ });
+
+ it('returns false if data only exists in system indices', async () => {
+ const esClient = elasticsearchServiceMock.createElasticsearchClient();
+ esClient.cat.indices.mockResolvedValue(
+ elasticsearchServiceMock.createApiResponse({
+ body: [
+ {
+ index: '.kibana',
+ 'docs.count': 500,
+ },
+ {
+ index: 'kibana_sample_ecommerce_data',
+ 'docs.count': 20,
+ },
+ {
+ index: '.somethingElse',
+ 'docs.count': 20,
+ },
+ ],
+ })
+ );
+
+ const log = loggingSystemMock.createLogger();
+
+ const response = await createClusterDataCheck()(esClient, log);
+ expect(response).toEqual(false);
+ expect(esClient.cat.indices).toHaveBeenCalledTimes(1);
+ });
+
+ it('returns true if data exists in non-system indices', async () => {
+ const esClient = elasticsearchServiceMock.createElasticsearchClient();
+ esClient.cat.indices.mockResolvedValue(
+ elasticsearchServiceMock.createApiResponse({
+ body: [
+ {
+ index: '.kibana',
+ 'docs.count': 500,
+ },
+ {
+ index: 'some_real_index',
+ 'docs.count': 20,
+ },
+ ],
+ })
+ );
+
+ const log = loggingSystemMock.createLogger();
+
+ const response = await createClusterDataCheck()(esClient, log);
+ expect(response).toEqual(true);
+ });
+
+ it('checks each time until the first true response is returned, then stops checking', async () => {
+ const esClient = elasticsearchServiceMock.createElasticsearchClient();
+ esClient.cat.indices
+ .mockResolvedValueOnce(
+ elasticsearchServiceMock.createApiResponse({
+ body: [],
+ })
+ )
+ .mockRejectedValueOnce(new Error('something terrible happened'))
+ .mockResolvedValueOnce(
+ elasticsearchServiceMock.createApiResponse({
+ body: [
+ {
+ index: '.kibana',
+ 'docs.count': 500,
+ },
+ ],
+ })
+ )
+ .mockResolvedValueOnce(
+ elasticsearchServiceMock.createApiResponse({
+ body: [
+ {
+ index: 'some_real_index',
+ 'docs.count': 20,
+ },
+ ],
+ })
+ );
+
+ const log = loggingSystemMock.createLogger();
+
+ const doesClusterHaveUserData = createClusterDataCheck();
+
+ let response = await doesClusterHaveUserData(esClient, log);
+ expect(response).toEqual(false);
+
+ response = await doesClusterHaveUserData(esClient, log);
+ expect(response).toEqual(false);
+
+ response = await doesClusterHaveUserData(esClient, log);
+ expect(response).toEqual(false);
+
+ response = await doesClusterHaveUserData(esClient, log);
+ expect(response).toEqual(true);
+
+ expect(esClient.cat.indices).toHaveBeenCalledTimes(4);
+ expect(log.warn.mock.calls).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "Error encountered while checking cluster for user data: Error: something terrible happened",
+ ],
+ ]
+ `);
+
+ response = await doesClusterHaveUserData(esClient, log);
+ expect(response).toEqual(true);
+ // Same number of calls as above. We should not have to interrogate again.
+ expect(esClient.cat.indices).toHaveBeenCalledTimes(4);
+ });
+});
diff --git a/src/plugins/security_oss/server/check_cluster_data.ts b/src/plugins/security_oss/server/check_cluster_data.ts
new file mode 100644
index 0000000000000..a3aeb50ae280a
--- /dev/null
+++ b/src/plugins/security_oss/server/check_cluster_data.ts
@@ -0,0 +1,47 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ElasticsearchClient, Logger } from 'kibana/server';
+
+export const createClusterDataCheck = () => {
+ let clusterHasUserData = false;
+
+ return async function doesClusterHaveUserData(esClient: ElasticsearchClient, log: Logger) {
+ if (!clusterHasUserData) {
+ try {
+ const indices = await esClient.cat.indices<
+ Array<{ index: string; ['docs.count']: string }>
+ >({
+ format: 'json',
+ h: ['index', 'docs.count'],
+ });
+ clusterHasUserData = indices.body.some((indexCount) => {
+ const isInternalIndex =
+ indexCount.index.startsWith('.') || indexCount.index.startsWith('kibana_sample_');
+
+ return !isInternalIndex && parseInt(indexCount['docs.count'], 10) > 0;
+ });
+ } catch (e) {
+ log.warn(`Error encountered while checking cluster for user data: ${e}`);
+ clusterHasUserData = false;
+ }
+ }
+ return clusterHasUserData;
+ };
+};
diff --git a/src/plugins/security_oss/server/config.ts b/src/plugins/security_oss/server/config.ts
new file mode 100644
index 0000000000000..17fb46065aee5
--- /dev/null
+++ b/src/plugins/security_oss/server/config.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { schema, TypeOf } from '@kbn/config-schema';
+
+export type ConfigType = TypeOf;
+
+export const ConfigSchema = schema.object({
+ showInsecureClusterWarning: schema.boolean({ defaultValue: true }),
+});
diff --git a/src/plugins/security_oss/server/index.ts b/src/plugins/security_oss/server/index.ts
new file mode 100644
index 0000000000000..f35ae39daaff3
--- /dev/null
+++ b/src/plugins/security_oss/server/index.ts
@@ -0,0 +1,35 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { TypeOf } from '@kbn/config-schema';
+
+import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server';
+import { ConfigSchema } from './config';
+import { SecurityOssPlugin } from './plugin';
+
+export { SecurityOssPluginSetup } from './plugin';
+
+export const config: PluginConfigDescriptor> = {
+ schema: ConfigSchema,
+ exposeToBrowser: {
+ showInsecureClusterWarning: true,
+ },
+};
+
+export const plugin = (context: PluginInitializerContext) => new SecurityOssPlugin(context);
diff --git a/src/plugins/security_oss/server/plugin.test.ts b/src/plugins/security_oss/server/plugin.test.ts
new file mode 100644
index 0000000000000..417da0c7e73bb
--- /dev/null
+++ b/src/plugins/security_oss/server/plugin.test.ts
@@ -0,0 +1,37 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { coreMock } from '../../../core/server/mocks';
+import { SecurityOssPlugin } from './plugin';
+
+describe('SecurityOss Plugin', () => {
+ describe('#setup', () => {
+ it('exposes the proper contract', async () => {
+ const context = coreMock.createPluginInitializerContext();
+ const plugin = new SecurityOssPlugin(context);
+ const core = coreMock.createSetup();
+ const contract = plugin.setup(core);
+ expect(Object.keys(contract)).toMatchInlineSnapshot(`
+ Array [
+ "showInsecureClusterWarning$",
+ ]
+ `);
+ });
+ });
+});
diff --git a/src/plugins/security_oss/server/plugin.ts b/src/plugins/security_oss/server/plugin.ts
new file mode 100644
index 0000000000000..e48827f21a13a
--- /dev/null
+++ b/src/plugins/security_oss/server/plugin.ts
@@ -0,0 +1,62 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { createClusterDataCheck } from './check_cluster_data';
+import { ConfigType } from './config';
+import { setupDisplayInsecureClusterAlertRoute } from './routes';
+
+export interface SecurityOssPluginSetup {
+ /**
+ * Allows consumers to show/hide the insecure cluster warning.
+ */
+ showInsecureClusterWarning$: BehaviorSubject;
+}
+
+export class SecurityOssPlugin implements Plugin {
+ private readonly config$: Observable;
+ private readonly logger: Logger;
+
+ constructor(initializerContext: PluginInitializerContext) {
+ this.config$ = initializerContext.config.create();
+ this.logger = initializerContext.logger.get();
+ }
+
+ public setup(core: CoreSetup) {
+ const router = core.http.createRouter();
+ const showInsecureClusterWarning$ = new BehaviorSubject(true);
+
+ setupDisplayInsecureClusterAlertRoute({
+ router,
+ log: this.logger,
+ config$: this.config$,
+ displayModifier$: showInsecureClusterWarning$,
+ doesClusterHaveUserData: createClusterDataCheck(),
+ });
+
+ return {
+ showInsecureClusterWarning$,
+ };
+ }
+
+ public start() {}
+
+ public stop() {}
+}
diff --git a/src/plugins/security_oss/server/routes/display_insecure_cluster_alert.ts b/src/plugins/security_oss/server/routes/display_insecure_cluster_alert.ts
new file mode 100644
index 0000000000000..0f0f72f054b4c
--- /dev/null
+++ b/src/plugins/security_oss/server/routes/display_insecure_cluster_alert.ts
@@ -0,0 +1,63 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { IRouter, Logger } from 'kibana/server';
+import { combineLatest, Observable } from 'rxjs';
+import { createClusterDataCheck } from '../check_cluster_data';
+import { ConfigType } from '../config';
+
+interface Deps {
+ router: IRouter;
+ log: Logger;
+ config$: Observable;
+ displayModifier$: Observable;
+ doesClusterHaveUserData: ReturnType;
+}
+
+export const setupDisplayInsecureClusterAlertRoute = ({
+ router,
+ log,
+ config$,
+ displayModifier$,
+ doesClusterHaveUserData,
+}: Deps) => {
+ let showInsecureClusterWarning = false;
+
+ combineLatest([config$, displayModifier$]).subscribe(([config, displayModifier]) => {
+ showInsecureClusterWarning = config.showInsecureClusterWarning && displayModifier;
+ });
+
+ router.get(
+ {
+ path: '/internal/security_oss/display_insecure_cluster_alert',
+ validate: false,
+ },
+ async (context, request, response) => {
+ if (!showInsecureClusterWarning) {
+ return response.ok({ body: { displayAlert: false } });
+ }
+
+ const hasData = await doesClusterHaveUserData(
+ context.core.elasticsearch.client.asInternalUser,
+ log
+ );
+ return response.ok({ body: { displayAlert: hasData } });
+ }
+ );
+};
diff --git a/src/plugins/security_oss/server/routes/index.ts b/src/plugins/security_oss/server/routes/index.ts
new file mode 100644
index 0000000000000..ceff0b12c9cb1
--- /dev/null
+++ b/src/plugins/security_oss/server/routes/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { setupDisplayInsecureClusterAlertRoute } from './display_insecure_cluster_alert';
diff --git a/src/plugins/security_oss/server/routes/integration_tests/display_insecure_cluster_alert.test.ts b/src/plugins/security_oss/server/routes/integration_tests/display_insecure_cluster_alert.test.ts
new file mode 100644
index 0000000000000..d62a5040be6b3
--- /dev/null
+++ b/src/plugins/security_oss/server/routes/integration_tests/display_insecure_cluster_alert.test.ts
@@ -0,0 +1,134 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { loggingSystemMock } from '../../../../../core/server/mocks';
+import { setupServer } from '../../../../../core/server/test_utils';
+import { setupDisplayInsecureClusterAlertRoute } from '../display_insecure_cluster_alert';
+import { ConfigType } from '../../config';
+import { BehaviorSubject, of } from 'rxjs';
+import { UnwrapPromise } from '@kbn/utility-types';
+import { createClusterDataCheck } from '../../check_cluster_data';
+import supertest from 'supertest';
+
+type SetupServerReturn = UnwrapPromise>;
+const pluginId = Symbol('securityOss');
+
+interface SetupOpts {
+ config?: ConfigType;
+ displayModifier$?: BehaviorSubject;
+ doesClusterHaveUserData?: ReturnType;
+}
+
+describe('GET /internal/security_oss/display_insecure_cluster_alert', () => {
+ let server: SetupServerReturn['server'];
+ let httpSetup: SetupServerReturn['httpSetup'];
+
+ const setupTestServer = async ({
+ config = { showInsecureClusterWarning: true },
+ displayModifier$ = new BehaviorSubject(true),
+ doesClusterHaveUserData = jest.fn().mockResolvedValue(true),
+ }: SetupOpts) => {
+ ({ server, httpSetup } = await setupServer(pluginId));
+
+ const router = httpSetup.createRouter('/');
+ const log = loggingSystemMock.createLogger();
+
+ setupDisplayInsecureClusterAlertRoute({
+ router,
+ log,
+ config$: of(config),
+ displayModifier$,
+ doesClusterHaveUserData,
+ });
+
+ await server.start();
+
+ return {
+ log,
+ };
+ };
+
+ afterEach(async () => {
+ await server.stop();
+ });
+
+ it('responds `false` if plugin is not configured to display alerts', async () => {
+ await setupTestServer({
+ config: { showInsecureClusterWarning: false },
+ });
+
+ await supertest(httpSetup.server.listener)
+ .get('/internal/security_oss/display_insecure_cluster_alert')
+ .expect(200, { displayAlert: false });
+ });
+
+ it('responds `false` if cluster does not contain user data', async () => {
+ await setupTestServer({
+ config: { showInsecureClusterWarning: true },
+ doesClusterHaveUserData: jest.fn().mockResolvedValue(false),
+ });
+
+ await supertest(httpSetup.server.listener)
+ .get('/internal/security_oss/display_insecure_cluster_alert')
+ .expect(200, { displayAlert: false });
+ });
+
+ it('responds `false` if displayModifier$ is set to false', async () => {
+ await setupTestServer({
+ config: { showInsecureClusterWarning: true },
+ doesClusterHaveUserData: jest.fn().mockResolvedValue(true),
+ displayModifier$: new BehaviorSubject(false),
+ });
+
+ await supertest(httpSetup.server.listener)
+ .get('/internal/security_oss/display_insecure_cluster_alert')
+ .expect(200, { displayAlert: false });
+ });
+
+ it('responds `true` if cluster contains user data', async () => {
+ await setupTestServer({
+ config: { showInsecureClusterWarning: true },
+ doesClusterHaveUserData: jest.fn().mockResolvedValue(true),
+ });
+
+ await supertest(httpSetup.server.listener)
+ .get('/internal/security_oss/display_insecure_cluster_alert')
+ .expect(200, { displayAlert: true });
+ });
+
+ it('responds to changing displayModifier$ values', async () => {
+ const displayModifier$ = new BehaviorSubject(true);
+
+ await setupTestServer({
+ config: { showInsecureClusterWarning: true },
+ doesClusterHaveUserData: jest.fn().mockResolvedValue(true),
+ displayModifier$,
+ });
+
+ await supertest(httpSetup.server.listener)
+ .get('/internal/security_oss/display_insecure_cluster_alert')
+ .expect(200, { displayAlert: true });
+
+ displayModifier$.next(false);
+
+ await supertest(httpSetup.server.listener)
+ .get('/internal/security_oss/display_insecure_cluster_alert')
+ .expect(200, { displayAlert: false });
+ });
+});
diff --git a/tasks/function_test_groups.js b/tasks/function_test_groups.js
index d60f3ae53eecc..ac41b3f36be0f 100644
--- a/tasks/function_test_groups.js
+++ b/tasks/function_test_groups.js
@@ -43,6 +43,8 @@ const getDefaultArgs = (tag) => {
'--debug',
'--config',
'test/new_visualize_flow/config.js',
+ '--config',
+ 'test/security_functional/config.ts',
];
};
diff --git a/test/common/config.js b/test/common/config.js
index 6a62151b12814..dbbd75d1f9577 100644
--- a/test/common/config.js
+++ b/test/common/config.js
@@ -48,6 +48,7 @@ export default function () {
`--elasticsearch.username=${kibanaServerTestUser.username}`,
`--elasticsearch.password=${kibanaServerTestUser.password}`,
`--home.disableWelcomeScreen=true`,
+ `--security.showInsecureClusterWarning=false`,
'--telemetry.banner=false',
'--telemetry.optIn=false',
// These are *very* important to have them pointing to staging
diff --git a/test/security_functional/config.ts b/test/security_functional/config.ts
new file mode 100644
index 0000000000000..2a35d40678fd2
--- /dev/null
+++ b/test/security_functional/config.ts
@@ -0,0 +1,54 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import path from 'path';
+import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
+
+export default async function ({ readConfigFile }: FtrConfigProviderContext) {
+ const functionalConfig = await readConfigFile(require.resolve('../functional/config'));
+
+ return {
+ testFiles: [require.resolve('./index.ts')],
+ services: functionalConfig.get('services'),
+ pageObjects: functionalConfig.get('pageObjects'),
+ servers: functionalConfig.get('servers'),
+ esTestCluster: functionalConfig.get('esTestCluster'),
+ apps: {},
+ esArchiver: {
+ directory: path.resolve(__dirname, '../functional/fixtures/es_archiver'),
+ },
+ snapshots: {
+ directory: path.resolve(__dirname, 'snapshots'),
+ },
+ junit: {
+ reportName: 'Security OSS Functional Tests',
+ },
+ kbnTestServer: {
+ ...functionalConfig.get('kbnTestServer'),
+ serverArgs: [
+ ...functionalConfig
+ .get('kbnTestServer.serverArgs')
+ .filter((arg: string) => !arg.startsWith('--security.showInsecureClusterWarning')),
+ '--security.showInsecureClusterWarning=true',
+ // Required to load new platform plugins via `--plugin-path` flag.
+ '--env.name=development',
+ ],
+ },
+ };
+}
diff --git a/test/security_functional/index.ts b/test/security_functional/index.ts
new file mode 100644
index 0000000000000..8066a4eacf61a
--- /dev/null
+++ b/test/security_functional/index.ts
@@ -0,0 +1,27 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { FtrProviderContext } from '../functional/ftr_provider_context';
+
+// eslint-disable-next-line import/no-default-export
+export default function ({ loadTestFile }: FtrProviderContext) {
+ describe('Security OSS', function () {
+ this.tags(['skipCloud', 'ciGroup2']);
+ loadTestFile(require.resolve('./insecure_cluster_warning'));
+ });
+}
diff --git a/test/security_functional/insecure_cluster_warning.ts b/test/security_functional/insecure_cluster_warning.ts
new file mode 100644
index 0000000000000..03d9d248d6790
--- /dev/null
+++ b/test/security_functional/insecure_cluster_warning.ts
@@ -0,0 +1,87 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { FtrProviderContext } from 'test/functional/ftr_provider_context';
+
+// eslint-disable-next-line import/no-default-export
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const pageObjects = getPageObjects(['common']);
+ const testSubjects = getService('testSubjects');
+ const browser = getService('browser');
+ const esArchiver = getService('esArchiver');
+
+ describe('Insecure Cluster Warning', () => {
+ before(async () => {
+ await pageObjects.common.navigateToApp('home');
+ await browser.setLocalStorageItem('insecureClusterWarningVisibility', '');
+ // starting without user data
+ await esArchiver.unload('hamlet');
+ });
+
+ after(async () => {
+ await esArchiver.unload('hamlet');
+ });
+
+ describe('without user data', () => {
+ before(async () => {
+ await browser.setLocalStorageItem('insecureClusterWarningVisibility', '');
+ await esArchiver.unload('hamlet');
+ });
+
+ it('should not warn when the cluster contains no user data', async () => {
+ await browser.setLocalStorageItem(
+ 'insecureClusterWarningVisibility',
+ JSON.stringify({ show: false })
+ );
+ await pageObjects.common.navigateToApp('home');
+ await testSubjects.missingOrFail('insecureClusterDefaultAlertText');
+ });
+ });
+
+ describe('with user data', () => {
+ before(async () => {
+ await pageObjects.common.navigateToApp('home');
+ await browser.setLocalStorageItem('insecureClusterWarningVisibility', '');
+ await esArchiver.load('hamlet');
+ });
+
+ after(async () => {
+ await esArchiver.unload('hamlet');
+ });
+
+ it('should warn about an insecure cluster, and hide when dismissed', async () => {
+ await pageObjects.common.navigateToApp('home');
+ await testSubjects.existOrFail('insecureClusterDefaultAlertText');
+
+ await testSubjects.click('defaultDismissAlertButton');
+
+ await testSubjects.missingOrFail('insecureClusterDefaultAlertText');
+ });
+
+ it('should not warn when local storage is configured to hide', async () => {
+ await browser.setLocalStorageItem(
+ 'insecureClusterWarningVisibility',
+ JSON.stringify({ show: false })
+ );
+ await pageObjects.common.navigateToApp('home');
+ await testSubjects.missingOrFail('insecureClusterDefaultAlertText');
+ });
+ });
+ });
+}
diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json
index 40d7e293eaf66..40629dbe4f3b3 100644
--- a/x-pack/plugins/security/kibana.json
+++ b/x-pack/plugins/security/kibana.json
@@ -3,7 +3,7 @@
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "security"],
- "requiredPlugins": ["data", "features", "licensing", "taskManager"],
+ "requiredPlugins": ["data", "features", "licensing", "taskManager", "securityOss"],
"optionalPlugins": ["home", "management", "usageCollection"],
"server": true,
"ui": true,
diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx
index fb8034da11731..d86d4812af5e3 100644
--- a/x-pack/plugins/security/public/plugin.test.tsx
+++ b/x-pack/plugins/security/public/plugin.test.tsx
@@ -8,6 +8,7 @@ import { Observable } from 'rxjs';
import BroadcastChannel from 'broadcast-channel';
import { CoreSetup } from 'src/core/public';
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
+import { mockSecurityOssPlugin } from '../../../../src/plugins/security_oss/public/mocks';
import { SessionTimeout } from './session';
import { PluginStartDependencies, SecurityPlugin } from './plugin';
@@ -35,6 +36,7 @@ describe('Security Plugin', () => {
>,
{
licensing: licensingMock.createSetup(),
+ securityOss: mockSecurityOssPlugin.createSetup(),
}
)
).toEqual({
@@ -61,6 +63,7 @@ describe('Security Plugin', () => {
plugin.setup(coreSetupMock as CoreSetup, {
licensing: licensingMock.createSetup(),
+ securityOss: mockSecurityOssPlugin.createSetup(),
management: managementSetupMock,
});
@@ -85,11 +88,12 @@ describe('Security Plugin', () => {
const plugin = new SecurityPlugin(coreMock.createPluginInitializerContext());
plugin.setup(
coreMock.createSetup({ basePath: '/some-base-path' }) as CoreSetup,
- { licensing: licensingMock.createSetup() }
+ { licensing: licensingMock.createSetup(), securityOss: mockSecurityOssPlugin.createSetup() }
);
expect(
plugin.start(coreMock.createStart({ basePath: '/some-base-path' }), {
+ securityOss: mockSecurityOssPlugin.createStart(),
data: {} as DataPublicPluginStart,
features: {} as FeaturesPluginStart,
})
@@ -110,12 +114,14 @@ describe('Security Plugin', () => {
coreMock.createSetup({ basePath: '/some-base-path' }) as CoreSetup,
{
licensing: licensingMock.createSetup(),
+ securityOss: mockSecurityOssPlugin.createSetup(),
management: managementSetupMock,
}
);
const coreStart = coreMock.createStart({ basePath: '/some-base-path' });
plugin.start(coreStart, {
+ securityOss: mockSecurityOssPlugin.createStart(),
data: {} as DataPublicPluginStart,
features: {} as FeaturesPluginStart,
management: managementStartMock,
@@ -130,7 +136,7 @@ describe('Security Plugin', () => {
const plugin = new SecurityPlugin(coreMock.createPluginInitializerContext());
plugin.setup(
coreMock.createSetup({ basePath: '/some-base-path' }) as CoreSetup,
- { licensing: licensingMock.createSetup() }
+ { licensing: licensingMock.createSetup(), securityOss: mockSecurityOssPlugin.createSetup() }
);
expect(() => plugin.stop()).not.toThrow();
@@ -141,10 +147,11 @@ describe('Security Plugin', () => {
plugin.setup(
coreMock.createSetup({ basePath: '/some-base-path' }) as CoreSetup,
- { licensing: licensingMock.createSetup() }
+ { licensing: licensingMock.createSetup(), securityOss: mockSecurityOssPlugin.createSetup() }
);
plugin.start(coreMock.createStart({ basePath: '/some-base-path' }), {
+ securityOss: mockSecurityOssPlugin.createStart(),
data: {} as DataPublicPluginStart,
features: {} as FeaturesPluginStart,
});
diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx
index f5770ae2bc35c..87bcc96d1f9d4 100644
--- a/x-pack/plugins/security/public/plugin.tsx
+++ b/x-pack/plugins/security/public/plugin.tsx
@@ -5,6 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
+import { SecurityOssPluginSetup, SecurityOssPluginStart } from 'src/plugins/security_oss/public';
import {
CoreSetup,
CoreStart,
@@ -32,9 +33,11 @@ import { AuthenticationService, AuthenticationServiceSetup } from './authenticat
import { ConfigType } from './config';
import { ManagementService } from './management';
import { accountManagementApp } from './account_management';
+import { SecurityCheckupService } from './security_checkup';
export interface PluginSetupDependencies {
licensing: LicensingPluginSetup;
+ securityOss: SecurityOssPluginSetup;
home?: HomePublicPluginSetup;
management?: ManagementSetup;
}
@@ -42,6 +45,7 @@ export interface PluginSetupDependencies {
export interface PluginStartDependencies {
data: DataPublicPluginStart;
features: FeaturesPluginStart;
+ securityOss: SecurityOssPluginStart;
management?: ManagementStart;
}
@@ -58,6 +62,7 @@ export class SecurityPlugin
private readonly navControlService = new SecurityNavControlService();
private readonly securityLicenseService = new SecurityLicenseService();
private readonly managementService = new ManagementService();
+ private readonly securityCheckupService = new SecurityCheckupService();
private authc!: AuthenticationServiceSetup;
private readonly config: ConfigType;
@@ -67,7 +72,7 @@ export class SecurityPlugin
public setup(
core: CoreSetup,
- { home, licensing, management }: PluginSetupDependencies
+ { home, licensing, management, securityOss }: PluginSetupDependencies
) {
const { http, notifications } = core;
const { anonymousPaths } = http;
@@ -82,6 +87,8 @@ export class SecurityPlugin
const { license } = this.securityLicenseService.setup({ license$: licensing.license$ });
+ this.securityCheckupService.setup({ securityOssSetup: securityOss });
+
this.authc = this.authenticationService.setup({
application: core.application,
fatalErrors: core.fatalErrors,
@@ -137,9 +144,10 @@ export class SecurityPlugin
};
}
- public start(core: CoreStart, { management }: PluginStartDependencies) {
+ public start(core: CoreStart, { management, securityOss }: PluginStartDependencies) {
this.sessionTimeout.start();
this.navControlService.start({ core });
+ this.securityCheckupService.start({ securityOssStart: securityOss });
if (management) {
this.managementService.start({ capabilities: core.application.capabilities });
}
@@ -150,6 +158,7 @@ export class SecurityPlugin
this.navControlService.stop();
this.securityLicenseService.stop();
this.managementService.stop();
+ this.securityCheckupService.stop();
}
}
diff --git a/x-pack/plugins/security/public/security_checkup/components/index.ts b/x-pack/plugins/security/public/security_checkup/components/index.ts
new file mode 100644
index 0000000000000..685d0fe67db74
--- /dev/null
+++ b/x-pack/plugins/security/public/security_checkup/components/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { insecureClusterAlertTitle, insecureClusterAlertText } from './insecure_cluster_alert';
diff --git a/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx b/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx
new file mode 100644
index 0000000000000..6ba06e0cc4770
--- /dev/null
+++ b/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx
@@ -0,0 +1,83 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { I18nProvider, FormattedMessage } from '@kbn/i18n/react';
+import { render, unmountComponentAtNode } from 'react-dom';
+import { MountPoint } from 'kibana/public';
+import {
+ EuiCheckbox,
+ EuiText,
+ EuiSpacer,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButton,
+} from '@elastic/eui';
+
+export const insecureClusterAlertTitle = i18n.translate(
+ 'xpack.security.checkup.insecureClusterTitle',
+ { defaultMessage: 'Please secure your installation' }
+);
+
+export const insecureClusterAlertText = (onDismiss: (persist: boolean) => void) =>
+ ((e) => {
+ const AlertText = () => {
+ const [persist, setPersist] = useState(false);
+
+ return (
+
+
+
+
+
+
+ setPersist(changeEvent.target.checked)}
+ label={i18n.translate('xpack.security.checkup.dontShowAgain', {
+ defaultMessage: `Don't show again`,
+ })}
+ />
+
+
+
+
+ {i18n.translate('xpack.security.checkup.enableButtonText', {
+ defaultMessage: `Enable security`,
+ })}
+
+
+
+ onDismiss(persist)}
+ data-test-subj="dismissAlertButton"
+ >
+ {i18n.translate('xpack.security.checkup.dismissButtonText', {
+ defaultMessage: `Dismiss`,
+ })}
+
+
+
+
+
+ );
+ };
+
+ render(, e);
+
+ return () => unmountComponentAtNode(e);
+ }) as MountPoint;
diff --git a/x-pack/plugins/security/public/security_checkup/index.ts b/x-pack/plugins/security/public/security_checkup/index.ts
new file mode 100644
index 0000000000000..691a99a5c6fc0
--- /dev/null
+++ b/x-pack/plugins/security/public/security_checkup/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { SecurityCheckupService } from './security_checkup_service';
diff --git a/x-pack/plugins/security/public/security_checkup/security_checkup_service.test.ts b/x-pack/plugins/security/public/security_checkup/security_checkup_service.test.ts
new file mode 100644
index 0000000000000..3709f52d29ffb
--- /dev/null
+++ b/x-pack/plugins/security/public/security_checkup/security_checkup_service.test.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { mockSecurityOssPlugin } from '../../../../../src/plugins/security_oss/public/mocks';
+import { insecureClusterAlertTitle } from './components';
+import { SecurityCheckupService } from './security_checkup_service';
+
+let mockOnDismiss = jest.fn();
+
+jest.mock('./components', () => {
+ return {
+ insecureClusterAlertTitle: 'mock insecure cluster title',
+ insecureClusterAlertText: (onDismiss: any) => {
+ mockOnDismiss = onDismiss;
+ return 'mock insecure cluster text';
+ },
+ };
+});
+
+describe('SecurityCheckupService', () => {
+ describe('#setup', () => {
+ it('configures the alert title and text for the default distribution', async () => {
+ const securityOssSetup = mockSecurityOssPlugin.createSetup();
+ const service = new SecurityCheckupService();
+ service.setup({ securityOssSetup });
+
+ expect(securityOssSetup.insecureCluster.setAlertTitle).toHaveBeenCalledWith(
+ insecureClusterAlertTitle
+ );
+
+ expect(securityOssSetup.insecureCluster.setAlertText).toHaveBeenCalledWith(
+ 'mock insecure cluster text'
+ );
+ });
+ });
+ describe('#start', () => {
+ it('onDismiss triggers hiding of the alert', async () => {
+ const securityOssSetup = mockSecurityOssPlugin.createSetup();
+ const securityOssStart = mockSecurityOssPlugin.createStart();
+ const service = new SecurityCheckupService();
+ service.setup({ securityOssSetup });
+ service.start({ securityOssStart });
+
+ expect(securityOssStart.insecureCluster.hideAlert).toHaveBeenCalledTimes(0);
+
+ mockOnDismiss();
+
+ expect(securityOssStart.insecureCluster.hideAlert).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx b/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx
new file mode 100644
index 0000000000000..899a74083656b
--- /dev/null
+++ b/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ SecurityOssPluginSetup,
+ SecurityOssPluginStart,
+} from '../../../../../src/plugins/security_oss/public';
+import { insecureClusterAlertTitle, insecureClusterAlertText } from './components';
+
+interface SetupDeps {
+ securityOssSetup: SecurityOssPluginSetup;
+}
+
+interface StartDeps {
+ securityOssStart: SecurityOssPluginStart;
+}
+
+export class SecurityCheckupService {
+ private securityOssStart?: SecurityOssPluginStart;
+
+ public setup({ securityOssSetup }: SetupDeps) {
+ securityOssSetup.insecureCluster.setAlertTitle(insecureClusterAlertTitle);
+ securityOssSetup.insecureCluster.setAlertText(
+ insecureClusterAlertText((persist: boolean) => this.onDismiss(persist))
+ );
+ }
+
+ public start({ securityOssStart }: StartDeps) {
+ this.securityOssStart = securityOssStart;
+ }
+
+ private onDismiss(persist: boolean) {
+ if (this.securityOssStart) {
+ this.securityOssStart.insecureCluster.hideAlert(persist);
+ }
+ }
+
+ public stop() {}
+}
diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts
index 69b55fcb3d0a4..7d44160c52e53 100644
--- a/x-pack/plugins/security/server/plugin.ts
+++ b/x-pack/plugins/security/server/plugin.ts
@@ -9,6 +9,7 @@ import { first, map } from 'rxjs/operators';
import { TypeOf } from '@kbn/config-schema';
import { deepFreeze } from '@kbn/std';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
+import { SecurityOssPluginSetup } from 'src/plugins/security_oss/server';
import {
CoreSetup,
CoreStart,
@@ -84,6 +85,7 @@ export interface PluginSetupDependencies {
licensing: LicensingPluginSetup;
taskManager: TaskManagerSetupContract;
usageCollection?: UsageCollectionSetup;
+ securityOss?: SecurityOssPluginSetup;
}
export interface PluginStartDependencies {
@@ -133,7 +135,7 @@ export class Plugin {
public async setup(
core: CoreSetup,
- { features, licensing, taskManager, usageCollection }: PluginSetupDependencies
+ { features, licensing, taskManager, usageCollection, securityOss }: PluginSetupDependencies
) {
const [config, legacyConfig] = await combineLatest([
this.initializerContext.config.create>().pipe(
@@ -153,6 +155,13 @@ export class Plugin {
license$: licensing.license$,
});
+ if (securityOss) {
+ license.features$.subscribe(({ allowRbac }) => {
+ const showInsecureClusterWarning = !allowRbac;
+ securityOss.showInsecureClusterWarning$.next(showInsecureClusterWarning);
+ });
+ }
+
securityFeatures.forEach((securityFeature) =>
features.registerElasticsearchFeature(securityFeature)
);