mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Remove securityOss plugin (#113946)
This commit is contained in:
parent
920ea03829
commit
64f37e7414
89 changed files with 940 additions and 2124 deletions
|
@ -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
2
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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": []
|
||||
}
|
||||
}
|
|
@ -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}/>
|
||||
|
|
@ -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.
|
||||
|
|
|
@ -64,7 +64,6 @@ pageLoadAssetSize:
|
|||
savedObjectsTaggingOss: 20590
|
||||
searchprofiler: 67080
|
||||
security: 95864
|
||||
securityOss: 30806
|
||||
share: 99061
|
||||
snapshotRestore: 79032
|
||||
spaces: 57868
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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}'],
|
||||
};
|
|
@ -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": []
|
||||
}
|
|
@ -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,
|
||||
}),
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 };
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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);
|
|
@ -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 />`);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
};
|
|
@ -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';
|
|
@ -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';
|
|
@ -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;
|
||||
},
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 }));
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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>;
|
||||
},
|
||||
};
|
|
@ -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'
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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 }),
|
||||
});
|
|
@ -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);
|
|
@ -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",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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() {}
|
||||
}
|
|
@ -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) });
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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' },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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" }]
|
||||
}
|
|
@ -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(),
|
||||
}),
|
||||
};
|
|
@ -6,4 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export type { AppState } from './app_state';
|
||||
export type { AnonymousAccessServiceContract, AnonymousAccessState } from './types';
|
39
src/plugins/share/common/anonymous_access/types.ts
Normal file
39
src/plugins/share/common/anonymous_access/types.ts
Normal 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;
|
||||
}
|
|
@ -7,3 +7,4 @@
|
|||
*/
|
||||
|
||||
export { LocatorDefinition, LocatorPublic, useLocatorUrl, formatSearchParams } from './url_service';
|
||||
export type { AnonymousAccessServiceContract, AnonymousAccessState } from './anonymous_access';
|
||||
|
|
|
@ -9,5 +9,5 @@
|
|||
},
|
||||
"description": "Adds URL Service and sharing capabilities to Kibana",
|
||||
"requiredBundles": ["kibanaUtils"],
|
||||
"optionalPlugins": ["securityOss"]
|
||||
"optionalPlugins": []
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -44,6 +44,7 @@ const createSetupContract = (): Setup => {
|
|||
},
|
||||
url,
|
||||
navigate: jest.fn(),
|
||||
setAnonymousAccessServiceProvider: jest.fn(),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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!,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -10,6 +10,5 @@
|
|||
"references": [
|
||||
{ "path": "../../core/tsconfig.json" },
|
||||
{ "path": "../kibana_utils/tsconfig.json" },
|
||||
{ "path": "../security_oss/tsconfig.json" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -19,3 +19,7 @@ export enum LogoutReason {
|
|||
'LOGGED_OUT' = 'LOGGED_OUT',
|
||||
'UNAUTHENTICATED' = 'UNAUTHENTICATED',
|
||||
}
|
||||
|
||||
export interface SecurityCheckupState {
|
||||
displayAlert: boolean;
|
||||
}
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
8
x-pack/plugins/security/public/anonymous_access/index.ts
Normal file
8
x-pack/plugins/security/public/anonymous_access/index.ts
Normal 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';
|
|
@ -7,4 +7,5 @@
|
|||
|
||||
export interface ConfigType {
|
||||
loginAssistanceMessage: string;
|
||||
showInsecureClusterWarning: boolean;
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 }));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>>;
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -40,6 +40,7 @@ export const config: PluginConfigDescriptor<TypeOf<typeof ConfigSchema>> = {
|
|||
deprecations: securityConfigDeprecationProvider,
|
||||
exposeToBrowser: {
|
||||
loginAssistanceMessage: true,
|
||||
showInsecureClusterWarning: true,
|
||||
},
|
||||
};
|
||||
export const plugin: PluginInitializer<
|
||||
|
|
|
@ -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>({
|
||||
|
|
|
@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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) });
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 });
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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(),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}));
|
|
@ -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
|
||||
});
|
||||
});
|
|
@ -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 });
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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';
|
|
@ -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';
|
|
@ -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';
|
8
x-pack/plugins/security/server/security_checkup/index.ts
Normal file
8
x-pack/plugins/security/server/security_checkup/index.ts
Normal 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';
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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": "エクスポートされた値をこの文字列で区切ります",
|
||||
|
|
|
@ -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": "使用此字符串分隔导出的值",
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue