Skip to content

Commit

Permalink
Add Mock IDP login page and role switcher (elastic#172257)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomheymann authored Jan 12, 2024
1 parent 1865d4d commit 7bee86d
Show file tree
Hide file tree
Showing 38 changed files with 684 additions and 159 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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/url_state @elastic/ml-ui
packages/kbn-mock-idp-plugin @elastic/kibana-security
packages/kbn-mock-idp-utils @elastic/kibana-security
packages/kbn-monaco @elastic/appex-sharedux
x-pack/plugins/monitoring_collection @elastic/obs-ux-infra_services-team
x-pack/plugins/monitoring @elastic/obs-ux-infra_services-team
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,7 @@
"@kbn/managed-vscode-config-cli": "link:packages/kbn-managed-vscode-config-cli",
"@kbn/management-storybook-config": "link:packages/kbn-management/storybook/config",
"@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-generator": "link:packages/kbn-openapi-generator",
"@kbn/optimizer": "link:packages/kbn-optimizer",
Expand Down
7 changes: 6 additions & 1 deletion packages/core/http/core-http-resources-server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ export type HttpResourcesResponseOptions = HttpResponseOptions;
export interface HttpResourcesServiceToolkit {
/** To respond with HTML page bootstrapping Kibana application. */
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>;
/** To respond with a custom HTML page. */
renderHtml: (options: HttpResourcesResponseOptions) => IKibanaResponse;
Expand Down
4 changes: 2 additions & 2 deletions packages/kbn-es/src/utils/docker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
} from '../paths';
import * as waitClusterUtil from './wait_until_cluster_ready';
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');
const execa = jest.requireMock('execa');
Expand All @@ -59,7 +59,7 @@ jest.mock('./wait_for_security_index', () => ({
waitForSecurityIndex: jest.fn(),
}));

jest.mock('@kbn/mock-idp-plugin/common');
jest.mock('@kbn/mock-idp-utils');

const log = new ToolingLog();
const logWriter = new ToolingLogCollectingWriter();
Expand Down
2 changes: 1 addition & 1 deletion packages/kbn-es/src/utils/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
MOCK_IDP_ATTRIBUTE_NAME,
ensureSAMLRoleMapping,
createMockIdpMetadata,
} from '@kbn/mock-idp-plugin/common';
} from '@kbn/mock-idp-utils';

import { waitForSecurityIndex } from './wait_for_security_index';
import { createCliError } from '../errors';
Expand Down
2 changes: 1 addition & 1 deletion packages/kbn-es/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@kbn/dev-utils",
"@kbn/dev-proc-runner",
"@kbn/ci-stats-reporter",
"@kbn/mock-idp-plugin",
"@kbn/mock-idp-utils",
"@kbn/jest-serializers",
"@kbn/repo-info"
]
Expand Down
20 changes: 1 addition & 19 deletions packages/kbn-mock-idp-plugin/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,4 @@
* Side Public License, v 1.
*/

export {
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';
export { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-utils';
9 changes: 8 additions & 1 deletion packages/kbn-mock-idp-plugin/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
"plugin": {
"id": "mockIdpPlugin",
"server": true,
"browser": false
"browser": true,
"enabledOnAnonymousPages": true,
"requiredPlugins": [
"cloud"
],
"requiredBundles": [
"kibanaReact"
]
}
}
9 changes: 9 additions & 0 deletions packages/kbn-mock-idp-plugin/public/index.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 152 additions & 0 deletions packages/kbn-mock-idp-plugin/public/login_page.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 84 additions & 0 deletions packages/kbn-mock-idp-plugin/public/plugin.tsx
Original file line number Diff line number Diff line change
@@ -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() {},
});
Loading

0 comments on commit 7bee86d

Please sign in to comment.