mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
Add Mock IDP login page and role switcher (#172257)
This commit is contained in:
parent
1865d4dab4
commit
7bee86d6eb
38 changed files with 686 additions and 161 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -553,6 +553,7 @@ x-pack/packages/ml/trained_models_utils @elastic/ml-ui
|
||||||
x-pack/packages/ml/ui_actions @elastic/ml-ui
|
x-pack/packages/ml/ui_actions @elastic/ml-ui
|
||||||
x-pack/packages/ml/url_state @elastic/ml-ui
|
x-pack/packages/ml/url_state @elastic/ml-ui
|
||||||
packages/kbn-mock-idp-plugin @elastic/kibana-security
|
packages/kbn-mock-idp-plugin @elastic/kibana-security
|
||||||
|
packages/kbn-mock-idp-utils @elastic/kibana-security
|
||||||
packages/kbn-monaco @elastic/appex-sharedux
|
packages/kbn-monaco @elastic/appex-sharedux
|
||||||
x-pack/plugins/monitoring_collection @elastic/obs-ux-infra_services-team
|
x-pack/plugins/monitoring_collection @elastic/obs-ux-infra_services-team
|
||||||
x-pack/plugins/monitoring @elastic/obs-ux-infra_services-team
|
x-pack/plugins/monitoring @elastic/obs-ux-infra_services-team
|
||||||
|
|
|
@ -1269,6 +1269,7 @@
|
||||||
"@kbn/managed-vscode-config-cli": "link:packages/kbn-managed-vscode-config-cli",
|
"@kbn/managed-vscode-config-cli": "link:packages/kbn-managed-vscode-config-cli",
|
||||||
"@kbn/management-storybook-config": "link:packages/kbn-management/storybook/config",
|
"@kbn/management-storybook-config": "link:packages/kbn-management/storybook/config",
|
||||||
"@kbn/mock-idp-plugin": "link:packages/kbn-mock-idp-plugin",
|
"@kbn/mock-idp-plugin": "link:packages/kbn-mock-idp-plugin",
|
||||||
|
"@kbn/mock-idp-utils": "link:packages/kbn-mock-idp-utils",
|
||||||
"@kbn/openapi-bundler": "link:packages/kbn-openapi-bundler",
|
"@kbn/openapi-bundler": "link:packages/kbn-openapi-bundler",
|
||||||
"@kbn/openapi-generator": "link:packages/kbn-openapi-generator",
|
"@kbn/openapi-generator": "link:packages/kbn-openapi-generator",
|
||||||
"@kbn/optimizer": "link:packages/kbn-optimizer",
|
"@kbn/optimizer": "link:packages/kbn-optimizer",
|
||||||
|
|
|
@ -47,7 +47,12 @@ export type HttpResourcesResponseOptions = HttpResponseOptions;
|
||||||
export interface HttpResourcesServiceToolkit {
|
export interface HttpResourcesServiceToolkit {
|
||||||
/** To respond with HTML page bootstrapping Kibana application. */
|
/** To respond with HTML page bootstrapping Kibana application. */
|
||||||
renderCoreApp: (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse>;
|
renderCoreApp: (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse>;
|
||||||
/** To respond with HTML page bootstrapping Kibana application without retrieving user-specific information. */
|
/**
|
||||||
|
* To respond with HTML page bootstrapping Kibana application without retrieving user-specific information.
|
||||||
|
* **Note:**
|
||||||
|
* - Your client-side JavaScript bundle will only be loaded on an anonymous page if `plugin.enabledOnAnonymousPages` is enabled in your plugin's `kibana.jsonc` manifest file.
|
||||||
|
* - You will also need to register the route serving your anonymous app with the `coreSetup.http.anonymousPaths` service in your plugin's client-side `setup` method.
|
||||||
|
* */
|
||||||
renderAnonymousCoreApp: (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse>;
|
renderAnonymousCoreApp: (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse>;
|
||||||
/** To respond with a custom HTML page. */
|
/** To respond with a custom HTML page. */
|
||||||
renderHtml: (options: HttpResourcesResponseOptions) => IKibanaResponse;
|
renderHtml: (options: HttpResourcesResponseOptions) => IKibanaResponse;
|
||||||
|
|
|
@ -41,7 +41,7 @@ import {
|
||||||
} from '../paths';
|
} from '../paths';
|
||||||
import * as waitClusterUtil from './wait_until_cluster_ready';
|
import * as waitClusterUtil from './wait_until_cluster_ready';
|
||||||
import * as waitForSecurityIndexUtil from './wait_for_security_index';
|
import * as waitForSecurityIndexUtil from './wait_for_security_index';
|
||||||
import * as mockIdpPluginUtil from '@kbn/mock-idp-plugin/common';
|
import * as mockIdpPluginUtil from '@kbn/mock-idp-utils';
|
||||||
|
|
||||||
jest.mock('execa');
|
jest.mock('execa');
|
||||||
const execa = jest.requireMock('execa');
|
const execa = jest.requireMock('execa');
|
||||||
|
@ -59,7 +59,7 @@ jest.mock('./wait_for_security_index', () => ({
|
||||||
waitForSecurityIndex: jest.fn(),
|
waitForSecurityIndex: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('@kbn/mock-idp-plugin/common');
|
jest.mock('@kbn/mock-idp-utils');
|
||||||
|
|
||||||
const log = new ToolingLog();
|
const log = new ToolingLog();
|
||||||
const logWriter = new ToolingLogCollectingWriter();
|
const logWriter = new ToolingLogCollectingWriter();
|
||||||
|
|
|
@ -24,7 +24,7 @@ import {
|
||||||
MOCK_IDP_ATTRIBUTE_NAME,
|
MOCK_IDP_ATTRIBUTE_NAME,
|
||||||
ensureSAMLRoleMapping,
|
ensureSAMLRoleMapping,
|
||||||
createMockIdpMetadata,
|
createMockIdpMetadata,
|
||||||
} from '@kbn/mock-idp-plugin/common';
|
} from '@kbn/mock-idp-utils';
|
||||||
|
|
||||||
import { waitForSecurityIndex } from './wait_for_security_index';
|
import { waitForSecurityIndex } from './wait_for_security_index';
|
||||||
import { createCliError } from '../errors';
|
import { createCliError } from '../errors';
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
"@kbn/dev-utils",
|
"@kbn/dev-utils",
|
||||||
"@kbn/dev-proc-runner",
|
"@kbn/dev-proc-runner",
|
||||||
"@kbn/ci-stats-reporter",
|
"@kbn/ci-stats-reporter",
|
||||||
"@kbn/mock-idp-plugin",
|
"@kbn/mock-idp-utils",
|
||||||
"@kbn/jest-serializers",
|
"@kbn/jest-serializers",
|
||||||
"@kbn/repo-info"
|
"@kbn/repo-info"
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,22 +6,4 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export {
|
export { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-utils';
|
||||||
MOCK_IDP_PLUGIN_PATH,
|
|
||||||
MOCK_IDP_METADATA_PATH,
|
|
||||||
MOCK_IDP_LOGIN_PATH,
|
|
||||||
MOCK_IDP_LOGOUT_PATH,
|
|
||||||
MOCK_IDP_REALM_NAME,
|
|
||||||
MOCK_IDP_ENTITY_ID,
|
|
||||||
MOCK_IDP_ROLE_MAPPING_NAME,
|
|
||||||
MOCK_IDP_ATTRIBUTE_PRINCIPAL,
|
|
||||||
MOCK_IDP_ATTRIBUTE_ROLES,
|
|
||||||
MOCK_IDP_ATTRIBUTE_EMAIL,
|
|
||||||
MOCK_IDP_ATTRIBUTE_NAME,
|
|
||||||
} from './constants';
|
|
||||||
export {
|
|
||||||
createMockIdpMetadata,
|
|
||||||
createSAMLResponse,
|
|
||||||
ensureSAMLRoleMapping,
|
|
||||||
parseSAMLAuthnRequest,
|
|
||||||
} from './utils';
|
|
||||||
|
|
|
@ -6,6 +6,13 @@
|
||||||
"plugin": {
|
"plugin": {
|
||||||
"id": "mockIdpPlugin",
|
"id": "mockIdpPlugin",
|
||||||
"server": true,
|
"server": true,
|
||||||
"browser": false
|
"browser": true,
|
||||||
|
"enabledOnAnonymousPages": true,
|
||||||
|
"requiredPlugins": [
|
||||||
|
"cloud"
|
||||||
|
],
|
||||||
|
"requiredBundles": [
|
||||||
|
"kibanaReact"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
9
packages/kbn-mock-idp-plugin/public/index.ts
Normal file
9
packages/kbn-mock-idp-plugin/public/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* 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 { plugin } from './plugin';
|
152
packages/kbn-mock-idp-plugin/public/login_page.tsx
Normal file
152
packages/kbn-mock-idp-plugin/public/login_page.tsx
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
/*
|
||||||
|
* 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,
|
||||||
|
EuiPageTemplate,
|
||||||
|
EuiEmptyPrompt,
|
||||||
|
EuiComboBox,
|
||||||
|
EuiInlineEditTitle,
|
||||||
|
EuiFormRow,
|
||||||
|
EuiSpacer,
|
||||||
|
EuiComboBoxOptionOption,
|
||||||
|
EuiButtonEmpty,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import React, { ChangeEvent, FunctionComponent } from 'react';
|
||||||
|
import { FormikProvider, useFormik, Field, Form } from 'formik';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MOCK_IDP_SECURITY_ROLE_NAMES,
|
||||||
|
MOCK_IDP_OBSERVABILITY_ROLE_NAMES,
|
||||||
|
MOCK_IDP_SEARCH_ROLE_NAMES,
|
||||||
|
} from '@kbn/mock-idp-utils/src/constants';
|
||||||
|
import { useAuthenticator } from './role_switcher';
|
||||||
|
|
||||||
|
export interface LoginPageProps {
|
||||||
|
projectType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoginPage: FunctionComponent<LoginPageProps> = ({ projectType }) => {
|
||||||
|
const roles =
|
||||||
|
projectType === 'security'
|
||||||
|
? MOCK_IDP_SECURITY_ROLE_NAMES
|
||||||
|
: projectType === 'observability'
|
||||||
|
? MOCK_IDP_OBSERVABILITY_ROLE_NAMES
|
||||||
|
: MOCK_IDP_SEARCH_ROLE_NAMES;
|
||||||
|
|
||||||
|
const [, switchCurrentUser] = useAuthenticator(true);
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues: {
|
||||||
|
full_name: 'Test User',
|
||||||
|
role: roles[0],
|
||||||
|
},
|
||||||
|
async onSubmit(values) {
|
||||||
|
await switchCurrentUser({
|
||||||
|
username: sanitizeUsername(values.full_name),
|
||||||
|
full_name: values.full_name,
|
||||||
|
email: sanitizeEmail(values.full_name),
|
||||||
|
roles: [values.role],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormikProvider value={formik}>
|
||||||
|
<EuiPageTemplate panelled={false}>
|
||||||
|
<EuiPageTemplate.Section alignment="center">
|
||||||
|
<Form>
|
||||||
|
<EuiEmptyPrompt
|
||||||
|
iconType="user"
|
||||||
|
layout="vertical"
|
||||||
|
color="plain"
|
||||||
|
body={
|
||||||
|
<>
|
||||||
|
<Field
|
||||||
|
as={EuiInlineEditTitle}
|
||||||
|
name="full_name"
|
||||||
|
heading="h2"
|
||||||
|
inputAriaLabel="Edit name inline"
|
||||||
|
value={formik.values.full_name}
|
||||||
|
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
formik.setFieldValue('full_name', event.target.value);
|
||||||
|
}}
|
||||||
|
onCancel={(previousValue: string) => {
|
||||||
|
formik.setFieldValue('full_name', previousValue);
|
||||||
|
}}
|
||||||
|
isReadOnly={formik.isSubmitting}
|
||||||
|
editModeProps={{
|
||||||
|
formRowProps: {
|
||||||
|
error: formik.errors.full_name,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
validate={(value: string) => {
|
||||||
|
if (value.trim().length === 0) {
|
||||||
|
return 'Name cannot be empty';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isInvalid={!!formik.errors.full_name}
|
||||||
|
placeholder="Enter your name"
|
||||||
|
css={{ width: 350 }}
|
||||||
|
/>
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
|
||||||
|
<EuiFormRow error={formik.errors.role} isInvalid={!!formik.errors.role}>
|
||||||
|
<Field
|
||||||
|
as={EuiComboBox}
|
||||||
|
name="role"
|
||||||
|
placeholder="Select your role"
|
||||||
|
singleSelection={{ asPlainText: true }}
|
||||||
|
options={roles.map((role) => ({ label: role }))}
|
||||||
|
selectedOptions={
|
||||||
|
formik.values.role ? [{ label: formik.values.role }] : undefined
|
||||||
|
}
|
||||||
|
onCreateOption={(value: string) => {
|
||||||
|
formik.setFieldValue('role', value);
|
||||||
|
}}
|
||||||
|
onChange={(selectedOptions: EuiComboBoxOptionOption[]) => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'role',
|
||||||
|
selectedOptions.length === 0 ? '' : selectedOptions[0].label
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
validate={(value: string) => {
|
||||||
|
if (value.trim().length === 0) {
|
||||||
|
return 'Role cannot be empty';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isInvalid={!!formik.errors.role}
|
||||||
|
isClearable={false}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
actions={[
|
||||||
|
<EuiButton
|
||||||
|
type="submit"
|
||||||
|
disabled={!formik.isValid}
|
||||||
|
isLoading={formik.isSubmitting}
|
||||||
|
fill
|
||||||
|
>
|
||||||
|
Log in
|
||||||
|
</EuiButton>,
|
||||||
|
<EuiButtonEmpty size="xs" href="/">
|
||||||
|
More login options
|
||||||
|
</EuiButtonEmpty>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</EuiPageTemplate.Section>
|
||||||
|
</EuiPageTemplate>
|
||||||
|
</FormikProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeUsername = (username: string) =>
|
||||||
|
username.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase();
|
||||||
|
const sanitizeEmail = (email: string) => `${sanitizeUsername(email)}@elastic.co`;
|
84
packages/kbn-mock-idp-plugin/public/plugin.tsx
Normal file
84
packages/kbn-mock-idp-plugin/public/plugin.tsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* 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 { PluginInitializer } from '@kbn/core-plugins-browser';
|
||||||
|
import { AppNavLinkStatus } from '@kbn/core-application-browser';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
|
||||||
|
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||||
|
import { I18nProvider } from '@kbn/i18n-react';
|
||||||
|
import { MOCK_IDP_LOGIN_PATH } from '@kbn/mock-idp-utils/src/constants';
|
||||||
|
import type { CloudStart, CloudSetup } from '@kbn/cloud-plugin/public';
|
||||||
|
import { RoleSwitcher } from './role_switcher';
|
||||||
|
|
||||||
|
export interface PluginSetupDependencies {
|
||||||
|
cloud?: CloudSetup;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginStartDependencies {
|
||||||
|
cloud?: CloudStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const plugin: PluginInitializer<
|
||||||
|
void,
|
||||||
|
void,
|
||||||
|
PluginSetupDependencies,
|
||||||
|
PluginStartDependencies
|
||||||
|
> = () => ({
|
||||||
|
setup(coreSetup, plugins) {
|
||||||
|
// Register Mock IDP login page
|
||||||
|
coreSetup.http.anonymousPaths.register(MOCK_IDP_LOGIN_PATH);
|
||||||
|
coreSetup.application.register({
|
||||||
|
id: 'mock_idp',
|
||||||
|
title: 'Mock IDP',
|
||||||
|
chromeless: true,
|
||||||
|
appRoute: MOCK_IDP_LOGIN_PATH,
|
||||||
|
navLinkStatus: AppNavLinkStatus.hidden,
|
||||||
|
mount: async (params) => {
|
||||||
|
const [[coreStart], { LoginPage }] = await Promise.all([
|
||||||
|
coreSetup.getStartServices(),
|
||||||
|
import('./login_page'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<KibanaThemeProvider theme={coreStart.theme}>
|
||||||
|
<KibanaContextProvider services={coreStart}>
|
||||||
|
<I18nProvider>
|
||||||
|
<LoginPage projectType={plugins.cloud?.serverless.projectType} />
|
||||||
|
</I18nProvider>
|
||||||
|
</KibanaContextProvider>
|
||||||
|
</KibanaThemeProvider>,
|
||||||
|
params.element
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => ReactDOM.unmountComponentAtNode(params.element);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
start(coreStart, plugins) {
|
||||||
|
// Register role switcher dropdown menu in the top right navigation of the Kibana UI
|
||||||
|
coreStart.chrome.navControls.registerRight({
|
||||||
|
order: 4000 + 1, // Make sure it comes after the user menu
|
||||||
|
mount: (element: HTMLElement) => {
|
||||||
|
ReactDOM.render(
|
||||||
|
<KibanaThemeProvider theme={coreStart.theme}>
|
||||||
|
<KibanaContextProvider services={coreStart}>
|
||||||
|
<I18nProvider>
|
||||||
|
<RoleSwitcher projectType={plugins.cloud?.serverless.projectType} />
|
||||||
|
</I18nProvider>
|
||||||
|
</KibanaContextProvider>
|
||||||
|
</KibanaThemeProvider>,
|
||||||
|
element
|
||||||
|
);
|
||||||
|
return () => ReactDOM.unmountComponentAtNode(element);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
stop() {},
|
||||||
|
});
|
50
packages/kbn-mock-idp-plugin/public/reload_page_toast.tsx
Normal file
50
packages/kbn-mock-idp-plugin/public/reload_page_toast.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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 React from 'react';
|
||||||
|
|
||||||
|
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||||
|
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||||
|
import type { ToastInput } from '@kbn/core-notifications-browser';
|
||||||
|
import type { I18nStart } from '@kbn/core-i18n-browser';
|
||||||
|
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||||
|
import type { AuthenticatedUser } from '@kbn/security-plugin-types-common';
|
||||||
|
|
||||||
|
export const DATA_TEST_SUBJ_PAGE_RELOAD_BUTTON = 'pageReloadButton';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function for returning a {@link ToastInput} for displaying a prompt for reloading the page.
|
||||||
|
* @param theme The {@link ThemeServiceStart} contract.
|
||||||
|
* @param i18nStart The {@link I18nStart} contract.
|
||||||
|
* @returns A toast.
|
||||||
|
*/
|
||||||
|
export const createReloadPageToast = (options: {
|
||||||
|
user: Pick<AuthenticatedUser, 'roles'>;
|
||||||
|
theme: ThemeServiceStart;
|
||||||
|
i18n: I18nStart;
|
||||||
|
}): ToastInput => {
|
||||||
|
return {
|
||||||
|
title: `Your role has been set to '${options.user.roles.join(`', '`)}'.`,
|
||||||
|
text: toMountPoint(
|
||||||
|
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButton
|
||||||
|
size="s"
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
data-test-subj={DATA_TEST_SUBJ_PAGE_RELOAD_BUTTON}
|
||||||
|
>
|
||||||
|
Reload page
|
||||||
|
</EuiButton>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>,
|
||||||
|
{ i18n: options.i18n, theme: options.theme }
|
||||||
|
),
|
||||||
|
color: 'success',
|
||||||
|
toastLifeTimeMs: 0x7fffffff, // Do not auto-hide toast since page is in an unknown state
|
||||||
|
};
|
||||||
|
};
|
168
packages/kbn-mock-idp-plugin/public/role_switcher.tsx
Normal file
168
packages/kbn-mock-idp-plugin/public/role_switcher.tsx
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
/*
|
||||||
|
* 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 React, { FunctionComponent, useEffect, useState } from 'react';
|
||||||
|
import { EuiButton, EuiPopover, EuiContextMenu } from '@elastic/eui';
|
||||||
|
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||||
|
import { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||||
|
import useAsyncFn from 'react-use/lib/useAsyncFn';
|
||||||
|
import type { AuthenticatedUser } from '@kbn/security-plugin-types-common';
|
||||||
|
import {
|
||||||
|
MOCK_IDP_REALM_NAME,
|
||||||
|
MOCK_IDP_REALM_TYPE,
|
||||||
|
MOCK_IDP_SECURITY_ROLE_NAMES,
|
||||||
|
MOCK_IDP_OBSERVABILITY_ROLE_NAMES,
|
||||||
|
MOCK_IDP_SEARCH_ROLE_NAMES,
|
||||||
|
} from '@kbn/mock-idp-utils/src/constants';
|
||||||
|
import { createReloadPageToast } from './reload_page_toast';
|
||||||
|
import type { CreateSAMLResponseParams } from '../server';
|
||||||
|
|
||||||
|
const useCurrentUser = () => {
|
||||||
|
const { services } = useKibana<CoreStart>();
|
||||||
|
return useAsyncFn(() => services.http.get<AuthenticatedUser>('/internal/security/me'));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuthenticator = (reloadPage = false) => {
|
||||||
|
const { services } = useKibana<CoreStart>();
|
||||||
|
|
||||||
|
return useAsyncFn(async (params: CreateSAMLResponseParams) => {
|
||||||
|
// Create SAML Response using Mock IDP
|
||||||
|
const response = await services.http.post<Record<string, string>>('/mock_idp/saml_response', {
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Authenticate user with SAML response
|
||||||
|
if (reloadPage) {
|
||||||
|
const form = createForm('/api/security/saml/callback', response);
|
||||||
|
form.submit();
|
||||||
|
await new Promise(() => {}); // Never resolve
|
||||||
|
} else {
|
||||||
|
await services.http.post('/api/security/saml/callback', {
|
||||||
|
body: JSON.stringify(response),
|
||||||
|
asResponse: true,
|
||||||
|
rawResponse: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface RoleSwitcherProps {
|
||||||
|
projectType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RoleSwitcher: FunctionComponent<RoleSwitcherProps> = ({ projectType }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { services } = useKibana<CoreStart>();
|
||||||
|
const [currentUserState, getCurrentUser] = useCurrentUser();
|
||||||
|
const [authenticateUserState, authenticateUser] = useAuthenticator();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCurrentUser();
|
||||||
|
}, [getCurrentUser, authenticateUserState.value]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (authenticateUserState.value) {
|
||||||
|
services.notifications.toasts.add(
|
||||||
|
createReloadPageToast({
|
||||||
|
user: authenticateUserState.value,
|
||||||
|
theme: services.theme,
|
||||||
|
i18n: services.i18n,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [authenticateUserState.value]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
if (!currentUserState.value || !isAuthenticatedWithMockIDP(currentUserState.value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [currentRole] = currentUserState.value.roles;
|
||||||
|
|
||||||
|
const roles =
|
||||||
|
projectType === 'security'
|
||||||
|
? MOCK_IDP_SECURITY_ROLE_NAMES
|
||||||
|
: projectType === 'observability'
|
||||||
|
? MOCK_IDP_OBSERVABILITY_ROLE_NAMES
|
||||||
|
: MOCK_IDP_SEARCH_ROLE_NAMES;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiPopover
|
||||||
|
button={
|
||||||
|
<EuiButton
|
||||||
|
color="text"
|
||||||
|
size="s"
|
||||||
|
iconType="arrowDown"
|
||||||
|
iconSide="right"
|
||||||
|
minWidth={false}
|
||||||
|
onClick={() => setIsOpen((toggle) => !toggle)}
|
||||||
|
isLoading={currentUserState.loading || authenticateUserState.loading}
|
||||||
|
>
|
||||||
|
{currentRole}
|
||||||
|
</EuiButton>
|
||||||
|
}
|
||||||
|
panelPaddingSize="none"
|
||||||
|
offset={4}
|
||||||
|
anchorPosition="downRight"
|
||||||
|
repositionOnScroll
|
||||||
|
repositionToCrossAxis={false}
|
||||||
|
isOpen={isOpen}
|
||||||
|
closePopover={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
<EuiContextMenu
|
||||||
|
initialPanelId={0}
|
||||||
|
panels={[
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
title: 'Switch role',
|
||||||
|
items: roles.map((role) => ({
|
||||||
|
name: role,
|
||||||
|
icon: currentUserState.value!.roles.includes(role) ? 'check' : 'empty',
|
||||||
|
onClick: () => {
|
||||||
|
authenticateUser({
|
||||||
|
username: currentUserState.value!.username,
|
||||||
|
full_name: currentUserState.value!.full_name,
|
||||||
|
email: currentUserState.value!.email,
|
||||||
|
roles: [role],
|
||||||
|
});
|
||||||
|
setIsOpen(false);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</EuiPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function isAuthenticatedWithMockIDP(user: AuthenticatedUser) {
|
||||||
|
return (
|
||||||
|
user.authentication_provider.type === MOCK_IDP_REALM_TYPE &&
|
||||||
|
user.authentication_provider.name === MOCK_IDP_REALM_NAME
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createForm = (url: string, fields: Record<string, string>) => {
|
||||||
|
const form = document.createElement('form');
|
||||||
|
form.setAttribute('method', 'post');
|
||||||
|
form.setAttribute('action', url);
|
||||||
|
|
||||||
|
for (const key in fields) {
|
||||||
|
if (!fields.hasOwnProperty(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.setAttribute('type', 'hidden');
|
||||||
|
input.setAttribute('name', key);
|
||||||
|
input.setAttribute('value', fields[key]);
|
||||||
|
form.appendChild(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
return document.body.appendChild(form);
|
||||||
|
};
|
|
@ -6,4 +6,5 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export type { CreateSAMLResponseParams } from './plugin';
|
||||||
export { plugin } from './plugin';
|
export { plugin } from './plugin';
|
||||||
|
|
|
@ -8,102 +8,59 @@
|
||||||
|
|
||||||
import type { PluginInitializer, Plugin } from '@kbn/core-plugins-server';
|
import type { PluginInitializer, Plugin } from '@kbn/core-plugins-server';
|
||||||
import { schema } from '@kbn/config-schema';
|
import { schema } from '@kbn/config-schema';
|
||||||
|
import type { TypeOf } from '@kbn/config-schema';
|
||||||
|
import { MOCK_IDP_LOGIN_PATH, MOCK_IDP_LOGOUT_PATH, createSAMLResponse } from '@kbn/mock-idp-utils';
|
||||||
|
|
||||||
import {
|
const createSAMLResponseSchema = schema.object({
|
||||||
MOCK_IDP_LOGIN_PATH,
|
username: schema.string(),
|
||||||
MOCK_IDP_LOGOUT_PATH,
|
full_name: schema.maybe(schema.nullable(schema.string())),
|
||||||
createSAMLResponse,
|
email: schema.maybe(schema.nullable(schema.string())),
|
||||||
parseSAMLAuthnRequest,
|
roles: schema.arrayOf(schema.string()),
|
||||||
} from '../common';
|
});
|
||||||
|
|
||||||
|
export type CreateSAMLResponseParams = TypeOf<typeof createSAMLResponseSchema>;
|
||||||
|
|
||||||
export const plugin: PluginInitializer<void, void> = async (): Promise<Plugin> => ({
|
export const plugin: PluginInitializer<void, void> = async (): Promise<Plugin> => ({
|
||||||
setup(core) {
|
setup(core) {
|
||||||
|
const router = core.http.createRouter();
|
||||||
|
|
||||||
core.http.resources.register(
|
core.http.resources.register(
|
||||||
{
|
{
|
||||||
path: MOCK_IDP_LOGIN_PATH,
|
path: MOCK_IDP_LOGIN_PATH,
|
||||||
|
validate: false,
|
||||||
|
options: { authRequired: false },
|
||||||
|
},
|
||||||
|
async (context, request, response) => {
|
||||||
|
return response.renderAnonymousCoreApp();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
{
|
||||||
|
path: '/mock_idp/saml_response',
|
||||||
validate: {
|
validate: {
|
||||||
query: schema.object({
|
body: createSAMLResponseSchema,
|
||||||
SAMLRequest: schema.string(),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
options: { authRequired: false },
|
options: { authRequired: false },
|
||||||
},
|
},
|
||||||
async (context, request, response) => {
|
async (context, request, response) => {
|
||||||
let samlRequest: Awaited<ReturnType<typeof parseSAMLAuthnRequest>>;
|
const { protocol, hostname, port } = core.http.getServerInfo();
|
||||||
try {
|
const pathname = core.http.basePath.prepend('/api/security/saml/callback');
|
||||||
samlRequest = await parseSAMLAuthnRequest(request.query.SAMLRequest);
|
|
||||||
} catch (error) {
|
|
||||||
return response.badRequest({
|
|
||||||
body: '[request query.SAMLRequest]: value is not valid SAMLRequest.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const userRoles: Array<[string, string]> = [
|
return response.ok({
|
||||||
['system_indices_superuser', 'system_indices_superuser'],
|
body: {
|
||||||
['t1_analyst', 't1_analyst'],
|
SAMLResponse: await createSAMLResponse({
|
||||||
['t2_analyst', 't2_analyst'],
|
kibanaUrl: `${protocol}://${hostname}:${port}${pathname}`,
|
||||||
['t3_analyst', 't3_analyst'],
|
username: request.body.username,
|
||||||
['threat_intelligence_analyst', 'threat_intelligence_analyst'],
|
full_name: request.body.full_name ?? undefined,
|
||||||
['rule_author', 'rule_author'],
|
email: request.body.email ?? undefined,
|
||||||
['soc_manager', 'soc_manager'],
|
roles: request.body.roles,
|
||||||
['detections_admin', 'detections_admin'],
|
}),
|
||||||
['platform_engineer', 'platform_engineer'],
|
},
|
||||||
['endpoint_operations_analyst', 'endpoint_operations_analyst'],
|
|
||||||
['endpoint_policy_manager', 'endpoint_policy_manager'],
|
|
||||||
];
|
|
||||||
|
|
||||||
const samlResponses = await Promise.all(
|
|
||||||
userRoles.map(([username, role]) =>
|
|
||||||
createSAMLResponse({
|
|
||||||
authnRequestId: samlRequest.ID,
|
|
||||||
kibanaUrl: samlRequest.AssertionConsumerServiceURL,
|
|
||||||
username,
|
|
||||||
roles: [role],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.renderHtml({
|
|
||||||
body: `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<title>Mock Identity Provider</title>
|
|
||||||
<link rel="icon" href="data:,">
|
|
||||||
<body>
|
|
||||||
<h2>Mock Identity Provider</h2>
|
|
||||||
<form id="loginForm" method="post" action="${
|
|
||||||
samlRequest.AssertionConsumerServiceURL
|
|
||||||
}">
|
|
||||||
<h3>Pick a role:<h3>
|
|
||||||
<ul>
|
|
||||||
${userRoles
|
|
||||||
.map(
|
|
||||||
([username], i) =>
|
|
||||||
`
|
|
||||||
<li>
|
|
||||||
<button name="SAMLResponse" value="${samlResponses[i]}">${username}</button>
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join('')}
|
|
||||||
</ul>
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
`,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
core.http.resources.register(
|
|
||||||
{
|
|
||||||
path: `${MOCK_IDP_LOGIN_PATH}/submit.js`,
|
|
||||||
validate: false,
|
|
||||||
options: { authRequired: false },
|
|
||||||
},
|
|
||||||
(context, request, response) => {
|
|
||||||
return response.renderJs({ body: 'document.getElementById("loginForm").submit();' });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
core.http.resources.register(
|
core.http.resources.register(
|
||||||
{
|
{
|
||||||
path: MOCK_IDP_LOGOUT_PATH,
|
path: MOCK_IDP_LOGOUT_PATH,
|
||||||
|
|
|
@ -11,8 +11,20 @@
|
||||||
"target/**/*"
|
"target/**/*"
|
||||||
],
|
],
|
||||||
"kbn_references": [
|
"kbn_references": [
|
||||||
"@kbn/core-plugins-server",
|
|
||||||
"@kbn/config-schema",
|
"@kbn/config-schema",
|
||||||
"@kbn/dev-utils"
|
"@kbn/core-application-browser",
|
||||||
|
"@kbn/core-i18n-browser",
|
||||||
|
"@kbn/core-lifecycle-browser",
|
||||||
|
"@kbn/core-notifications-browser",
|
||||||
|
"@kbn/core-plugins-browser",
|
||||||
|
"@kbn/core-plugins-server",
|
||||||
|
"@kbn/core-theme-browser",
|
||||||
|
"@kbn/i18n-react",
|
||||||
|
"@kbn/kibana-react-plugin",
|
||||||
|
"@kbn/react-kibana-context-theme",
|
||||||
|
"@kbn/react-kibana-mount",
|
||||||
|
"@kbn/security-plugin-types-common",
|
||||||
|
"@kbn/mock-idp-utils",
|
||||||
|
"@kbn/cloud-plugin",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
9
packages/kbn-mock-idp-utils/index.ts
Normal file
9
packages/kbn-mock-idp-utils/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* 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 * from './src';
|
6
packages/kbn-mock-idp-utils/kibana.jsonc
Normal file
6
packages/kbn-mock-idp-utils/kibana.jsonc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"type": "shared-common",
|
||||||
|
"id": "@kbn/mock-idp-utils",
|
||||||
|
"owner": "@elastic/kibana-security",
|
||||||
|
"devOnly": true,
|
||||||
|
}
|
6
packages/kbn-mock-idp-utils/package.json
Normal file
6
packages/kbn-mock-idp-utils/package.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "@kbn/mock-idp-utils",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||||
|
}
|
|
@ -6,15 +6,11 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { resolve } from 'path';
|
|
||||||
|
|
||||||
export const MOCK_IDP_PLUGIN_PATH = resolve(__dirname, '..');
|
|
||||||
export const MOCK_IDP_METADATA_PATH = resolve(MOCK_IDP_PLUGIN_PATH, 'metadata.xml');
|
|
||||||
|
|
||||||
export const MOCK_IDP_LOGIN_PATH = '/mock_idp/login';
|
export const MOCK_IDP_LOGIN_PATH = '/mock_idp/login';
|
||||||
export const MOCK_IDP_LOGOUT_PATH = '/mock_idp/logout';
|
export const MOCK_IDP_LOGOUT_PATH = '/mock_idp/logout';
|
||||||
|
|
||||||
export const MOCK_IDP_REALM_NAME = 'mock-idp';
|
export const MOCK_IDP_REALM_NAME = 'mock-idp';
|
||||||
|
export const MOCK_IDP_REALM_TYPE = 'saml';
|
||||||
export const MOCK_IDP_ENTITY_ID = 'urn:mock-idp'; // Must match `entityID` in `metadata.xml`
|
export const MOCK_IDP_ENTITY_ID = 'urn:mock-idp'; // Must match `entityID` in `metadata.xml`
|
||||||
export const MOCK_IDP_ROLE_MAPPING_NAME = 'mock-idp-mapping';
|
export const MOCK_IDP_ROLE_MAPPING_NAME = 'mock-idp-mapping';
|
||||||
|
|
||||||
|
@ -22,3 +18,20 @@ export const MOCK_IDP_ATTRIBUTE_PRINCIPAL = 'http://saml.elastic-cloud.com/attri
|
||||||
export const MOCK_IDP_ATTRIBUTE_ROLES = 'http://saml.elastic-cloud.com/attributes/roles';
|
export const MOCK_IDP_ATTRIBUTE_ROLES = 'http://saml.elastic-cloud.com/attributes/roles';
|
||||||
export const MOCK_IDP_ATTRIBUTE_EMAIL = 'http://saml.elastic-cloud.com/attributes/email';
|
export const MOCK_IDP_ATTRIBUTE_EMAIL = 'http://saml.elastic-cloud.com/attributes/email';
|
||||||
export const MOCK_IDP_ATTRIBUTE_NAME = 'http://saml.elastic-cloud.com/attributes/name';
|
export const MOCK_IDP_ATTRIBUTE_NAME = 'http://saml.elastic-cloud.com/attributes/name';
|
||||||
|
|
||||||
|
/** List of roles from `packages/kbn-es/src/serverless_resources/roles.yml` */
|
||||||
|
export const MOCK_IDP_SEARCH_ROLE_NAMES = ['viewer', 'editor', 'system_indices_superuser'];
|
||||||
|
export const MOCK_IDP_OBSERVABILITY_ROLE_NAMES = ['viewer', 'editor', 'system_indices_superuser'];
|
||||||
|
export const MOCK_IDP_SECURITY_ROLE_NAMES = [
|
||||||
|
't1_analyst',
|
||||||
|
't2_analyst',
|
||||||
|
't3_analyst',
|
||||||
|
'threat_intelligence_analyst',
|
||||||
|
'rule_author',
|
||||||
|
'soc_manager',
|
||||||
|
'detections_admin',
|
||||||
|
'platform_engineer',
|
||||||
|
'endpoint_operations_analyst',
|
||||||
|
'endpoint_policy_manager',
|
||||||
|
'system_indices_superuser',
|
||||||
|
];
|
25
packages/kbn-mock-idp-utils/src/index.ts
Normal file
25
packages/kbn-mock-idp-utils/src/index.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
MOCK_IDP_LOGIN_PATH,
|
||||||
|
MOCK_IDP_LOGOUT_PATH,
|
||||||
|
MOCK_IDP_REALM_NAME,
|
||||||
|
MOCK_IDP_REALM_TYPE,
|
||||||
|
MOCK_IDP_ENTITY_ID,
|
||||||
|
MOCK_IDP_ROLE_MAPPING_NAME,
|
||||||
|
MOCK_IDP_ATTRIBUTE_PRINCIPAL,
|
||||||
|
MOCK_IDP_ATTRIBUTE_ROLES,
|
||||||
|
MOCK_IDP_ATTRIBUTE_EMAIL,
|
||||||
|
MOCK_IDP_ATTRIBUTE_NAME,
|
||||||
|
MOCK_IDP_SEARCH_ROLE_NAMES,
|
||||||
|
MOCK_IDP_OBSERVABILITY_ROLE_NAMES,
|
||||||
|
MOCK_IDP_SECURITY_ROLE_NAMES,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
|
export { createMockIdpMetadata, createSAMLResponse, ensureSAMLRoleMapping } from './utils';
|
|
@ -7,12 +7,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Client } from '@elastic/elasticsearch';
|
import { Client } from '@elastic/elasticsearch';
|
||||||
|
|
||||||
import { SignedXml } from 'xml-crypto';
|
import { SignedXml } from 'xml-crypto';
|
||||||
import { KBN_KEY_PATH, KBN_CERT_PATH } from '@kbn/dev-utils';
|
import { KBN_KEY_PATH, KBN_CERT_PATH } from '@kbn/dev-utils';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import zlib from 'zlib';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
import { parseString } from 'xml2js';
|
|
||||||
import { X509Certificate } from 'crypto';
|
import { X509Certificate } from 'crypto';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -27,9 +25,6 @@ import {
|
||||||
MOCK_IDP_LOGOUT_PATH,
|
MOCK_IDP_LOGOUT_PATH,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
const inflateRawAsync = promisify(zlib.inflateRaw);
|
|
||||||
const parseStringAsync = promisify(parseString);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates XML metadata for our mock identity provider.
|
* Creates XML metadata for our mock identity provider.
|
||||||
*
|
*
|
||||||
|
@ -76,7 +71,7 @@ export async function createMockIdpMetadata(kibanaUrl: string) {
|
||||||
* const samlResponse = await createSAMLResponse({
|
* const samlResponse = await createSAMLResponse({
|
||||||
* username: '1234567890',
|
* username: '1234567890',
|
||||||
* email: 'mail@elastic.co',
|
* email: 'mail@elastic.co',
|
||||||
* fullname: 'Test User',
|
* full_nname: 'Test User',
|
||||||
* roles: ['t1_analyst', 'editor'],
|
* roles: ['t1_analyst', 'editor'],
|
||||||
* })
|
* })
|
||||||
* ```
|
* ```
|
||||||
|
@ -87,7 +82,6 @@ export async function createMockIdpMetadata(kibanaUrl: string) {
|
||||||
* fetch('/api/security/saml/callback', {
|
* fetch('/api/security/saml/callback', {
|
||||||
* method: 'POST',
|
* method: 'POST',
|
||||||
* body: JSON.stringify({ SAMLResponse: samlResponse }),
|
* body: JSON.stringify({ SAMLResponse: samlResponse }),
|
||||||
* redirect: 'manual'
|
|
||||||
* })
|
* })
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
@ -97,7 +91,7 @@ export async function createSAMLResponse(options: {
|
||||||
/** ID from SAML authentication request */
|
/** ID from SAML authentication request */
|
||||||
authnRequestId?: string;
|
authnRequestId?: string;
|
||||||
username: string;
|
username: string;
|
||||||
fullname?: string;
|
full_name?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
}) {
|
}) {
|
||||||
|
@ -139,9 +133,9 @@ export async function createSAMLResponse(options: {
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
${
|
${
|
||||||
options.fullname
|
options.full_name
|
||||||
? `<saml:Attribute Name="${MOCK_IDP_ATTRIBUTE_NAME}" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
|
? `<saml:Attribute Name="${MOCK_IDP_ATTRIBUTE_NAME}" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
|
||||||
<saml:AttributeValue xsi:type="xs:string">${options.fullname}</saml:AttributeValue>
|
<saml:AttributeValue xsi:type="xs:string">${options.full_name}</saml:AttributeValue>
|
||||||
</saml:Attribute>`
|
</saml:Attribute>`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
@ -210,22 +204,3 @@ export async function ensureSAMLRoleMapping(client: Client) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SAMLAuthnRequest {
|
|
||||||
'saml2p:AuthnRequest': {
|
|
||||||
$: {
|
|
||||||
AssertionConsumerServiceURL: string;
|
|
||||||
Destination: string;
|
|
||||||
ID: string;
|
|
||||||
IssueInstant: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function parseSAMLAuthnRequest(samlRequest: string) {
|
|
||||||
const inflatedSAMLRequest = (await inflateRawAsync(Buffer.from(samlRequest, 'base64'))) as Buffer;
|
|
||||||
const parsedSAMLRequest = (await parseStringAsync(
|
|
||||||
inflatedSAMLRequest.toString()
|
|
||||||
)) as SAMLAuthnRequest;
|
|
||||||
return parsedSAMLRequest['saml2p:AuthnRequest'].$;
|
|
||||||
}
|
|
16
packages/kbn-mock-idp-utils/tsconfig.json
Normal file
16
packages/kbn-mock-idp-utils/tsconfig.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "target/types"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"target/**/*"
|
||||||
|
],
|
||||||
|
"kbn_references": [
|
||||||
|
"@kbn/dev-utils",
|
||||||
|
]
|
||||||
|
}
|
|
@ -98,6 +98,7 @@ pageLoadAssetSize:
|
||||||
mapsEms: 26072
|
mapsEms: 26072
|
||||||
metricsDataAccess: 73287
|
metricsDataAccess: 73287
|
||||||
ml: 82187
|
ml: 82187
|
||||||
|
mockIdpPlugin: 30000
|
||||||
monitoring: 80000
|
monitoring: 80000
|
||||||
navigation: 37269
|
navigation: 37269
|
||||||
newsfeed: 42228
|
newsfeed: 42228
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createSAMLResponse as createMockedSAMLResponse } from '@kbn/mock-idp-plugin/common';
|
import { createSAMLResponse as createMockedSAMLResponse } from '@kbn/mock-idp-utils';
|
||||||
import { ToolingLog } from '@kbn/tooling-log';
|
import { ToolingLog } from '@kbn/tooling-log';
|
||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
|
@ -217,7 +217,7 @@ export const createLocalSAMLSession = async (params: LocalSamlSessionParams) =>
|
||||||
const samlResponse = await createMockedSAMLResponse({
|
const samlResponse = await createMockedSAMLResponse({
|
||||||
kibanaUrl: kbnHost + '/api/security/saml/callback',
|
kibanaUrl: kbnHost + '/api/security/saml/callback',
|
||||||
username,
|
username,
|
||||||
fullname,
|
full_name: fullname,
|
||||||
email,
|
email,
|
||||||
roles: [role],
|
roles: [role],
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "target/types",
|
"outDir": "target/types",
|
||||||
"stripInternal": true,
|
"stripInternal": true,
|
||||||
"types": ["jest", "node"]
|
"types": [
|
||||||
|
"jest",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
|
@ -32,7 +35,7 @@
|
||||||
"@kbn/babel-register",
|
"@kbn/babel-register",
|
||||||
"@kbn/repo-packages",
|
"@kbn/repo-packages",
|
||||||
"@kbn/core-saved-objects-api-server",
|
"@kbn/core-saved-objects-api-server",
|
||||||
"@kbn/mock-idp-plugin",
|
"@kbn/mock-idp-utils",
|
||||||
"@kbn/code-owners",
|
"@kbn/code-owners",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ import { getConfigFromFiles } from '@kbn/config';
|
||||||
|
|
||||||
const DEV_MODE_PATH = '@kbn/cli-dev-mode';
|
const DEV_MODE_PATH = '@kbn/cli-dev-mode';
|
||||||
const DEV_MODE_SUPPORTED = canRequire(DEV_MODE_PATH);
|
const DEV_MODE_SUPPORTED = canRequire(DEV_MODE_PATH);
|
||||||
|
const MOCK_IDP_PLUGIN_PATH = '@kbn/mock-idp-plugin/common';
|
||||||
|
const MOCK_IDP_PLUGIN_SUPPORTED = canRequire(MOCK_IDP_PLUGIN_PATH);
|
||||||
|
|
||||||
function canRequire(path) {
|
function canRequire(path) {
|
||||||
try {
|
try {
|
||||||
|
@ -111,12 +113,11 @@ export function applyConfigOverrides(rawConfig, opts, extraCliOptions) {
|
||||||
setServerlessKibanaDevServiceAccountIfPossible(get, set, opts);
|
setServerlessKibanaDevServiceAccountIfPossible(get, set, opts);
|
||||||
|
|
||||||
// Load mock identity provider plugin and configure realm if supported (ES only supports SAML when run with SSL)
|
// Load mock identity provider plugin and configure realm if supported (ES only supports SAML when run with SSL)
|
||||||
if (opts.ssl && canRequire('@kbn/mock-idp-plugin/common')) {
|
if (opts.ssl && MOCK_IDP_PLUGIN_SUPPORTED) {
|
||||||
// Ensure the plugin is loaded in dynamically to exclude from production build
|
// Ensure the plugin is loaded in dynamically to exclude from production build
|
||||||
const {
|
// eslint-disable-next-line import/no-dynamic-require
|
||||||
MOCK_IDP_PLUGIN_PATH,
|
const { MOCK_IDP_REALM_NAME } = require(MOCK_IDP_PLUGIN_PATH);
|
||||||
MOCK_IDP_REALM_NAME,
|
const pluginPath = resolve(require.resolve(MOCK_IDP_PLUGIN_PATH), '..');
|
||||||
} = require('@kbn/mock-idp-plugin/common');
|
|
||||||
|
|
||||||
if (has('server.basePath')) {
|
if (has('server.basePath')) {
|
||||||
console.log(
|
console.log(
|
||||||
|
@ -125,7 +126,7 @@ export function applyConfigOverrides(rawConfig, opts, extraCliOptions) {
|
||||||
_.unset(rawConfig, 'server.basePath');
|
_.unset(rawConfig, 'server.basePath');
|
||||||
}
|
}
|
||||||
|
|
||||||
set('plugins.paths', _.compact([].concat(get('plugins.paths'), MOCK_IDP_PLUGIN_PATH)));
|
set('plugins.paths', _.compact([].concat(get('plugins.paths'), pluginPath)));
|
||||||
set(`xpack.security.authc.providers.saml.${MOCK_IDP_REALM_NAME}`, {
|
set(`xpack.security.authc.providers.saml.${MOCK_IDP_REALM_NAME}`, {
|
||||||
order: Number.MAX_SAFE_INTEGER,
|
order: Number.MAX_SAFE_INTEGER,
|
||||||
realm: MOCK_IDP_REALM_NAME,
|
realm: MOCK_IDP_REALM_NAME,
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
"@kbn/config",
|
"@kbn/config",
|
||||||
"@kbn/dev-utils",
|
"@kbn/dev-utils",
|
||||||
"@kbn/apm-config-loader",
|
"@kbn/apm-config-loader",
|
||||||
"@kbn/mock-idp-plugin",
|
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
|
@ -246,7 +246,7 @@ export class Config {
|
||||||
return getPackages(this.repoRoot).filter(
|
return getPackages(this.repoRoot).filter(
|
||||||
(p) =>
|
(p) =>
|
||||||
(this.pluginSelector.testPlugins || !p.isDevOnly()) &&
|
(this.pluginSelector.testPlugins || !p.isDevOnly()) &&
|
||||||
(!p.isPlugin() || this.pluginFilter(p))
|
(!p.isPlugin() || (this.pluginFilter(p) && !p.isDevOnly()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1100,6 +1100,8 @@
|
||||||
"@kbn/ml-url-state/*": ["x-pack/packages/ml/url_state/*"],
|
"@kbn/ml-url-state/*": ["x-pack/packages/ml/url_state/*"],
|
||||||
"@kbn/mock-idp-plugin": ["packages/kbn-mock-idp-plugin"],
|
"@kbn/mock-idp-plugin": ["packages/kbn-mock-idp-plugin"],
|
||||||
"@kbn/mock-idp-plugin/*": ["packages/kbn-mock-idp-plugin/*"],
|
"@kbn/mock-idp-plugin/*": ["packages/kbn-mock-idp-plugin/*"],
|
||||||
|
"@kbn/mock-idp-utils": ["packages/kbn-mock-idp-utils"],
|
||||||
|
"@kbn/mock-idp-utils/*": ["packages/kbn-mock-idp-utils/*"],
|
||||||
"@kbn/monaco": ["packages/kbn-monaco"],
|
"@kbn/monaco": ["packages/kbn-monaco"],
|
||||||
"@kbn/monaco/*": ["packages/kbn-monaco/*"],
|
"@kbn/monaco/*": ["packages/kbn-monaco/*"],
|
||||||
"@kbn/monitoring-collection-plugin": ["x-pack/plugins/monitoring_collection"],
|
"@kbn/monitoring-collection-plugin": ["x-pack/plugins/monitoring_collection"],
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
"plugin": {
|
"plugin": {
|
||||||
"id": "samlProviderPlugin",
|
"id": "samlProviderPlugin",
|
||||||
"server": true,
|
"server": true,
|
||||||
"browser": false
|
"browser": false,
|
||||||
|
"optionalPlugins": [
|
||||||
|
"cloud"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { PluginInitializer, Plugin } from '@kbn/core/server';
|
import type { PluginInitializer, Plugin } from '@kbn/core/server';
|
||||||
|
import { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||||
import { initRoutes } from './init_routes';
|
import { initRoutes } from './init_routes';
|
||||||
|
|
||||||
export const plugin: PluginInitializer<void, void> = async (): Promise<Plugin> => ({
|
export interface PluginSetupDependencies {
|
||||||
setup: (core) => initRoutes(core),
|
cloud?: CloudSetup;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const plugin: PluginInitializer<void, void, PluginSetupDependencies> = async (
|
||||||
|
context
|
||||||
|
): Promise<Plugin> => ({
|
||||||
|
setup: (core, plugins: PluginSetupDependencies) => initRoutes(context, core, plugins),
|
||||||
start: () => {},
|
start: () => {},
|
||||||
stop: () => {},
|
stop: () => {},
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,13 +5,18 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CoreSetup } from '@kbn/core/server';
|
import { CoreSetup, PluginInitializerContext } from '@kbn/core/server';
|
||||||
import {
|
import {
|
||||||
getSAMLResponse,
|
getSAMLResponse,
|
||||||
getSAMLRequestId,
|
getSAMLRequestId,
|
||||||
} from '@kbn/security-api-integration-helpers/saml/saml_tools';
|
} from '@kbn/security-api-integration-helpers/saml/saml_tools';
|
||||||
|
import { PluginSetupDependencies } from '.';
|
||||||
|
|
||||||
export function initRoutes(core: CoreSetup) {
|
export function initRoutes(
|
||||||
|
pluginContext: PluginInitializerContext,
|
||||||
|
core: CoreSetup,
|
||||||
|
plugins: PluginSetupDependencies
|
||||||
|
) {
|
||||||
const serverInfo = core.http.getServerInfo();
|
const serverInfo = core.http.getServerInfo();
|
||||||
core.http.resources.register(
|
core.http.resources.register(
|
||||||
{
|
{
|
||||||
|
@ -59,6 +64,26 @@ export function initRoutes(core: CoreSetup) {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// [HACK]: On CI, Kibana runs Serverless functional tests against the production Kibana build but still relies on Mock
|
||||||
|
// IdP for SAML authentication in tests. The Mock IdP SAML realm, in turn, is linked to a Mock IDP plugin in Kibana
|
||||||
|
// that's only included in development mode and not available in the production Kibana build. Until our testing
|
||||||
|
// framework can properly support all SAML flows, we should forward all relevant Mock IDP plugin endpoints to a logout
|
||||||
|
// destination normally used in the Serverless setup.
|
||||||
|
if (pluginContext.env.mode.prod) {
|
||||||
|
core.http.resources.register(
|
||||||
|
{
|
||||||
|
path: '/mock_idp/login',
|
||||||
|
validate: false,
|
||||||
|
options: { authRequired: false },
|
||||||
|
},
|
||||||
|
async (context, request, response) => {
|
||||||
|
return response.redirected({
|
||||||
|
headers: { location: plugins.cloud?.projectsUrl ?? '/login' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let attemptsCounter = 0;
|
let attemptsCounter = 0;
|
||||||
core.http.resources.register(
|
core.http.resources.register(
|
||||||
{
|
{
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
],
|
],
|
||||||
"kbn_references": [
|
"kbn_references": [
|
||||||
|
"@kbn/cloud-plugin",
|
||||||
"@kbn/core",
|
"@kbn/core",
|
||||||
"@kbn/security-api-integration-helpers",
|
"@kbn/security-api-integration-helpers",
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,7 +9,7 @@ import expect from '@kbn/expect';
|
||||||
import { parse as parseCookie } from 'tough-cookie';
|
import { parse as parseCookie } from 'tough-cookie';
|
||||||
import Url from 'url';
|
import Url from 'url';
|
||||||
|
|
||||||
import { createSAMLResponse } from '@kbn/mock-idp-plugin/common';
|
import { createSAMLResponse } from '@kbn/mock-idp-utils';
|
||||||
import { FtrProviderContext } from '../ftr_provider_context';
|
import { FtrProviderContext } from '../ftr_provider_context';
|
||||||
|
|
||||||
export function SamlToolsProvider({ getService }: FtrProviderContext) {
|
export function SamlToolsProvider({ getService }: FtrProviderContext) {
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
} from '@kbn/test';
|
} from '@kbn/test';
|
||||||
import { CA_CERT_PATH, kibanaDevServiceAccount } from '@kbn/dev-utils';
|
import { CA_CERT_PATH, kibanaDevServiceAccount } from '@kbn/dev-utils';
|
||||||
import { commonFunctionalServices } from '@kbn/ftr-common-functional-services';
|
import { commonFunctionalServices } from '@kbn/ftr-common-functional-services';
|
||||||
import { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-plugin/common';
|
import { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-utils';
|
||||||
import { services } from './services';
|
import { services } from './services';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
"@kbn/apm-synthtrace",
|
"@kbn/apm-synthtrace",
|
||||||
"@kbn/apm-synthtrace-client",
|
"@kbn/apm-synthtrace-client",
|
||||||
"@kbn/reporting-export-types-csv-common",
|
"@kbn/reporting-export-types-csv-common",
|
||||||
"@kbn/mock-idp-plugin",
|
"@kbn/mock-idp-utils",
|
||||||
"@kbn/io-ts-utils",
|
"@kbn/io-ts-utils",
|
||||||
"@kbn/log-explorer-plugin",
|
"@kbn/log-explorer-plugin",
|
||||||
"@kbn/index-management-plugin",
|
"@kbn/index-management-plugin",
|
||||||
|
|
|
@ -5260,6 +5260,10 @@
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
uid ""
|
uid ""
|
||||||
|
|
||||||
|
"@kbn/mock-idp-utils@link:packages/kbn-mock-idp-utils":
|
||||||
|
version "0.0.0"
|
||||||
|
uid ""
|
||||||
|
|
||||||
"@kbn/monaco@link:packages/kbn-monaco":
|
"@kbn/monaco@link:packages/kbn-monaco":
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
uid ""
|
uid ""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue