mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Custom Branding] Fetch custom branding on unauthenticated pages (#149207)
## Summary Addresses: https://github.com/elastic/kibana/issues/148898 This PR adds custom branding to the login page. In order to do so, `customBranding` plugin utilizes the saved object client with `SECURITY_EXTENSION_ID` excluded. This PR also adds the appropriate license check for fetching of custom branding. I've also added some minor fixes to `template.tsx` and `loading_indicator.tsx` as there were some errors in the logic and the places that I've missed in the first iteration. ### Checklist Delete any items that are not applicable to this PR. - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [X] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [X] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [X] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) ~- [X] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~ - [X] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [X] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
ae07320ca1
commit
3592cebe6e
39 changed files with 420 additions and 72 deletions
|
@ -20,11 +20,11 @@ exports[`kbnLoadingIndicator is visible when loadingCount is > 0 1`] = `
|
|||
/>
|
||||
`;
|
||||
|
||||
exports[`kbnLoadingIndicator shows EuiLoadingSpinner when showPlainSpinner is true 1`] = `
|
||||
<EuiLoadingSpinner
|
||||
aria-hidden={false}
|
||||
aria-label="Loading content"
|
||||
data-test-subj="globalLoadingIndicator"
|
||||
size="l"
|
||||
exports[`kbnLoadingIndicator shows logo image when customLogo is set 1`] = `
|
||||
<EuiImage
|
||||
alt="logo"
|
||||
aria-label="User logo"
|
||||
size={24}
|
||||
src="customLogo"
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -102,10 +102,7 @@ export function HeaderLogo({ href, navigateToApp, loadingCount$, ...observables
|
|||
defaultMessage: 'Elastic home',
|
||||
})}
|
||||
>
|
||||
<LoadingIndicator
|
||||
loadingCount$={loadingCount$!}
|
||||
showPlainSpinner={Boolean(logo || customizedLogo)}
|
||||
/>
|
||||
<LoadingIndicator loadingCount$={loadingCount$!} customLogo={logo} />
|
||||
{customizedLogo ? (
|
||||
<img src={customizedLogo} width="200" height="84" alt="custom mark" />
|
||||
) : (
|
||||
|
|
|
@ -28,9 +28,9 @@ describe('kbnLoadingIndicator', () => {
|
|||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows EuiLoadingSpinner when showPlainSpinner is true', () => {
|
||||
it('shows logo image when customLogo is set', () => {
|
||||
const wrapper = shallow(
|
||||
<LoadingIndicator loadingCount$={new BehaviorSubject(1)} showPlainSpinner={true} />
|
||||
<LoadingIndicator loadingCount$={new BehaviorSubject(1)} customLogo={'customLogo'} />
|
||||
);
|
||||
// Pause the check beyond the 250ms delay that it has
|
||||
setTimeout(() => {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiLoadingSpinner, EuiProgress, EuiIcon } from '@elastic/eui';
|
||||
import { EuiLoadingSpinner, EuiProgress, EuiIcon, EuiImage } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
@ -18,7 +18,7 @@ import './loading_indicator.scss';
|
|||
export interface LoadingIndicatorProps {
|
||||
loadingCount$: ReturnType<HttpStart['getLoadingCount$']>;
|
||||
showAsBar?: boolean;
|
||||
showPlainSpinner?: boolean;
|
||||
customLogo?: string;
|
||||
}
|
||||
|
||||
export class LoadingIndicator extends React.Component<LoadingIndicatorProps, { visible: boolean }> {
|
||||
|
@ -58,10 +58,9 @@ export class LoadingIndicator extends React.Component<LoadingIndicatorProps, { v
|
|||
render() {
|
||||
const className = classNames(!this.state.visible && 'kbnLoadingIndicator-hidden');
|
||||
|
||||
const testSubj =
|
||||
this.state.visible || this.props.showPlainSpinner
|
||||
? 'globalLoadingIndicator'
|
||||
: 'globalLoadingIndicator-hidden';
|
||||
const testSubj = this.state.visible
|
||||
? 'globalLoadingIndicator'
|
||||
: 'globalLoadingIndicator-hidden';
|
||||
|
||||
const ariaHidden = !this.state.visible;
|
||||
|
||||
|
@ -69,25 +68,37 @@ export class LoadingIndicator extends React.Component<LoadingIndicatorProps, { v
|
|||
defaultMessage: 'Loading content',
|
||||
});
|
||||
|
||||
const logo =
|
||||
this.state.visible || this.props.showPlainSpinner ? (
|
||||
<EuiLoadingSpinner
|
||||
size="l"
|
||||
data-test-subj={testSubj}
|
||||
aria-hidden={false}
|
||||
aria-label={ariaLabel}
|
||||
/>
|
||||
) : (
|
||||
<EuiIcon
|
||||
type={'logoElastic'}
|
||||
size="l"
|
||||
data-test-subj={testSubj}
|
||||
className="chrHeaderLogo__cluster"
|
||||
aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.logoAriaLabel', {
|
||||
defaultMessage: 'Elastic Logo',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
const logoImage = this.props.customLogo ? (
|
||||
<EuiImage
|
||||
src={this.props.customLogo}
|
||||
size={24}
|
||||
alt="logo"
|
||||
aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.customLogoAriaLabel', {
|
||||
defaultMessage: 'User logo',
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<EuiIcon
|
||||
type={'logoElastic'}
|
||||
size="l"
|
||||
data-test-subj={testSubj}
|
||||
className="chrHeaderLogo__cluster"
|
||||
aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.logoAriaLabel', {
|
||||
defaultMessage: 'Elastic Logo',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
const logo = this.state.visible ? (
|
||||
<EuiLoadingSpinner
|
||||
size="l"
|
||||
data-test-subj={testSubj}
|
||||
aria-hidden={false}
|
||||
aria-label={ariaLabel}
|
||||
/>
|
||||
) : (
|
||||
logoImage
|
||||
);
|
||||
|
||||
return !this.props.showAsBar ? (
|
||||
logo
|
||||
|
|
|
@ -61,4 +61,16 @@ describe('#setup', () => {
|
|||
expect(fetchFn).toHaveBeenCalledTimes(1);
|
||||
expect(customBranding).toEqual({ logo: 'myLogo' });
|
||||
});
|
||||
|
||||
it('calls fetchFn correctly when unauthenticated', async () => {
|
||||
const service = new CustomBrandingService(coreContext);
|
||||
const { register, getBrandingFor } = service.setup();
|
||||
service.start();
|
||||
const fetchFn = jest.fn();
|
||||
fetchFn.mockImplementation(() => Promise.resolve({ logo: 'myLogo' }));
|
||||
register('customBranding', fetchFn);
|
||||
const kibanaRequest: jest.Mocked<KibanaRequest> = {} as unknown as jest.Mocked<KibanaRequest>;
|
||||
await getBrandingFor(kibanaRequest, { unauthenticated: true });
|
||||
expect(fetchFn).toHaveBeenCalledWith(kibanaRequest, true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,13 +10,15 @@ import type { CustomBranding } from '@kbn/core-custom-branding-common';
|
|||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
import type { CoreContext } from '@kbn/core-base-server-internal';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface InternalCustomBrandingSetup {
|
||||
register: (pluginName: string, fetchFn: CustomBrandingFetchFn) => void;
|
||||
getBrandingFor: (request: KibanaRequest) => Promise<CustomBranding>;
|
||||
getBrandingFor: (
|
||||
request: KibanaRequest,
|
||||
options?: { unauthenticated?: boolean }
|
||||
) => Promise<CustomBranding>;
|
||||
}
|
||||
|
||||
export class CustomBrandingService {
|
||||
|
@ -55,13 +57,16 @@ export class CustomBrandingService {
|
|||
|
||||
public stop() {}
|
||||
|
||||
private getBrandingFor = async (request: KibanaRequest): Promise<CustomBranding> => {
|
||||
private getBrandingFor = async (
|
||||
request: KibanaRequest,
|
||||
options?: { unauthenticated?: boolean }
|
||||
): Promise<CustomBranding> => {
|
||||
if (!this.startCalled) {
|
||||
throw new Error('Cannot be called before #start');
|
||||
}
|
||||
if (!this.pluginName || this.pluginName !== 'customBranding' || !this.fetchFn) {
|
||||
return {};
|
||||
}
|
||||
return this.fetchFn!(request);
|
||||
return this.fetchFn!(request, Boolean(options?.unauthenticated));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,9 +13,16 @@ import type { MaybePromise } from '@kbn/utility-types';
|
|||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface CustomBrandingStart {}
|
||||
|
||||
export type CustomBrandingFetchFn = (request: KibanaRequest) => MaybePromise<CustomBranding>;
|
||||
export type CustomBrandingFetchFn = (
|
||||
request: KibanaRequest,
|
||||
unauthenticated: boolean
|
||||
) => MaybePromise<CustomBranding>;
|
||||
|
||||
/** @public */
|
||||
export interface CustomBrandingSetup {
|
||||
register: (fetchFn: CustomBrandingFetchFn) => void;
|
||||
getBrandingFor: (
|
||||
request: KibanaRequest,
|
||||
options: { unauthenticated?: boolean }
|
||||
) => Promise<CustomBranding>;
|
||||
}
|
||||
|
|
|
@ -71,5 +71,5 @@ export interface InjectedMetadata {
|
|||
user: Record<string, any>; // unreferencing UserProvidedValues here
|
||||
};
|
||||
};
|
||||
customBranding: Pick<CustomBranding, 'logo' | 'customizedLogo'>;
|
||||
customBranding: Pick<CustomBranding, 'logo' | 'customizedLogo' | 'pageTitle'>;
|
||||
}
|
||||
|
|
|
@ -198,6 +198,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
|
|||
register: (fetchFn) => {
|
||||
deps.customBranding.register(plugin.name, fetchFn);
|
||||
},
|
||||
getBrandingFor: deps.customBranding.getBrandingFor,
|
||||
},
|
||||
docLinks: deps.docLinks,
|
||||
elasticsearch: {
|
||||
|
|
|
@ -113,15 +113,18 @@ export class RenderingService {
|
|||
let branding: CustomBranding = {};
|
||||
try {
|
||||
// Only provide the clusterInfo if the request is authenticated and the elasticsearch service is available.
|
||||
if (isAuthenticated(http.auth, request) && elasticsearch) {
|
||||
const authenticated = isAuthenticated(http.auth, request);
|
||||
if (authenticated && elasticsearch) {
|
||||
clusterInfo = await firstValueFrom(
|
||||
elasticsearch.clusterInfo$.pipe(
|
||||
timeout(50), // If not available, just return undefined
|
||||
catchError(() => of({}))
|
||||
)
|
||||
);
|
||||
branding = await customBranding?.getBrandingFor(request);
|
||||
}
|
||||
branding = await customBranding?.getBrandingFor(request, {
|
||||
unauthenticated: !authenticated,
|
||||
})!;
|
||||
} catch (err) {
|
||||
// swallow error
|
||||
}
|
||||
|
@ -151,6 +154,7 @@ export class RenderingService {
|
|||
faviconSVG: branding?.faviconSVG,
|
||||
faviconPNG: branding?.faviconPNG,
|
||||
pageTitle: branding?.pageTitle,
|
||||
logo: branding?.logo,
|
||||
},
|
||||
injectedMetadata: {
|
||||
version: env.packageInfo.version,
|
||||
|
|
|
@ -34,6 +34,11 @@ export const Template: FunctionComponent<Props> = ({
|
|||
const title = customBranding.pageTitle ?? 'Elastic';
|
||||
const favIcon = customBranding.faviconSVG ?? `${uiPublicUrl}/favicons/favicon.svg`;
|
||||
const favIconPng = customBranding.faviconPNG ?? `${uiPublicUrl}/favicons/favicon.png`;
|
||||
const logo = customBranding.logo ? (
|
||||
<img src={customBranding.logo} width="64" height="64" alt="logo" />
|
||||
) : (
|
||||
<Logo />
|
||||
);
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<head>
|
||||
|
@ -67,7 +72,7 @@ export const Template: FunctionComponent<Props> = ({
|
|||
data-test-subj="kbnLoadingMessage"
|
||||
>
|
||||
<div className="kbnLoaderWrap">
|
||||
<Logo />
|
||||
{logo}
|
||||
<div
|
||||
className="kbnWelcomeText"
|
||||
data-error-message={i18n('core.ui.welcomeErrorMessage', {
|
||||
|
@ -75,14 +80,16 @@ export const Template: FunctionComponent<Props> = ({
|
|||
'Elastic did not load properly. Check the server output for more information.',
|
||||
})}
|
||||
>
|
||||
{i18n('core.ui.welcomeMessage', { defaultMessage: 'Loading Elastic' })}
|
||||
{i18n('core.ui.welcomeMessage', {
|
||||
defaultMessage: 'Loading Elastic',
|
||||
})}
|
||||
</div>
|
||||
<div className="kbnProgress" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="kbnWelcomeView" id="kbn_legacy_browser_error" style={{ display: 'none' }}>
|
||||
<Logo />
|
||||
{logo}
|
||||
|
||||
<h2 className="kbnWelcomeTitle">
|
||||
{i18n('core.ui.legacyBrowserTitle', {
|
||||
|
|
|
@ -209,6 +209,8 @@ export type {
|
|||
|
||||
export type { ToastsApi } from '@kbn/core-notifications-browser-internal';
|
||||
|
||||
export type { CustomBrandingStart, CustomBrandingSetup } from '@kbn/core-custom-branding-browser';
|
||||
|
||||
export type { ThemeServiceSetup, ThemeServiceStart, CoreTheme } from '@kbn/core-theme-browser';
|
||||
|
||||
export type {
|
||||
|
|
|
@ -530,3 +530,5 @@ export type {
|
|||
PublicHttpServiceSetup as HttpServiceSetup,
|
||||
HttpServiceSetup as BaseHttpServiceSetup,
|
||||
};
|
||||
|
||||
export type { CustomBrandingSetup } from '@kbn/core-custom-branding-server';
|
||||
|
|
|
@ -146,6 +146,8 @@
|
|||
"@kbn/core-mount-utils-browser",
|
||||
"@kbn/core-execution-context-browser",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/core-custom-branding-browser",
|
||||
"@kbn/core-custom-branding-server",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -13,10 +13,12 @@ import {
|
|||
Logger,
|
||||
Plugin,
|
||||
PluginInitializerContext,
|
||||
SECURITY_EXTENSION_ID,
|
||||
} from '@kbn/core/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { License } from '@kbn/license-api-guard-plugin/server';
|
||||
import { CustomBranding } from '@kbn/core-custom-branding-common';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { PLUGIN } from '../common/constants';
|
||||
import type { CustomBrandingRequestHandlerContext } from './types';
|
||||
import { Dependencies } from './types';
|
||||
|
@ -33,6 +35,8 @@ const settingsKeys: Array<keyof CustomBranding> = [
|
|||
export class CustomBrandingPlugin implements Plugin {
|
||||
private readonly license: License;
|
||||
private readonly logger: Logger;
|
||||
private licensingSubscription?: Subscription;
|
||||
private isValidLicense: boolean = false;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.logger = initializerContext.logger.get();
|
||||
|
@ -48,14 +52,25 @@ export class CustomBrandingPlugin implements Plugin {
|
|||
const router = core.http.createRouter<CustomBrandingRequestHandlerContext>();
|
||||
registerRoutes(router);
|
||||
|
||||
const fetchFn = async (request: KibanaRequest): Promise<CustomBranding> => {
|
||||
const fetchFn = async (
|
||||
request: KibanaRequest,
|
||||
unauthenticated: boolean
|
||||
): Promise<CustomBranding> => {
|
||||
if (!this.isValidLicense) {
|
||||
return {};
|
||||
}
|
||||
const [coreStart] = await core.getStartServices();
|
||||
const soClient = coreStart.savedObjects.getScopedClient(request);
|
||||
const soClient = unauthenticated
|
||||
? coreStart.savedObjects.getScopedClient(request, {
|
||||
excludedExtensions: [SECURITY_EXTENSION_ID],
|
||||
})
|
||||
: coreStart.savedObjects.getScopedClient(request);
|
||||
const uiSettings = coreStart.uiSettings.globalAsScopedToClient(soClient);
|
||||
return await this.getBrandingFrom(uiSettings);
|
||||
};
|
||||
|
||||
core.customBranding.register(fetchFn);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
@ -66,10 +81,15 @@ export class CustomBrandingPlugin implements Plugin {
|
|||
minimumLicenseType: PLUGIN.MINIMUM_LICENSE_REQUIRED,
|
||||
licensing,
|
||||
});
|
||||
this.licensingSubscription = licensing.license$.subscribe((next) => {
|
||||
this.isValidLicense = next.hasAtLeast('enterprise');
|
||||
});
|
||||
return {};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
public stop() {
|
||||
this.licensingSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
private getBrandingFrom = async (uiSettingsClient: IUiSettingsClient) => {
|
||||
const branding: CustomBranding = {};
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
|
||||
import './authentication_state_page.scss';
|
||||
|
||||
import { EuiIcon, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { EuiIcon, EuiImage, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
title: React.ReactNode;
|
||||
logo?: string;
|
||||
}
|
||||
|
||||
export const AuthenticationStatePage: React.FC<Props> = (props) => (
|
||||
|
@ -21,7 +22,11 @@ export const AuthenticationStatePage: React.FC<Props> = (props) => (
|
|||
<div className="secAuthenticationStatePage__content eui-textCenter">
|
||||
<EuiSpacer size="xxl" />
|
||||
<span className="secAuthenticationStatePage__logo">
|
||||
<EuiIcon type="logoElastic" size="xxl" />
|
||||
{props.logo ? (
|
||||
<EuiImage src={props.logo} size={40} alt={'logo'} />
|
||||
) : (
|
||||
<EuiIcon type="logoElastic" size="xxl" />
|
||||
)}
|
||||
</span>
|
||||
<EuiTitle size="l" className="secAuthenticationStatePage__title">
|
||||
<h1>{props.title}</h1>
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
jest.mock('./logged_out_page');
|
||||
|
||||
import type { AppMount } from '@kbn/core/public';
|
||||
|
@ -56,7 +55,7 @@ describe('loggedOutApp', () => {
|
|||
expect(mockRenderApp).toHaveBeenCalledWith(
|
||||
coreStartMock.i18n,
|
||||
{ element: appMountParams.element, theme$: appMountParams.theme$ },
|
||||
{ basePath: coreStartMock.http.basePath }
|
||||
{ basePath: coreStartMock.http.basePath, customBranding: coreStartMock.customBranding }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,7 +36,7 @@ export const loggedOutApp = Object.freeze({
|
|||
return renderLoggedOutPage(
|
||||
coreStart.i18n,
|
||||
{ element, theme$ },
|
||||
{ basePath: coreStart.http.basePath }
|
||||
{ basePath: coreStart.http.basePath, customBranding: coreStart.customBranding }
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { EuiButton } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-mocks';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
||||
|
@ -23,7 +24,10 @@ describe('LoggedOutPage', () => {
|
|||
|
||||
it('points to a base path if `next` parameter is not provided', async () => {
|
||||
const basePathMock = coreMock.createStart({ basePath: '/mock-base-path' }).http.basePath;
|
||||
const wrapper = mountWithIntl(<LoggedOutPage basePath={basePathMock} />);
|
||||
const customBranding = customBrandingServiceMock.createStartContract();
|
||||
const wrapper = mountWithIntl(
|
||||
<LoggedOutPage basePath={basePathMock} customBranding={customBranding} />
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiButton).prop('href')).toBe('/mock-base-path/');
|
||||
});
|
||||
|
@ -33,8 +37,11 @@ describe('LoggedOutPage', () => {
|
|||
'/mock-base-path/app/home#/?_g=()'
|
||||
)}`;
|
||||
|
||||
const customBranding = customBrandingServiceMock.createStartContract();
|
||||
const basePathMock = coreMock.createStart({ basePath: '/mock-base-path' }).http.basePath;
|
||||
const wrapper = mountWithIntl(<LoggedOutPage basePath={basePathMock} />);
|
||||
const wrapper = mountWithIntl(
|
||||
<LoggedOutPage basePath={basePathMock} customBranding={customBranding} />
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiButton).prop('href')).toBe('/mock-base-path/app/home#/?_g=()');
|
||||
});
|
||||
|
|
|
@ -8,8 +8,14 @@
|
|||
import { EuiButton } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
|
||||
import type { AppMountParameters, CoreStart, IBasePath } from '@kbn/core/public';
|
||||
import type {
|
||||
AppMountParameters,
|
||||
CoreStart,
|
||||
CustomBrandingStart,
|
||||
IBasePath,
|
||||
} from '@kbn/core/public';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
|
@ -18,9 +24,11 @@ import { AuthenticationStatePage } from '../components';
|
|||
|
||||
interface Props {
|
||||
basePath: IBasePath;
|
||||
customBranding: CustomBrandingStart;
|
||||
}
|
||||
|
||||
export function LoggedOutPage({ basePath }: Props) {
|
||||
export function LoggedOutPage({ basePath, customBranding }: Props) {
|
||||
const customBrandingValue = useObservable(customBranding.customBranding$);
|
||||
return (
|
||||
<AuthenticationStatePage
|
||||
title={
|
||||
|
@ -29,6 +37,7 @@ export function LoggedOutPage({ basePath }: Props) {
|
|||
defaultMessage="Successfully logged out"
|
||||
/>
|
||||
}
|
||||
logo={customBrandingValue?.logo}
|
||||
>
|
||||
<EuiButton href={parseNext(window.location.href, basePath.serverBasePath)}>
|
||||
<FormattedMessage id="xpack.security.loggedOut.login" defaultMessage="Log in" />
|
||||
|
|
|
@ -395,3 +395,91 @@ exports[`LoginPage page renders as expected 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LoginPage page renders with custom branding 1`] = `
|
||||
<div
|
||||
className="loginWelcome login-form"
|
||||
>
|
||||
<header
|
||||
className="loginWelcome__header"
|
||||
>
|
||||
<div
|
||||
className="loginWelcome__content eui-textCenter"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<span
|
||||
className="loginWelcome__logo"
|
||||
>
|
||||
<EuiImage
|
||||
alt="logo"
|
||||
size={40}
|
||||
src="logo"
|
||||
/>
|
||||
</span>
|
||||
<EuiTitle
|
||||
className="loginWelcome__title"
|
||||
data-test-subj="loginWelcomeTitle"
|
||||
size="m"
|
||||
>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Welcome to Elastic"
|
||||
id="xpack.security.loginPage.welcomeTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="loginWelcome__content loginWelcome-body"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="l"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<LoginForm
|
||||
http={
|
||||
Object {
|
||||
"addLoadingCountSource": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
}
|
||||
}
|
||||
loginAssistanceMessage=""
|
||||
notifications={
|
||||
Object {
|
||||
"toasts": Object {
|
||||
"add": [MockFunction],
|
||||
"addDanger": [MockFunction],
|
||||
"addError": [MockFunction],
|
||||
"addInfo": [MockFunction],
|
||||
"addSuccess": [MockFunction],
|
||||
"addWarning": [MockFunction],
|
||||
"get$": [MockFunction],
|
||||
"remove": [MockFunction],
|
||||
},
|
||||
}
|
||||
}
|
||||
selector={
|
||||
Object {
|
||||
"enabled": false,
|
||||
"providers": Array [
|
||||
Object {
|
||||
"name": "basic1",
|
||||
"type": "basic",
|
||||
"usesLoginForm": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -64,6 +64,7 @@ describe('loginApp', () => {
|
|||
{ element: appMountParams.element, theme$: appMountParams.theme$ },
|
||||
{
|
||||
http: coreStartMock.http,
|
||||
customBranding: coreStartMock.customBranding,
|
||||
notifications: coreStartMock.notifications,
|
||||
fatalErrors: coreStartMock.fatalErrors,
|
||||
loginAssistanceMessage: 'some-message',
|
||||
|
|
|
@ -40,6 +40,7 @@ export const loginApp = Object.freeze({
|
|||
coreStart.i18n,
|
||||
{ element, theme$ },
|
||||
{
|
||||
customBranding: coreStart.customBranding,
|
||||
http: coreStart.http,
|
||||
notifications: coreStart.notifications,
|
||||
fatalErrors: coreStart.fatalErrors,
|
||||
|
|
|
@ -9,7 +9,9 @@ import { EuiFlexItem } from '@elastic/eui';
|
|||
import { act } from '@testing-library/react';
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-mocks';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { nextTick } from '@kbn/test-jest-helpers';
|
||||
|
||||
|
@ -27,6 +29,7 @@ const createLoginState = (options?: Partial<LoginState>) => {
|
|||
enabled: false,
|
||||
providers: [{ type: 'basic', name: 'basic1', usesLoginForm: true }],
|
||||
},
|
||||
customBranding: {},
|
||||
...options,
|
||||
} as LoginState;
|
||||
};
|
||||
|
@ -41,6 +44,7 @@ describe('LoginPage', () => {
|
|||
httpMock.get.mockReset();
|
||||
httpMock.addLoadingCountSource.mockReset();
|
||||
};
|
||||
const customBrandingMock = customBrandingServiceMock.createStartContract();
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
|
@ -54,11 +58,37 @@ describe('LoginPage', () => {
|
|||
describe('page', () => {
|
||||
it('renders as expected', async () => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
customBrandingMock.customBranding$ = of({});
|
||||
httpMock.get.mockResolvedValue(createLoginState());
|
||||
|
||||
const wrapper = shallow(
|
||||
<LoginPage
|
||||
http={httpMock}
|
||||
customBranding={customBrandingMock}
|
||||
notifications={coreStartMock.notifications}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
resetHttpMock(); // so the calls don't show in the BasicLoginForm snapshot
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders with custom branding', async () => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
customBrandingMock.customBranding$ = of({ logo: 'logo' });
|
||||
httpMock.get.mockResolvedValue(createLoginState());
|
||||
|
||||
const wrapper = shallow(
|
||||
<LoginPage
|
||||
http={httpMock}
|
||||
customBranding={customBrandingMock}
|
||||
notifications={coreStartMock.notifications}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
|
@ -100,6 +130,7 @@ describe('LoginPage', () => {
|
|||
notifications={coreStartMock.notifications}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
customBranding={customBrandingMock}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -121,6 +152,7 @@ describe('LoginPage', () => {
|
|||
notifications={coreStartMock.notifications}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
customBranding={customBrandingMock}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -142,6 +174,7 @@ describe('LoginPage', () => {
|
|||
notifications={coreStartMock.notifications}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
customBranding={customBrandingMock}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -165,6 +198,7 @@ describe('LoginPage', () => {
|
|||
notifications={coreStartMock.notifications}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
customBranding={customBrandingMock}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -188,6 +222,7 @@ describe('LoginPage', () => {
|
|||
notifications={coreStartMock.notifications}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
customBranding={customBrandingMock}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -219,6 +254,7 @@ describe('LoginPage', () => {
|
|||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
sameSiteCookies="Lax"
|
||||
customBranding={customBrandingMock}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -250,6 +286,7 @@ describe('LoginPage', () => {
|
|||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
sameSiteCookies="None"
|
||||
customBranding={customBrandingMock}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -276,6 +313,7 @@ describe('LoginPage', () => {
|
|||
notifications={coreStartMock.notifications}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
customBranding={customBrandingMock}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -299,6 +337,7 @@ describe('LoginPage', () => {
|
|||
notifications={coreStartMock.notifications}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
customBranding={customBrandingMock}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -322,6 +361,7 @@ describe('LoginPage', () => {
|
|||
notifications={coreStartMock.notifications}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
customBranding={customBrandingMock}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -349,6 +389,7 @@ describe('LoginPage', () => {
|
|||
notifications={coreStartMock.notifications}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage="This is an *important* message"
|
||||
customBranding={customBrandingMock}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -371,6 +412,7 @@ describe('LoginPage', () => {
|
|||
notifications={coreStartMock.notifications}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
customBranding={customBrandingMock}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -395,6 +437,7 @@ describe('LoginPage', () => {
|
|||
notifications={coreStartMock.notifications}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
customBranding={customBrandingMock}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -420,6 +463,7 @@ describe('LoginPage', () => {
|
|||
notifications={coreStartMock.notifications}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
customBranding={customBrandingMock}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiImage,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
|
@ -19,11 +20,14 @@ import {
|
|||
import classNames from 'classnames';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import type { Subscription } from 'rxjs';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import type { CustomBranding } from '@kbn/core-custom-branding-common';
|
||||
import type {
|
||||
AppMountParameters,
|
||||
CoreStart,
|
||||
CustomBrandingStart,
|
||||
FatalErrorsStart,
|
||||
HttpStart,
|
||||
NotificationsStart,
|
||||
|
@ -48,10 +52,12 @@ interface Props {
|
|||
fatalErrors: FatalErrorsStart;
|
||||
loginAssistanceMessage: string;
|
||||
sameSiteCookies?: ConfigType['sameSiteCookies'];
|
||||
customBranding: CustomBrandingStart;
|
||||
}
|
||||
|
||||
interface State {
|
||||
loginState: LoginState | null;
|
||||
customBranding: CustomBranding;
|
||||
}
|
||||
|
||||
const loginFormMessages: Record<LogoutReason, NonNullable<LoginFormProps['message']>> = {
|
||||
|
@ -83,14 +89,22 @@ const loginFormMessages: Record<LogoutReason, NonNullable<LoginFormProps['messag
|
|||
};
|
||||
|
||||
export class LoginPage extends Component<Props, State> {
|
||||
state = { loginState: null } as State;
|
||||
state = { loginState: null, customBranding: {} } as State;
|
||||
private customBrandingSubscription?: Subscription;
|
||||
|
||||
public async componentDidMount() {
|
||||
const loadingCount$ = new BehaviorSubject(1);
|
||||
this.customBrandingSubscription = this.props.customBranding.customBranding$.subscribe(
|
||||
(next) => {
|
||||
this.setState({ ...this.state, customBranding: next });
|
||||
}
|
||||
);
|
||||
this.props.http.addLoadingCountSource(loadingCount$.asObservable());
|
||||
|
||||
try {
|
||||
this.setState({ loginState: await this.props.http.get('/internal/security/login_state') });
|
||||
this.setState({
|
||||
loginState: await this.props.http.get('/internal/security/login_state'),
|
||||
});
|
||||
} catch (err) {
|
||||
this.props.fatalErrors.add(err as Error);
|
||||
}
|
||||
|
@ -99,6 +113,10 @@ export class LoginPage extends Component<Props, State> {
|
|||
loadingCount$.complete();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.customBrandingSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const loginState = this.state.loginState;
|
||||
if (!loginState) {
|
||||
|
@ -122,14 +140,18 @@ export class LoginPage extends Component<Props, State> {
|
|||
['loginWelcome__contentDisabledForm']: !loginIsSupported,
|
||||
});
|
||||
|
||||
const customLogo = this.state.customBranding?.logo;
|
||||
const logo = customLogo ? (
|
||||
<EuiImage src={customLogo} size={40} alt="logo" />
|
||||
) : (
|
||||
<EuiIcon type="logoElastic" size="xxl" />
|
||||
);
|
||||
return (
|
||||
<div className="loginWelcome login-form">
|
||||
<header className="loginWelcome__header">
|
||||
<div className={contentHeaderClasses}>
|
||||
<EuiSpacer size="xxl" />
|
||||
<span className="loginWelcome__logo">
|
||||
<EuiIcon type="logoElastic" size="xxl" />
|
||||
</span>
|
||||
<span className="loginWelcome__logo">{logo}</span>
|
||||
<EuiTitle size="m" className="loginWelcome__title" data-test-subj="loginWelcomeTitle">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UnauthenticatedPage renders as expected 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/node_modules/@kbn/ui-framework/dist/kui_light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/ui/legacy_light_theme.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div class=\\"euiPage eui-1po165g-euiPage-row-grow\\" style=\\"min-height:100vh\\" data-test-subj=\\"promptPage\\"><div class=\\"euiPageBody eui-30ejv0-euiPageBody\\"><div class=\\"euiPanel euiPanel--plain euiPanel--paddingLarge euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter eui-15y4d5v-euiPanel-grow-m-l-plain-hasShadow\\" role=\\"main\\"><div class=\\"euiPanel euiPanel--transparent euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-7iihzj-euiPanel-m-transparent\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"alert\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">We hit an authentication error</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-1mn0tf7-euiText-m-euiTextColor-subdued\\"><p>Try logging in again, and if the problem persists, contact your system administrator.</p></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-17kdkag-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><a href=\\"/some/url?some-query=some-value#some-hash\\" rel=\\"noreferrer\\" class=\\"euiButton eui-1u1unpk-euiButtonDisplay-m-defaultMinWidth-m-fill-primary\\" data-test-subj=\\"logInButton\\"><span class=\\"eui-1km4ln8-euiButtonDisplayContent\\">Log in</span></a></div></div></div></div></div></div></div></div></div></body></html>"`;
|
||||
|
||||
exports[`UnauthenticatedPage renders as expected with custom title 1`] = `"<html lang=\\"en\\"><head><title>My Company Name</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/node_modules/@kbn/ui-framework/dist/kui_light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/ui/legacy_light_theme.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div class=\\"euiPage eui-1po165g-euiPage-row-grow\\" style=\\"min-height:100vh\\" data-test-subj=\\"promptPage\\"><div class=\\"euiPageBody eui-30ejv0-euiPageBody\\"><div class=\\"euiPanel euiPanel--plain euiPanel--paddingLarge euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter eui-15y4d5v-euiPanel-grow-m-l-plain-hasShadow\\" role=\\"main\\"><div class=\\"euiPanel euiPanel--transparent euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-7iihzj-euiPanel-m-transparent\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"alert\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">We hit an authentication error</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-1mn0tf7-euiText-m-euiTextColor-subdued\\"><p>Try logging in again, and if the problem persists, contact your system administrator.</p></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-17kdkag-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><a href=\\"/some/url?some-query=some-value#some-hash\\" rel=\\"noreferrer\\" class=\\"euiButton eui-1u1unpk-euiButtonDisplay-m-defaultMinWidth-m-fill-primary\\" data-test-subj=\\"logInButton\\"><span class=\\"eui-1km4ln8-euiButtonDisplayContent\\">Log in</span></a></div></div></div></div></div></div></div></div></div></body></html>"`;
|
||||
|
|
|
@ -12,9 +12,11 @@ import { mockCanRedirectRequest } from './authentication_service.test.mocks';
|
|||
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
|
||||
import { customBrandingServiceMock } from '@kbn/core-custom-branding-server-mocks';
|
||||
import type {
|
||||
AuthenticationHandler,
|
||||
AuthToolkit,
|
||||
CustomBrandingSetup,
|
||||
ElasticsearchServiceSetup,
|
||||
HttpServiceSetup,
|
||||
HttpServiceStart,
|
||||
|
@ -62,6 +64,7 @@ describe('AuthenticationService', () => {
|
|||
config: ConfigType;
|
||||
license: jest.Mocked<SecurityLicense>;
|
||||
buildNumber: number;
|
||||
customBranding: jest.Mocked<CustomBrandingSetup>;
|
||||
};
|
||||
let mockStartAuthenticationParams: {
|
||||
audit: jest.Mocked<AuditServiceSetup>;
|
||||
|
@ -93,6 +96,7 @@ describe('AuthenticationService', () => {
|
|||
}),
|
||||
license: licenseMock.create(),
|
||||
buildNumber: 100500,
|
||||
customBranding: customBrandingServiceMock.createSetupContract(),
|
||||
};
|
||||
mockCanRedirectRequest.mockReturnValue(false);
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import type {
|
||||
CustomBrandingSetup,
|
||||
ElasticsearchServiceSetup,
|
||||
HttpServiceSetup,
|
||||
HttpServiceStart,
|
||||
|
@ -37,6 +38,7 @@ import { renderUnauthenticatedPage } from './unauthenticated_page';
|
|||
|
||||
interface AuthenticationServiceSetupParams {
|
||||
http: Pick<HttpServiceSetup, 'basePath' | 'csp' | 'registerAuth' | 'registerOnPreResponse'>;
|
||||
customBranding: CustomBrandingSetup;
|
||||
elasticsearch: Pick<ElasticsearchServiceSetup, 'setUnauthorizedErrorHandler'>;
|
||||
config: ConfigType;
|
||||
license: SecurityLicense;
|
||||
|
@ -97,7 +99,14 @@ export class AuthenticationService {
|
|||
|
||||
constructor(private readonly logger: Logger) {}
|
||||
|
||||
setup({ config, http, license, buildNumber, elasticsearch }: AuthenticationServiceSetupParams) {
|
||||
setup({
|
||||
config,
|
||||
http,
|
||||
license,
|
||||
buildNumber,
|
||||
elasticsearch,
|
||||
customBranding,
|
||||
}: AuthenticationServiceSetupParams) {
|
||||
this.license = license;
|
||||
|
||||
// If we cannot automatically authenticate users we should redirect them straight to the login
|
||||
|
@ -201,8 +210,16 @@ export class AuthenticationService {
|
|||
? this.authenticator.getRequestOriginalURL(request)
|
||||
: `${http.basePath.get(request)}/`;
|
||||
if (!isLoginPageAvailable) {
|
||||
const customBrandingValue = await customBranding.getBrandingFor(request, {
|
||||
unauthenticated: true,
|
||||
});
|
||||
return toolkit.render({
|
||||
body: renderUnauthenticatedPage({ buildNumber, basePath: http.basePath, originalURL }),
|
||||
body: renderUnauthenticatedPage({
|
||||
buildNumber,
|
||||
basePath: http.basePath,
|
||||
originalURL,
|
||||
customBranding: customBrandingValue,
|
||||
}),
|
||||
headers: { 'Content-Security-Policy': http.csp.header },
|
||||
});
|
||||
}
|
||||
|
|
|
@ -28,6 +28,25 @@ describe('UnauthenticatedPage', () => {
|
|||
originalURL="/some/url?some-query=some-value#some-hash"
|
||||
buildNumber={100500}
|
||||
basePath={mockCoreSetup.http.basePath}
|
||||
customBranding={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders as expected with custom title', async () => {
|
||||
const mockCoreSetup = coreMock.createSetup();
|
||||
(mockCoreSetup.http.basePath.prepend as jest.Mock).mockImplementation(
|
||||
(path) => `/mock-basepath${path}`
|
||||
);
|
||||
|
||||
const body = renderToStaticMarkup(
|
||||
<UnauthenticatedPage
|
||||
originalURL="/some/url?some-query=some-value#some-hash"
|
||||
buildNumber={100500}
|
||||
basePath={mockCoreSetup.http.basePath}
|
||||
customBranding={{ pageTitle: 'My Company Name' }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { EuiButton } from '@elastic/eui/lib/components/button';
|
|||
import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
|
||||
import type { CustomBranding } from '@kbn/core-custom-branding-common';
|
||||
import type { IBasePath } from '@kbn/core/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -20,9 +21,10 @@ interface Props {
|
|||
originalURL: string;
|
||||
buildNumber: number;
|
||||
basePath: IBasePath;
|
||||
customBranding: CustomBranding;
|
||||
}
|
||||
|
||||
export function UnauthenticatedPage({ basePath, originalURL, buildNumber }: Props) {
|
||||
export function UnauthenticatedPage({ basePath, originalURL, buildNumber, customBranding }: Props) {
|
||||
return (
|
||||
<PromptPage
|
||||
buildNumber={buildNumber}
|
||||
|
@ -46,6 +48,7 @@ export function UnauthenticatedPage({ basePath, originalURL, buildNumber }: Prop
|
|||
/>
|
||||
</EuiButton>,
|
||||
]}
|
||||
customBranding={customBranding}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ResetSessionPage renders as expected 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/node_modules/@kbn/ui-framework/dist/kui_light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/ui/legacy_light_theme.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><script src=\\"/mock-basepath/internal/security/reset_session_page.js\\"></script><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div class=\\"euiPage eui-1po165g-euiPage-row-grow\\" style=\\"min-height:100vh\\" data-test-subj=\\"promptPage\\"><div class=\\"euiPageBody eui-30ejv0-euiPageBody\\"><div class=\\"euiPanel euiPanel--plain euiPanel--paddingLarge euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter eui-15y4d5v-euiPanel-grow-m-l-plain-hasShadow\\" role=\\"main\\"><div class=\\"euiPanel euiPanel--transparent euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-7iihzj-euiPanel-m-transparent\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"alert\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">You do not have permission to access the requested page</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-1mn0tf7-euiText-m-euiTextColor-subdued\\"><p>Either go back to the previous page or log in as a different user.</p></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-17kdkag-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><a href=\\"/path/to/logout\\" rel=\\"noreferrer\\" class=\\"euiButton eui-1u1unpk-euiButtonDisplay-m-defaultMinWidth-m-fill-primary\\" data-test-subj=\\"ResetSessionButton\\"><span class=\\"eui-1km4ln8-euiButtonDisplayContent\\">Log in as different user</span></a></div><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><button class=\\"euiButtonEmpty eui-9t7nyf-empty-primary\\" type=\\"button\\" id=\\"goBackButton\\"><span class=\\"euiButtonContent euiButtonEmpty__content\\"><span class=\\"euiButtonEmpty__text\\">Go back</span></span></button></div></div></div></div></div></div></div></div></div></body></html>"`;
|
||||
|
||||
exports[`ResetSessionPage renders as expected with custom page title 1`] = `"<html lang=\\"en\\"><head><title>My Company Name</title><style></style><style data-emotion=\\"eui \\"></style></style><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/node_modules/@kbn/ui-framework/dist/kui_light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/ui/legacy_light_theme.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><script src=\\"/mock-basepath/internal/security/reset_session_page.js\\"></script><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div class=\\"euiPage eui-1po165g-euiPage-row-grow\\" style=\\"min-height:100vh\\" data-test-subj=\\"promptPage\\"><div class=\\"euiPageBody eui-30ejv0-euiPageBody\\"><div class=\\"euiPanel euiPanel--plain euiPanel--paddingLarge euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter eui-15y4d5v-euiPanel-grow-m-l-plain-hasShadow\\" role=\\"main\\"><div class=\\"euiPanel euiPanel--transparent euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge eui-7iihzj-euiPanel-m-transparent\\"><div class=\\"euiEmptyPrompt__main\\"><div class=\\"euiEmptyPrompt__icon\\"><span data-euiicon-type=\\"alert\\" color=\\"danger\\"></span></div><div class=\\"euiEmptyPrompt__content\\"><div class=\\"euiEmptyPrompt__contentInner\\"><h2 class=\\"euiTitle eui-smz32e-euiTitle-m\\">You do not have permission to access the requested page</h2><div class=\\"euiSpacer euiSpacer--m eui-jv9za2-euiSpacer-m\\"></div><div class=\\"euiText eui-1mn0tf7-euiText-m-euiTextColor-subdued\\"><p>Either go back to the previous page or log in as a different user.</p></div><div class=\\"euiSpacer euiSpacer--l eui-p2o3x6-euiSpacer-l\\"></div><div class=\\"euiFlexGroup euiEmptyPrompt__actions eui-17kdkag-euiFlexGroup-m-center-center-column\\"><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><a href=\\"/path/to/logout\\" rel=\\"noreferrer\\" class=\\"euiButton eui-1u1unpk-euiButtonDisplay-m-defaultMinWidth-m-fill-primary\\" data-test-subj=\\"ResetSessionButton\\"><span class=\\"eui-1km4ln8-euiButtonDisplayContent\\">Log in as different user</span></a></div><div class=\\"euiFlexItem eui-kpsrin-euiFlexItem-growZero\\"><button class=\\"euiButtonEmpty eui-9t7nyf-empty-primary\\" type=\\"button\\" id=\\"goBackButton\\"><span class=\\"euiButtonContent euiButtonEmpty__content\\"><span class=\\"euiButtonEmpty__text\\">Go back</span></span></button></div></div></div></div></div></div></div></div></div></body></html>"`;
|
||||
|
|
|
@ -81,6 +81,7 @@ it(`#setup returns exposed services`, () => {
|
|||
features: mockFeaturesSetup,
|
||||
getSpacesService: mockGetSpacesService,
|
||||
getCurrentUser: jest.fn(),
|
||||
customBranding: mockCoreSetup.customBranding,
|
||||
});
|
||||
|
||||
expect(authz.actions.version).toBe('version:some-version');
|
||||
|
@ -143,6 +144,7 @@ describe('#start', () => {
|
|||
.fn()
|
||||
.mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }),
|
||||
getCurrentUser: jest.fn(),
|
||||
customBranding: mockCoreSetup.customBranding,
|
||||
});
|
||||
|
||||
const featuresStart = featuresPluginMock.createStart();
|
||||
|
@ -215,6 +217,7 @@ it('#stop unsubscribes from license and ES updates.', async () => {
|
|||
.fn()
|
||||
.mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }),
|
||||
getCurrentUser: jest.fn(),
|
||||
customBranding: mockCoreSetup.customBranding,
|
||||
});
|
||||
|
||||
const featuresStart = featuresPluginMock.createStart();
|
||||
|
|
|
@ -12,6 +12,7 @@ import type { Observable, Subscription } from 'rxjs';
|
|||
|
||||
import type {
|
||||
CapabilitiesSetup,
|
||||
CustomBrandingSetup,
|
||||
HttpServiceSetup,
|
||||
IClusterClient,
|
||||
KibanaRequest,
|
||||
|
@ -64,6 +65,7 @@ interface AuthorizationServiceSetupParams {
|
|||
kibanaIndexName: string;
|
||||
getSpacesService(): SpacesService | undefined;
|
||||
getCurrentUser(request: KibanaRequest): AuthenticatedUser | null;
|
||||
customBranding: CustomBrandingSetup;
|
||||
}
|
||||
|
||||
interface AuthorizationServiceStartParams {
|
||||
|
@ -117,6 +119,7 @@ export class AuthorizationService {
|
|||
kibanaIndexName,
|
||||
getSpacesService,
|
||||
getCurrentUser,
|
||||
customBranding,
|
||||
}: AuthorizationServiceSetupParams): AuthorizationServiceSetupInternal {
|
||||
this.logger = loggers.get('authorization');
|
||||
this.applicationName = `${APPLICATION_PREFIX}${kibanaIndexName}`;
|
||||
|
@ -176,8 +179,11 @@ export class AuthorizationService {
|
|||
initAPIAuthorization(http, authz, loggers.get('api-authorization'));
|
||||
initAppAuthorization(http, authz, loggers.get('app-authorization'), features);
|
||||
|
||||
http.registerOnPreResponse((request, preResponse, toolkit) => {
|
||||
http.registerOnPreResponse(async (request, preResponse, toolkit) => {
|
||||
if (preResponse.statusCode === 403 && canRedirectRequest(request)) {
|
||||
const customBrandingValue = await customBranding.getBrandingFor(request, {
|
||||
unauthenticated: false,
|
||||
});
|
||||
const next = `${http.basePath.get(request)}${request.url.pathname}${request.url.search}`;
|
||||
const body = renderToString(
|
||||
<ResetSessionPage
|
||||
|
@ -186,6 +192,7 @@ export class AuthorizationService {
|
|||
logoutUrl={http.basePath.prepend(
|
||||
`/api/security/logout?${querystring.stringify({ next })}`
|
||||
)}
|
||||
customBranding={customBrandingValue}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -28,6 +28,25 @@ describe('ResetSessionPage', () => {
|
|||
logoutUrl="/path/to/logout"
|
||||
buildNumber={100500}
|
||||
basePath={mockCoreSetup.http.basePath}
|
||||
customBranding={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders as expected with custom page title', async () => {
|
||||
const mockCoreSetup = coreMock.createSetup();
|
||||
(mockCoreSetup.http.basePath.prepend as jest.Mock).mockImplementation(
|
||||
(path) => `/mock-basepath${path}`
|
||||
);
|
||||
|
||||
const body = renderToStaticMarkup(
|
||||
<ResetSessionPage
|
||||
logoutUrl="/path/to/logout"
|
||||
buildNumber={100500}
|
||||
basePath={mockCoreSetup.http.basePath}
|
||||
customBranding={{ pageTitle: 'My Company Name' }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { EuiButton, EuiButtonEmpty } from '@elastic/eui/lib/components/button';
|
||||
import React from 'react';
|
||||
|
||||
import type { CustomBranding } from '@kbn/core-custom-branding-common';
|
||||
import type { IBasePath } from '@kbn/core/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -19,10 +20,12 @@ export function ResetSessionPage({
|
|||
logoutUrl,
|
||||
buildNumber,
|
||||
basePath,
|
||||
customBranding,
|
||||
}: {
|
||||
logoutUrl: string;
|
||||
buildNumber: number;
|
||||
basePath: IBasePath;
|
||||
customBranding: CustomBranding;
|
||||
}) {
|
||||
return (
|
||||
<PromptPage
|
||||
|
@ -54,6 +57,7 @@ export function ResetSessionPage({
|
|||
/>
|
||||
</EuiButtonEmpty>,
|
||||
]}
|
||||
customBranding={customBranding}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -267,6 +267,7 @@ export class SecurityPlugin
|
|||
config,
|
||||
license,
|
||||
buildNumber: this.initializerContext.env.packageInfo.buildNum,
|
||||
customBranding: core.customBranding,
|
||||
});
|
||||
|
||||
registerSecurityUsageCollector({ usageCollection, config, license });
|
||||
|
@ -297,6 +298,7 @@ export class SecurityPlugin
|
|||
getSpacesService: () => spaces?.spacesService,
|
||||
features,
|
||||
getCurrentUser: (request) => this.getAuthentication().getCurrentUser(request),
|
||||
customBranding: core.customBranding,
|
||||
});
|
||||
|
||||
this.userProfileService.setup({ authz: this.authorizationSetup, license });
|
||||
|
|
|
@ -29,6 +29,7 @@ describe('PromptPage', () => {
|
|||
basePath={mockCoreSetup.http.basePath}
|
||||
title="Some Title"
|
||||
body={<div>Some Body</div>}
|
||||
customBranding={{}}
|
||||
actions={[<span>Action#1</span>, <span>Action#2</span>]}
|
||||
/>
|
||||
);
|
||||
|
@ -49,6 +50,7 @@ describe('PromptPage', () => {
|
|||
scriptPaths={['/some/script1.js', '/some/script2.js']}
|
||||
title="Some Title"
|
||||
body={<div>Some Body</div>}
|
||||
customBranding={{}}
|
||||
actions={[<span>Action#1</span>, <span>Action#2</span>]}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -22,6 +22,7 @@ import type { ReactNode } from 'react';
|
|||
import React from 'react';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
|
||||
import type { CustomBranding } from '@kbn/core-custom-branding-common';
|
||||
import { Fonts } from '@kbn/core-rendering-server-internal';
|
||||
import type { IBasePath } from '@kbn/core/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -45,6 +46,7 @@ interface Props {
|
|||
title: ReactNode;
|
||||
body: ReactNode;
|
||||
actions: ReactNode;
|
||||
customBranding: CustomBranding;
|
||||
}
|
||||
|
||||
export function PromptPage({
|
||||
|
@ -54,6 +56,7 @@ export function PromptPage({
|
|||
title,
|
||||
body,
|
||||
actions,
|
||||
customBranding,
|
||||
}: Props) {
|
||||
const content = (
|
||||
<I18nProvider>
|
||||
|
@ -92,7 +95,7 @@ export function PromptPage({
|
|||
return (
|
||||
<html lang={i18n.getLocale()}>
|
||||
<head>
|
||||
<title>Elastic</title>
|
||||
<title>{customBranding.pageTitle ? customBranding.pageTitle : 'Elastic'}</title>
|
||||
{/* eslint-disable-next-line react/no-danger */}
|
||||
<style dangerouslySetInnerHTML={{ __html: `</style>${emotionStyles}` }} />
|
||||
{styleSheetPaths.map((path) => (
|
||||
|
@ -100,8 +103,20 @@ export function PromptPage({
|
|||
))}
|
||||
<Fonts url={uiPublicURL} />
|
||||
{/* The alternate icon is a fallback for Safari which does not yet support SVG favicons */}
|
||||
<link rel="alternate icon" type="image/png" href={`${uiPublicURL}/favicons/favicon.png`} />
|
||||
<link rel="icon" type="image/svg+xml" href={`${uiPublicURL}/favicons/favicon.svg`} />
|
||||
{customBranding.faviconPNG ? (
|
||||
<link rel="alternate icon" type="image/png" href={customBranding.faviconPNG} />
|
||||
) : (
|
||||
<link
|
||||
rel="alternate icon"
|
||||
type="image/png"
|
||||
href={`${uiPublicURL}/favicons/favicon.png`}
|
||||
/>
|
||||
)}
|
||||
{customBranding.faviconSVG ? (
|
||||
<link rel="icon" type="image/svg+xml" href={customBranding.faviconSVG} />
|
||||
) : (
|
||||
<link rel="icon" type="image/svg+xml" href={`${uiPublicURL}/favicons/favicon.svg`} />
|
||||
)}
|
||||
{scriptPaths.map((path) => (
|
||||
<script src={basePath.prepend(path)} key={path} />
|
||||
))}
|
||||
|
|
|
@ -49,6 +49,9 @@
|
|||
"@kbn/logging-mocks",
|
||||
"@kbn/web-worker-stub",
|
||||
"@kbn/core-saved-objects-utils-server",
|
||||
"@kbn/core-custom-branding-browser-mocks",
|
||||
"@kbn/core-custom-branding-common",
|
||||
"@kbn/core-custom-branding-server-mocks",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue