mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Warn users when security is not configured (#78545)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
4d4e53f7c7
commit
49c669ca61
42 changed files with 1867 additions and 7 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -93,6 +93,7 @@ kibana_vars=(
|
|||
path.data
|
||||
pid.file
|
||||
regionmap
|
||||
security.showInsecureClusterWarning
|
||||
server.basePath
|
||||
server.customResponseHeaders
|
||||
server.compression.enabled
|
||||
|
|
4
src/plugins/security_oss/README.md
Normal file
4
src/plugins/security_oss/README.md
Normal file
|
@ -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.
|
10
src/plugins/security_oss/kibana.json
Normal file
10
src/plugins/security_oss/kibana.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "securityOss",
|
||||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["security"],
|
||||
"ui": true,
|
||||
"server": true,
|
||||
"requiredPlugins": [],
|
||||
"requiredBundles": []
|
||||
}
|
22
src/plugins/security_oss/public/config.ts
Normal file
22
src/plugins/security_oss/public/config.ts
Normal file
|
@ -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;
|
||||
}
|
26
src/plugins/security_oss/public/index.ts
Normal file
26
src/plugins/security_oss/public/index.ts
Normal file
|
@ -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);
|
|
@ -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(`<div />`);
|
||||
});
|
||||
});
|
|
@ -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 (
|
||||
<I18nProvider>
|
||||
<div data-test-subj="insecureClusterDefaultAlertText">
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="security.checkup.insecureClusterMessage"
|
||||
defaultMessage="Our free security features can protect against unauthorized access."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
<EuiCheckbox
|
||||
id="persistDismissedAlertPreference"
|
||||
checked={persist}
|
||||
onChange={(changeEvent) => setPersist(changeEvent.target.checked)}
|
||||
label={i18n.translate('security.checkup.dontShowAgain', {
|
||||
defaultMessage: `Don't show again`,
|
||||
})}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
color="primary"
|
||||
fill
|
||||
href="https://www.elastic.co/what-is/elastic-stack-security"
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate('security.checkup.learnMoreButtonText', {
|
||||
defaultMessage: `Learn more`,
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
onClick={() => onDismiss(persist)}
|
||||
data-test-subj="defaultDismissAlertButton"
|
||||
>
|
||||
{i18n.translate('security.checkup.dismissButtonText', {
|
||||
defaultMessage: `Dismiss`,
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
|
||||
render(<AlertText />, e);
|
||||
|
||||
return () => unmountComponentAtNode(e);
|
||||
};
|
|
@ -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';
|
|
@ -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';
|
|
@ -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;
|
||||
},
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<CoreSetup, 'http'>;
|
||||
}
|
||||
|
||||
interface StartDeps {
|
||||
core: Pick<CoreStart, 'notifications' | 'http' | 'application'>;
|
||||
}
|
||||
|
||||
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<boolean>;
|
||||
|
||||
private storage: Storage;
|
||||
|
||||
private alertToast?: Toast;
|
||||
|
||||
private alertTitle?: string | MountPoint;
|
||||
|
||||
private alertText?: string | MountPoint;
|
||||
|
||||
private storageKey?: string;
|
||||
|
||||
constructor(config: Pick<ConfigType, 'showInsecureClusterWarning'>, 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 }));
|
||||
}
|
||||
}
|
20
src/plugins/security_oss/public/mocks.ts
Normal file
20
src/plugins/security_oss/public/mocks.ts
Normal file
|
@ -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';
|
34
src/plugins/security_oss/public/plugin.mock.ts
Normal file
34
src/plugins/security_oss/public/plugin.mock.ts
Normal file
|
@ -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<SecurityOssPluginSetup>;
|
||||
},
|
||||
createStart: () => {
|
||||
return {
|
||||
insecureCluster: mockInsecureClusterService.createStart(),
|
||||
} as DeeplyMockedKeys<SecurityOssPluginStart>;
|
||||
},
|
||||
};
|
58
src/plugins/security_oss/public/plugin.ts
Normal file
58
src/plugins/security_oss/public/plugin.ts
Normal file
|
@ -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<SecurityOssPluginSetup, SecurityOssPluginStart, {}, {}> {
|
||||
private readonly config: ConfigType;
|
||||
|
||||
private insecureClusterService: InsecureClusterService;
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<ConfigType>();
|
||||
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 }),
|
||||
};
|
||||
}
|
||||
}
|
148
src/plugins/security_oss/server/check_cluster_data.test.ts
Normal file
148
src/plugins/security_oss/server/check_cluster_data.test.ts
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
47
src/plugins/security_oss/server/check_cluster_data.ts
Normal file
47
src/plugins/security_oss/server/check_cluster_data.ts
Normal file
|
@ -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;
|
||||
};
|
||||
};
|
26
src/plugins/security_oss/server/config.ts
Normal file
26
src/plugins/security_oss/server/config.ts
Normal file
|
@ -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<typeof ConfigSchema>;
|
||||
|
||||
export const ConfigSchema = schema.object({
|
||||
showInsecureClusterWarning: schema.boolean({ defaultValue: true }),
|
||||
});
|
35
src/plugins/security_oss/server/index.ts
Normal file
35
src/plugins/security_oss/server/index.ts
Normal file
|
@ -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<TypeOf<typeof ConfigSchema>> = {
|
||||
schema: ConfigSchema,
|
||||
exposeToBrowser: {
|
||||
showInsecureClusterWarning: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const plugin = (context: PluginInitializerContext) => new SecurityOssPlugin(context);
|
37
src/plugins/security_oss/server/plugin.test.ts
Normal file
37
src/plugins/security_oss/server/plugin.test.ts
Normal file
|
@ -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$",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
62
src/plugins/security_oss/server/plugin.ts
Normal file
62
src/plugins/security_oss/server/plugin.ts
Normal file
|
@ -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<boolean>;
|
||||
}
|
||||
|
||||
export class SecurityOssPlugin implements Plugin<SecurityOssPluginSetup, void, {}, {}> {
|
||||
private readonly config$: Observable<ConfigType>;
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext<ConfigType>) {
|
||||
this.config$ = initializerContext.config.create();
|
||||
this.logger = initializerContext.logger.get();
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup) {
|
||||
const router = core.http.createRouter();
|
||||
const showInsecureClusterWarning$ = new BehaviorSubject<boolean>(true);
|
||||
|
||||
setupDisplayInsecureClusterAlertRoute({
|
||||
router,
|
||||
log: this.logger,
|
||||
config$: this.config$,
|
||||
displayModifier$: showInsecureClusterWarning$,
|
||||
doesClusterHaveUserData: createClusterDataCheck(),
|
||||
});
|
||||
|
||||
return {
|
||||
showInsecureClusterWarning$,
|
||||
};
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
}
|
|
@ -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<ConfigType>;
|
||||
displayModifier$: Observable<boolean>;
|
||||
doesClusterHaveUserData: ReturnType<typeof createClusterDataCheck>;
|
||||
}
|
||||
|
||||
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 } });
|
||||
}
|
||||
);
|
||||
};
|
20
src/plugins/security_oss/server/routes/index.ts
Normal file
20
src/plugins/security_oss/server/routes/index.ts
Normal file
|
@ -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';
|
|
@ -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<ReturnType<typeof setupServer>>;
|
||||
const pluginId = Symbol('securityOss');
|
||||
|
||||
interface SetupOpts {
|
||||
config?: ConfigType;
|
||||
displayModifier$?: BehaviorSubject<boolean>;
|
||||
doesClusterHaveUserData?: ReturnType<typeof createClusterDataCheck>;
|
||||
}
|
||||
|
||||
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<boolean>(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<boolean>(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<boolean>(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 });
|
||||
});
|
||||
});
|
|
@ -43,6 +43,8 @@ const getDefaultArgs = (tag) => {
|
|||
'--debug',
|
||||
'--config',
|
||||
'test/new_visualize_flow/config.js',
|
||||
'--config',
|
||||
'test/security_functional/config.ts',
|
||||
];
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
54
test/security_functional/config.ts
Normal file
54
test/security_functional/config.ts
Normal file
|
@ -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',
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
27
test/security_functional/index.ts
Normal file
27
test/security_functional/index.ts
Normal file
|
@ -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'));
|
||||
});
|
||||
}
|
87
test/security_functional/insecure_cluster_warning.ts
Normal file
87
test/security_functional/insecure_cluster_warning.ts
Normal file
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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<PluginStartDependencies>, {
|
||||
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<PluginStartDependencies>,
|
||||
{ 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<PluginStartDependencies>,
|
||||
{
|
||||
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<PluginStartDependencies>,
|
||||
{ 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<PluginStartDependencies>,
|
||||
{ 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,
|
||||
});
|
||||
|
|
|
@ -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<PluginStartDependencies>,
|
||||
{ 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
|
@ -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 (
|
||||
<I18nProvider>
|
||||
<div data-test-subj="insecureClusterAlertText">
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.security.checkup.insecureClusterMessage"
|
||||
defaultMessage="Our free security features can protect against unauthorized access."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
<EuiCheckbox
|
||||
id="persistDismissedAlertPreference"
|
||||
checked={persist}
|
||||
onChange={(changeEvent) => setPersist(changeEvent.target.checked)}
|
||||
label={i18n.translate('xpack.security.checkup.dontShowAgain', {
|
||||
defaultMessage: `Don't show again`,
|
||||
})}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
color="primary"
|
||||
fill
|
||||
href="https://www.elastic.co/what-is/elastic-stack-security"
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate('xpack.security.checkup.enableButtonText', {
|
||||
defaultMessage: `Enable security`,
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
onClick={() => onDismiss(persist)}
|
||||
data-test-subj="dismissAlertButton"
|
||||
>
|
||||
{i18n.translate('xpack.security.checkup.dismissButtonText', {
|
||||
defaultMessage: `Dismiss`,
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
|
||||
render(<AlertText />, e);
|
||||
|
||||
return () => unmountComponentAtNode(e);
|
||||
}) as MountPoint;
|
7
x-pack/plugins/security/public/security_checkup/index.ts
Normal file
7
x-pack/plugins/security/public/security_checkup/index.ts
Normal file
|
@ -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';
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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() {}
|
||||
}
|
|
@ -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<PluginStartDependencies>,
|
||||
{ features, licensing, taskManager, usageCollection }: PluginSetupDependencies
|
||||
{ features, licensing, taskManager, usageCollection, securityOss }: PluginSetupDependencies
|
||||
) {
|
||||
const [config, legacyConfig] = await combineLatest([
|
||||
this.initializerContext.config.create<TypeOf<typeof ConfigSchema>>().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)
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue