Remove securityOss plugin (#113946)

This commit is contained in:
Joe Portner 2021-10-07 11:57:37 -04:00 committed by GitHub
parent 920ea03829
commit 64f37e7414
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
89 changed files with 940 additions and 2124 deletions

View file

@ -1595,7 +1595,6 @@ module.exports = {
*/
{
files: [
'src/plugins/security_oss/**/*.{js,mjs,ts,tsx}',
'src/plugins/interactive_setup/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/encrypted_saved_objects/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/security/**/*.{js,mjs,ts,tsx}',

2
.github/CODEOWNERS vendored
View file

@ -287,9 +287,7 @@
# Security
/src/core/server/csp/ @elastic/kibana-security @elastic/kibana-core
/src/plugins/security_oss/ @elastic/kibana-security
/src/plugins/interactive_setup/ @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

View file

@ -55,7 +55,6 @@
"newsfeed": "src/plugins/newsfeed",
"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": ["src/plugins/telemetry", "src/plugins/telemetry_management_section"],

View file

@ -1,203 +0,0 @@
{
"id": "securityOss",
"client": {
"classes": [],
"functions": [],
"interfaces": [],
"enums": [],
"misc": [],
"objects": [],
"setup": {
"parentPluginId": "securityOss",
"id": "def-public.SecurityOssPluginSetup",
"type": "Interface",
"tags": [],
"label": "SecurityOssPluginSetup",
"description": [],
"path": "src/plugins/security_oss/public/plugin.ts",
"deprecated": false,
"children": [
{
"parentPluginId": "securityOss",
"id": "def-public.SecurityOssPluginSetup.insecureCluster",
"type": "Object",
"tags": [],
"label": "insecureCluster",
"description": [],
"signature": [
"InsecureClusterServiceSetup"
],
"path": "src/plugins/security_oss/public/plugin.ts",
"deprecated": false
}
],
"lifecycle": "setup",
"initialIsOpen": true
},
"start": {
"parentPluginId": "securityOss",
"id": "def-public.SecurityOssPluginStart",
"type": "Interface",
"tags": [],
"label": "SecurityOssPluginStart",
"description": [],
"path": "src/plugins/security_oss/public/plugin.ts",
"deprecated": false,
"children": [
{
"parentPluginId": "securityOss",
"id": "def-public.SecurityOssPluginStart.insecureCluster",
"type": "Object",
"tags": [],
"label": "insecureCluster",
"description": [],
"signature": [
"InsecureClusterServiceStart"
],
"path": "src/plugins/security_oss/public/plugin.ts",
"deprecated": false
},
{
"parentPluginId": "securityOss",
"id": "def-public.SecurityOssPluginStart.anonymousAccess",
"type": "Object",
"tags": [],
"label": "anonymousAccess",
"description": [],
"signature": [
"{ getAccessURLParameters: () => Promise<Record<string, string> | null>; getCapabilities: () => Promise<",
"Capabilities",
">; }"
],
"path": "src/plugins/security_oss/public/plugin.ts",
"deprecated": false
}
],
"lifecycle": "start",
"initialIsOpen": true
}
},
"server": {
"classes": [],
"functions": [],
"interfaces": [],
"enums": [],
"misc": [],
"objects": [],
"setup": {
"parentPluginId": "securityOss",
"id": "def-server.SecurityOssPluginSetup",
"type": "Interface",
"tags": [],
"label": "SecurityOssPluginSetup",
"description": [],
"path": "src/plugins/security_oss/server/plugin.ts",
"deprecated": false,
"children": [
{
"parentPluginId": "securityOss",
"id": "def-server.SecurityOssPluginSetup.showInsecureClusterWarning$",
"type": "Object",
"tags": [],
"label": "showInsecureClusterWarning$",
"description": [
"\nAllows consumers to show/hide the insecure cluster warning."
],
"signature": [
"BehaviorSubject",
"<boolean>"
],
"path": "src/plugins/security_oss/server/plugin.ts",
"deprecated": false
},
{
"parentPluginId": "securityOss",
"id": "def-server.SecurityOssPluginSetup.setAnonymousAccessServiceProvider",
"type": "Function",
"tags": [],
"label": "setAnonymousAccessServiceProvider",
"description": [
"\nSet the provider function that returns a service to deal with the anonymous access."
],
"signature": [
"(provider: () => ",
"AnonymousAccessService",
") => void"
],
"path": "src/plugins/security_oss/server/plugin.ts",
"deprecated": false,
"children": [
{
"parentPluginId": "securityOss",
"id": "def-server.SecurityOssPluginSetup.setAnonymousAccessServiceProvider.$1",
"type": "Function",
"tags": [],
"label": "provider",
"description": [],
"signature": [
"() => ",
"AnonymousAccessService"
],
"path": "src/plugins/security_oss/server/plugin.ts",
"deprecated": false,
"isRequired": true
}
],
"returnComment": []
}
],
"lifecycle": "setup",
"initialIsOpen": true
}
},
"common": {
"classes": [],
"functions": [],
"interfaces": [
{
"parentPluginId": "securityOss",
"id": "def-common.AppState",
"type": "Interface",
"tags": [],
"label": "AppState",
"description": [
"\nDefines Security OSS application state."
],
"path": "src/plugins/security_oss/common/app_state.ts",
"deprecated": false,
"children": [
{
"parentPluginId": "securityOss",
"id": "def-common.AppState.insecureClusterAlert",
"type": "Object",
"tags": [],
"label": "insecureClusterAlert",
"description": [],
"signature": [
"{ displayAlert: boolean; }"
],
"path": "src/plugins/security_oss/common/app_state.ts",
"deprecated": false
},
{
"parentPluginId": "securityOss",
"id": "def-common.AppState.anonymousAccess",
"type": "Object",
"tags": [],
"label": "anonymousAccess",
"description": [],
"signature": [
"{ isEnabled: boolean; accessURLParameters: Record<string, string> | null; }"
],
"path": "src/plugins/security_oss/common/app_state.ts",
"deprecated": false
}
],
"initialIsOpen": false
}
],
"enums": [],
"misc": [],
"objects": []
}
}

View file

@ -1,40 +0,0 @@
---
id: kibSecurityOssPluginApi
slug: /kibana-dev-docs/api/securityOss
title: "securityOss"
image: https://source.unsplash.com/400x175/?github
summary: API docs for the securityOss plugin
date: 2020-11-16
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securityOss']
warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info.
---
import securityOssObj from './security_oss.json';
This plugin exposes a limited set of security functionality to OSS plugins.
Contact [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) for questions regarding this plugin.
**Code health stats**
| Public API count | Any count | Items lacking comments | Missing exports |
|-------------------|-----------|------------------------|-----------------|
| 12 | 0 | 9 | 3 |
## Client
### Setup
<DocDefinitionList data={[securityOssObj.client.setup]}/>
### Start
<DocDefinitionList data={[securityOssObj.client.start]}/>
## Server
### Setup
<DocDefinitionList data={[securityOssObj.server.setup]}/>
## Common
### Interfaces
<DocDefinitionList data={securityOssObj.common.interfaces}/>

View file

@ -223,11 +223,6 @@ oss plugins.
|The service exposed by this plugin informs consumers whether they should optimize for non-interactivity. In this way plugins can avoid loading unnecessary code, data or other services.
|{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]
|The share plugin contains various utilities for displaying sharing context menu,
generating deep links to other apps, and creating short URLs.

View file

@ -64,7 +64,6 @@ pageLoadAssetSize:
savedObjectsTaggingOss: 20590
searchprofiler: 67080
security: 95864
securityOss: 30806
share: 99061
snapshotRestore: 79032
spaces: 57868

View file

@ -12,7 +12,6 @@ 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.ts'),
require.resolve('../test/security_functional/config.ts'),
];
// eslint-disable-next-line no-restricted-syntax
const onlyNotInCoverageTests = [

View file

@ -366,6 +366,7 @@ kibana_vars=(
xpack.security.session.idleTimeout
xpack.security.session.lifespan
xpack.security.sessionTimeout
xpack.security.showInsecureClusterWarning
xpack.securitySolution.alertMergeStrategy
xpack.securitySolution.alertIgnoreFields
xpack.securitySolution.endpointResultListDefaultFirstPageIndex

View file

@ -1,4 +0,0 @@
# `securityOss` plugin
`securityOss` is responsible for educating users about Elastic's free security features,
so they can properly protect the data within their clusters.

View file

@ -1,18 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/**
* Defines Security OSS application state.
*/
export interface AppState {
insecureClusterAlert: { displayAlert: boolean };
anonymousAccess: {
isEnabled: boolean;
accessURLParameters: Record<string, string> | null;
};
}

View file

@ -1,16 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/src/plugins/security_oss'],
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/src/plugins/security_oss',
coverageReporters: ['text', 'html'],
collectCoverageFrom: ['<rootDir>/src/plugins/security_oss/{common,public,server}/**/*.{ts,tsx}'],
};

View file

@ -1,15 +0,0 @@
{
"id": "securityOss",
"owner": {
"name": "Platform Security",
"githubTeam": "kibana-security"
},
"description": "This plugin exposes a limited set of security functionality to OSS plugins.",
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["security"],
"ui": true,
"server": true,
"requiredPlugins": [],
"requiredBundles": []
}

View file

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { AppState } from '../../common';
import type { AppStateServiceStart } from './app_state_service';
export const mockAppStateService = {
createStart: (): jest.Mocked<AppStateServiceStart> => {
return { getState: jest.fn() };
},
createAppState: (appState: Partial<AppState> = {}) => ({
insecureClusterAlert: { displayAlert: false },
anonymousAccess: { isEnabled: false, accessURLParameters: null },
...appState,
}),
};

View file

@ -1,62 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { coreMock } from 'src/core/public/mocks';
import { AppStateService } from './app_state_service';
describe('AppStateService', () => {
describe('#start', () => {
it('returns default state for the anonymous routes', async () => {
const coreStart = coreMock.createStart();
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true);
const appStateService = new AppStateService();
await expect(appStateService.start({ core: coreStart }).getState()).resolves.toEqual({
insecureClusterAlert: { displayAlert: false },
anonymousAccess: { isEnabled: false, accessURLParameters: null },
});
expect(coreStart.http.get).not.toHaveBeenCalled();
});
it('returns default state if current state cannot be retrieved', async () => {
const coreStart = coreMock.createStart();
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false);
const failureReason = new Error('Uh oh.');
coreStart.http.get.mockRejectedValue(failureReason);
const appStateService = new AppStateService();
await expect(appStateService.start({ core: coreStart }).getState()).resolves.toEqual({
insecureClusterAlert: { displayAlert: false },
anonymousAccess: { isEnabled: false, accessURLParameters: null },
});
expect(coreStart.http.get).toHaveBeenCalledTimes(1);
expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security_oss/app_state');
});
it('returns retrieved state', async () => {
const coreStart = coreMock.createStart();
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false);
const state = {
insecureClusterAlert: { displayAlert: true },
anonymousAccess: { isEnabled: true, accessURLParameters: { hint: 'some-hint' } },
};
coreStart.http.get.mockResolvedValue(state);
const appStateService = new AppStateService();
await expect(appStateService.start({ core: coreStart }).getState()).resolves.toEqual(state);
expect(coreStart.http.get).toHaveBeenCalledTimes(1);
expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security_oss/app_state');
});
});
});

View file

@ -1,37 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { CoreStart } from 'src/core/public';
import type { AppState } from '../../common';
const DEFAULT_APP_STATE = Object.freeze({
insecureClusterAlert: { displayAlert: false },
anonymousAccess: { isEnabled: false, accessURLParameters: null },
});
interface StartDeps {
core: Pick<CoreStart, 'http'>;
}
export interface AppStateServiceStart {
getState: () => Promise<AppState>;
}
/**
* Service that allows to retrieve application state.
*/
export class AppStateService {
start({ core }: StartDeps): AppStateServiceStart {
const appStatePromise = core.http.anonymousPaths.isAnonymous(window.location.pathname)
? Promise.resolve(DEFAULT_APP_STATE)
: core.http.get<AppState>('/internal/security_oss/app_state').catch(() => DEFAULT_APP_STATE);
return { getState: () => appStatePromise };
}
}

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { AppStateService, AppStateServiceStart } from './app_state_service';

View file

@ -1,16 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { PluginInitializerContext } from 'src/core/public';
import { SecurityOssPlugin } from './plugin';
export { SecurityOssPluginSetup, SecurityOssPluginStart } from './plugin';
export const plugin = (initializerContext: PluginInitializerContext) =>
new SecurityOssPlugin(initializerContext);

View file

@ -1,26 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { 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 />`);
});
});

View file

@ -1,86 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
EuiButton,
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import React, { useState } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import type { MountPoint } from 'src/core/public';
export const defaultAlertTitle = i18n.translate('security.checkup.insecureClusterTitle', {
defaultMessage: 'Your data is not secure',
});
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="Don't lose one bit. Secure your data for free with Elastic."
/>
</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?blade=kibanasecuritymessage"
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);
};

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { defaultAlertTitle, defaultAlertText } from './default_alert';

View file

@ -1,13 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export {
InsecureClusterService,
InsecureClusterServiceSetup,
InsecureClusterServiceStart,
} from './insecure_cluster_service';

View file

@ -1,26 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type {
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;
},
};

View file

@ -1,342 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { nextTick } from '@kbn/test/jest';
import { coreMock } from 'src/core/public/mocks';
import { mockAppStateService } from '../app_state/app_state_service.mock';
import type { ConfigType } from '../config';
import { InsecureClusterService } from './insecure_cluster_service';
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 {
tenant?: string;
}
function initCore({ tenant = '/server-base-path' }: InitOpts = {}) {
const coreSetup = coreMock.createSetup();
(coreSetup.http.basePath.serverBasePath as string) = tenant;
const coreStart = coreMock.createStart();
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();
const storage = coreMock.createStorage();
const appState = mockAppStateService.createStart();
appState.getState.mockResolvedValue(
mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } })
);
const service = new InsecureClusterService(config, storage);
service.setup({ core: coreSetup });
service.start({ core: coreStart, appState });
await nextTick();
expect(appState.getState).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 state indicates that alert should not be shown', async () => {
const config: ConfigType = { showInsecureClusterWarning: true };
const { coreSetup, coreStart } = initCore();
const storage = coreMock.createStorage();
const appState = mockAppStateService.createStart();
appState.getState.mockResolvedValue(
mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: false } })
);
const service = new InsecureClusterService(config, storage);
service.setup({ core: coreSetup });
service.start({ core: coreStart, appState });
await nextTick();
expect(appState.getState).toHaveBeenCalledTimes(1);
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({ tenant: '/my-specific-tenant' });
const storage = coreMock.createStorage();
storage.getItem.mockReturnValue(JSON.stringify({ show: false }));
const appState = mockAppStateService.createStart();
appState.getState.mockResolvedValue(
mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } })
);
const service = new InsecureClusterService(config, storage);
service.setup({ core: coreSetup });
service.start({ core: coreStart, appState });
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();
const storage = coreMock.createStorage();
storage.getItem.mockReturnValue(JSON.stringify({ show: false }));
const appState = mockAppStateService.createStart();
appState.getState.mockResolvedValue(
mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } })
);
const service = new InsecureClusterService(config, storage);
service.setup({ core: coreSetup });
service.start({ core: coreStart, appState });
await nextTick();
expect(appState.getState).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();
const storage = coreMock.createStorage();
storage.getItem.mockReturnValue('{ this is a string of invalid JSON');
const appState = mockAppStateService.createStart();
appState.getState.mockResolvedValue(
mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } })
);
const service = new InsecureClusterService(config, storage);
service.setup({ core: coreSetup });
service.start({ core: coreStart, appState });
await nextTick();
expect(appState.getState).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();
const storage = coreMock.createStorage();
const appState = mockAppStateService.createStart();
appState.getState.mockResolvedValue(
mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } })
);
const service = new InsecureClusterService(config, storage);
service.setup({ core: coreSetup });
service.start({ core: coreStart, appState });
await nextTick();
expect(appState.getState).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();
const storage = coreMock.createStorage();
const appState = mockAppStateService.createStart();
appState.getState.mockResolvedValue(
mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } })
);
const service = new InsecureClusterService(config, storage);
service.setup({ core: coreSetup });
service.start({ core: coreStart, appState });
await nextTick();
expect(appState.getState).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();
const storage = coreMock.createStorage();
const appState = mockAppStateService.createStart();
appState.getState.mockResolvedValue(
mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } })
);
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, appState });
await nextTick();
expect(appState.getState).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();
const storage = coreMock.createStorage();
const appState = mockAppStateService.createStart();
appState.getState.mockResolvedValue(
mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } })
);
const service = new InsecureClusterService(config, storage);
service.setup({ core: coreSetup });
const { hideAlert } = service.start({ core: coreStart, appState });
await nextTick();
expect(appState.getState).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();
const storage = coreMock.createStorage();
const appState = mockAppStateService.createStart();
appState.getState.mockResolvedValue(
mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } })
);
const service = new InsecureClusterService(config, storage);
service.setup({ core: coreSetup });
const { hideAlert } = service.start({ core: coreStart, appState });
await nextTick();
expect(appState.getState).toHaveBeenCalledTimes(1);
expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1);
hideAlert(false);
expect(coreStart.notifications.toasts.remove).toHaveBeenCalledTimes(1);
expect(storage.setItem).not.toHaveBeenCalled();
});
});
});

View file

@ -1,149 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { BehaviorSubject, combineLatest, from } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import type { CoreSetup, CoreStart, MountPoint, Toast } from 'src/core/public';
import type { AppStateServiceStart } from '../app_state';
import type { ConfigType } from '../config';
import { defaultAlertText, defaultAlertTitle } from './components';
interface SetupDeps {
core: Pick<CoreSetup, 'http'>;
}
interface StartDeps {
core: Pick<CoreStart, 'notifications' | 'application'>;
appState: AppStateServiceStart;
}
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, appState }: StartDeps): InsecureClusterServiceStart {
if (this.enabled) {
this.initializeAlert(core, appState);
}
return {
hideAlert: (persist: boolean) => this.setAlertVisibility(false, persist),
};
}
private initializeAlert(core: StartDeps['core'], appState: AppStateServiceStart) {
const appState$ = from(appState.getState());
// 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([appState$, this.alertVisibility$])
.pipe(
map(
([{ insecureClusterAlert }, isAlertVisible]) =>
insecureClusterAlert.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 }));
}
}

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { mockSecurityOssPlugin } from './plugin.mock';

View file

@ -1,31 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import type { InsecureClusterServiceStart } from './insecure_cluster_service';
import { mockInsecureClusterService } from './insecure_cluster_service/insecure_cluster_service.mock';
import type { SecurityOssPluginSetup, SecurityOssPluginStart } from './plugin';
export const mockSecurityOssPlugin = {
createSetup: () => {
return {
insecureCluster: mockInsecureClusterService.createSetup(),
} as DeeplyMockedKeys<SecurityOssPluginSetup>;
},
createStart: () => {
return {
insecureCluster:
mockInsecureClusterService.createStart() as jest.Mocked<InsecureClusterServiceStart>,
anonymousAccess: {
getAccessURLParameters: jest.fn().mockResolvedValue(null),
getCapabilities: jest.fn().mockResolvedValue({}),
},
} as DeeplyMockedKeys<SecurityOssPluginStart>;
},
};

View file

@ -1,69 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type {
Capabilities,
CoreSetup,
CoreStart,
Plugin,
PluginInitializerContext,
} from 'src/core/public';
import { AppStateService } from './app_state';
import type { ConfigType } from './config';
import type {
InsecureClusterServiceSetup,
InsecureClusterServiceStart,
} from './insecure_cluster_service';
import { InsecureClusterService } from './insecure_cluster_service';
export interface SecurityOssPluginSetup {
insecureCluster: InsecureClusterServiceSetup;
}
export interface SecurityOssPluginStart {
insecureCluster: InsecureClusterServiceStart;
anonymousAccess: {
getAccessURLParameters: () => Promise<Record<string, string> | null>;
getCapabilities: () => Promise<Capabilities>;
};
}
export class SecurityOssPlugin
implements Plugin<SecurityOssPluginSetup, SecurityOssPluginStart, {}, {}>
{
private readonly config = this.initializerContext.config.get<ConfigType>();
private readonly insecureClusterService = new InsecureClusterService(this.config, localStorage);
private readonly appStateService = new AppStateService();
constructor(private readonly initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup) {
return {
insecureCluster: this.insecureClusterService.setup({ core }),
};
}
public start(core: CoreStart) {
const appState = this.appStateService.start({ core });
return {
insecureCluster: this.insecureClusterService.start({ core, appState }),
anonymousAccess: {
async getAccessURLParameters() {
const { anonymousAccess } = await appState.getState();
return anonymousAccess.accessURLParameters;
},
getCapabilities() {
return core.http.get<Capabilities>(
'/internal/security_oss/anonymous_access/capabilities'
);
},
},
};
}
}

View file

@ -1,16 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { TypeOf } from '@kbn/config-schema';
import { schema } from '@kbn/config-schema';
export type ConfigType = TypeOf<typeof ConfigSchema>;
export const ConfigSchema = schema.object({
showInsecureClusterWarning: schema.boolean({ defaultValue: true }),
});

View file

@ -1,24 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { TypeOf } from '@kbn/config-schema';
import type { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/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);

View file

@ -1,28 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { coreMock } from 'src/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$",
"setAnonymousAccessServiceProvider",
]
`);
});
});
});

View file

@ -1,100 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { Observable } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import type {
Capabilities,
CoreSetup,
KibanaRequest,
Logger,
Plugin,
PluginInitializerContext,
} from 'src/core/server';
import { createClusterDataCheck } from './check_cluster_data';
import type { ConfigType } from './config';
import { setupAnonymousAccessCapabilitiesRoute, setupAppStateRoute } from './routes';
export interface SecurityOssPluginSetup {
/**
* Allows consumers to show/hide the insecure cluster warning.
*/
showInsecureClusterWarning$: BehaviorSubject<boolean>;
/**
* Set the provider function that returns a service to deal with the anonymous access.
* @param provider
*/
setAnonymousAccessServiceProvider: (provider: () => AnonymousAccessService) => void;
}
export interface AnonymousAccessService {
/**
* Indicates whether anonymous access is enabled.
*/
readonly isAnonymousAccessEnabled: boolean;
/**
* A map of query string parameters that should be specified in the URL pointing to Kibana so
* that anonymous user can automatically log in.
*/
readonly accessURLParameters: Readonly<Map<string, string>> | null;
/**
* Gets capabilities of the anonymous service account.
* @param request Kibana request instance.
*/
getCapabilities: (request: KibanaRequest) => Promise<Capabilities>;
}
export class SecurityOssPlugin implements Plugin<SecurityOssPluginSetup, void, {}, {}> {
private readonly config$: Observable<ConfigType>;
private readonly logger: Logger;
private anonymousAccessServiceProvider?: () => AnonymousAccessService;
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);
setupAppStateRoute({
router,
log: this.logger,
config$: this.config$,
displayModifier$: showInsecureClusterWarning$,
doesClusterHaveUserData: createClusterDataCheck(),
getAnonymousAccessService: () => this.anonymousAccessServiceProvider?.() ?? null,
});
setupAnonymousAccessCapabilitiesRoute({
router,
getAnonymousAccessService: () => this.anonymousAccessServiceProvider?.() ?? null,
});
return {
showInsecureClusterWarning$,
setAnonymousAccessServiceProvider: (provider: () => AnonymousAccessService) => {
if (this.anonymousAccessServiceProvider) {
throw new Error('Anonymous Access service provider is already set.');
}
this.anonymousAccessServiceProvider = provider;
},
};
}
public start() {}
public stop() {}
}

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { IRouter } from 'src/core/server';
import type { AnonymousAccessService } from '../plugin';
interface Deps {
router: IRouter;
getAnonymousAccessService: () => AnonymousAccessService | null;
}
/**
* Defines route that returns capabilities of the anonymous service account.
*/
export function setupAnonymousAccessCapabilitiesRoute({ router, getAnonymousAccessService }: Deps) {
router.get(
{ path: '/internal/security_oss/anonymous_access/capabilities', validate: false },
async (_context, request, response) => {
const anonymousAccessService = getAnonymousAccessService();
if (!anonymousAccessService) {
return response.custom({ statusCode: 501, body: 'Not Implemented' });
}
return response.ok({ body: await anonymousAccessService.getCapabilities(request) });
}
);
}

View file

@ -1,66 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { Observable } from 'rxjs';
import { combineLatest } from 'rxjs';
import type { IRouter, Logger } from 'src/core/server';
import type { AppState } from '../../common';
import type { createClusterDataCheck } from '../check_cluster_data';
import type { ConfigType } from '../config';
import type { AnonymousAccessService } from '../plugin';
interface Deps {
router: IRouter;
log: Logger;
config$: Observable<ConfigType>;
displayModifier$: Observable<boolean>;
doesClusterHaveUserData: ReturnType<typeof createClusterDataCheck>;
getAnonymousAccessService: () => AnonymousAccessService | null;
}
export const setupAppStateRoute = ({
router,
log,
config$,
displayModifier$,
doesClusterHaveUserData,
getAnonymousAccessService,
}: Deps) => {
let showInsecureClusterWarning = false;
combineLatest([config$, displayModifier$]).subscribe(([config, displayModifier]) => {
showInsecureClusterWarning = config.showInsecureClusterWarning && displayModifier;
});
router.get(
{ path: '/internal/security_oss/app_state', validate: false },
async (context, request, response) => {
let displayAlert = false;
if (showInsecureClusterWarning) {
displayAlert = await doesClusterHaveUserData(
context.core.elasticsearch.client.asInternalUser,
log
);
}
const anonymousAccessService = getAnonymousAccessService();
const appState: AppState = {
insecureClusterAlert: { displayAlert },
anonymousAccess: {
isEnabled: anonymousAccessService?.isAnonymousAccessEnabled ?? false,
accessURLParameters: anonymousAccessService?.accessURLParameters
? Object.fromEntries(anonymousAccessService.accessURLParameters.entries())
: null,
},
};
return response.ok({ body: appState });
}
);
};

View file

@ -1,10 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { setupAppStateRoute } from './app_state';
export { setupAnonymousAccessCapabilitiesRoute } from './anonymous_access_capabilities';

View file

@ -1,77 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import supertest from 'supertest';
import type { UnwrapPromise } from '@kbn/utility-types';
import { setupServer } from 'src/core/server/test_utils';
import type { AnonymousAccessService } from '../../plugin';
import { setupAnonymousAccessCapabilitiesRoute } from '../anonymous_access_capabilities';
type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
const pluginId = Symbol('securityOss');
interface SetupOpts {
getAnonymousAccessService?: () => AnonymousAccessService | null;
}
describe('GET /internal/security_oss/anonymous_access/capabilities', () => {
let server: SetupServerReturn['server'];
let httpSetup: SetupServerReturn['httpSetup'];
const setupTestServer = async ({ getAnonymousAccessService = () => null }: SetupOpts = {}) => {
({ server, httpSetup } = await setupServer(pluginId));
const router = httpSetup.createRouter('/');
setupAnonymousAccessCapabilitiesRoute({ router, getAnonymousAccessService });
await server.start();
};
afterEach(async () => {
await server.stop();
});
it('responds with 501 if anonymous access service is provided', async () => {
await setupTestServer();
await supertest(httpSetup.server.listener)
.get('/internal/security_oss/anonymous_access/capabilities')
.expect(501, {
statusCode: 501,
error: 'Not Implemented',
message: 'Not Implemented',
});
});
it('returns anonymous access state if anonymous access service is provided', async () => {
await setupTestServer({
getAnonymousAccessService: () => ({
isAnonymousAccessEnabled: true,
accessURLParameters: new Map([['auth_provider_hint', 'anonymous1']]),
getCapabilities: jest.fn().mockResolvedValue({
navLinks: {},
management: {},
catalogue: {},
custom: { something: true },
}),
}),
});
await supertest(httpSetup.server.listener)
.get('/internal/security_oss/anonymous_access/capabilities')
.expect(200, {
navLinks: {},
management: {},
catalogue: {},
custom: { something: true },
});
});
});

View file

@ -1,184 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { BehaviorSubject, of } from 'rxjs';
import supertest from 'supertest';
import type { UnwrapPromise } from '@kbn/utility-types';
import { loggingSystemMock } from 'src/core/server/mocks';
import { setupServer } from 'src/core/server/test_utils';
import type { createClusterDataCheck } from '../../check_cluster_data';
import type { ConfigType } from '../../config';
import type { AnonymousAccessService } from '../../plugin';
import { setupAppStateRoute } from '../app_state';
type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
const pluginId = Symbol('securityOss');
interface SetupOpts {
config?: ConfigType;
displayModifier$?: BehaviorSubject<boolean>;
doesClusterHaveUserData?: ReturnType<typeof createClusterDataCheck>;
getAnonymousAccessService?: () => AnonymousAccessService | null;
}
describe('GET /internal/security_oss/app_state', () => {
let server: SetupServerReturn['server'];
let httpSetup: SetupServerReturn['httpSetup'];
const setupTestServer = async ({
config = { showInsecureClusterWarning: true },
displayModifier$ = new BehaviorSubject<boolean>(true),
doesClusterHaveUserData = jest.fn().mockResolvedValue(true),
getAnonymousAccessService = () => null,
}: SetupOpts) => {
({ server, httpSetup } = await setupServer(pluginId));
const router = httpSetup.createRouter('/');
const log = loggingSystemMock.createLogger();
setupAppStateRoute({
router,
log,
config$: of(config),
displayModifier$,
doesClusterHaveUserData,
getAnonymousAccessService,
});
await server.start();
return {
log,
};
};
afterEach(async () => {
await server.stop();
});
it('responds `insecureClusterAlert.displayAlert == false` if plugin is not configured to display alerts', async () => {
await setupTestServer({
config: { showInsecureClusterWarning: false },
});
await supertest(httpSetup.server.listener)
.get('/internal/security_oss/app_state')
.expect(200, {
insecureClusterAlert: { displayAlert: false },
anonymousAccess: { isEnabled: false, accessURLParameters: null },
});
});
it('responds `insecureClusterAlert.displayAlert == 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/app_state')
.expect(200, {
insecureClusterAlert: { displayAlert: false },
anonymousAccess: { isEnabled: false, accessURLParameters: null },
});
});
it('responds `insecureClusterAlert.displayAlert == 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/app_state')
.expect(200, {
insecureClusterAlert: { displayAlert: false },
anonymousAccess: { isEnabled: false, accessURLParameters: null },
});
});
it('responds `insecureClusterAlert.displayAlert == 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/app_state')
.expect(200, {
insecureClusterAlert: { displayAlert: true },
anonymousAccess: { isEnabled: false, accessURLParameters: null },
});
});
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/app_state')
.expect(200, {
insecureClusterAlert: { displayAlert: true },
anonymousAccess: { isEnabled: false, accessURLParameters: null },
});
displayModifier$.next(false);
await supertest(httpSetup.server.listener)
.get('/internal/security_oss/app_state')
.expect(200, {
insecureClusterAlert: { displayAlert: false },
anonymousAccess: { isEnabled: false, accessURLParameters: null },
});
});
it('returns anonymous access state if anonymous access service is provided', async () => {
const displayModifier$ = new BehaviorSubject<boolean>(true);
await setupTestServer({
config: { showInsecureClusterWarning: true },
doesClusterHaveUserData: jest.fn().mockResolvedValue(true),
displayModifier$,
getAnonymousAccessService: () => ({
isAnonymousAccessEnabled: true,
accessURLParameters: new Map([['auth_provider_hint', 'anonymous1']]),
getCapabilities: jest.fn(),
}),
});
await supertest(httpSetup.server.listener)
.get('/internal/security_oss/app_state')
.expect(200, {
insecureClusterAlert: { displayAlert: true },
anonymousAccess: {
isEnabled: true,
accessURLParameters: { auth_provider_hint: 'anonymous1' },
},
});
displayModifier$.next(false);
await supertest(httpSetup.server.listener)
.get('/internal/security_oss/app_state')
.expect(200, {
insecureClusterAlert: { displayAlert: false },
anonymousAccess: {
isEnabled: true,
accessURLParameters: { auth_provider_hint: 'anonymous1' },
},
});
});
});

View file

@ -1,11 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": true
},
"include": ["common/**/*", "public/**/*", "server/**/*"],
"references": [{ "path": "../../core/tsconfig.json" }]
}

View file

@ -6,6 +6,11 @@
* Side Public License, v 1.
*/
export interface ConfigType {
showInsecureClusterWarning: boolean;
}
import type { AnonymousAccessServiceContract } from './types';
export const anonymousAccessMock = {
create: (): jest.Mocked<AnonymousAccessServiceContract> => ({
getState: jest.fn(),
getCapabilities: jest.fn(),
}),
};

View file

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export type { AppState } from './app_state';
export type { AnonymousAccessServiceContract, AnonymousAccessState } from './types';

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { Capabilities } from 'src/core/public';
/**
* The contract that is used to check anonymous access for the purposes of sharing public links. The implementation is intended to be
* provided by the security plugin.
*/
export interface AnonymousAccessServiceContract {
/**
* This function returns the current state of anonymous access.
*/
getState: () => Promise<AnonymousAccessState>;
/**
* This function returns the capabilities of the anonymous access user.
*/
getCapabilities: () => Promise<Capabilities>;
}
/**
* The state of anonymous access.
*/
export interface AnonymousAccessState {
/**
* Whether anonymous access is enabled or not.
*/
isEnabled: boolean;
/**
* If anonymous access is enabled, this reflects what URL parameters need to be added to a Kibana link to make it publicly accessible.
* Note that if anonymous access is the only authentication method, this will be null.
*/
accessURLParameters: Record<string, string> | null;
}

View file

@ -7,3 +7,4 @@
*/
export { LocatorDefinition, LocatorPublic, useLocatorUrl, formatSearchParams } from './url_service';
export type { AnonymousAccessServiceContract, AnonymousAccessState } from './anonymous_access';

View file

@ -9,5 +9,5 @@
},
"description": "Adds URL Service and sharing capabilities to Kibana",
"requiredBundles": ["kibanaUtils"],
"optionalPlugins": ["securityOss"]
"optionalPlugins": []
}

View file

@ -17,7 +17,7 @@ import type { Capabilities } from 'src/core/public';
import { UrlPanelContent } from './url_panel_content';
import { ShareMenuItem, ShareContextMenuPanelItem, UrlParamExtension } from '../types';
import type { SecurityOssPluginStart } from '../../../security_oss/public';
import { AnonymousAccessServiceContract } from '../../common/anonymous_access';
interface Props {
allowEmbed: boolean;
@ -31,7 +31,7 @@ interface Props {
basePath: string;
post: HttpStart['post'];
embedUrlParamExtensions?: UrlParamExtension[];
anonymousAccess?: SecurityOssPluginStart['anonymousAccess'];
anonymousAccess?: AnonymousAccessServiceContract;
showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean;
}

View file

@ -32,7 +32,10 @@ import type { Capabilities } from 'src/core/public';
import { shortenUrl } from '../lib/url_shortener';
import { UrlParamExtension } from '../types';
import type { SecurityOssPluginStart } from '../../../security_oss/public';
import {
AnonymousAccessServiceContract,
AnonymousAccessState,
} from '../../common/anonymous_access';
interface Props {
allowShortUrl: boolean;
@ -43,7 +46,7 @@ interface Props {
basePath: string;
post: HttpStart['post'];
urlParamExtensions?: UrlParamExtension[];
anonymousAccess?: SecurityOssPluginStart['anonymousAccess'];
anonymousAccess?: AnonymousAccessServiceContract;
showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean;
}
@ -66,7 +69,7 @@ interface State {
url?: string;
shortUrlErrorMsg?: string;
urlParams?: UrlParams;
anonymousAccessParameters: Record<string, string> | null;
anonymousAccessParameters: AnonymousAccessState['accessURLParameters'];
showPublicUrlSwitch: boolean;
}
@ -104,8 +107,8 @@ export class UrlPanelContent extends Component<Props, State> {
if (this.props.anonymousAccess) {
(async () => {
const anonymousAccessParameters =
await this.props.anonymousAccess!.getAccessURLParameters();
const { accessURLParameters: anonymousAccessParameters } =
await this.props.anonymousAccess!.getState();
if (!this.mounted) {
return;

View file

@ -44,6 +44,7 @@ const createSetupContract = (): Setup => {
},
url,
navigate: jest.fn(),
setAnonymousAccessServiceProvider: jest.fn(),
};
return setupContract;
};

View file

@ -10,7 +10,7 @@ import { registryMock, managerMock } from './plugin.test.mocks';
import { SharePlugin } from './plugin';
import { CoreStart } from 'kibana/public';
import { coreMock } from '../../../core/public/mocks';
import { mockSecurityOssPlugin } from '../../security_oss/public/mocks';
import { anonymousAccessMock } from '../common/anonymous_access/index.mock';
describe('SharePlugin', () => {
beforeEach(() => {
@ -22,12 +22,8 @@ describe('SharePlugin', () => {
describe('setup', () => {
test('wires up and returns registry', async () => {
const coreSetup = coreMock.createSetup();
const plugins = {
securityOss: mockSecurityOssPlugin.createSetup(),
};
const setup = await new SharePlugin(coreMock.createPluginInitializerContext()).setup(
coreSetup,
plugins
coreSetup
);
expect(registryMock.setup).toHaveBeenCalledWith();
expect(setup.register).toBeDefined();
@ -35,10 +31,7 @@ describe('SharePlugin', () => {
test('registers redirect app', async () => {
const coreSetup = coreMock.createSetup();
const plugins = {
securityOss: mockSecurityOssPlugin.createSetup(),
};
await new SharePlugin(coreMock.createPluginInitializerContext()).setup(coreSetup, plugins);
await new SharePlugin(coreMock.createPluginInitializerContext()).setup(coreSetup);
expect(coreSetup.application.register).toHaveBeenCalledWith(
expect.objectContaining({
id: 'short_url_redirect',
@ -50,22 +43,34 @@ describe('SharePlugin', () => {
describe('start', () => {
test('wires up and returns show function, but not registry', async () => {
const coreSetup = coreMock.createSetup();
const pluginsSetup = {
securityOss: mockSecurityOssPlugin.createSetup(),
};
const service = new SharePlugin(coreMock.createPluginInitializerContext());
await service.setup(coreSetup, pluginsSetup);
const pluginsStart = {
securityOss: mockSecurityOssPlugin.createStart(),
};
const start = await service.start({} as CoreStart, pluginsStart);
await service.setup(coreSetup);
const start = await service.start({} as CoreStart);
expect(registryMock.start).toHaveBeenCalled();
expect(managerMock.start).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
getShareMenuItems: expect.any(Function),
}),
expect.anything()
undefined
);
expect(start.toggleShareContextMenu).toBeDefined();
});
test('passes anonymous access service provider to the share menu manager when it is available', async () => {
const coreSetup = coreMock.createSetup();
const service = new SharePlugin(coreMock.createPluginInitializerContext());
const setup = await service.setup(coreSetup);
const anonymousAccessServiceProvider = () => anonymousAccessMock.create();
setup.setAnonymousAccessServiceProvider(anonymousAccessServiceProvider);
const start = await service.start({} as CoreStart);
expect(registryMock.start).toHaveBeenCalled();
expect(managerMock.start).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
getShareMenuItems: expect.any(Function),
}),
anonymousAccessServiceProvider
);
expect(start.toggleShareContextMenu).toBeDefined();
});

View file

@ -10,7 +10,6 @@ import './index.scss';
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
import { ShareMenuManager, ShareMenuManagerStart } from './services';
import type { SecurityOssPluginSetup, SecurityOssPluginStart } from '../../security_oss/public';
import { ShareMenuRegistry, ShareMenuRegistrySetup } from './services';
import { createShortUrlRedirectApp } from './services/short_url_redirect_app';
import {
@ -22,14 +21,7 @@ import { UrlService } from '../common/url_service';
import { RedirectManager } from './url_service';
import type { RedirectOptions } from '../common/url_service/locators/redirect';
import { LegacyShortUrlLocatorDefinition } from '../common/url_service/locators/legacy_short_url_locator';
export interface ShareSetupDependencies {
securityOss?: SecurityOssPluginSetup;
}
export interface ShareStartDependencies {
securityOss?: SecurityOssPluginStart;
}
import { AnonymousAccessServiceContract } from '../common';
/** @public */
export type SharePluginSetup = ShareMenuRegistrySetup & {
@ -50,6 +42,11 @@ export type SharePluginSetup = ShareMenuRegistrySetup & {
* the locator, then using the locator to navigate.
*/
navigate(options: RedirectOptions): void;
/**
* Sets the provider for the anonymous access service; this is consumed by the Security plugin to avoid a circular dependency.
*/
setAnonymousAccessServiceProvider: (provider: () => AnonymousAccessServiceContract) => void;
};
/** @public */
@ -80,10 +77,11 @@ export class SharePlugin implements Plugin<SharePluginSetup, SharePluginStart> {
private redirectManager?: RedirectManager;
private url?: UrlService;
private anonymousAccessServiceProvider?: () => AnonymousAccessServiceContract;
constructor(private readonly initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, plugins: ShareSetupDependencies): SharePluginSetup {
public setup(core: CoreSetup): SharePluginSetup {
const { application, http } = core;
const { basePath } = http;
@ -138,15 +136,21 @@ export class SharePlugin implements Plugin<SharePluginSetup, SharePluginStart> {
urlGenerators: this.urlGeneratorsService.setup(core),
url: this.url,
navigate: (options: RedirectOptions) => this.redirectManager!.navigate(options),
setAnonymousAccessServiceProvider: (provider: () => AnonymousAccessServiceContract) => {
if (this.anonymousAccessServiceProvider) {
throw new Error('Anonymous Access service provider is already set.');
}
this.anonymousAccessServiceProvider = provider;
},
};
}
public start(core: CoreStart, plugins: ShareStartDependencies): SharePluginStart {
public start(core: CoreStart): SharePluginStart {
return {
...this.shareContextMenu.start(
core,
this.shareMenuRegistry.start(),
plugins.securityOss?.anonymousAccess
this.anonymousAccessServiceProvider
),
urlGenerators: this.urlGeneratorsService.start(core),
url: this.url!,

View file

@ -15,7 +15,7 @@ import { CoreStart, HttpStart } from 'kibana/public';
import { ShareContextMenu } from '../components/share_context_menu';
import { ShareMenuItem, ShowShareMenuOptions } from '../types';
import { ShareMenuRegistryStart } from './share_menu_registry';
import type { SecurityOssPluginStart } from '../../../security_oss/public';
import { AnonymousAccessServiceContract } from '../../common/anonymous_access';
export class ShareMenuManager {
private isOpen = false;
@ -25,7 +25,7 @@ export class ShareMenuManager {
start(
core: CoreStart,
shareRegistry: ShareMenuRegistryStart,
anonymousAccess?: SecurityOssPluginStart['anonymousAccess']
anonymousAccessServiceProvider?: () => AnonymousAccessServiceContract
) {
return {
/**
@ -35,6 +35,7 @@ export class ShareMenuManager {
*/
toggleShareContextMenu: (options: ShowShareMenuOptions) => {
const menuItems = shareRegistry.getShareMenuItems({ ...options, onClose: this.onClose });
const anonymousAccess = anonymousAccessServiceProvider?.();
this.toggleShareContextMenu({
...options,
menuItems,
@ -69,7 +70,7 @@ export class ShareMenuManager {
menuItems: ShareMenuItem[];
post: HttpStart['post'];
basePath: string;
anonymousAccess?: SecurityOssPluginStart['anonymousAccess'];
anonymousAccess: AnonymousAccessServiceContract | undefined;
}) {
if (this.isOpen) {
this.onClose();

View file

@ -10,6 +10,5 @@
"references": [
{ "path": "../../core/tsconfig.json" },
{ "path": "../kibana_utils/tsconfig.json" },
{ "path": "../security_oss/tsconfig.json" }
]
}

View file

@ -1,40 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import path from 'path';
import { FtrConfigProviderContext } from '@kbn/test';
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: {},
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',
],
},
};
}

View file

@ -1,17 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { 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'));
});
}

View file

@ -1,77 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { 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('test/functional/fixtures/es_archiver/hamlet');
});
after(async () => {
await esArchiver.unload('test/functional/fixtures/es_archiver/hamlet');
});
describe('without user data', () => {
before(async () => {
await browser.setLocalStorageItem('insecureClusterWarningVisibility', '');
await esArchiver.unload('test/functional/fixtures/es_archiver/hamlet');
await esArchiver.emptyKibanaIndex();
});
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('test/functional/fixtures/es_archiver/hamlet');
});
after(async () => {
await esArchiver.unload('test/functional/fixtures/es_archiver/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');
});
});
});
}

View file

@ -19,3 +19,7 @@ export enum LogoutReason {
'LOGGED_OUT' = 'LOGGED_OUT',
'UNAUTHENTICATED' = 'UNAUTHENTICATED',
}
export interface SecurityCheckupState {
displayAlert: boolean;
}

View file

@ -8,8 +8,8 @@
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "security"],
"requiredPlugins": ["data", "features", "licensing", "taskManager", "securityOss"],
"optionalPlugins": ["home", "management", "usageCollection", "spaces"],
"requiredPlugins": ["data", "features", "licensing", "taskManager"],
"optionalPlugins": ["home", "management", "usageCollection", "spaces", "share"],
"server": true,
"ui": true,
"requiredBundles": [

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { Capabilities, HttpStart } from 'src/core/public';
import type {
AnonymousAccessServiceContract,
AnonymousAccessState,
} from '../../../../../src/plugins/share/common';
import type { SharePluginSetup } from '../../../../../src/plugins/share/public';
const DEFAULT_ANONYMOUS_ACCESS_STATE = Object.freeze<AnonymousAccessState>({
isEnabled: false,
accessURLParameters: null,
});
interface SetupDeps {
share: Pick<SharePluginSetup, 'setAnonymousAccessServiceProvider'>;
}
interface StartDeps {
http: HttpStart;
}
/**
* Service that allows to retrieve application state.
*/
export class AnonymousAccessService {
private internalService!: AnonymousAccessServiceContract;
setup({ share }: SetupDeps) {
share.setAnonymousAccessServiceProvider(() => this.internalService);
}
start({ http }: StartDeps) {
this.internalService = {
getCapabilities: () =>
http.get<Capabilities>('/internal/security/anonymous_access/capabilities'),
getState: () =>
http.anonymousPaths.isAnonymous(window.location.pathname)
? Promise.resolve(DEFAULT_ANONYMOUS_ACCESS_STATE)
: http
.get<AnonymousAccessState>('/internal/security/anonymous_access/state')
.catch(() => DEFAULT_ANONYMOUS_ACCESS_STATE),
};
}
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { AnonymousAccessService } from './anonymous_access_service';

View file

@ -7,4 +7,5 @@
export interface ConfigType {
loginAssistanceMessage: string;
showInsecureClusterWarning: boolean;
}

View file

@ -10,13 +10,11 @@ import type { MockAuthenticatedUserProps } from '../common/model/authenticated_u
import { mockAuthenticatedUser } from '../common/model/authenticated_user.mock';
import { authenticationMock } from './authentication/index.mock';
import { navControlServiceMock } from './nav_control/index.mock';
import { createSessionTimeoutMock } from './session/session_timeout.mock';
import { getUiApiMock } from './ui_api/index.mock';
function createSetupMock() {
return {
authc: authenticationMock.createSetup(),
sessionTimeout: createSessionTimeoutMock(),
license: licenseMock.create(),
};
}

View file

@ -12,7 +12,6 @@ import type { CoreSetup } from 'src/core/public';
import { coreMock } from 'src/core/public/mocks';
import type { DataPublicPluginStart } from 'src/plugins/data/public';
import { managementPluginMock } from 'src/plugins/management/public/mocks';
import { mockSecurityOssPlugin } from 'src/plugins/security_oss/public/mocks';
import type { FeaturesPluginStart } from '../../features/public';
import { licensingMock } from '../../licensing/public/mocks';
@ -38,7 +37,6 @@ describe('Security Plugin', () => {
}) as CoreSetup<PluginStartDependencies>,
{
licensing: licensingMock.createSetup(),
securityOss: mockSecurityOssPlugin.createSetup(),
}
)
).toEqual({
@ -64,7 +62,6 @@ describe('Security Plugin', () => {
plugin.setup(coreSetupMock as CoreSetup<PluginStartDependencies>, {
licensing: licensingMock.createSetup(),
securityOss: mockSecurityOssPlugin.createSetup(),
management: managementSetupMock,
});
@ -90,12 +87,11 @@ describe('Security Plugin', () => {
const plugin = new SecurityPlugin(coreMock.createPluginInitializerContext());
plugin.setup(
coreMock.createSetup({ basePath: '/some-base-path' }) as CoreSetup<PluginStartDependencies>,
{ licensing: licensingMock.createSetup(), securityOss: mockSecurityOssPlugin.createSetup() }
{ licensing: licensingMock.createSetup() }
);
expect(
plugin.start(coreMock.createStart({ basePath: '/some-base-path' }), {
securityOss: mockSecurityOssPlugin.createStart(),
data: {} as DataPublicPluginStart,
features: {} as FeaturesPluginStart,
})
@ -131,14 +127,12 @@ 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,
@ -153,7 +147,7 @@ describe('Security Plugin', () => {
const plugin = new SecurityPlugin(coreMock.createPluginInitializerContext());
plugin.setup(
coreMock.createSetup({ basePath: '/some-base-path' }) as CoreSetup<PluginStartDependencies>,
{ licensing: licensingMock.createSetup(), securityOss: mockSecurityOssPlugin.createSetup() }
{ licensing: licensingMock.createSetup() }
);
expect(() => plugin.stop()).not.toThrow();
@ -164,11 +158,10 @@ describe('Security Plugin', () => {
plugin.setup(
coreMock.createSetup({ basePath: '/some-base-path' }) as CoreSetup<PluginStartDependencies>,
{ licensing: licensingMock.createSetup(), securityOss: mockSecurityOssPlugin.createSetup() }
{ licensing: licensingMock.createSetup() }
);
plugin.start(coreMock.createStart({ basePath: '/some-base-path' }), {
securityOss: mockSecurityOssPlugin.createStart(),
data: {} as DataPublicPluginStart,
features: {} as FeaturesPluginStart,
});

View file

@ -10,18 +10,16 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src
import type { DataPublicPluginStart } from 'src/plugins/data/public';
import type { HomePublicPluginSetup } from 'src/plugins/home/public';
import type { ManagementSetup, ManagementStart } from 'src/plugins/management/public';
import type {
SecurityOssPluginSetup,
SecurityOssPluginStart,
} from 'src/plugins/security_oss/public';
import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public';
import type { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public';
import type { FeaturesPluginStart } from '../../features/public';
import type { LicensingPluginSetup } from '../../licensing/public';
import type { SpacesPluginStart } from '../../spaces/public';
import { SecurityLicenseService } from '../common/licensing';
import type { SecurityLicense } from '../common/licensing';
import { accountManagementApp } from './account_management';
import { AnonymousAccessService } from './anonymous_access';
import type { AuthenticationServiceSetup, AuthenticationServiceStart } from './authentication';
import { AuthenticationService } from './authentication';
import type { ConfigType } from './config';
@ -35,17 +33,17 @@ import { getUiApi } from './ui_api';
export interface PluginSetupDependencies {
licensing: LicensingPluginSetup;
securityOss: SecurityOssPluginSetup;
home?: HomePublicPluginSetup;
management?: ManagementSetup;
share?: SharePluginSetup;
}
export interface PluginStartDependencies {
data: DataPublicPluginStart;
features: FeaturesPluginStart;
securityOss: SecurityOssPluginStart;
management?: ManagementStart;
spaces?: SpacesPluginStart;
share?: SharePluginStart;
}
export class SecurityPlugin
@ -57,22 +55,21 @@ export class SecurityPlugin
PluginStartDependencies
>
{
private readonly config = this.initializerContext.config.get<ConfigType>();
private sessionTimeout!: SessionTimeout;
private readonly authenticationService = new AuthenticationService();
private readonly navControlService = new SecurityNavControlService();
private readonly securityLicenseService = new SecurityLicenseService();
private readonly managementService = new ManagementService();
private readonly securityCheckupService = new SecurityCheckupService();
private readonly securityCheckupService = new SecurityCheckupService(this.config, localStorage);
private readonly anonymousAccessService = new AnonymousAccessService();
private authc!: AuthenticationServiceSetup;
private readonly config: ConfigType;
constructor(private readonly initializerContext: PluginInitializerContext) {
this.config = this.initializerContext.config.get<ConfigType>();
}
constructor(private readonly initializerContext: PluginInitializerContext) {}
public setup(
core: CoreSetup<PluginStartDependencies>,
{ home, licensing, management, securityOss }: PluginSetupDependencies
{ home, licensing, management, share }: PluginSetupDependencies
): SecurityPluginSetup {
const { http, notifications } = core;
const { anonymousPaths } = http;
@ -86,7 +83,7 @@ export class SecurityPlugin
const { license } = this.securityLicenseService.setup({ license$: licensing.license$ });
this.securityCheckupService.setup({ securityOssSetup: securityOss });
this.securityCheckupService.setup({ http: core.http });
this.authc = this.authenticationService.setup({
application: core.application,
@ -135,6 +132,10 @@ export class SecurityPlugin
});
}
if (share) {
this.anonymousAccessService.setup({ share });
}
return {
authc: this.authc,
license,
@ -143,15 +144,23 @@ export class SecurityPlugin
public start(
core: CoreStart,
{ management, securityOss }: PluginStartDependencies
{ management, share }: PluginStartDependencies
): SecurityPluginStart {
this.sessionTimeout.start();
this.securityCheckupService.start({ securityOssStart: securityOss, docLinks: core.docLinks });
this.securityCheckupService.start({
http: core.http,
notifications: core.notifications,
docLinks: core.docLinks,
});
if (management) {
this.managementService.start({ capabilities: core.application.capabilities });
}
if (share) {
this.anonymousAccessService.start({ http: core.http });
}
return {
uiApi: getUiApi({ core }),
navControlService: this.navControlService.start({ core }),
@ -164,7 +173,6 @@ export class SecurityPlugin
this.navControlService.stop();
this.securityLicenseService.stop();
this.managementService.stop();
this.securityCheckupService.stop();
}
}

View file

@ -26,15 +26,13 @@ export const insecureClusterAlertTitle = i18n.translate(
);
export const insecureClusterAlertText = (
getDocLinks: () => DocLinksStart,
docLinks: DocLinksStart,
onDismiss: (persist: boolean) => void
) =>
((e) => {
const AlertText = () => {
const [persist, setPersist] = useState(false);
const enableSecurityDocLink = `${
getDocLinks().links.security.elasticsearchEnableSecurity
}?blade=kibanasecuritymessage`;
const enableSecurityDocLink = `${docLinks.links.security.elasticsearchEnableSecurity}?blade=kibanasecuritymessage`;
return (
<I18nProvider>

View file

@ -5,75 +5,176 @@
* 2.0.
*/
import type { MountPoint } from 'src/core/public';
import { docLinksServiceMock } from 'src/core/public/mocks';
import { mockSecurityOssPlugin } from 'src/plugins/security_oss/public/mocks';
import { nextTick } from '@kbn/test/jest';
import type { DocLinksStart } from 'src/core/public';
import { coreMock } from 'src/core/public/mocks';
import { insecureClusterAlertTitle } from './components';
import type { ConfigType } from '../config';
import { SecurityCheckupService } from './security_checkup_service';
let mockOnDismiss = jest.fn();
let mockOnDismissCallback: (persist: boolean) => void = jest.fn().mockImplementation(() => {
throw new Error('expected callback to be replaced!');
});
jest.mock('./components', () => {
return {
insecureClusterAlertTitle: 'mock insecure cluster title',
insecureClusterAlertText: (getDocLinksService: any, onDismiss: any) => {
mockOnDismiss = onDismiss;
const { insecureClusterAlertText } = jest.requireActual(
'./components/insecure_cluster_alert'
);
return insecureClusterAlertText(getDocLinksService, onDismiss);
insecureClusterAlertText: (
_getDocLinks: () => DocLinksStart,
onDismiss: (persist: boolean) => void
) => {
mockOnDismissCallback = onDismiss;
return 'mock insecure cluster text';
},
};
});
interface TestParams {
showInsecureClusterWarning: boolean;
displayAlert: boolean;
tenant?: string;
storageValue?: string;
}
async function setupAndStart({
showInsecureClusterWarning,
displayAlert,
tenant = '/server-base-path',
storageValue,
}: TestParams) {
const coreSetup = coreMock.createSetup();
(coreSetup.http.basePath.serverBasePath as string) = tenant;
const coreStart = coreMock.createStart();
coreStart.http.get.mockResolvedValue({ displayAlert });
coreStart.notifications.toasts.addWarning.mockReturnValue({ id: 'mock_alert_id' });
const config = { showInsecureClusterWarning } as ConfigType;
const storage = coreMock.createStorage();
if (storageValue) {
storage.getItem.mockReturnValue(storageValue);
}
const service = new SecurityCheckupService(config, storage);
service.setup(coreSetup);
service.start(coreStart);
await nextTick();
return { coreSetup, coreStart, storage };
}
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 });
describe('display scenarios', () => {
it('does not display an alert when the warning is explicitly disabled via config', async () => {
const testParams = {
showInsecureClusterWarning: false,
displayAlert: true,
};
const { coreStart, storage } = await setupAndStart(testParams);
expect(securityOssSetup.insecureCluster.setAlertTitle).toHaveBeenCalledWith(
insecureClusterAlertTitle
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 state indicates that alert should not be shown', async () => {
const testParams = {
showInsecureClusterWarning: true,
displayAlert: false,
};
const { coreStart, storage } = await setupAndStart(testParams);
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('only reads storage information from the current tenant', async () => {
const testParams = {
showInsecureClusterWarning: true,
displayAlert: false,
tenant: '/my-specific-tenant',
storageValue: JSON.stringify({ show: false }),
};
const { storage } = await setupAndStart(testParams);
expect(storage.getItem).toHaveBeenCalledTimes(1);
expect(storage.getItem).toHaveBeenCalledWith(
'insecureClusterWarningVisibility/my-specific-tenant'
);
expect(securityOssSetup.insecureCluster.setAlertText).toHaveBeenCalledTimes(1);
});
});
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, docLinks: docLinksServiceMock.createStartContract() });
expect(securityOssStart.insecureCluster.hideAlert).toHaveBeenCalledTimes(0);
mockOnDismiss();
expect(securityOssStart.insecureCluster.hideAlert).toHaveBeenCalledTimes(1);
});
it('configures the doc link correctly', async () => {
const securityOssSetup = mockSecurityOssPlugin.createSetup();
const securityOssStart = mockSecurityOssPlugin.createStart();
const service = new SecurityCheckupService();
service.setup({ securityOssSetup });
service.start({ securityOssStart, docLinks: docLinksServiceMock.createStartContract() });
it('does not display an alert when hidden via storage', async () => {
const testParams = {
showInsecureClusterWarning: true,
displayAlert: true,
storageValue: JSON.stringify({ show: false }),
};
const { coreStart, storage } = await setupAndStart(testParams);
const [alertText] = securityOssSetup.insecureCluster.setAlertText.mock.calls[0];
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();
});
const container = document.createElement('div');
(alertText as MountPoint)(container);
it('displays an alert when persisted preference is corrupted', async () => {
const testParams = {
showInsecureClusterWarning: true,
displayAlert: true,
storageValue: '{ this is a string of invalid JSON',
};
const { coreStart, storage } = await setupAndStart(testParams);
const docLink = container
.querySelector('[data-test-subj="learnMoreButton"]')
?.getAttribute('href');
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();
});
expect(docLink).toMatchInlineSnapshot(
`"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/configuring-stack-security.html?blade=kibanasecuritymessage"`
it('displays an alert when enabled via config and endpoint checks', async () => {
const testParams = {
showInsecureClusterWarning: true,
displayAlert: true,
};
const { coreStart, storage } = await setupAndStart(testParams);
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": "mock insecure cluster text",
"title": "mock insecure cluster 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 testParams = {
showInsecureClusterWarning: true,
displayAlert: true,
};
const { coreStart, storage } = await setupAndStart(testParams);
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 })
);
});
});

View file

@ -5,48 +5,128 @@
* 2.0.
*/
import type { DocLinksStart } from 'src/core/public';
import type {
SecurityOssPluginSetup,
SecurityOssPluginStart,
} from 'src/plugins/security_oss/public';
import { BehaviorSubject, combineLatest, from } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import type {
DocLinksStart,
HttpSetup,
HttpStart,
NotificationsStart,
Toast,
} from 'src/core/public';
import type { SecurityCheckupState } from '../../common/types';
import type { ConfigType } from '../config';
import { insecureClusterAlertText, insecureClusterAlertTitle } from './components';
interface SetupDeps {
securityOssSetup: SecurityOssPluginSetup;
http: HttpSetup;
}
interface StartDeps {
securityOssStart: SecurityOssPluginStart;
http: HttpStart;
notifications: NotificationsStart;
docLinks: DocLinksStart;
}
const DEFAULT_SECURITY_CHECKUP_STATE = Object.freeze<SecurityCheckupState>({
displayAlert: false,
});
export class SecurityCheckupService {
private securityOssStart?: SecurityOssPluginStart;
private enabled: boolean;
private docLinks?: DocLinksStart;
private alertVisibility$: BehaviorSubject<boolean>;
public setup({ securityOssSetup }: SetupDeps) {
securityOssSetup.insecureCluster.setAlertTitle(insecureClusterAlertTitle);
securityOssSetup.insecureCluster.setAlertText(
insecureClusterAlertText(
() => this.docLinks!,
(persist: boolean) => this.onDismiss(persist)
)
);
private storage: Storage;
private alertToast?: Toast;
private storageKey?: string;
constructor(config: Pick<ConfigType, 'showInsecureClusterWarning'>, storage: Storage) {
this.storage = storage;
this.enabled = config.showInsecureClusterWarning;
this.alertVisibility$ = new BehaviorSubject(this.enabled);
}
public start({ securityOssStart, docLinks }: StartDeps) {
this.securityOssStart = securityOssStart;
this.docLinks = docLinks;
public setup({ http }: SetupDeps) {
const tenant = http.basePath.serverBasePath;
this.storageKey = `insecureClusterWarningVisibility${tenant}`;
this.enabled = this.enabled && this.getPersistedVisibilityPreference();
this.alertVisibility$.next(this.enabled);
}
private onDismiss(persist: boolean) {
if (this.securityOssStart) {
this.securityOssStart.insecureCluster.hideAlert(persist);
public start(startDeps: StartDeps) {
if (this.enabled) {
this.initializeAlert(startDeps);
}
}
public stop() {}
private initializeAlert({ http, notifications, docLinks }: StartDeps) {
const appState$ = from(this.getSecurityCheckupState(http));
// 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([appState$, this.alertVisibility$])
.pipe(
map(([{ displayAlert }, isAlertVisible]) => displayAlert && isAlertVisible),
distinctUntilChanged()
)
.subscribe((showAlert) => {
if (showAlert && !this.alertToast) {
this.alertToast = notifications.toasts.addWarning(
{
title: insecureClusterAlertTitle,
text: insecureClusterAlertText(docLinks, (persist: boolean) =>
this.setAlertVisibility(false, persist)
),
iconType: 'alert',
},
{
toastLifeTimeMs: tenDays,
}
);
} else if (!showAlert && this.alertToast) {
notifications.toasts.remove(this.alertToast);
this.alertToast = undefined;
}
});
}
private getSecurityCheckupState(http: HttpStart) {
return http.anonymousPaths.isAnonymous(window.location.pathname)
? Promise.resolve(DEFAULT_SECURITY_CHECKUP_STATE)
: http
.get<SecurityCheckupState>('/internal/security/security_checkup/state')
.catch(() => DEFAULT_SECURITY_CHECKUP_STATE);
}
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 }));
}
}

View file

@ -1,17 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { PublicMethodsOf } from '@kbn/utility-types';
import type { SessionTimeout } from './session_timeout';
export function createSessionTimeoutMock() {
return {
start: jest.fn(),
stop: jest.fn(),
} as jest.Mocked<PublicMethodsOf<SessionTimeout>>;
}

View file

@ -65,6 +65,7 @@ describe('config schema', () => {
"idleTimeout": "PT1H",
"lifespan": "P30D",
},
"showInsecureClusterWarning": true,
}
`);
@ -117,6 +118,7 @@ describe('config schema', () => {
"idleTimeout": "PT1H",
"lifespan": "P30D",
},
"showInsecureClusterWarning": true,
}
`);
@ -168,6 +170,7 @@ describe('config schema', () => {
"idleTimeout": "PT1H",
"lifespan": "P30D",
},
"showInsecureClusterWarning": true,
}
`);
});

View file

@ -200,6 +200,7 @@ const providersConfigSchema = schema.object(
export const ConfigSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
loginAssistanceMessage: schema.string({ defaultValue: '' }),
showInsecureClusterWarning: schema.boolean({ defaultValue: true }),
loginHelp: schema.maybe(schema.string()),
cookieName: schema.string({ defaultValue: 'sid' }),
encryptionKey: schema.conditional(

View file

@ -171,6 +171,22 @@ describe('Config Deprecations', () => {
`);
});
it('renames security.showInsecureClusterWarning to xpack.security.showInsecureClusterWarning', () => {
const config = {
security: {
showInsecureClusterWarning: false,
},
};
const { messages, migrated } = applyConfigDeprecations(cloneDeep(config));
expect(migrated.security.showInsecureClusterWarning).not.toBeDefined();
expect(migrated.xpack.security.showInsecureClusterWarning).toEqual(false);
expect(messages).toMatchInlineSnapshot(`
Array [
"Setting \\"security.showInsecureClusterWarning\\" has been replaced by \\"xpack.security.showInsecureClusterWarning\\"",
]
`);
});
it('warns when using the legacy audit logger', () => {
const config = {
xpack: {

View file

@ -10,6 +10,7 @@ import type { ConfigDeprecationProvider } from 'src/core/server';
export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({
rename,
renameFromRoot,
unused,
}) => [
rename('sessionTimeout', 'session.idleTimeout'),
@ -21,6 +22,11 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({
rename('audit.appender.strategy.kind', 'audit.appender.strategy.type'),
rename('audit.appender.path', 'audit.appender.fileName'),
renameFromRoot(
'security.showInsecureClusterWarning',
'xpack.security.showInsecureClusterWarning'
),
unused('authorization.legacyFallback.enabled'),
unused('authc.saml.maxRedirectURLSize'),
// Deprecation warning for the legacy audit logger.

View file

@ -40,6 +40,7 @@ export const config: PluginConfigDescriptor<TypeOf<typeof ConfigSchema>> = {
deprecations: securityConfigDeprecationProvider,
exposeToBrowser: {
loginAssistanceMessage: true,
showInsecureClusterWarning: true,
},
};
export const plugin: PluginInitializer<

View file

@ -18,7 +18,6 @@ import type {
Plugin,
PluginInitializerContext,
} from 'src/core/server';
import type { SecurityOssPluginSetup } from 'src/plugins/security_oss/server';
import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import type {
@ -111,7 +110,6 @@ export interface PluginSetupDependencies {
licensing: LicensingPluginSetup;
taskManager: TaskManagerSetupContract;
usageCollection?: UsageCollectionSetup;
securityOss?: SecurityOssPluginSetup;
spaces?: SpacesPluginSetup;
}
@ -131,7 +129,6 @@ export class SecurityPlugin
private readonly logger: Logger;
private authorizationSetup?: AuthorizationServiceSetupInternal;
private auditSetup?: AuditServiceSetup;
private anonymousAccessStart?: AnonymousAccessServiceStart;
private configSubscription?: Subscription;
private config?: ConfigType;
@ -191,6 +188,13 @@ export class SecurityPlugin
this.initializerContext.logger.get('anonymous-access'),
this.getConfig
);
private anonymousAccessStart?: AnonymousAccessServiceStart;
private readonly getAnonymousAccess = () => {
if (!this.anonymousAccessStart) {
throw new Error(`anonymousAccessStart is not registered!`);
}
return this.anonymousAccessStart;
};
constructor(private readonly initializerContext: PluginInitializerContext) {
this.logger = this.initializerContext.logger.get();
@ -198,23 +202,17 @@ export class SecurityPlugin
public setup(
core: CoreSetup<PluginStartDependencies>,
{
features,
licensing,
taskManager,
usageCollection,
securityOss,
spaces,
}: PluginSetupDependencies
{ features, licensing, taskManager, usageCollection, spaces }: PluginSetupDependencies
) {
const config$ = this.initializerContext.config.create<TypeOf<typeof ConfigSchema>>().pipe(
map((rawConfig) =>
createConfig(rawConfig, this.initializerContext.logger.get('config'), {
isTLSEnabled: core.http.getServerInfo().protocol === 'https',
})
)
);
this.configSubscription = combineLatest([
this.initializerContext.config.create<TypeOf<typeof ConfigSchema>>().pipe(
map((rawConfig) =>
createConfig(rawConfig, this.initializerContext.logger.get('config'), {
isTLSEnabled: core.http.getServerInfo().protocol === 'https',
})
)
),
config$,
this.initializerContext.config.legacy.globalConfig$,
]).subscribe(([config, { kibana }]) => {
this.config = config;
@ -234,20 +232,6 @@ export class SecurityPlugin
license$: licensing.license$,
});
if (securityOss) {
license.features$.subscribe(({ allowRbac }) => {
const showInsecureClusterWarning = !allowRbac;
securityOss.showInsecureClusterWarning$.next(showInsecureClusterWarning);
});
securityOss.setAnonymousAccessServiceProvider(() => {
if (!this.anonymousAccessStart) {
throw new Error('AnonymousAccess service is not started!');
}
return this.anonymousAccessStart;
});
}
securityFeatures.forEach((securityFeature) =>
features.registerElasticsearchFeature(securityFeature)
);
@ -312,6 +296,7 @@ export class SecurityPlugin
httpResources: core.http.resources,
logger: this.initializerContext.logger.get('routes'),
config,
config$,
authz: this.authorizationSetup,
license,
getSession: this.getSession,
@ -319,6 +304,7 @@ export class SecurityPlugin
startServicesPromise.then((services) => services.features.getKibanaFeatures()),
getFeatureUsageService: this.getFeatureUsageService,
getAuthenticationService: this.getAuthentication,
getAnonymousAccessService: this.getAnonymousAccess,
});
return Object.freeze<SecurityPluginSetup>({

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { kibanaResponseFactory } from 'src/core/server';
import { httpServerMock } from 'src/core/server/mocks';
import { routeDefinitionParamsMock, securityRequestHandlerContextMock } from '../index.mock';
import { defineAnonymousAccessGetCapabilitiesRoutes } from './get_capabilities';
describe('GET /internal/security/anonymous_access/capabilities', () => {
it('returns anonymous access state', async () => {
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
mockRouteDefinitionParams.getAnonymousAccessService.mockReturnValue({
isAnonymousAccessEnabled: true,
accessURLParameters: new Map([['auth_provider_hint', 'anonymous1']]),
getCapabilities: jest.fn().mockResolvedValue({
navLinks: {},
management: {},
catalogue: {},
custom: { something: true },
}),
});
const mockContext = securityRequestHandlerContextMock.create();
defineAnonymousAccessGetCapabilitiesRoutes(mockRouteDefinitionParams);
const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls;
const headers = { authorization: 'foo' };
const mockRequest = httpServerMock.createKibanaRequest({
method: 'get',
path: `/internal/security/anonymous_access/capabilities`,
headers,
});
const response = await handler(mockContext, mockRequest, kibanaResponseFactory);
expect(response.status).toBe(200);
expect(response.payload).toEqual({
navLinks: {},
management: {},
catalogue: {},
custom: { something: true },
});
});
});

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { RouteDefinitionParams } from '../';
/**
* Defines route that returns capabilities of the anonymous service account.
*/
export function defineAnonymousAccessGetCapabilitiesRoutes({
router,
getAnonymousAccessService,
}: RouteDefinitionParams) {
router.get(
{ path: '/internal/security/anonymous_access/capabilities', validate: false },
async (_context, request, response) => {
const anonymousAccessService = getAnonymousAccessService();
return response.ok({ body: await anonymousAccessService.getCapabilities(request) });
}
);
}

View file

@ -0,0 +1,55 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { kibanaResponseFactory } from 'src/core/server';
import { httpServerMock } from 'src/core/server/mocks';
import type { AnonymousAccessServiceStart } from '../../anonymous_access';
import { routeDefinitionParamsMock, securityRequestHandlerContextMock } from '../index.mock';
import { defineAnonymousAccessGetStateRoutes } from './get_state';
describe('GET /internal/security/anonymous_access/state', () => {
function doMockAndTest(accessURLParameters: AnonymousAccessServiceStart['accessURLParameters']) {
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
mockRouteDefinitionParams.getAnonymousAccessService.mockReturnValue({
isAnonymousAccessEnabled: true,
accessURLParameters,
getCapabilities: jest.fn(),
});
const mockContext = securityRequestHandlerContextMock.create();
defineAnonymousAccessGetStateRoutes(mockRouteDefinitionParams);
const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls;
const headers = { authorization: 'foo' };
const mockRequest = httpServerMock.createKibanaRequest({
method: 'get',
path: `/internal/security/anonymous_access/state`,
headers,
});
return handler(mockContext, mockRequest, kibanaResponseFactory);
}
it('returns anonymous access state (with access URL parameters)', async () => {
const response = await doMockAndTest(new Map([['auth_provider_hint', 'anonymous1']]));
expect(response.status).toBe(200);
expect(response.payload).toEqual({
isEnabled: true,
accessURLParameters: { auth_provider_hint: 'anonymous1' },
});
});
it('returns anonymous access state (without access URL parameters)', async () => {
const response = await doMockAndTest(null);
expect(response.status).toBe(200);
expect(response.payload).toEqual({
isEnabled: true,
accessURLParameters: null,
});
});
});

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { RouteDefinitionParams } from '..';
import type { AnonymousAccessState } from '../../../../../../src/plugins/share/common';
/**
* Defines route that returns the state of anonymous access -- whether anonymous access is enabled, and what additional parameters should be
* added to the URL (if any).
*/
export function defineAnonymousAccessGetStateRoutes({
router,
getAnonymousAccessService,
}: RouteDefinitionParams) {
router.get(
{ path: '/internal/security/anonymous_access/state', validate: false },
async (_context, _request, response) => {
const anonymousAccessService = getAnonymousAccessService();
const accessURLParameters = anonymousAccessService.accessURLParameters
? Object.fromEntries(anonymousAccessService.accessURLParameters.entries())
: null;
const responseBody: AnonymousAccessState = {
isEnabled: anonymousAccessService.isAnonymousAccessEnabled,
accessURLParameters,
};
return response.ok({ body: responseBody });
}
);
}

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { RouteDefinitionParams } from '../';
import { defineAnonymousAccessGetCapabilitiesRoutes } from './get_capabilities';
import { defineAnonymousAccessGetStateRoutes } from './get_state';
export function defineAnonymousAccessRoutes(params: RouteDefinitionParams) {
defineAnonymousAccessGetCapabilitiesRoutes(params);
defineAnonymousAccessGetStateRoutes(params);
}

View file

@ -5,26 +5,39 @@
* 2.0.
*/
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import { httpResourcesMock, httpServiceMock, loggingSystemMock } from 'src/core/server/mocks';
import { BehaviorSubject } from 'rxjs';
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import {
coreMock,
httpResourcesMock,
httpServiceMock,
loggingSystemMock,
} from 'src/core/server/mocks';
import { licensingMock } from '../../../licensing/server/mocks';
import { licenseMock } from '../../common/licensing/index.mock';
import { authenticationServiceMock } from '../authentication/authentication_service.mock';
import { authorizationMock } from '../authorization/index.mock';
import { ConfigSchema, createConfig } from '../config';
import { sessionMock } from '../session_management/session.mock';
import type { SecurityRequestHandlerContext } from '../types';
import type { RouteDefinitionParams } from './';
export const routeDefinitionParamsMock = {
create: (config: Record<string, unknown> = {}) =>
({
create: (rawConfig: Record<string, unknown> = {}) => {
const config = createConfig(
ConfigSchema.validate(rawConfig),
loggingSystemMock.create().get(),
{ isTLSEnabled: false }
);
return {
router: httpServiceMock.createRouter(),
basePath: httpServiceMock.createBasePath(),
csp: httpServiceMock.createSetupContract().csp,
logger: loggingSystemMock.create().get(),
config: createConfig(ConfigSchema.validate(config), loggingSystemMock.create().get(), {
isTLSEnabled: false,
}),
config,
config$: new BehaviorSubject(config).asObservable(),
authz: authorizationMock.create(),
license: licenseMock.create(),
httpResources: httpResourcesMock.createRegistrar(),
@ -32,5 +45,14 @@ export const routeDefinitionParamsMock = {
getFeatureUsageService: jest.fn(),
getSession: jest.fn().mockReturnValue(sessionMock.create()),
getAuthenticationService: jest.fn().mockReturnValue(authenticationServiceMock.createStart()),
} as unknown as DeeplyMockedKeys<RouteDefinitionParams>),
getAnonymousAccessService: jest.fn(),
} as unknown as DeeplyMockedKeys<RouteDefinitionParams>;
},
};
export const securityRequestHandlerContextMock = {
create: (): SecurityRequestHandlerContext => ({
core: coreMock.createRequestHandlerContext(),
licensing: licensingMock.createRequestHandlerContext(),
}),
};

View file

@ -5,22 +5,27 @@
* 2.0.
*/
import type { Observable } from 'rxjs';
import type { PublicMethodsOf } from '@kbn/utility-types';
import type { HttpResources, IBasePath, Logger } from 'src/core/server';
import type { KibanaFeature } from '../../../features/server';
import type { SecurityLicense } from '../../common/licensing';
import type { AnonymousAccessServiceStart } from '../anonymous_access';
import type { InternalAuthenticationServiceStart } from '../authentication';
import type { AuthorizationServiceSetupInternal } from '../authorization';
import type { ConfigType } from '../config';
import type { SecurityFeatureUsageServiceStart } from '../feature_usage';
import type { Session } from '../session_management';
import type { SecurityRouter } from '../types';
import { defineAnonymousAccessRoutes } from './anonymous_access';
import { defineApiKeysRoutes } from './api_keys';
import { defineAuthenticationRoutes } from './authentication';
import { defineAuthorizationRoutes } from './authorization';
import { defineIndicesRoutes } from './indices';
import { defineRoleMappingRoutes } from './role_mapping';
import { defineSecurityCheckupGetStateRoutes } from './security_checkup';
import { defineSessionManagementRoutes } from './session_management';
import { defineUsersRoutes } from './users';
import { defineViewRoutes } from './views';
@ -34,12 +39,14 @@ export interface RouteDefinitionParams {
httpResources: HttpResources;
logger: Logger;
config: ConfigType;
config$: Observable<ConfigType>;
authz: AuthorizationServiceSetupInternal;
getSession: () => PublicMethodsOf<Session>;
license: SecurityLicense;
getFeatures: () => Promise<KibanaFeature[]>;
getFeatureUsageService: () => SecurityFeatureUsageServiceStart;
getAuthenticationService: () => InternalAuthenticationServiceStart;
getAnonymousAccessService: () => AnonymousAccessServiceStart;
}
export function defineRoutes(params: RouteDefinitionParams) {
@ -51,4 +58,6 @@ export function defineRoutes(params: RouteDefinitionParams) {
defineUsersRoutes(params);
defineRoleMappingRoutes(params);
defineViewRoutes(params);
defineAnonymousAccessRoutes(params);
defineSecurityCheckupGetStateRoutes(params);
}

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { createClusterDataCheck } from '../../security_checkup';
export const mockCreateClusterDataCheck = jest.fn() as jest.MockedFunction<
typeof createClusterDataCheck
>;
jest.mock('../../security_checkup', () => ({
createClusterDataCheck: mockCreateClusterDataCheck,
}));

View file

@ -0,0 +1,140 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
// eslint-disable-next-line import/order
import { mockCreateClusterDataCheck } from './get_state.test.mock';
import type { Observable } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import { kibanaResponseFactory } from 'src/core/server';
import { httpServerMock } from 'src/core/server/mocks';
import type { SecurityLicenseFeatures } from '../../../common/licensing';
import { licenseMock } from '../../../common/licensing/index.mock';
import { routeDefinitionParamsMock, securityRequestHandlerContextMock } from '../index.mock';
import { defineSecurityCheckupGetStateRoutes } from './get_state';
interface SetupParams {
showInsecureClusterWarning: boolean;
allowRbac: boolean;
doesClusterHaveUserData: boolean;
}
function setup({ showInsecureClusterWarning, allowRbac, doesClusterHaveUserData }: SetupParams) {
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
const configSubject = new BehaviorSubject({ showInsecureClusterWarning });
(mockRouteDefinitionParams.config$ as Observable<{ showInsecureClusterWarning: boolean }>) =
configSubject.asObservable();
const licenseWithFeatures = licenseMock.create();
const featuresSubject = new BehaviorSubject({ allowRbac } as SecurityLicenseFeatures);
licenseWithFeatures.features$ = featuresSubject.asObservable();
const mockClusterDataCheck = jest.fn().mockResolvedValue(doesClusterHaveUserData);
mockCreateClusterDataCheck.mockReturnValue(mockClusterDataCheck);
const mockContext = securityRequestHandlerContextMock.create();
defineSecurityCheckupGetStateRoutes({
...mockRouteDefinitionParams,
license: licenseWithFeatures,
});
const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls;
const headers = { authorization: 'foo' };
const mockRequest = httpServerMock.createKibanaRequest({
method: 'get',
path: `/internal/security/anonymous_access/state`,
headers,
});
return {
configSubject,
featuresSubject,
mockClusterDataCheck,
simulateRequest: () => handler(mockContext, mockRequest, kibanaResponseFactory),
};
}
describe('GET /internal/security/security_checkup/state', () => {
it('responds `displayAlert == false` if plugin is not configured to display alerts', async () => {
const { simulateRequest, mockClusterDataCheck } = setup({
showInsecureClusterWarning: false,
allowRbac: false,
doesClusterHaveUserData: true,
});
const response = await simulateRequest();
expect(response.status).toBe(200);
expect(response.payload).toEqual({ displayAlert: false });
expect(mockClusterDataCheck).not.toHaveBeenCalled();
});
it('responds `displayAlert == false` if Elasticsearch security is already enabled', async () => {
const { simulateRequest, mockClusterDataCheck } = setup({
showInsecureClusterWarning: true,
allowRbac: true,
doesClusterHaveUserData: true,
});
const response = await simulateRequest();
expect(response.status).toBe(200);
expect(response.payload).toEqual({ displayAlert: false });
expect(mockClusterDataCheck).not.toHaveBeenCalled();
});
it('responds `displayAlert == false` if the cluster does not contain user data', async () => {
const { simulateRequest, mockClusterDataCheck } = setup({
showInsecureClusterWarning: true,
allowRbac: false,
doesClusterHaveUserData: false,
});
const response = await simulateRequest();
expect(response.status).toBe(200);
expect(response.payload).toEqual({ displayAlert: false });
// since the plugin is configured to display alerts AND Elasticsearch security is disabled, we checked the cluster to see if it contained user data
expect(mockClusterDataCheck).toHaveBeenCalledTimes(1);
});
it('responds `displayAlert == true` if all conditions are met', async () => {
const { simulateRequest, mockClusterDataCheck } = setup({
showInsecureClusterWarning: true,
allowRbac: false,
doesClusterHaveUserData: true,
});
const response = await simulateRequest();
expect(response.status).toBe(200);
expect(response.payload).toEqual({ displayAlert: true });
expect(mockClusterDataCheck).toHaveBeenCalledTimes(1);
});
it('handles state changes', async () => {
const { configSubject, featuresSubject, simulateRequest, mockClusterDataCheck } = setup({
showInsecureClusterWarning: false,
allowRbac: false,
doesClusterHaveUserData: true,
});
const response1 = await simulateRequest();
expect(response1.status).toBe(200);
expect(response1.payload).toEqual({ displayAlert: false });
expect(mockClusterDataCheck).not.toHaveBeenCalled();
configSubject.next({ showInsecureClusterWarning: true }); // enable insecure cluster warning
const response2 = await simulateRequest();
expect(response2.status).toBe(200);
expect(response2.payload).toEqual({ displayAlert: true }); // now that the warning is enabled, all conditions are met and it should be displayed
expect(mockClusterDataCheck).toHaveBeenCalledTimes(1);
featuresSubject.next({ allowRbac: true } as SecurityLicenseFeatures); // enable Elasticsearch security
const response3 = await simulateRequest();
expect(response3.status).toBe(200);
expect(response3.payload).toEqual({ displayAlert: false }); // now that Elasticsearch security is enabled, we don't need to display the alert anymore
expect(mockClusterDataCheck).toHaveBeenCalledTimes(1); // we did not check the cluster for data again because Elasticsearch security is enabled
});
});

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { combineLatest } from 'rxjs';
import type { RouteDefinitionParams } from '..';
import type { SecurityCheckupState } from '../../../common/types';
import { createClusterDataCheck } from '../../security_checkup';
/**
* Defines route that returns the state of the security checkup feature.
*/
export function defineSecurityCheckupGetStateRoutes({
router,
logger,
config$,
license,
}: RouteDefinitionParams) {
let showInsecureClusterWarning = false;
combineLatest([config$, license.features$]).subscribe(([config, { allowRbac }]) => {
showInsecureClusterWarning = config.showInsecureClusterWarning && !allowRbac;
});
const doesClusterHaveUserData = createClusterDataCheck();
router.get(
{ path: '/internal/security/security_checkup/state', validate: false },
async (context, _request, response) => {
let displayAlert = false;
if (showInsecureClusterWarning) {
displayAlert = await doesClusterHaveUserData(
context.core.elasticsearch.client.asInternalUser,
logger
);
}
const state: SecurityCheckupState = {
displayAlert,
};
return response.ok({ body: state });
}
);
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { defineSecurityCheckupGetStateRoutes } from './get_state';

View file

@ -1,9 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks';

View file

@ -1,9 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ElasticsearchClient, Logger } from 'src/core/server';

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { createClusterDataCheck } from './check_cluster_data';

View file

@ -17,7 +17,7 @@
{ "path": "../../../src/plugins/home/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/management/tsconfig.json" },
{ "path": "../../../src/plugins/security_oss/tsconfig.json" },
{ "path": "../../../src/plugins/share/tsconfig.json" },
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" }
]
}

View file

@ -4455,11 +4455,6 @@
"savedObjectsManagement.view.indexPatternDoesNotExistErrorMessage": "このオブジェクトに関連付けられたインデックスパターンは現在存在しません。",
"savedObjectsManagement.view.savedObjectProblemErrorMessage": "この保存されたオブジェクトに問題があります",
"savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage": "このオブジェクトに関連付けられた保存された検索は現在存在しません。",
"security.checkup.dismissButtonText": "閉じる",
"security.checkup.dontShowAgain": "今後表示しない",
"security.checkup.insecureClusterMessage": "1 ビットを失わないでください。Elastic では無料でデータを保護できます。",
"security.checkup.insecureClusterTitle": "データが保護されていません",
"security.checkup.learnMoreButtonText": "詳細",
"share.advancedSettings.csv.quoteValuesText": "csvエクスポートに値を引用するかどうかです",
"share.advancedSettings.csv.quoteValuesTitle": "CSVの値を引用",
"share.advancedSettings.csv.separatorText": "エクスポートされた値をこの文字列で区切ります",

View file

@ -4498,11 +4498,6 @@
"savedObjectsManagement.view.indexPatternDoesNotExistErrorMessage": "与此对象关联的索引模式已不存在。",
"savedObjectsManagement.view.savedObjectProblemErrorMessage": "此已保存对象有问题",
"savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage": "与此对象关联的已保存搜索已不存在。",
"security.checkup.dismissButtonText": "关闭",
"security.checkup.dontShowAgain": "不再显示",
"security.checkup.insecureClusterMessage": "不要丢失一位。使用 Elastic免费保护您的数据。",
"security.checkup.insecureClusterTitle": "您的数据并非安全无忧",
"security.checkup.learnMoreButtonText": "了解详情",
"share.advancedSettings.csv.quoteValuesText": "在 CSV 导出中是否应使用引号引起值?",
"share.advancedSettings.csv.quoteValuesTitle": "使用引号引起 CSV 值",
"share.advancedSettings.csv.separatorText": "使用此字符串分隔导出的值",

View file

@ -19,7 +19,7 @@ export default function ({ getService }: FtrProviderContext) {
async function getAnonymousCapabilities(spaceId?: string) {
const apiResponse = await supertest
.get(`${spaceId ? `/s/${spaceId}` : ''}/internal/security_oss/anonymous_access/capabilities`)
.get(`${spaceId ? `/s/${spaceId}` : ''}/internal/security/anonymous_access/capabilities`)
.expect(200);
return Object.fromEntries(