mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[7.x] Migrate security chromeless views to Kibana Platform plugin (#59282)
Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>
This commit is contained in:
parent
f09a7ae69f
commit
91babc7628
123 changed files with 2672 additions and 2104 deletions
|
@ -92,10 +92,6 @@ export default async function({ readConfigFile }) {
|
|||
pathname: '/app/kibana',
|
||||
hash: '/dev_tools/console',
|
||||
},
|
||||
account: {
|
||||
pathname: '/app/kibana',
|
||||
hash: '/account',
|
||||
},
|
||||
home: {
|
||||
pathname: '/app/kibana',
|
||||
hash: '/home',
|
||||
|
|
15
x-pack/legacy/plugins/security/index.d.ts
vendored
15
x-pack/legacy/plugins/security/index.d.ts
vendored
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Legacy } from 'kibana';
|
||||
import { AuthenticatedUser } from '../../../plugins/security/public';
|
||||
|
||||
/**
|
||||
* Public interface of the security plugin.
|
||||
*/
|
||||
export interface SecurityPlugin {
|
||||
getUser: (request: Legacy.Request) => Promise<AuthenticatedUser>;
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import { get, has } from 'lodash';
|
||||
import { initOverwrittenSessionView } from './server/routes/views/overwritten_session';
|
||||
import { initLoginView } from './server/routes/views/login';
|
||||
import { initLogoutView } from './server/routes/views/logout';
|
||||
import { initLoggedOutView } from './server/routes/views/logged_out';
|
||||
import { AuditLogger } from '../../server/lib/audit_logger';
|
||||
import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize';
|
||||
import { KibanaRequest } from '../../../../src/core/server';
|
||||
|
||||
export const security = kibana =>
|
||||
new kibana.Plugin({
|
||||
id: 'security',
|
||||
configPrefix: 'xpack.security',
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
require: ['kibana', 'elasticsearch', 'xpack_main'],
|
||||
|
||||
config(Joi) {
|
||||
const HANDLED_IN_NEW_PLATFORM = Joi.any().description(
|
||||
'This key is handled in the new platform security plugin ONLY'
|
||||
);
|
||||
return Joi.object({
|
||||
enabled: Joi.boolean().default(true),
|
||||
cookieName: HANDLED_IN_NEW_PLATFORM,
|
||||
encryptionKey: HANDLED_IN_NEW_PLATFORM,
|
||||
session: HANDLED_IN_NEW_PLATFORM,
|
||||
secureCookies: HANDLED_IN_NEW_PLATFORM,
|
||||
public: HANDLED_IN_NEW_PLATFORM,
|
||||
loginAssistanceMessage: HANDLED_IN_NEW_PLATFORM,
|
||||
authorization: HANDLED_IN_NEW_PLATFORM,
|
||||
audit: Joi.object({
|
||||
enabled: Joi.boolean().default(false),
|
||||
}).default(),
|
||||
authc: HANDLED_IN_NEW_PLATFORM,
|
||||
}).default();
|
||||
},
|
||||
|
||||
deprecations: function({ rename }) {
|
||||
return [
|
||||
rename('authProviders', 'authc.providers'),
|
||||
(settings, log) => {
|
||||
const hasSAMLProvider = get(settings, 'authc.providers', []).includes('saml');
|
||||
if (hasSAMLProvider && !get(settings, 'authc.saml.realm')) {
|
||||
log(
|
||||
'Config key "authc.saml.realm" will become mandatory when using the SAML authentication provider in the next major version.'
|
||||
);
|
||||
}
|
||||
|
||||
if (has(settings, 'public')) {
|
||||
log(
|
||||
'Config key "public" is deprecated and will be removed in the next major version. ' +
|
||||
'Specify "authc.saml.realm" instead.'
|
||||
);
|
||||
}
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
uiExports: {
|
||||
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
|
||||
apps: [
|
||||
{
|
||||
id: 'login',
|
||||
title: 'Login',
|
||||
main: 'plugins/security/views/login',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
id: 'overwritten_session',
|
||||
title: 'Overwritten Session',
|
||||
main: 'plugins/security/views/overwritten_session',
|
||||
description:
|
||||
'The view is shown when user had an active session previously, but logged in as a different user.',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
id: 'logout',
|
||||
title: 'Logout',
|
||||
main: 'plugins/security/views/logout',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
id: 'logged_out',
|
||||
title: 'Logged out',
|
||||
main: 'plugins/security/views/logged_out',
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
hacks: [
|
||||
'plugins/security/hacks/on_session_timeout',
|
||||
'plugins/security/hacks/on_unauthorized_response',
|
||||
'plugins/security/hacks/register_account_management_app',
|
||||
],
|
||||
injectDefaultVars: server => {
|
||||
const securityPlugin = server.newPlatform.setup.plugins.security;
|
||||
if (!securityPlugin) {
|
||||
throw new Error('New Platform XPack Security plugin is not available.');
|
||||
}
|
||||
|
||||
return {
|
||||
secureCookies: securityPlugin.__legacyCompat.config.secureCookies,
|
||||
session: {
|
||||
tenant: server.newPlatform.setup.core.http.basePath.serverBasePath,
|
||||
},
|
||||
enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled'),
|
||||
logoutUrl: `${server.newPlatform.setup.core.http.basePath.serverBasePath}/logout`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
async postInit(server) {
|
||||
const securityPlugin = server.newPlatform.setup.plugins.security;
|
||||
if (!securityPlugin) {
|
||||
throw new Error('New Platform XPack Security plugin is not available.');
|
||||
}
|
||||
|
||||
watchStatusAndLicenseToInitialize(server.plugins.xpack_main, this, async () => {
|
||||
const xpackInfo = server.plugins.xpack_main.info;
|
||||
if (xpackInfo.isAvailable() && xpackInfo.feature('security').isEnabled()) {
|
||||
await securityPlugin.__legacyCompat.registerPrivilegesWithCluster();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async init(server) {
|
||||
const securityPlugin = server.newPlatform.setup.plugins.security;
|
||||
if (!securityPlugin) {
|
||||
throw new Error('New Platform XPack Security plugin is not available.');
|
||||
}
|
||||
|
||||
const config = server.config();
|
||||
const xpackInfo = server.plugins.xpack_main.info;
|
||||
securityPlugin.__legacyCompat.registerLegacyAPI({
|
||||
auditLogger: new AuditLogger(server, 'security', config, xpackInfo),
|
||||
});
|
||||
|
||||
// Legacy xPack Info endpoint returns whatever we return in a callback for `registerLicenseCheckResultsGenerator`
|
||||
// and the result is consumed by the legacy plugins all over the place, so we should keep it here for now. We assume
|
||||
// that when legacy callback is called license has been already propagated to the new platform security plugin and
|
||||
// features are up to date.
|
||||
xpackInfo
|
||||
.feature(this.id)
|
||||
.registerLicenseCheckResultsGenerator(() =>
|
||||
securityPlugin.__legacyCompat.license.getFeatures()
|
||||
);
|
||||
|
||||
server.expose({
|
||||
getUser: async request => securityPlugin.authc.getCurrentUser(KibanaRequest.from(request)),
|
||||
});
|
||||
|
||||
initLoginView(securityPlugin, server);
|
||||
initLogoutView(server);
|
||||
initLoggedOutView(securityPlugin, server);
|
||||
initOverwrittenSessionView(server);
|
||||
|
||||
server.injectUiAppVars('login', () => {
|
||||
const {
|
||||
showLogin,
|
||||
allowLogin,
|
||||
layout = 'form',
|
||||
} = securityPlugin.__legacyCompat.license.getFeatures();
|
||||
const { loginAssistanceMessage } = securityPlugin.__legacyCompat.config;
|
||||
return {
|
||||
loginAssistanceMessage,
|
||||
loginState: {
|
||||
showLogin,
|
||||
allowLogin,
|
||||
layout,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
93
x-pack/legacy/plugins/security/index.ts
Normal file
93
x-pack/legacy/plugins/security/index.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Root } from 'joi';
|
||||
import { resolve } from 'path';
|
||||
import { Server } from 'src/legacy/server/kbn_server';
|
||||
import { KibanaRequest, LegacyRequest } from '../../../../src/core/server';
|
||||
// @ts-ignore
|
||||
import { AuditLogger } from '../../server/lib/audit_logger';
|
||||
// @ts-ignore
|
||||
import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize';
|
||||
import { AuthenticatedUser, SecurityPluginSetup } from '../../../plugins/security/server';
|
||||
|
||||
/**
|
||||
* Public interface of the security plugin.
|
||||
*/
|
||||
export interface SecurityPlugin {
|
||||
getUser: (request: LegacyRequest) => Promise<AuthenticatedUser>;
|
||||
}
|
||||
|
||||
function getSecurityPluginSetup(server: Server) {
|
||||
const securityPlugin = server.newPlatform.setup.plugins.security as SecurityPluginSetup;
|
||||
if (!securityPlugin) {
|
||||
throw new Error('Kibana Platform Security plugin is not available.');
|
||||
}
|
||||
|
||||
return securityPlugin;
|
||||
}
|
||||
|
||||
export const security = (kibana: Record<string, any>) =>
|
||||
new kibana.Plugin({
|
||||
id: 'security',
|
||||
configPrefix: 'xpack.security',
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
require: ['kibana', 'elasticsearch', 'xpack_main'],
|
||||
|
||||
// This config is only used by `AuditLogger` and should be removed as soon as `AuditLogger`
|
||||
// is migrated to Kibana Platform.
|
||||
config(Joi: Root) {
|
||||
return Joi.object({
|
||||
enabled: Joi.boolean().default(true),
|
||||
audit: Joi.object({ enabled: Joi.boolean().default(false) }).default(),
|
||||
})
|
||||
.unknown()
|
||||
.default();
|
||||
},
|
||||
|
||||
uiExports: {
|
||||
hacks: ['plugins/security/hacks/legacy'],
|
||||
injectDefaultVars: (server: Server) => {
|
||||
return {
|
||||
secureCookies: getSecurityPluginSetup(server).__legacyCompat.config.secureCookies,
|
||||
enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled'),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
async postInit(server: Server) {
|
||||
watchStatusAndLicenseToInitialize(server.plugins.xpack_main, this, async () => {
|
||||
const xpackInfo = server.plugins.xpack_main.info;
|
||||
if (xpackInfo.isAvailable() && xpackInfo.feature('security').isEnabled()) {
|
||||
await getSecurityPluginSetup(server).__legacyCompat.registerPrivilegesWithCluster();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async init(server: Server) {
|
||||
const securityPlugin = getSecurityPluginSetup(server);
|
||||
|
||||
const xpackInfo = server.plugins.xpack_main.info;
|
||||
securityPlugin.__legacyCompat.registerLegacyAPI({
|
||||
auditLogger: new AuditLogger(server, 'security', server.config(), xpackInfo),
|
||||
});
|
||||
|
||||
// Legacy xPack Info endpoint returns whatever we return in a callback for `registerLicenseCheckResultsGenerator`
|
||||
// and the result is consumed by the legacy plugins all over the place, so we should keep it here for now. We assume
|
||||
// that when legacy callback is called license has been already propagated to the new platform security plugin and
|
||||
// features are up to date.
|
||||
xpackInfo
|
||||
.feature(this.id)
|
||||
.registerLicenseCheckResultsGenerator(() =>
|
||||
securityPlugin.__legacyCompat.license.getFeatures()
|
||||
);
|
||||
|
||||
server.expose({
|
||||
getUser: async (request: LegacyRequest) =>
|
||||
securityPlugin.authc.getCurrentUser(KibanaRequest.from(request)),
|
||||
});
|
||||
},
|
||||
});
|
64
x-pack/legacy/plugins/security/public/hacks/legacy.ts
Normal file
64
x-pack/legacy/plugins/security/public/hacks/legacy.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { npSetup, npStart } from 'ui/new_platform';
|
||||
import routes from 'ui/routes';
|
||||
import { isSystemApiRequest } from '../../../../../../src/plugins/kibana_legacy/public';
|
||||
import { SecurityPluginSetup } from '../../../../../plugins/security/public';
|
||||
|
||||
const securityPluginSetup = (npSetup.plugins as any).security as SecurityPluginSetup;
|
||||
if (securityPluginSetup) {
|
||||
routes.when('/account', {
|
||||
template: '<div />',
|
||||
controller: () => npStart.core.application.navigateToApp('security_account'),
|
||||
});
|
||||
|
||||
const getNextParameter = () => {
|
||||
const { location } = window;
|
||||
const next = encodeURIComponent(`${location.pathname}${location.search}${location.hash}`);
|
||||
return `&next=${next}`;
|
||||
};
|
||||
|
||||
const getProviderParameter = (tenant: string) => {
|
||||
const key = `${tenant}/session_provider`;
|
||||
const providerName = sessionStorage.getItem(key);
|
||||
return providerName ? `&provider=${encodeURIComponent(providerName)}` : '';
|
||||
};
|
||||
|
||||
const module = uiModules.get('security', []);
|
||||
module.config(($httpProvider: ng.IHttpProvider) => {
|
||||
$httpProvider.interceptors.push(($q, $window, Promise) => {
|
||||
const isAnonymous = npSetup.core.http.anonymousPaths.isAnonymous(window.location.pathname);
|
||||
|
||||
function interceptorFactory(responseHandler: (response: ng.IHttpResponse<unknown>) => any) {
|
||||
return function interceptor(response: ng.IHttpResponse<unknown>) {
|
||||
if (!isAnonymous && !isSystemApiRequest(response.config)) {
|
||||
securityPluginSetup.sessionTimeout.extend(response.config.url);
|
||||
}
|
||||
|
||||
if (response.status !== 401 || isAnonymous) {
|
||||
return responseHandler(response);
|
||||
}
|
||||
|
||||
const { logoutUrl, tenant } = securityPluginSetup.__legacyCompat;
|
||||
const next = getNextParameter();
|
||||
const provider = getProviderParameter(tenant);
|
||||
|
||||
$window.location.href = `${logoutUrl}?msg=SESSION_EXPIRED${next}${provider}`;
|
||||
|
||||
return Promise.halt();
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
response: interceptorFactory(response => response),
|
||||
responseError: interceptorFactory($q.reject),
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { isSystemApiRequest } from 'ui/system_api';
|
||||
import { npSetup } from 'ui/new_platform';
|
||||
|
||||
const module = uiModules.get('security', []);
|
||||
module.config($httpProvider => {
|
||||
$httpProvider.interceptors.push($q => {
|
||||
const isAnonymous = npSetup.core.http.anonymousPaths.isAnonymous(window.location.pathname);
|
||||
|
||||
function interceptorFactory(responseHandler) {
|
||||
return function interceptor(response) {
|
||||
if (!isAnonymous && !isSystemApiRequest(response.config)) {
|
||||
npSetup.plugins.security.sessionTimeout.extend(response.config.url);
|
||||
}
|
||||
return responseHandler(response);
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
response: interceptorFactory(_.identity),
|
||||
responseError: interceptorFactory($q.reject),
|
||||
};
|
||||
});
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { identity } from 'lodash';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { Path } from 'plugins/xpack_main/services/path';
|
||||
import 'plugins/security/services/auto_logout';
|
||||
|
||||
function isUnauthorizedResponseAllowed(response) {
|
||||
const API_WHITELIST = ['/internal/security/login', '/internal/security/users/.*/password'];
|
||||
|
||||
const url = response.config.url;
|
||||
return API_WHITELIST.some(api => url.match(api));
|
||||
}
|
||||
|
||||
const module = uiModules.get('security');
|
||||
module.factory('onUnauthorizedResponse', ($q, autoLogout) => {
|
||||
const isUnauthenticated = Path.isUnauthenticated();
|
||||
function interceptorFactory(responseHandler) {
|
||||
return function interceptor(response) {
|
||||
if (response.status === 401 && !isUnauthorizedResponseAllowed(response) && !isUnauthenticated)
|
||||
return autoLogout();
|
||||
return responseHandler(response);
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
response: interceptorFactory(identity),
|
||||
responseError: interceptorFactory($q.reject),
|
||||
};
|
||||
});
|
||||
|
||||
module.config($httpProvider => {
|
||||
$httpProvider.interceptors.push('onUnauthorizedResponse');
|
||||
});
|
|
@ -1,15 +0,0 @@
|
|||
@import 'src/legacy/ui/public/styles/styling_constants';
|
||||
|
||||
// Prefix all styles with "kbn" to avoid conflicts.
|
||||
// Examples
|
||||
// secChart
|
||||
// secChart__legend
|
||||
// secChart__legend--small
|
||||
// secChart__legend-isLoading
|
||||
|
||||
// Public components
|
||||
@import './components/index';
|
||||
|
||||
// Public views
|
||||
@import './views/index';
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
const module = uiModules.get('security');
|
||||
|
||||
const getNextParameter = () => {
|
||||
const { location } = window;
|
||||
const next = encodeURIComponent(`${location.pathname}${location.search}${location.hash}`);
|
||||
return `&next=${next}`;
|
||||
};
|
||||
|
||||
const getProviderParameter = tenant => {
|
||||
const key = `${tenant}/session_provider`;
|
||||
const providerName = sessionStorage.getItem(key);
|
||||
return providerName ? `&provider=${encodeURIComponent(providerName)}` : '';
|
||||
};
|
||||
|
||||
module.service('autoLogout', ($window, Promise) => {
|
||||
return () => {
|
||||
const logoutUrl = chrome.getInjected('logoutUrl');
|
||||
const tenant = `${chrome.getInjected('session.tenant', '')}`;
|
||||
const next = getNextParameter();
|
||||
const provider = getProviderParameter(tenant);
|
||||
$window.location.href = `${logoutUrl}?msg=SESSION_EXPIRED${next}${provider}`;
|
||||
return Promise.halt();
|
||||
};
|
||||
});
|
|
@ -1,2 +0,0 @@
|
|||
// Login styles
|
||||
@import './login/index';
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
import routes from 'ui/routes';
|
||||
|
||||
routes.when('/account', {
|
||||
template: '<div id="userProfileReactRoot" />',
|
||||
k7Breadcrumbs: () => [
|
||||
{
|
||||
text: i18n.translate('xpack.security.account.breadcrumb', {
|
||||
defaultMessage: 'Account Management',
|
||||
}),
|
||||
},
|
||||
],
|
||||
controllerAs: 'accountController',
|
||||
controller($scope) {
|
||||
$scope.$$postDigest(() => {
|
||||
const domNode = document.getElementById('userProfileReactRoot');
|
||||
|
||||
render(
|
||||
<npStart.plugins.security.__legacyCompat.account_management.AccountManagementPage />,
|
||||
domNode
|
||||
);
|
||||
|
||||
$scope.$on('$destroy', () => unmountComponentAtNode(domNode));
|
||||
});
|
||||
},
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
<div id="reactLoggedOutRoot" />
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { AuthenticationStatePage } from 'plugins/security/components/authentication_state_page';
|
||||
// @ts-ignore
|
||||
import template from 'plugins/security/views/logged_out/logged_out.html';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import chrome from 'ui/chrome';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
|
||||
chrome
|
||||
.setVisible(false)
|
||||
.setRootTemplate(template)
|
||||
.setRootController('logout', ($scope: any) => {
|
||||
$scope.$$postDigest(() => {
|
||||
const domNode = document.getElementById('reactLoggedOutRoot');
|
||||
render(
|
||||
<I18nContext>
|
||||
<AuthenticationStatePage
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.security.loggedOut.title"
|
||||
defaultMessage="Successfully logged out"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiButton href={chrome.addBasePath('/')}>
|
||||
<FormattedMessage id="xpack.security.loggedOut.login" defaultMessage="Log in" />
|
||||
</EuiButton>
|
||||
</AuthenticationStatePage>
|
||||
</I18nContext>,
|
||||
domNode
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,8 +0,0 @@
|
|||
// Prefix all styles with "login" to avoid conflicts.
|
||||
// Examples
|
||||
// loginChart
|
||||
// loginChart__legend
|
||||
// loginChart__legend--small
|
||||
// loginChart__legend-isLoading
|
||||
|
||||
@import './components/index';
|
|
@ -1 +0,0 @@
|
|||
@import './login_page/index';
|
|
@ -1,109 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiCallOut } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { LoginState } from '../../login_state';
|
||||
import { BasicLoginForm } from './basic_login_form';
|
||||
|
||||
const createMockHttp = ({ simulateError = false } = {}) => {
|
||||
return {
|
||||
post: jest.fn(async () => {
|
||||
if (simulateError) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw {
|
||||
data: {
|
||||
statusCode: 401,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const createLoginState = (options?: Partial<LoginState>) => {
|
||||
return {
|
||||
allowLogin: true,
|
||||
layout: 'form',
|
||||
...options,
|
||||
} as LoginState;
|
||||
};
|
||||
|
||||
describe('BasicLoginForm', () => {
|
||||
it('renders as expected', () => {
|
||||
const mockHttp = createMockHttp();
|
||||
const mockWindow = {};
|
||||
const loginState = createLoginState();
|
||||
expect(
|
||||
shallowWithIntl(
|
||||
<BasicLoginForm.WrappedComponent
|
||||
http={mockHttp}
|
||||
window={mockWindow}
|
||||
loginState={loginState}
|
||||
next={''}
|
||||
intl={null as any}
|
||||
loginAssistanceMessage=""
|
||||
/>
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders an info message when provided', () => {
|
||||
const mockHttp = createMockHttp();
|
||||
const mockWindow = {};
|
||||
const loginState = createLoginState();
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<BasicLoginForm.WrappedComponent
|
||||
http={mockHttp}
|
||||
window={mockWindow}
|
||||
loginState={loginState}
|
||||
next={''}
|
||||
infoMessage={'Hey this is an info message'}
|
||||
intl={null as any}
|
||||
loginAssistanceMessage=""
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiCallOut).props().title).toEqual('Hey this is an info message');
|
||||
});
|
||||
|
||||
it('renders an invalid credentials message', async () => {
|
||||
const mockHttp = createMockHttp({ simulateError: true });
|
||||
const mockWindow = {};
|
||||
const loginState = createLoginState();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<BasicLoginForm.WrappedComponent
|
||||
http={mockHttp}
|
||||
window={mockWindow}
|
||||
loginState={loginState}
|
||||
next={''}
|
||||
intl={null as any}
|
||||
loginAssistanceMessage=""
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find('input[name="username"]').simulate('change', { target: { value: 'username' } });
|
||||
wrapper.find('input[name="password"]').simulate('change', { target: { value: 'password' } });
|
||||
wrapper.find(EuiButton).simulate('click');
|
||||
|
||||
// Wait for ajax + rerender
|
||||
await Promise.resolve();
|
||||
wrapper.update();
|
||||
await Promise.resolve();
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find(EuiCallOut).props().title).toEqual(
|
||||
`Invalid username or password. Please try again.`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,485 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LoginPage disabled form states renders as expected when a connection to ES is not available 1`] = `
|
||||
<div
|
||||
className="loginWelcome login-form"
|
||||
>
|
||||
<header
|
||||
className="loginWelcome__header"
|
||||
>
|
||||
<div
|
||||
className="loginWelcome__content eui-textCenter loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<span
|
||||
className="loginWelcome__logo"
|
||||
>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
type="logoKibana"
|
||||
/>
|
||||
</span>
|
||||
<EuiTitle
|
||||
className="loginWelcome__title"
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Welcome to Kibana"
|
||||
id="xpack.security.loginPage.welcomeTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText
|
||||
className="loginWelcome__subtitle"
|
||||
color="subdued"
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Your window into the Elastic Stack"
|
||||
id="xpack.security.loginPage.welcomeDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="loginWelcome__content loginWelcome-body loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="l"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<DisabledLoginForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
defaultMessage="See the Kibana logs for details and try reloading the page."
|
||||
id="xpack.security.loginPage.esUnavailableMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Cannot connect to the Elasticsearch cluster"
|
||||
id="xpack.security.loginPage.esUnavailableTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LoginPage disabled form states renders as expected when an unknown loginState layout is provided 1`] = `
|
||||
<div
|
||||
className="loginWelcome login-form"
|
||||
>
|
||||
<header
|
||||
className="loginWelcome__header"
|
||||
>
|
||||
<div
|
||||
className="loginWelcome__content eui-textCenter loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<span
|
||||
className="loginWelcome__logo"
|
||||
>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
type="logoKibana"
|
||||
/>
|
||||
</span>
|
||||
<EuiTitle
|
||||
className="loginWelcome__title"
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Welcome to Kibana"
|
||||
id="xpack.security.loginPage.welcomeTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText
|
||||
className="loginWelcome__subtitle"
|
||||
color="subdued"
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Your window into the Elastic Stack"
|
||||
id="xpack.security.loginPage.welcomeDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="loginWelcome__content loginWelcome-body loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="l"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<DisabledLoginForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
defaultMessage="Refer to the Kibana logs for more details and refresh to try again."
|
||||
id="xpack.security.loginPage.unknownLayoutMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Unsupported login form layout."
|
||||
id="xpack.security.loginPage.unknownLayoutTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LoginPage disabled form states renders as expected when loginAssistanceMessage is set 1`] = `
|
||||
<div
|
||||
className="loginWelcome login-form"
|
||||
>
|
||||
<header
|
||||
className="loginWelcome__header"
|
||||
>
|
||||
<div
|
||||
className="loginWelcome__content eui-textCenter"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<span
|
||||
className="loginWelcome__logo"
|
||||
>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
type="logoKibana"
|
||||
/>
|
||||
</span>
|
||||
<EuiTitle
|
||||
className="loginWelcome__title"
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Welcome to Kibana"
|
||||
id="xpack.security.loginPage.welcomeTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText
|
||||
className="loginWelcome__subtitle"
|
||||
color="subdued"
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Your window into the Elastic Stack"
|
||||
id="xpack.security.loginPage.welcomeDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="loginWelcome__content loginWelcome-body"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="l"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<InjectIntl(BasicLoginFormUI)
|
||||
http={
|
||||
Object {
|
||||
"post": [MockFunction],
|
||||
}
|
||||
}
|
||||
isSecureConnection={false}
|
||||
loginAssistanceMessage="This is an *important* message"
|
||||
loginState={
|
||||
Object {
|
||||
"allowLogin": true,
|
||||
"layout": "form",
|
||||
}
|
||||
}
|
||||
next=""
|
||||
requiresSecureConnection={false}
|
||||
window={Object {}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LoginPage disabled form states renders as expected when secure cookies are required but not present 1`] = `
|
||||
<div
|
||||
className="loginWelcome login-form"
|
||||
>
|
||||
<header
|
||||
className="loginWelcome__header"
|
||||
>
|
||||
<div
|
||||
className="loginWelcome__content eui-textCenter loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<span
|
||||
className="loginWelcome__logo"
|
||||
>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
type="logoKibana"
|
||||
/>
|
||||
</span>
|
||||
<EuiTitle
|
||||
className="loginWelcome__title"
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Welcome to Kibana"
|
||||
id="xpack.security.loginPage.welcomeTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText
|
||||
className="loginWelcome__subtitle"
|
||||
color="subdued"
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Your window into the Elastic Stack"
|
||||
id="xpack.security.loginPage.welcomeDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="loginWelcome__content loginWelcome-body loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="l"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<DisabledLoginForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
defaultMessage="Contact your system administrator."
|
||||
id="xpack.security.loginPage.requiresSecureConnectionMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="A secure connection is required for log in"
|
||||
id="xpack.security.loginPage.requiresSecureConnectionTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LoginPage disabled form states renders as expected when xpack is not available 1`] = `
|
||||
<div
|
||||
className="loginWelcome login-form"
|
||||
>
|
||||
<header
|
||||
className="loginWelcome__header"
|
||||
>
|
||||
<div
|
||||
className="loginWelcome__content eui-textCenter loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<span
|
||||
className="loginWelcome__logo"
|
||||
>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
type="logoKibana"
|
||||
/>
|
||||
</span>
|
||||
<EuiTitle
|
||||
className="loginWelcome__title"
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Welcome to Kibana"
|
||||
id="xpack.security.loginPage.welcomeTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText
|
||||
className="loginWelcome__subtitle"
|
||||
color="subdued"
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Your window into the Elastic Stack"
|
||||
id="xpack.security.loginPage.welcomeDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="loginWelcome__content loginWelcome-body loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="l"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<DisabledLoginForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
defaultMessage="To use the full set of free features in this distribution of Kibana, please update Elasticsearch to the default distribution."
|
||||
id="xpack.security.loginPage.xpackUnavailableMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Cannot connect to the Elasticsearch cluster currently configured for Kibana."
|
||||
id="xpack.security.loginPage.xpackUnavailableTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LoginPage enabled form state renders as expected 1`] = `
|
||||
<div
|
||||
className="loginWelcome login-form"
|
||||
>
|
||||
<header
|
||||
className="loginWelcome__header"
|
||||
>
|
||||
<div
|
||||
className="loginWelcome__content eui-textCenter"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<span
|
||||
className="loginWelcome__logo"
|
||||
>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
type="logoKibana"
|
||||
/>
|
||||
</span>
|
||||
<EuiTitle
|
||||
className="loginWelcome__title"
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Welcome to Kibana"
|
||||
id="xpack.security.loginPage.welcomeTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText
|
||||
className="loginWelcome__subtitle"
|
||||
color="subdued"
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Your window into the Elastic Stack"
|
||||
id="xpack.security.loginPage.welcomeDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="loginWelcome__content loginWelcome-body"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="l"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<InjectIntl(BasicLoginFormUI)
|
||||
http={
|
||||
Object {
|
||||
"post": [MockFunction],
|
||||
}
|
||||
}
|
||||
isSecureConnection={false}
|
||||
loginAssistanceMessage=""
|
||||
loginState={
|
||||
Object {
|
||||
"allowLogin": true,
|
||||
"layout": "form",
|
||||
}
|
||||
}
|
||||
next=""
|
||||
requiresSecureConnection={false}
|
||||
window={Object {}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -1,133 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { LoginLayout, LoginState } from '../../login_state';
|
||||
import { LoginPage } from './login_page';
|
||||
|
||||
const createMockHttp = ({ simulateError = false } = {}) => {
|
||||
return {
|
||||
post: jest.fn(async () => {
|
||||
if (simulateError) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw {
|
||||
data: {
|
||||
statusCode: 401,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const createLoginState = (options?: Partial<LoginState>) => {
|
||||
return {
|
||||
allowLogin: true,
|
||||
layout: 'form',
|
||||
...options,
|
||||
} as LoginState;
|
||||
};
|
||||
|
||||
describe('LoginPage', () => {
|
||||
describe('disabled form states', () => {
|
||||
it('renders as expected when secure cookies are required but not present', () => {
|
||||
const props = {
|
||||
http: createMockHttp(),
|
||||
window: {},
|
||||
next: '',
|
||||
loginState: createLoginState(),
|
||||
isSecureConnection: false,
|
||||
requiresSecureConnection: true,
|
||||
loginAssistanceMessage: '',
|
||||
};
|
||||
|
||||
expect(shallow(<LoginPage {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders as expected when a connection to ES is not available', () => {
|
||||
const props = {
|
||||
http: createMockHttp(),
|
||||
window: {},
|
||||
next: '',
|
||||
loginState: createLoginState({
|
||||
layout: 'error-es-unavailable',
|
||||
}),
|
||||
isSecureConnection: false,
|
||||
requiresSecureConnection: false,
|
||||
loginAssistanceMessage: '',
|
||||
};
|
||||
|
||||
expect(shallow(<LoginPage {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders as expected when xpack is not available', () => {
|
||||
const props = {
|
||||
http: createMockHttp(),
|
||||
window: {},
|
||||
next: '',
|
||||
loginState: createLoginState({
|
||||
layout: 'error-xpack-unavailable',
|
||||
}),
|
||||
isSecureConnection: false,
|
||||
requiresSecureConnection: false,
|
||||
loginAssistanceMessage: '',
|
||||
};
|
||||
|
||||
expect(shallow(<LoginPage {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders as expected when an unknown loginState layout is provided', () => {
|
||||
const props = {
|
||||
http: createMockHttp(),
|
||||
window: {},
|
||||
next: '',
|
||||
loginState: createLoginState({
|
||||
layout: 'error-asdf-asdf-unknown' as LoginLayout,
|
||||
}),
|
||||
isSecureConnection: false,
|
||||
requiresSecureConnection: false,
|
||||
loginAssistanceMessage: '',
|
||||
};
|
||||
|
||||
expect(shallow(<LoginPage {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders as expected when loginAssistanceMessage is set', () => {
|
||||
const props = {
|
||||
http: createMockHttp(),
|
||||
window: {},
|
||||
next: '',
|
||||
loginState: createLoginState(),
|
||||
isSecureConnection: false,
|
||||
requiresSecureConnection: false,
|
||||
loginAssistanceMessage: 'This is an *important* message',
|
||||
};
|
||||
|
||||
expect(shallow(<LoginPage {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('enabled form state', () => {
|
||||
it('renders as expected', () => {
|
||||
const props = {
|
||||
http: createMockHttp(),
|
||||
window: {},
|
||||
next: '',
|
||||
loginState: createLoginState(),
|
||||
isSecureConnection: false,
|
||||
requiresSecureConnection: false,
|
||||
loginAssistanceMessage: '',
|
||||
};
|
||||
|
||||
expect(shallow(<LoginPage {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { get } from 'lodash';
|
||||
import { LoginPage } from 'plugins/security/views/login/components';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import chrome from 'ui/chrome';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import { parse } from 'url';
|
||||
import { parseNext } from './parse_next';
|
||||
import { LoginState } from './login_state';
|
||||
const messageMap = {
|
||||
SESSION_EXPIRED: i18n.translate('xpack.security.login.sessionExpiredDescription', {
|
||||
defaultMessage: 'Your session has timed out. Please log in again.',
|
||||
}),
|
||||
LOGGED_OUT: i18n.translate('xpack.security.login.loggedOutDescription', {
|
||||
defaultMessage: 'You have logged out of Kibana.',
|
||||
}),
|
||||
};
|
||||
|
||||
interface AnyObject {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
(chrome as AnyObject)
|
||||
.setVisible(false)
|
||||
.setRootTemplate('<div id="reactLoginRoot" />')
|
||||
.setRootController(
|
||||
'login',
|
||||
(
|
||||
$scope: AnyObject,
|
||||
$http: AnyObject,
|
||||
$window: AnyObject,
|
||||
secureCookies: boolean,
|
||||
loginState: LoginState,
|
||||
loginAssistanceMessage: string
|
||||
) => {
|
||||
const basePath = chrome.getBasePath();
|
||||
const next = parseNext($window.location.href, basePath);
|
||||
const isSecure = !!$window.location.protocol.match(/^https/);
|
||||
|
||||
$scope.$$postDigest(() => {
|
||||
const domNode = document.getElementById('reactLoginRoot');
|
||||
|
||||
const msgQueryParam = parse($window.location.href, true).query.msg || '';
|
||||
|
||||
render(
|
||||
<I18nContext>
|
||||
<LoginPage
|
||||
http={$http}
|
||||
window={$window}
|
||||
infoMessage={get(messageMap, msgQueryParam)}
|
||||
loginState={loginState}
|
||||
isSecureConnection={isSecure}
|
||||
requiresSecureConnection={secureCookies}
|
||||
loginAssistanceMessage={loginAssistanceMessage}
|
||||
next={next}
|
||||
/>
|
||||
</I18nContext>,
|
||||
domNode
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './logout';
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
chrome.setVisible(false).setRootController('logout', $window => {
|
||||
$window.sessionStorage.clear();
|
||||
|
||||
// Redirect user to the server logout endpoint to complete logout.
|
||||
$window.location.href = chrome.addBasePath(`/api/security/logout${$window.location.search}`);
|
||||
});
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './overwritten_session';
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import chrome from 'ui/chrome';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import { npSetup } from 'ui/new_platform';
|
||||
import { AuthenticatedUser, SecurityPluginSetup } from '../../../../../../plugins/security/public';
|
||||
import { AuthenticationStatePage } from '../../components/authentication_state_page';
|
||||
|
||||
chrome
|
||||
.setVisible(false)
|
||||
.setRootTemplate('<div id="reactOverwrittenSessionRoot" />')
|
||||
.setRootController('overwritten_session', ($scope: any) => {
|
||||
$scope.$$postDigest(() => {
|
||||
((npSetup.plugins as unknown) as { security: SecurityPluginSetup }).security.authc
|
||||
.getCurrentUser()
|
||||
.then((user: AuthenticatedUser) => {
|
||||
const overwrittenSessionPage = (
|
||||
<I18nContext>
|
||||
<AuthenticationStatePage
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.security.overwrittenSession.title"
|
||||
defaultMessage="You previously logged in as a different user."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiButton href={chrome.addBasePath('/')}>
|
||||
<FormattedMessage
|
||||
id="xpack.security.overwrittenSession.continueAsUserText"
|
||||
defaultMessage="Continue as {username}"
|
||||
values={{ username: user.username }}
|
||||
/>
|
||||
</EuiButton>
|
||||
</AuthenticationStatePage>
|
||||
</I18nContext>
|
||||
);
|
||||
render(overwrittenSessionPage, document.getElementById('reactOverwrittenSessionRoot'));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,172 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { parseNext } from '../parse_next';
|
||||
|
||||
describe('parseNext', () => {
|
||||
it('should return a function', () => {
|
||||
expect(parseNext).to.be.a('function');
|
||||
});
|
||||
|
||||
describe('with basePath defined', () => {
|
||||
// trailing slash is important since it must match the cookie path exactly
|
||||
it('should return basePath with a trailing slash when next is not specified', () => {
|
||||
const basePath = '/iqf';
|
||||
const href = `${basePath}/login`;
|
||||
expect(parseNext(href, basePath)).to.equal(`${basePath}/`);
|
||||
});
|
||||
|
||||
it('should properly handle next without hash', () => {
|
||||
const basePath = '/iqf';
|
||||
const next = `${basePath}/app/kibana`;
|
||||
const href = `${basePath}/login?next=${next}`;
|
||||
expect(parseNext(href, basePath)).to.equal(next);
|
||||
});
|
||||
|
||||
it('should properly handle next with hash', () => {
|
||||
const basePath = '/iqf';
|
||||
const next = `${basePath}/app/kibana`;
|
||||
const hash = '/discover/New-Saved-Search';
|
||||
const href = `${basePath}/login?next=${next}#${hash}`;
|
||||
expect(parseNext(href, basePath)).to.equal(`${next}#${hash}`);
|
||||
});
|
||||
|
||||
it('should properly decode special characters', () => {
|
||||
const basePath = '/iqf';
|
||||
const next = `${encodeURIComponent(basePath)}%2Fapp%2Fkibana`;
|
||||
const hash = '/discover/New-Saved-Search';
|
||||
const href = `${basePath}/login?next=${next}#${hash}`;
|
||||
expect(parseNext(href, basePath)).to.equal(decodeURIComponent(`${next}#${hash}`));
|
||||
});
|
||||
|
||||
// to help prevent open redirect to a different url
|
||||
it('should return basePath if next includes a protocol/hostname', () => {
|
||||
const basePath = '/iqf';
|
||||
const next = `https://example.com${basePath}/app/kibana`;
|
||||
const href = `${basePath}/login?next=${next}`;
|
||||
expect(parseNext(href, basePath)).to.equal(`${basePath}/`);
|
||||
});
|
||||
|
||||
// to help prevent open redirect to a different url by abusing encodings
|
||||
it('should return basePath if including a protocol/host even if it is encoded', () => {
|
||||
const basePath = '/iqf';
|
||||
const baseUrl = `http://example.com${basePath}`;
|
||||
const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`;
|
||||
const hash = '/discover/New-Saved-Search';
|
||||
const href = `${basePath}/login?next=${next}#${hash}`;
|
||||
expect(parseNext(href, basePath)).to.equal(`${basePath}/`);
|
||||
});
|
||||
|
||||
// to help prevent open redirect to a different port
|
||||
it('should return basePath if next includes a port', () => {
|
||||
const basePath = '/iqf';
|
||||
const next = `http://localhost:5601${basePath}/app/kibana`;
|
||||
const href = `${basePath}/login?next=${next}`;
|
||||
expect(parseNext(href, basePath)).to.equal(`${basePath}/`);
|
||||
});
|
||||
|
||||
// to help prevent open redirect to a different port by abusing encodings
|
||||
it('should return basePath if including a port even if it is encoded', () => {
|
||||
const basePath = '/iqf';
|
||||
const baseUrl = `http://example.com:5601${basePath}`;
|
||||
const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`;
|
||||
const hash = '/discover/New-Saved-Search';
|
||||
const href = `${basePath}/login?next=${next}#${hash}`;
|
||||
expect(parseNext(href, basePath)).to.equal(`${basePath}/`);
|
||||
});
|
||||
|
||||
// to help prevent open redirect to a different base path
|
||||
it('should return basePath if next does not begin with basePath', () => {
|
||||
const basePath = '/iqf';
|
||||
const next = '/notbasepath/app/kibana';
|
||||
const href = `${basePath}/login?next=${next}`;
|
||||
expect(parseNext(href, basePath)).to.equal(`${basePath}/`);
|
||||
});
|
||||
|
||||
// disallow network-path references
|
||||
it('should return / if next is url without protocol', () => {
|
||||
const nextWithTwoSlashes = '//example.com';
|
||||
const hrefWithTwoSlashes = `/login?next=${nextWithTwoSlashes}`;
|
||||
expect(parseNext(hrefWithTwoSlashes)).to.equal('/');
|
||||
|
||||
const nextWithThreeSlashes = '///example.com';
|
||||
const hrefWithThreeSlashes = `/login?next=${nextWithThreeSlashes}`;
|
||||
expect(parseNext(hrefWithThreeSlashes)).to.equal('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('without basePath defined', () => {
|
||||
// trailing slash is important since it must match the cookie path exactly
|
||||
it('should return / with a trailing slash when next is not specified', () => {
|
||||
const href = '/login';
|
||||
expect(parseNext(href)).to.equal('/');
|
||||
});
|
||||
|
||||
it('should properly handle next without hash', () => {
|
||||
const next = '/app/kibana';
|
||||
const href = `/login?next=${next}`;
|
||||
expect(parseNext(href)).to.equal(next);
|
||||
});
|
||||
|
||||
it('should properly handle next with hash', () => {
|
||||
const next = '/app/kibana';
|
||||
const hash = '/discover/New-Saved-Search';
|
||||
const href = `/login?next=${next}#${hash}`;
|
||||
expect(parseNext(href)).to.equal(`${next}#${hash}`);
|
||||
});
|
||||
|
||||
it('should properly decode special characters', () => {
|
||||
const next = '%2Fapp%2Fkibana';
|
||||
const hash = '/discover/New-Saved-Search';
|
||||
const href = `/login?next=${next}#${hash}`;
|
||||
expect(parseNext(href)).to.equal(decodeURIComponent(`${next}#${hash}`));
|
||||
});
|
||||
|
||||
// to help prevent open redirect to a different url
|
||||
it('should return / if next includes a protocol/hostname', () => {
|
||||
const next = 'https://example.com/app/kibana';
|
||||
const href = `/login?next=${next}`;
|
||||
expect(parseNext(href)).to.equal('/');
|
||||
});
|
||||
|
||||
// to help prevent open redirect to a different url by abusing encodings
|
||||
it('should return / if including a protocol/host even if it is encoded', () => {
|
||||
const baseUrl = 'http://example.com';
|
||||
const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`;
|
||||
const hash = '/discover/New-Saved-Search';
|
||||
const href = `/login?next=${next}#${hash}`;
|
||||
expect(parseNext(href)).to.equal('/');
|
||||
});
|
||||
|
||||
// to help prevent open redirect to a different port
|
||||
it('should return / if next includes a port', () => {
|
||||
const next = 'http://localhost:5601/app/kibana';
|
||||
const href = `/login?next=${next}`;
|
||||
expect(parseNext(href)).to.equal('/');
|
||||
});
|
||||
|
||||
// to help prevent open redirect to a different port by abusing encodings
|
||||
it('should return / if including a port even if it is encoded', () => {
|
||||
const baseUrl = 'http://example.com:5601';
|
||||
const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`;
|
||||
const hash = '/discover/New-Saved-Search';
|
||||
const href = `/login?next=${next}#${hash}`;
|
||||
expect(parseNext(href)).to.equal('/');
|
||||
});
|
||||
|
||||
// disallow network-path references
|
||||
it('should return / if next is url without protocol', () => {
|
||||
const nextWithTwoSlashes = '//example.com';
|
||||
const hrefWithTwoSlashes = `/login?next=${nextWithTwoSlashes}`;
|
||||
expect(parseNext(hrefWithTwoSlashes)).to.equal('/');
|
||||
|
||||
const nextWithThreeSlashes = '///example.com';
|
||||
const hrefWithThreeSlashes = `/login?next=${nextWithThreeSlashes}`;
|
||||
expect(parseNext(hrefWithThreeSlashes)).to.equal('/');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { parse } from 'url';
|
||||
|
||||
export function parseNext(href, basePath = '') {
|
||||
const { query, hash } = parse(href, true);
|
||||
if (!query.next) {
|
||||
return `${basePath}/`;
|
||||
}
|
||||
|
||||
// validate that `next` is not attempting a redirect to somewhere
|
||||
// outside of this Kibana install
|
||||
const { protocol, hostname, port, pathname } = parse(
|
||||
query.next,
|
||||
false /* parseQueryString */,
|
||||
true /* slashesDenoteHost */
|
||||
);
|
||||
|
||||
// We should explicitly compare `protocol`, `port` and `hostname` to null to make sure these are not
|
||||
// detected in the URL at all. For example `hostname` can be empty string for Node URL parser, but
|
||||
// browser (because of various bwc reasons) processes URL differently (e.g. `///abc.com` - for browser
|
||||
// hostname is `abc.com`, but for Node hostname is an empty string i.e. everything between schema (`//`)
|
||||
// and the first slash that belongs to path.
|
||||
if (protocol !== null || hostname !== null || port !== null) {
|
||||
return `${basePath}/`;
|
||||
}
|
||||
|
||||
if (!String(pathname).startsWith(basePath)) {
|
||||
return `${basePath}/`;
|
||||
}
|
||||
|
||||
return query.next + (hash || '');
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export function initLoggedOutView(
|
||||
{
|
||||
__legacyCompat: {
|
||||
config: { cookieName },
|
||||
},
|
||||
},
|
||||
server
|
||||
) {
|
||||
const config = server.config();
|
||||
const loggedOut = server.getHiddenUiAppById('logged_out');
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/logged_out',
|
||||
handler(request, h) {
|
||||
const isUserAlreadyLoggedIn = !!request.state[cookieName];
|
||||
if (isUserAlreadyLoggedIn) {
|
||||
const basePath = config.get('server.basePath');
|
||||
return h.redirect(`${basePath}/`);
|
||||
}
|
||||
return h.renderAppWithDefaultConfig(loggedOut);
|
||||
},
|
||||
config: {
|
||||
auth: false,
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { parseNext } from '../../lib/parse_next';
|
||||
|
||||
export function initLoginView(
|
||||
{
|
||||
__legacyCompat: {
|
||||
config: { cookieName },
|
||||
license,
|
||||
},
|
||||
},
|
||||
server
|
||||
) {
|
||||
const config = server.config();
|
||||
const login = server.getHiddenUiAppById('login');
|
||||
|
||||
function shouldShowLogin() {
|
||||
if (license.isEnabled()) {
|
||||
return Boolean(license.getFeatures().showLogin);
|
||||
}
|
||||
|
||||
// default to true if xpack info isn't available or
|
||||
// it can't be resolved for some reason
|
||||
return true;
|
||||
}
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/login',
|
||||
handler(request, h) {
|
||||
const isUserAlreadyLoggedIn = !!request.state[cookieName];
|
||||
if (isUserAlreadyLoggedIn || !shouldShowLogin()) {
|
||||
const basePath = config.get('server.basePath');
|
||||
const url = get(request, 'raw.req.url');
|
||||
const next = parseNext(url, basePath);
|
||||
return h.redirect(next);
|
||||
}
|
||||
return h.renderAppWithDefaultConfig(login);
|
||||
},
|
||||
config: {
|
||||
auth: false,
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export function initLogoutView(server) {
|
||||
const logout = server.getHiddenUiAppById('logout');
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/logout',
|
||||
handler(request, h) {
|
||||
// eslint-disable-line no-unused-vars
|
||||
return h.renderAppWithDefaultConfig(logout);
|
||||
},
|
||||
config: {
|
||||
auth: false,
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Request, ResponseToolkit } from 'hapi';
|
||||
import { Legacy } from 'kibana';
|
||||
|
||||
export function initOverwrittenSessionView(server: Legacy.Server) {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/overwritten_session',
|
||||
handler(request: Request, h: ResponseToolkit) {
|
||||
return h.renderAppWithDefaultConfig(server.getHiddenUiAppById('overwritten_session'));
|
||||
},
|
||||
});
|
||||
}
|
|
@ -9,6 +9,11 @@ import chrome from 'ui/chrome';
|
|||
export const Path = {
|
||||
isUnauthenticated() {
|
||||
const path = chrome.removeBasePath(window.location.pathname);
|
||||
return path === '/login' || path === '/logout' || path === '/logged_out' || path === '/status';
|
||||
return (
|
||||
path === '/login' ||
|
||||
path === '/logout' ||
|
||||
path === '/security/logged_out' ||
|
||||
path === '/status'
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -58,7 +58,6 @@ describe('XPackInfo routes', () => {
|
|||
showLinks: false,
|
||||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
linksMessage: 'Message',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -79,7 +78,6 @@ describe('XPackInfo routes', () => {
|
|||
show_links: false,
|
||||
allow_role_document_level_security: false,
|
||||
allow_role_field_level_security: false,
|
||||
links_message: 'Message',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,4 +6,4 @@
|
|||
|
||||
export { SecurityLicenseService, SecurityLicense } from './license_service';
|
||||
|
||||
export { SecurityLicenseFeatures } from './license_features';
|
||||
export { LoginLayout, SecurityLicenseFeatures } from './license_features';
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents types of login form layouts.
|
||||
*/
|
||||
export type LoginLayout = 'form' | 'error-es-unavailable' | 'error-xpack-unavailable';
|
||||
|
||||
/**
|
||||
* Describes Security plugin features that depend on license.
|
||||
*/
|
||||
|
@ -46,10 +51,5 @@ export interface SecurityLicenseFeatures {
|
|||
/**
|
||||
* Describes the layout of the login form if it's displayed.
|
||||
*/
|
||||
readonly layout?: string;
|
||||
|
||||
/**
|
||||
* Message to show when security links are clicked throughout the kibana app.
|
||||
*/
|
||||
readonly linksMessage?: string;
|
||||
readonly layout?: LoginLayout;
|
||||
}
|
||||
|
|
|
@ -79,7 +79,6 @@ describe('license features', function() {
|
|||
"allowRbac": false,
|
||||
"allowRoleDocumentLevelSecurity": false,
|
||||
"allowRoleFieldLevelSecurity": false,
|
||||
"linksMessage": "Access is denied because Security is disabled in Elasticsearch.",
|
||||
"showLinks": false,
|
||||
"showLogin": false,
|
||||
"showRoleMappingsManagement": false,
|
||||
|
@ -130,7 +129,6 @@ describe('license features', function() {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRbac: false,
|
||||
linksMessage: 'Access is denied because Security is disabled in Elasticsearch.',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -90,7 +90,6 @@ export class SecurityLicenseService {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRbac: false,
|
||||
linksMessage: 'Access is denied because Security is disabled in Elasticsearch.',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
jest.mock('./account_management_page');
|
||||
|
||||
import { AppMount, AppNavLinkStatus, ScopedHistory } from 'src/core/public';
|
||||
import { UserAPIClient } from '../management';
|
||||
import { accountManagementApp } from './account_management_app';
|
||||
|
||||
import { coreMock, scopedHistoryMock } from '../../../../../src/core/public/mocks';
|
||||
import { securityMock } from '../mocks';
|
||||
|
||||
describe('accountManagementApp', () => {
|
||||
it('properly registers application', () => {
|
||||
const coreSetupMock = coreMock.createSetup();
|
||||
|
||||
accountManagementApp.create({
|
||||
application: coreSetupMock.application,
|
||||
getStartServices: coreSetupMock.getStartServices,
|
||||
authc: securityMock.createSetup().authc,
|
||||
});
|
||||
|
||||
expect(coreSetupMock.application.register).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [[appRegistration]] = coreSetupMock.application.register.mock.calls;
|
||||
expect(appRegistration).toEqual({
|
||||
id: 'security_account',
|
||||
appRoute: '/security/account',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
title: 'Account Management',
|
||||
mount: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('properly sets breadcrumbs and renders application', async () => {
|
||||
const coreSetupMock = coreMock.createSetup();
|
||||
const coreStartMock = coreMock.createStart();
|
||||
coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}]);
|
||||
|
||||
const authcMock = securityMock.createSetup().authc;
|
||||
const containerMock = document.createElement('div');
|
||||
|
||||
accountManagementApp.create({
|
||||
application: coreSetupMock.application,
|
||||
getStartServices: coreSetupMock.getStartServices,
|
||||
authc: authcMock,
|
||||
});
|
||||
|
||||
const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
|
||||
await (mount as AppMount)({
|
||||
element: containerMock,
|
||||
appBasePath: '',
|
||||
onAppLeave: jest.fn(),
|
||||
history: (scopedHistoryMock.create() as unknown) as ScopedHistory,
|
||||
});
|
||||
|
||||
expect(coreStartMock.chrome.setBreadcrumbs).toHaveBeenCalledTimes(1);
|
||||
expect(coreStartMock.chrome.setBreadcrumbs).toHaveBeenCalledWith([
|
||||
{ text: 'Account Management' },
|
||||
]);
|
||||
|
||||
const mockRenderApp = jest.requireMock('./account_management_page').renderAccountManagementPage;
|
||||
expect(mockRenderApp).toHaveBeenCalledTimes(1);
|
||||
expect(mockRenderApp).toHaveBeenCalledWith(coreStartMock.i18n, containerMock, {
|
||||
userAPIClient: expect.any(UserAPIClient),
|
||||
authc: authcMock,
|
||||
notifications: coreStartMock.notifications,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CoreSetup, AppMountParameters } from 'src/core/public';
|
||||
import { AuthenticationServiceSetup } from '../authentication';
|
||||
import { UserAPIClient } from '../management';
|
||||
|
||||
interface CreateDeps {
|
||||
application: CoreSetup['application'];
|
||||
authc: AuthenticationServiceSetup;
|
||||
getStartServices: CoreSetup['getStartServices'];
|
||||
}
|
||||
|
||||
export const accountManagementApp = Object.freeze({
|
||||
id: 'security_account',
|
||||
create({ application, authc, getStartServices }: CreateDeps) {
|
||||
const title = i18n.translate('xpack.security.account.breadcrumb', {
|
||||
defaultMessage: 'Account Management',
|
||||
});
|
||||
application.register({
|
||||
id: this.id,
|
||||
title,
|
||||
// TODO: switch to proper enum once https://github.com/elastic/kibana/issues/58327 is resolved.
|
||||
navLinkStatus: 3,
|
||||
appRoute: '/security/account',
|
||||
async mount({ element }: AppMountParameters) {
|
||||
const [[coreStart], { renderAccountManagementPage }] = await Promise.all([
|
||||
getStartServices(),
|
||||
import('./account_management_page'),
|
||||
]);
|
||||
|
||||
coreStart.chrome.setBreadcrumbs([{ text: title }]);
|
||||
|
||||
return renderAccountManagementPage(coreStart.i18n, element, {
|
||||
authc,
|
||||
notifications: coreStart.notifications,
|
||||
userAPIClient: new UserAPIClient(coreStart.http),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
|
@ -3,13 +3,14 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiPage, EuiPageBody, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { NotificationsStart } from 'src/core/public';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { EuiPage, EuiPageBody, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { CoreStart, NotificationsStart } from 'src/core/public';
|
||||
import { getUserDisplayName, AuthenticatedUser } from '../../common/model';
|
||||
import { AuthenticationServiceSetup } from '../authentication';
|
||||
import { ChangePassword } from './change_password';
|
||||
import { UserAPIClient } from '../management';
|
||||
import { ChangePassword } from './change_password';
|
||||
import { PersonalInfo } from './personal_info';
|
||||
|
||||
interface Props {
|
||||
|
@ -50,3 +51,18 @@ export const AccountManagementPage = ({ userAPIClient, authc, notifications }: P
|
|||
</EuiPage>
|
||||
);
|
||||
};
|
||||
|
||||
export function renderAccountManagementPage(
|
||||
i18nStart: CoreStart['i18n'],
|
||||
element: Element,
|
||||
props: Props
|
||||
) {
|
||||
ReactDOM.render(
|
||||
<i18nStart.Context>
|
||||
<AccountManagementPage {...props} />
|
||||
</i18nStart.Context>,
|
||||
element
|
||||
);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
}
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { AccountManagementPage } from './account_management_page';
|
||||
export { accountManagementApp } from './account_management_app';
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
// Component styles
|
||||
@import './components/index';
|
||||
|
||||
// Login styles
|
||||
@import './login/index';
|
|
@ -4,11 +4,20 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { HttpSetup } from 'src/core/public';
|
||||
import { ApplicationSetup, CoreSetup, HttpSetup } from 'src/core/public';
|
||||
import { AuthenticatedUser } from '../../common/model';
|
||||
import { ConfigType } from '../config';
|
||||
import { PluginStartDependencies } from '../plugin';
|
||||
import { loginApp } from './login';
|
||||
import { logoutApp } from './logout';
|
||||
import { loggedOutApp } from './logged_out';
|
||||
import { overwrittenSessionApp } from './overwritten_session';
|
||||
|
||||
interface SetupParams {
|
||||
application: ApplicationSetup;
|
||||
config: ConfigType;
|
||||
http: HttpSetup;
|
||||
getStartServices: CoreSetup<PluginStartDependencies>['getStartServices'];
|
||||
}
|
||||
|
||||
export interface AuthenticationServiceSetup {
|
||||
|
@ -19,13 +28,20 @@ export interface AuthenticationServiceSetup {
|
|||
}
|
||||
|
||||
export class AuthenticationService {
|
||||
public setup({ http }: SetupParams): AuthenticationServiceSetup {
|
||||
return {
|
||||
async getCurrentUser() {
|
||||
return (await http.get('/internal/security/me', {
|
||||
asSystemRequest: true,
|
||||
})) as AuthenticatedUser;
|
||||
},
|
||||
};
|
||||
public setup({
|
||||
application,
|
||||
config,
|
||||
getStartServices,
|
||||
http,
|
||||
}: SetupParams): AuthenticationServiceSetup {
|
||||
const getCurrentUser = async () =>
|
||||
(await http.get('/internal/security/me', { asSystemRequest: true })) as AuthenticatedUser;
|
||||
|
||||
loginApp.create({ application, config, getStartServices, http });
|
||||
logoutApp.create({ application, http });
|
||||
loggedOutApp.create({ application, getStartServices, http });
|
||||
overwrittenSessionApp.create({ application, authc: { getCurrentUser }, getStartServices });
|
||||
|
||||
return { getCurrentUser };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { AuthenticationStatePage } from './authentication_state_page';
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { LoginPage } from './login_page';
|
||||
export { loggedOutApp } from './logged_out_app';
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
jest.mock('./logged_out_page');
|
||||
|
||||
import { AppMount, ScopedHistory } from 'src/core/public';
|
||||
import { loggedOutApp } from './logged_out_app';
|
||||
|
||||
import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks';
|
||||
|
||||
describe('loggedOutApp', () => {
|
||||
it('properly registers application', () => {
|
||||
const coreSetupMock = coreMock.createSetup();
|
||||
|
||||
loggedOutApp.create(coreSetupMock);
|
||||
|
||||
expect(coreSetupMock.http.anonymousPaths.register).toHaveBeenCalledTimes(1);
|
||||
expect(coreSetupMock.http.anonymousPaths.register).toHaveBeenCalledWith('/security/logged_out');
|
||||
|
||||
expect(coreSetupMock.application.register).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [[appRegistration]] = coreSetupMock.application.register.mock.calls;
|
||||
expect(appRegistration).toEqual({
|
||||
id: 'security_logged_out',
|
||||
chromeless: true,
|
||||
appRoute: '/security/logged_out',
|
||||
title: 'Logged out',
|
||||
mount: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('properly renders application', async () => {
|
||||
const coreSetupMock = coreMock.createSetup();
|
||||
const coreStartMock = coreMock.createStart();
|
||||
coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}]);
|
||||
|
||||
const containerMock = document.createElement('div');
|
||||
|
||||
loggedOutApp.create(coreSetupMock);
|
||||
|
||||
const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
|
||||
await (mount as AppMount)({
|
||||
element: containerMock,
|
||||
appBasePath: '',
|
||||
onAppLeave: jest.fn(),
|
||||
history: (scopedHistoryMock.create() as unknown) as ScopedHistory,
|
||||
});
|
||||
|
||||
const mockRenderApp = jest.requireMock('./logged_out_page').renderLoggedOutPage;
|
||||
expect(mockRenderApp).toHaveBeenCalledTimes(1);
|
||||
expect(mockRenderApp).toHaveBeenCalledWith(coreStartMock.i18n, containerMock, {
|
||||
basePath: coreStartMock.http.basePath,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CoreSetup, AppMountParameters, HttpSetup } from 'src/core/public';
|
||||
|
||||
interface CreateDeps {
|
||||
application: CoreSetup['application'];
|
||||
http: HttpSetup;
|
||||
getStartServices: CoreSetup['getStartServices'];
|
||||
}
|
||||
|
||||
export const loggedOutApp = Object.freeze({
|
||||
id: 'security_logged_out',
|
||||
create({ application, http, getStartServices }: CreateDeps) {
|
||||
http.anonymousPaths.register('/security/logged_out');
|
||||
application.register({
|
||||
id: this.id,
|
||||
title: i18n.translate('xpack.security.loggedOutAppTitle', { defaultMessage: 'Logged out' }),
|
||||
chromeless: true,
|
||||
appRoute: '/security/logged_out',
|
||||
async mount({ element }: AppMountParameters) {
|
||||
const [[coreStart], { renderLoggedOutPage }] = await Promise.all([
|
||||
getStartServices(),
|
||||
import('./logged_out_page'),
|
||||
]);
|
||||
return renderLoggedOutPage(coreStart.i18n, element, { basePath: coreStart.http.basePath });
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { CoreStart, IBasePath } from 'src/core/public';
|
||||
import { AuthenticationStatePage } from '../components';
|
||||
|
||||
interface Props {
|
||||
basePath: IBasePath;
|
||||
}
|
||||
|
||||
export function LoggedOutPage({ basePath }: Props) {
|
||||
return (
|
||||
<AuthenticationStatePage
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.security.loggedOut.title"
|
||||
defaultMessage="Successfully logged out"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiButton href={basePath.prepend('/')}>
|
||||
<FormattedMessage id="xpack.security.loggedOut.login" defaultMessage="Log in" />
|
||||
</EuiButton>
|
||||
</AuthenticationStatePage>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderLoggedOutPage(i18nStart: CoreStart['i18n'], element: Element, props: Props) {
|
||||
ReactDOM.render(
|
||||
<i18nStart.Context>
|
||||
<LoggedOutPage {...props} />
|
||||
</i18nStart.Context>,
|
||||
element
|
||||
);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
}
|
188
x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap
generated
Normal file
188
x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,188 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LoginPage disabled form states renders as expected when a connection to ES is not available 1`] = `
|
||||
<DisabledLoginForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
defaultMessage="See the Kibana logs for details and try reloading the page."
|
||||
id="xpack.security.loginPage.esUnavailableMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Cannot connect to the Elasticsearch cluster"
|
||||
id="xpack.security.loginPage.esUnavailableTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`LoginPage disabled form states renders as expected when an unknown loginState layout is provided 1`] = `
|
||||
<DisabledLoginForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
defaultMessage="Refer to the Kibana logs for more details and refresh to try again."
|
||||
id="xpack.security.loginPage.unknownLayoutMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Unsupported login form layout."
|
||||
id="xpack.security.loginPage.unknownLayoutTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`LoginPage disabled form states renders as expected when secure connection is required but not present 1`] = `
|
||||
<DisabledLoginForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
defaultMessage="Contact your system administrator."
|
||||
id="xpack.security.loginPage.requiresSecureConnectionMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="A secure connection is required for log in"
|
||||
id="xpack.security.loginPage.requiresSecureConnectionTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`LoginPage disabled form states renders as expected when xpack is not available 1`] = `
|
||||
<DisabledLoginForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
defaultMessage="To use the full set of free features in this distribution of Kibana, please update Elasticsearch to the default distribution."
|
||||
id="xpack.security.loginPage.xpackUnavailableMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Cannot connect to the Elasticsearch cluster currently configured for Kibana."
|
||||
id="xpack.security.loginPage.xpackUnavailableTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`LoginPage enabled form state renders as expected 1`] = `
|
||||
<BasicLoginForm
|
||||
http={
|
||||
Object {
|
||||
"addLoadingCountSource": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
}
|
||||
}
|
||||
loginAssistanceMessage=""
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`LoginPage enabled form state renders as expected when info message is set 1`] = `
|
||||
<BasicLoginForm
|
||||
http={
|
||||
Object {
|
||||
"addLoadingCountSource": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
}
|
||||
}
|
||||
infoMessage="Your session has timed out. Please log in again."
|
||||
loginAssistanceMessage=""
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`LoginPage enabled form state renders as expected when loginAssistanceMessage is set 1`] = `
|
||||
<BasicLoginForm
|
||||
http={
|
||||
Object {
|
||||
"addLoadingCountSource": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
}
|
||||
}
|
||||
infoMessage="Your session has timed out. Please log in again."
|
||||
loginAssistanceMessage="This is an *important* message"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`LoginPage page renders as expected 1`] = `
|
||||
<div
|
||||
className="loginWelcome login-form"
|
||||
>
|
||||
<header
|
||||
className="loginWelcome__header"
|
||||
>
|
||||
<div
|
||||
className="loginWelcome__content eui-textCenter"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<span
|
||||
className="loginWelcome__logo"
|
||||
>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
type="logoKibana"
|
||||
/>
|
||||
</span>
|
||||
<EuiTitle
|
||||
className="loginWelcome__title"
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Welcome to Kibana"
|
||||
id="xpack.security.loginPage.welcomeTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText
|
||||
className="loginWelcome__subtitle"
|
||||
color="subdued"
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Your window into the Elastic Stack"
|
||||
id="xpack.security.loginPage.welcomeDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="loginWelcome__content loginWelcome-body"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="l"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<BasicLoginForm
|
||||
http={
|
||||
Object {
|
||||
"addLoadingCountSource": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
}
|
||||
}
|
||||
loginAssistanceMessage=""
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act } from '@testing-library/react';
|
||||
import { EuiButton, EuiCallOut } from '@elastic/eui';
|
||||
import { mountWithIntl, nextTick, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { BasicLoginForm } from './basic_login_form';
|
||||
|
||||
import { coreMock } from '../../../../../../../../src/core/public/mocks';
|
||||
|
||||
describe('BasicLoginForm', () => {
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { href: 'https://some-host/bar' },
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete (window as any).location;
|
||||
});
|
||||
|
||||
it('renders as expected', () => {
|
||||
expect(
|
||||
shallowWithIntl(
|
||||
<BasicLoginForm http={coreMock.createStart().http} loginAssistanceMessage="" />
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders an info message when provided.', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<BasicLoginForm
|
||||
http={coreMock.createStart().http}
|
||||
infoMessage={'Hey this is an info message'}
|
||||
loginAssistanceMessage=""
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiCallOut).props().title).toEqual('Hey this is an info message');
|
||||
});
|
||||
|
||||
it('renders an invalid credentials message', async () => {
|
||||
const mockHTTP = coreMock.createStart({ basePath: '/some-base-path' }).http;
|
||||
mockHTTP.post.mockRejectedValue({ response: { status: 401 } });
|
||||
|
||||
const wrapper = mountWithIntl(<BasicLoginForm http={mockHTTP} loginAssistanceMessage="" />);
|
||||
|
||||
wrapper.find('input[name="username"]').simulate('change', { target: { value: 'username' } });
|
||||
wrapper.find('input[name="password"]').simulate('change', { target: { value: 'password' } });
|
||||
wrapper.find(EuiButton).simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find(EuiCallOut).props().title).toEqual(
|
||||
`Invalid username or password. Please try again.`
|
||||
);
|
||||
});
|
||||
|
||||
it('renders unknown error message', async () => {
|
||||
const mockHTTP = coreMock.createStart({ basePath: '/some-base-path' }).http;
|
||||
mockHTTP.post.mockRejectedValue({ response: { status: 500 } });
|
||||
|
||||
const wrapper = mountWithIntl(<BasicLoginForm http={mockHTTP} loginAssistanceMessage="" />);
|
||||
|
||||
wrapper.find('input[name="username"]').simulate('change', { target: { value: 'username' } });
|
||||
wrapper.find('input[name="password"]').simulate('change', { target: { value: 'password' } });
|
||||
wrapper.find(EuiButton).simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find(EuiCallOut).props().title).toEqual(`Oops! Error. Try again.`);
|
||||
});
|
||||
|
||||
it('properly redirects after successful login', async () => {
|
||||
window.location.href = `https://some-host/login?next=${encodeURIComponent(
|
||||
'/some-base-path/app/kibana#/home?_g=()'
|
||||
)}`;
|
||||
const mockHTTP = coreMock.createStart({ basePath: '/some-base-path' }).http;
|
||||
mockHTTP.post.mockResolvedValue({});
|
||||
|
||||
const wrapper = mountWithIntl(<BasicLoginForm http={mockHTTP} loginAssistanceMessage="" />);
|
||||
|
||||
wrapper.find('input[name="username"]').simulate('change', { target: { value: 'username1' } });
|
||||
wrapper.find('input[name="password"]').simulate('change', { target: { value: 'password1' } });
|
||||
wrapper.find(EuiButton).simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(mockHTTP.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockHTTP.post).toHaveBeenCalledWith('/internal/security/login', {
|
||||
body: JSON.stringify({ username: 'username1', password: 'password1' }),
|
||||
});
|
||||
|
||||
expect(window.location.href).toBe('/some-base-path/app/kibana#/home?_g=()');
|
||||
expect(wrapper.find(EuiCallOut).exists()).toBe(false);
|
||||
});
|
||||
});
|
|
@ -4,20 +4,25 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiCallOut, EuiFieldText, EuiFormRow, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import React, { ChangeEvent, Component, FormEvent, Fragment, MouseEvent } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { LoginState } from '../../login_state';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { HttpStart, IHttpFetchError } from 'src/core/public';
|
||||
import { parseNext } from '../../../../../common/parse_next';
|
||||
|
||||
interface Props {
|
||||
http: any;
|
||||
window: any;
|
||||
http: HttpStart;
|
||||
infoMessage?: string;
|
||||
loginState: LoginState;
|
||||
next: string;
|
||||
intl: InjectedIntl;
|
||||
loginAssistanceMessage: string;
|
||||
}
|
||||
|
||||
|
@ -29,7 +34,7 @@ interface State {
|
|||
message: string;
|
||||
}
|
||||
|
||||
class BasicLoginFormUI extends Component<Props, State> {
|
||||
export class BasicLoginForm extends Component<Props, State> {
|
||||
public state = {
|
||||
hasError: false,
|
||||
isLoading: false,
|
||||
|
@ -175,7 +180,7 @@ class BasicLoginFormUI extends Component<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
private submit = (e: MouseEvent<HTMLButtonElement> | FormEvent<HTMLFormElement>) => {
|
||||
private submit = async (e: MouseEvent<HTMLButtonElement> | FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.isFormValid()) {
|
||||
|
@ -187,34 +192,28 @@ class BasicLoginFormUI extends Component<Props, State> {
|
|||
message: '',
|
||||
});
|
||||
|
||||
const { http, window, next, intl } = this.props;
|
||||
|
||||
const { http } = this.props;
|
||||
const { username, password } = this.state;
|
||||
|
||||
http.post('./internal/security/login', { username, password }).then(
|
||||
() => (window.location.href = next),
|
||||
(error: any) => {
|
||||
const { statusCode = 500 } = error.data || {};
|
||||
try {
|
||||
await http.post('/internal/security/login', { body: JSON.stringify({ username, password }) });
|
||||
window.location.href = parseNext(window.location.href, http.basePath.serverBasePath);
|
||||
} catch (error) {
|
||||
const message =
|
||||
(error as IHttpFetchError).response?.status === 401
|
||||
? i18n.translate(
|
||||
'xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage',
|
||||
{ defaultMessage: 'Invalid username or password. Please try again.' }
|
||||
)
|
||||
: i18n.translate('xpack.security.login.basicLoginForm.unknownErrorMessage', {
|
||||
defaultMessage: 'Oops! Error. Try again.',
|
||||
});
|
||||
|
||||
let message = intl.formatMessage({
|
||||
id: 'xpack.security.login.basicLoginForm.unknownErrorMessage',
|
||||
defaultMessage: 'Oops! Error. Try again.',
|
||||
});
|
||||
if (statusCode === 401) {
|
||||
message = intl.formatMessage({
|
||||
id: 'xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage',
|
||||
defaultMessage: 'Invalid username or password. Please try again.',
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
hasError: true,
|
||||
message,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
);
|
||||
this.setState({
|
||||
hasError: true,
|
||||
message,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const BasicLoginForm = injectI18n(BasicLoginFormUI);
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { BasicLoginForm } from './basic_login_form';
|
||||
export { DisabledLoginForm } from './disabled_login_form';
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './login';
|
||||
export { loginApp } from './login_app';
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
jest.mock('./login_page');
|
||||
|
||||
import { AppMount, ScopedHistory } from 'src/core/public';
|
||||
import { loginApp } from './login_app';
|
||||
|
||||
import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks';
|
||||
|
||||
describe('loginApp', () => {
|
||||
it('properly registers application', () => {
|
||||
const coreSetupMock = coreMock.createSetup();
|
||||
|
||||
loginApp.create({
|
||||
...coreSetupMock,
|
||||
config: { loginAssistanceMessage: '' },
|
||||
});
|
||||
|
||||
expect(coreSetupMock.http.anonymousPaths.register).toHaveBeenCalledTimes(1);
|
||||
expect(coreSetupMock.http.anonymousPaths.register).toHaveBeenCalledWith('/login');
|
||||
|
||||
expect(coreSetupMock.application.register).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [[appRegistration]] = coreSetupMock.application.register.mock.calls;
|
||||
expect(appRegistration).toEqual({
|
||||
id: 'security_login',
|
||||
chromeless: true,
|
||||
appRoute: '/login',
|
||||
title: 'Login',
|
||||
mount: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('properly renders application', async () => {
|
||||
const coreSetupMock = coreMock.createSetup();
|
||||
const coreStartMock = coreMock.createStart();
|
||||
coreStartMock.injectedMetadata.getInjectedVar.mockReturnValue(true);
|
||||
coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}]);
|
||||
const containerMock = document.createElement('div');
|
||||
|
||||
loginApp.create({
|
||||
...coreSetupMock,
|
||||
config: { loginAssistanceMessage: 'some-message' },
|
||||
});
|
||||
|
||||
const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
|
||||
await (mount as AppMount)({
|
||||
element: containerMock,
|
||||
appBasePath: '',
|
||||
onAppLeave: jest.fn(),
|
||||
history: (scopedHistoryMock.create() as unknown) as ScopedHistory,
|
||||
});
|
||||
|
||||
expect(coreStartMock.injectedMetadata.getInjectedVar).toHaveBeenCalledTimes(1);
|
||||
expect(coreStartMock.injectedMetadata.getInjectedVar).toHaveBeenCalledWith('secureCookies');
|
||||
|
||||
const mockRenderApp = jest.requireMock('./login_page').renderLoginPage;
|
||||
expect(mockRenderApp).toHaveBeenCalledTimes(1);
|
||||
expect(mockRenderApp).toHaveBeenCalledWith(coreStartMock.i18n, containerMock, {
|
||||
http: coreStartMock.http,
|
||||
fatalErrors: coreStartMock.fatalErrors,
|
||||
loginAssistanceMessage: 'some-message',
|
||||
requiresSecureConnection: true,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CoreSetup, AppMountParameters, HttpSetup } from 'src/core/public';
|
||||
import { ConfigType } from '../../config';
|
||||
|
||||
interface CreateDeps {
|
||||
application: CoreSetup['application'];
|
||||
http: HttpSetup;
|
||||
getStartServices: CoreSetup['getStartServices'];
|
||||
config: Pick<ConfigType, 'loginAssistanceMessage'>;
|
||||
}
|
||||
|
||||
export const loginApp = Object.freeze({
|
||||
id: 'security_login',
|
||||
create({ application, http, getStartServices, config }: CreateDeps) {
|
||||
http.anonymousPaths.register('/login');
|
||||
application.register({
|
||||
id: this.id,
|
||||
title: i18n.translate('xpack.security.loginAppTitle', { defaultMessage: 'Login' }),
|
||||
chromeless: true,
|
||||
appRoute: '/login',
|
||||
async mount({ element }: AppMountParameters) {
|
||||
const [[coreStart], { renderLoginPage }] = await Promise.all([
|
||||
getStartServices(),
|
||||
import('./login_page'),
|
||||
]);
|
||||
return renderLoginPage(coreStart.i18n, element, {
|
||||
http: coreStart.http,
|
||||
fatalErrors: coreStart.fatalErrors,
|
||||
loginAssistanceMessage: config.loginAssistanceMessage,
|
||||
requiresSecureConnection: coreStart.injectedMetadata.getInjectedVar(
|
||||
'secureCookies'
|
||||
) as boolean,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,282 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { act } from '@testing-library/react';
|
||||
import { nextTick } from 'test_utils/enzyme_helpers';
|
||||
import { LoginState } from './login_state';
|
||||
import { LoginPage } from './login_page';
|
||||
import { coreMock } from '../../../../../../src/core/public/mocks';
|
||||
import { DisabledLoginForm, BasicLoginForm } from './components';
|
||||
|
||||
const createLoginState = (options?: Partial<LoginState>) => {
|
||||
return {
|
||||
allowLogin: true,
|
||||
layout: 'form',
|
||||
...options,
|
||||
} as LoginState;
|
||||
};
|
||||
|
||||
describe('LoginPage', () => {
|
||||
// mock a minimal subset of the HttpSetup
|
||||
const httpMock = {
|
||||
get: jest.fn(),
|
||||
addLoadingCountSource: jest.fn(),
|
||||
} as any;
|
||||
const resetHttpMock = () => {
|
||||
httpMock.get.mockReset();
|
||||
httpMock.addLoadingCountSource.mockReset();
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { href: 'http://some-host/bar', protocol: 'http' },
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resetHttpMock();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete (window as any).location;
|
||||
});
|
||||
|
||||
describe('page', () => {
|
||||
it('renders as expected', async () => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
httpMock.get.mockResolvedValue(createLoginState());
|
||||
|
||||
const wrapper = shallow(
|
||||
<LoginPage
|
||||
http={httpMock}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
requiresSecureConnection={false}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
resetHttpMock(); // so the calls don't show in the BasicLoginForm snapshot
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('disabled form states', () => {
|
||||
it('renders as expected when secure connection is required but not present', async () => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
httpMock.get.mockResolvedValue(createLoginState());
|
||||
|
||||
const wrapper = shallow(
|
||||
<LoginPage
|
||||
http={httpMock}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
requiresSecureConnection={true}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find(DisabledLoginForm)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders as expected when a connection to ES is not available', async () => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
httpMock.get.mockResolvedValue(createLoginState({ layout: 'error-es-unavailable' }));
|
||||
|
||||
const wrapper = shallow(
|
||||
<LoginPage
|
||||
http={httpMock}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
requiresSecureConnection={false}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find(DisabledLoginForm)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders as expected when xpack is not available', async () => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
httpMock.get.mockResolvedValue(createLoginState({ layout: 'error-xpack-unavailable' }));
|
||||
|
||||
const wrapper = shallow(
|
||||
<LoginPage
|
||||
http={httpMock}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
requiresSecureConnection={false}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find(DisabledLoginForm)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders as expected when an unknown loginState layout is provided', async () => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
httpMock.get.mockResolvedValue(
|
||||
createLoginState({ layout: 'error-asdf-asdf-unknown' as any })
|
||||
);
|
||||
|
||||
const wrapper = shallow(
|
||||
<LoginPage
|
||||
http={httpMock}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
requiresSecureConnection={false}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find(DisabledLoginForm)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('enabled form state', () => {
|
||||
it('renders as expected', async () => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
httpMock.get.mockResolvedValue(createLoginState());
|
||||
|
||||
const wrapper = shallow(
|
||||
<LoginPage
|
||||
http={httpMock}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
requiresSecureConnection={false}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
resetHttpMock(); // so the calls don't show in the BasicLoginForm snapshot
|
||||
});
|
||||
|
||||
expect(wrapper.find(BasicLoginForm)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders as expected when info message is set', async () => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
httpMock.get.mockResolvedValue(createLoginState());
|
||||
window.location.href = 'http://some-host/bar?msg=SESSION_EXPIRED';
|
||||
|
||||
const wrapper = shallow(
|
||||
<LoginPage
|
||||
http={httpMock}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
requiresSecureConnection={false}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
resetHttpMock(); // so the calls don't show in the BasicLoginForm snapshot
|
||||
});
|
||||
|
||||
expect(wrapper.find(BasicLoginForm)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders as expected when loginAssistanceMessage is set', async () => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
httpMock.get.mockResolvedValue(createLoginState());
|
||||
|
||||
const wrapper = shallow(
|
||||
<LoginPage
|
||||
http={httpMock}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage="This is an *important* message"
|
||||
requiresSecureConnection={false}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
resetHttpMock(); // so the calls don't show in the BasicLoginForm snapshot
|
||||
});
|
||||
|
||||
expect(wrapper.find(BasicLoginForm)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('API calls', () => {
|
||||
it('GET login_state success', async () => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
httpMock.get.mockResolvedValue(createLoginState());
|
||||
|
||||
const wrapper = shallow(
|
||||
<LoginPage
|
||||
http={httpMock}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
requiresSecureConnection={false}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(httpMock.addLoadingCountSource).toHaveBeenCalledTimes(1);
|
||||
expect(httpMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(httpMock.get).toHaveBeenCalledWith('/internal/security/login_state');
|
||||
expect(coreStartMock.fatalErrors.add).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('GET login_state failure', async () => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
const error = Symbol();
|
||||
httpMock.get.mockRejectedValue(error);
|
||||
|
||||
const wrapper = shallow(
|
||||
<LoginPage
|
||||
http={httpMock}
|
||||
fatalErrors={coreStartMock.fatalErrors}
|
||||
loginAssistanceMessage=""
|
||||
requiresSecureConnection={false}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(httpMock.addLoadingCountSource).toHaveBeenCalledTimes(1);
|
||||
expect(httpMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(httpMock.get).toHaveBeenCalledWith('/internal/security/login_state');
|
||||
expect(coreStartMock.fatalErrors.add).toHaveBeenCalledTimes(1);
|
||||
expect(coreStartMock.fatalErrors.add).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,45 +5,81 @@
|
|||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
// @ts-ignore
|
||||
EuiCard,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import { LoginState } from '../../login_state';
|
||||
import { BasicLoginForm } from '../basic_login_form';
|
||||
import { DisabledLoginForm } from '../disabled_login_form';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { parse } from 'url';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { CoreStart, FatalErrorsStart, HttpStart } from 'src/core/public';
|
||||
import { LoginLayout } from '../../../common/licensing';
|
||||
import { BasicLoginForm, DisabledLoginForm } from './components';
|
||||
import { LoginState } from './login_state';
|
||||
|
||||
interface Props {
|
||||
http: any;
|
||||
window: any;
|
||||
next: string;
|
||||
infoMessage?: string;
|
||||
loginState: LoginState;
|
||||
isSecureConnection: boolean;
|
||||
requiresSecureConnection: boolean;
|
||||
http: HttpStart;
|
||||
fatalErrors: FatalErrorsStart;
|
||||
loginAssistanceMessage: string;
|
||||
requiresSecureConnection: boolean;
|
||||
}
|
||||
|
||||
export class LoginPage extends Component<Props, {}> {
|
||||
interface State {
|
||||
loginState: LoginState | null;
|
||||
}
|
||||
|
||||
const infoMessageMap = new Map([
|
||||
[
|
||||
'SESSION_EXPIRED',
|
||||
i18n.translate('xpack.security.login.sessionExpiredDescription', {
|
||||
defaultMessage: 'Your session has timed out. Please log in again.',
|
||||
}),
|
||||
],
|
||||
[
|
||||
'LOGGED_OUT',
|
||||
i18n.translate('xpack.security.login.loggedOutDescription', {
|
||||
defaultMessage: 'You have logged out of Kibana.',
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
export class LoginPage extends Component<Props, State> {
|
||||
state = { loginState: null };
|
||||
|
||||
public async componentDidMount() {
|
||||
const loadingCount$ = new BehaviorSubject(1);
|
||||
this.props.http.addLoadingCountSource(loadingCount$.asObservable());
|
||||
|
||||
try {
|
||||
this.setState({ loginState: await this.props.http.get('/internal/security/login_state') });
|
||||
} catch (err) {
|
||||
this.props.fatalErrors.add(err);
|
||||
}
|
||||
|
||||
loadingCount$.next(0);
|
||||
loadingCount$.complete();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const allowLogin = this.allowLogin();
|
||||
const loginState = this.state.loginState;
|
||||
if (!loginState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isSecureConnection = !!window.location.protocol.match(/^https/);
|
||||
const { allowLogin, layout } = loginState;
|
||||
|
||||
const loginIsSupported =
|
||||
this.props.requiresSecureConnection && !isSecureConnection
|
||||
? false
|
||||
: allowLogin && layout === 'form';
|
||||
|
||||
const contentHeaderClasses = classNames('loginWelcome__content', 'eui-textCenter', {
|
||||
['loginWelcome__contentDisabledForm']: !allowLogin,
|
||||
['loginWelcome__contentDisabledForm']: !loginIsSupported,
|
||||
});
|
||||
|
||||
const contentBodyClasses = classNames('loginWelcome__content', 'loginWelcome-body', {
|
||||
['loginWelcome__contentDisabledForm']: !allowLogin,
|
||||
['loginWelcome__contentDisabledForm']: !loginIsSupported,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -75,23 +111,21 @@ export class LoginPage extends Component<Props, {}> {
|
|||
</header>
|
||||
<div className={contentBodyClasses}>
|
||||
<EuiFlexGroup gutterSize="l">
|
||||
<EuiFlexItem>{this.getLoginForm()}</EuiFlexItem>
|
||||
<EuiFlexItem>{this.getLoginForm({ isSecureConnection, layout })}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private allowLogin = () => {
|
||||
if (this.props.requiresSecureConnection && !this.props.isSecureConnection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.props.loginState.allowLogin && this.props.loginState.layout === 'form';
|
||||
};
|
||||
|
||||
private getLoginForm = () => {
|
||||
if (this.props.requiresSecureConnection && !this.props.isSecureConnection) {
|
||||
private getLoginForm = ({
|
||||
isSecureConnection,
|
||||
layout,
|
||||
}: {
|
||||
isSecureConnection: boolean;
|
||||
layout: LoginLayout;
|
||||
}) => {
|
||||
if (this.props.requiresSecureConnection && !isSecureConnection) {
|
||||
return (
|
||||
<DisabledLoginForm
|
||||
title={
|
||||
|
@ -110,10 +144,17 @@ export class LoginPage extends Component<Props, {}> {
|
|||
);
|
||||
}
|
||||
|
||||
const layout = this.props.loginState.layout;
|
||||
switch (layout) {
|
||||
case 'form':
|
||||
return <BasicLoginForm {...this.props} />;
|
||||
return (
|
||||
<BasicLoginForm
|
||||
http={this.props.http}
|
||||
infoMessage={infoMessageMap.get(
|
||||
parse(window.location.href, true).query.msg?.toString()
|
||||
)}
|
||||
loginAssistanceMessage={this.props.loginAssistanceMessage}
|
||||
/>
|
||||
);
|
||||
case 'error-es-unavailable':
|
||||
return (
|
||||
<DisabledLoginForm
|
||||
|
@ -168,3 +209,14 @@ export class LoginPage extends Component<Props, {}> {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function renderLoginPage(i18nStart: CoreStart['i18n'], element: Element, props: Props) {
|
||||
ReactDOM.render(
|
||||
<i18nStart.Context>
|
||||
<LoginPage {...props} />
|
||||
</i18nStart.Context>,
|
||||
element
|
||||
);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export type LoginLayout = 'form' | 'error-es-unavailable' | 'error-xpack-unavailable';
|
||||
import { LoginLayout } from '../../../common/licensing';
|
||||
|
||||
export interface LoginState {
|
||||
layout: LoginLayout;
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { LoginPage } from './login_page';
|
||||
export { logoutApp } from './logout_app';
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AppMount, ScopedHistory } from 'src/core/public';
|
||||
import { logoutApp } from './logout_app';
|
||||
|
||||
import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks';
|
||||
|
||||
describe('logoutApp', () => {
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, 'sessionStorage', {
|
||||
value: { clear: jest.fn() },
|
||||
writable: true,
|
||||
});
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { href: 'https://some-host/bar?arg=true', search: '?arg=true' },
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete (window as any).sessionStorage;
|
||||
delete (window as any).location;
|
||||
});
|
||||
|
||||
it('properly registers application', () => {
|
||||
const coreSetupMock = coreMock.createSetup();
|
||||
|
||||
logoutApp.create(coreSetupMock);
|
||||
|
||||
expect(coreSetupMock.http.anonymousPaths.register).toHaveBeenCalledTimes(1);
|
||||
expect(coreSetupMock.http.anonymousPaths.register).toHaveBeenCalledWith('/logout');
|
||||
|
||||
expect(coreSetupMock.application.register).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [[appRegistration]] = coreSetupMock.application.register.mock.calls;
|
||||
expect(appRegistration).toEqual({
|
||||
id: 'security_logout',
|
||||
chromeless: true,
|
||||
appRoute: '/logout',
|
||||
title: 'Logout',
|
||||
mount: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('properly mounts application', async () => {
|
||||
const coreSetupMock = coreMock.createSetup({ basePath: '/mock-base-path' });
|
||||
const containerMock = document.createElement('div');
|
||||
|
||||
logoutApp.create(coreSetupMock);
|
||||
|
||||
const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
|
||||
await (mount as AppMount)({
|
||||
element: containerMock,
|
||||
appBasePath: '',
|
||||
onAppLeave: jest.fn(),
|
||||
history: (scopedHistoryMock.create() as unknown) as ScopedHistory,
|
||||
});
|
||||
|
||||
expect(window.sessionStorage.clear).toHaveBeenCalledTimes(1);
|
||||
expect(window.location.href).toBe('/mock-base-path/api/security/logout?arg=true');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CoreSetup, HttpSetup } from 'src/core/public';
|
||||
|
||||
interface CreateDeps {
|
||||
application: CoreSetup['application'];
|
||||
http: HttpSetup;
|
||||
}
|
||||
|
||||
export const logoutApp = Object.freeze({
|
||||
id: 'security_logout',
|
||||
create({ application, http }: CreateDeps) {
|
||||
http.anonymousPaths.register('/logout');
|
||||
application.register({
|
||||
id: this.id,
|
||||
title: i18n.translate('xpack.security.logoutAppTitle', { defaultMessage: 'Logout' }),
|
||||
chromeless: true,
|
||||
appRoute: '/logout',
|
||||
async mount() {
|
||||
window.sessionStorage.clear();
|
||||
|
||||
// Redirect user to the server logout endpoint to complete logout.
|
||||
window.location.href = http.basePath.prepend(
|
||||
`/api/security/logout${window.location.search}`
|
||||
);
|
||||
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,135 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OverwrittenSessionPage renders as expected 1`] = `
|
||||
<AuthenticationStatePage
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="You previously logged in as a different user."
|
||||
id="xpack.security.overwrittenSession.title"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="secAuthenticationStatePage"
|
||||
>
|
||||
<header
|
||||
className="secAuthenticationStatePage__header"
|
||||
>
|
||||
<div
|
||||
className="secAuthenticationStatePage__content eui-textCenter"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
>
|
||||
<div
|
||||
className="euiSpacer euiSpacer--xxl"
|
||||
/>
|
||||
</EuiSpacer>
|
||||
<span
|
||||
className="secAuthenticationStatePage__logo"
|
||||
>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
type="logoKibana"
|
||||
>
|
||||
<EuiIconLogoKibana
|
||||
aria-hidden={true}
|
||||
className="euiIcon euiIcon--xxLarge euiIcon-isLoaded"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={null}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="euiIcon euiIcon--xxLarge euiIcon-isLoaded"
|
||||
focusable="false"
|
||||
height={32}
|
||||
role="img"
|
||||
style={null}
|
||||
viewBox="0 0 32 32"
|
||||
width={32}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<path
|
||||
d="M4 0v28.789L28.935.017z"
|
||||
fill="#F04E98"
|
||||
/>
|
||||
<path
|
||||
className="euiIcon__fillNegative"
|
||||
d="M4 12v16.789l11.906-13.738A24.721 24.721 0 004 12"
|
||||
/>
|
||||
<path
|
||||
d="M18.479 16.664L6.268 30.754l-1.073 1.237h23.191c-1.252-6.292-4.883-11.719-9.908-15.327"
|
||||
fill="#00BFB3"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</EuiIconLogoKibana>
|
||||
</EuiIcon>
|
||||
</span>
|
||||
<EuiTitle
|
||||
className="secAuthenticationStatePage__title"
|
||||
size="l"
|
||||
>
|
||||
<h1
|
||||
className="euiTitle euiTitle--large secAuthenticationStatePage__title"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You previously logged in as a different user."
|
||||
id="xpack.security.overwrittenSession.title"
|
||||
values={Object {}}
|
||||
>
|
||||
You previously logged in as a different user.
|
||||
</FormattedMessage>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
>
|
||||
<div
|
||||
className="euiSpacer euiSpacer--xl"
|
||||
/>
|
||||
</EuiSpacer>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="secAuthenticationStatePage__content eui-textCenter"
|
||||
>
|
||||
<EuiButton
|
||||
href="/mock-base-path/"
|
||||
>
|
||||
<a
|
||||
className="euiButton euiButton--primary"
|
||||
href="/mock-base-path/"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span
|
||||
className="euiButton__content"
|
||||
>
|
||||
<span
|
||||
className="euiButton__text"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Continue as {username}"
|
||||
id="xpack.security.overwrittenSession.continueAsUserText"
|
||||
values={
|
||||
Object {
|
||||
"username": "mock-user",
|
||||
}
|
||||
}
|
||||
>
|
||||
Continue as mock-user
|
||||
</FormattedMessage>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</EuiButton>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticationStatePage>
|
||||
`;
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './logged_out';
|
||||
export { overwrittenSessionApp } from './overwritten_session_app';
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
jest.mock('./overwritten_session_page');
|
||||
|
||||
import { AppMount, ScopedHistory } from 'src/core/public';
|
||||
import { overwrittenSessionApp } from './overwritten_session_app';
|
||||
|
||||
import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks';
|
||||
import { securityMock } from '../../mocks';
|
||||
|
||||
describe('overwrittenSessionApp', () => {
|
||||
it('properly registers application', () => {
|
||||
const coreSetupMock = coreMock.createSetup();
|
||||
|
||||
overwrittenSessionApp.create({
|
||||
application: coreSetupMock.application,
|
||||
getStartServices: coreSetupMock.getStartServices,
|
||||
authc: securityMock.createSetup().authc,
|
||||
});
|
||||
|
||||
expect(coreSetupMock.application.register).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [[appRegistration]] = coreSetupMock.application.register.mock.calls;
|
||||
expect(appRegistration).toEqual({
|
||||
id: 'security_overwritten_session',
|
||||
title: 'Overwritten Session',
|
||||
chromeless: true,
|
||||
appRoute: '/security/overwritten_session',
|
||||
mount: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('properly sets breadcrumbs and renders application', async () => {
|
||||
const coreSetupMock = coreMock.createSetup();
|
||||
const coreStartMock = coreMock.createStart();
|
||||
coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}]);
|
||||
|
||||
const authcMock = securityMock.createSetup().authc;
|
||||
const containerMock = document.createElement('div');
|
||||
|
||||
overwrittenSessionApp.create({
|
||||
application: coreSetupMock.application,
|
||||
getStartServices: coreSetupMock.getStartServices,
|
||||
authc: authcMock,
|
||||
});
|
||||
|
||||
const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
|
||||
await (mount as AppMount)({
|
||||
element: containerMock,
|
||||
appBasePath: '',
|
||||
onAppLeave: jest.fn(),
|
||||
history: (scopedHistoryMock.create() as unknown) as ScopedHistory,
|
||||
});
|
||||
|
||||
const mockRenderApp = jest.requireMock('./overwritten_session_page')
|
||||
.renderOverwrittenSessionPage;
|
||||
expect(mockRenderApp).toHaveBeenCalledTimes(1);
|
||||
expect(mockRenderApp).toHaveBeenCalledWith(coreStartMock.i18n, containerMock, {
|
||||
authc: authcMock,
|
||||
basePath: coreStartMock.http.basePath,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CoreSetup, AppMountParameters } from 'src/core/public';
|
||||
import { AuthenticationServiceSetup } from '../authentication_service';
|
||||
|
||||
interface CreateDeps {
|
||||
application: CoreSetup['application'];
|
||||
authc: AuthenticationServiceSetup;
|
||||
getStartServices: CoreSetup['getStartServices'];
|
||||
}
|
||||
|
||||
export const overwrittenSessionApp = Object.freeze({
|
||||
id: 'security_overwritten_session',
|
||||
create({ application, authc, getStartServices }: CreateDeps) {
|
||||
application.register({
|
||||
id: this.id,
|
||||
title: i18n.translate('xpack.security.overwrittenSessionAppTitle', {
|
||||
defaultMessage: 'Overwritten Session',
|
||||
}),
|
||||
chromeless: true,
|
||||
appRoute: '/security/overwritten_session',
|
||||
async mount({ element }: AppMountParameters) {
|
||||
const [[coreStart], { renderOverwrittenSessionPage }] = await Promise.all([
|
||||
getStartServices(),
|
||||
import('./overwritten_session_page'),
|
||||
]);
|
||||
return renderOverwrittenSessionPage(coreStart.i18n, element, {
|
||||
authc,
|
||||
basePath: coreStart.http.basePath,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act } from '@testing-library/react';
|
||||
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
|
||||
import { OverwrittenSessionPage } from './overwritten_session_page';
|
||||
|
||||
import { coreMock } from '../../../../../../src/core/public/mocks';
|
||||
import { authenticationMock } from '../index.mock';
|
||||
import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock';
|
||||
import { AuthenticationStatePage } from '../components/authentication_state_page';
|
||||
|
||||
describe('OverwrittenSessionPage', () => {
|
||||
it('renders as expected', async () => {
|
||||
const basePathMock = coreMock.createStart({ basePath: '/mock-base-path' }).http.basePath;
|
||||
const authenticationSetupMock = authenticationMock.createSetup();
|
||||
authenticationSetupMock.getCurrentUser.mockResolvedValue(
|
||||
mockAuthenticatedUser({ username: 'mock-user' })
|
||||
);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<OverwrittenSessionPage basePath={basePathMock} authc={authenticationSetupMock} />
|
||||
);
|
||||
|
||||
// Shouldn't render anything if username isn't yet available.
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find(AuthenticationStatePage)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { CoreStart, IBasePath } from 'src/core/public';
|
||||
import { AuthenticationServiceSetup } from '../authentication_service';
|
||||
import { AuthenticationStatePage } from '../components';
|
||||
|
||||
interface Props {
|
||||
basePath: IBasePath;
|
||||
authc: AuthenticationServiceSetup;
|
||||
}
|
||||
|
||||
export function OverwrittenSessionPage({ authc, basePath }: Props) {
|
||||
const [username, setUsername] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
authc.getCurrentUser().then(user => setUsername(user.username));
|
||||
}, [authc]);
|
||||
|
||||
if (username == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthenticationStatePage
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.security.overwrittenSession.title"
|
||||
defaultMessage="You previously logged in as a different user."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiButton href={basePath.prepend('/')}>
|
||||
<FormattedMessage
|
||||
id="xpack.security.overwrittenSession.continueAsUserText"
|
||||
defaultMessage="Continue as {username}"
|
||||
values={{ username }}
|
||||
/>
|
||||
</EuiButton>
|
||||
</AuthenticationStatePage>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderOverwrittenSessionPage(
|
||||
i18nStart: CoreStart['i18n'],
|
||||
element: Element,
|
||||
props: Props
|
||||
) {
|
||||
ReactDOM.render(
|
||||
<i18nStart.Context>
|
||||
<OverwrittenSessionPage {...props} />
|
||||
</i18nStart.Context>,
|
||||
element
|
||||
);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
}
|
|
@ -4,4 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import '../views/account/account';
|
||||
export interface ConfigType {
|
||||
loginAssistanceMessage: string;
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
$secFormWidth: 460px;
|
||||
|
||||
// Authentication styles
|
||||
@import './authentication/index';
|
||||
|
||||
// Management styles
|
||||
@import './management/index';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import './index.scss';
|
||||
import { PluginInitializer } from 'src/core/public';
|
||||
import { PluginInitializer, PluginInitializerContext } from 'src/core/public';
|
||||
import { SecurityPlugin, SecurityPluginSetup, SecurityPluginStart } from './plugin';
|
||||
|
||||
export { SecurityPluginSetup, SecurityPluginStart };
|
||||
|
@ -13,5 +13,6 @@ export { SessionInfo } from './types';
|
|||
export { AuthenticatedUser } from '../common/model';
|
||||
export { SecurityLicense, SecurityLicenseFeatures } from '../common/licensing';
|
||||
|
||||
export const plugin: PluginInitializer<SecurityPluginSetup, SecurityPluginStart> = () =>
|
||||
new SecurityPlugin();
|
||||
export const plugin: PluginInitializer<SecurityPluginSetup, SecurityPluginStart> = (
|
||||
initializerContext: PluginInitializerContext
|
||||
) => new SecurityPlugin(initializerContext);
|
||||
|
|
|
@ -38,6 +38,7 @@ describe('SecurityNavControlService', () => {
|
|||
navControlService.setup({
|
||||
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
|
||||
authc: mockSecuritySetup.authc,
|
||||
logoutUrl: '/some/logout/url',
|
||||
});
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
|
@ -100,6 +101,7 @@ describe('SecurityNavControlService', () => {
|
|||
navControlService.setup({
|
||||
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
|
||||
authc: securityMock.createSetup().authc,
|
||||
logoutUrl: '/some/logout/url',
|
||||
});
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
|
@ -119,6 +121,7 @@ describe('SecurityNavControlService', () => {
|
|||
navControlService.setup({
|
||||
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
|
||||
authc: securityMock.createSetup().authc,
|
||||
logoutUrl: '/some/logout/url',
|
||||
});
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
|
@ -135,6 +138,7 @@ describe('SecurityNavControlService', () => {
|
|||
navControlService.setup({
|
||||
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
|
||||
authc: securityMock.createSetup().authc,
|
||||
logoutUrl: '/some/logout/url',
|
||||
});
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
|
@ -156,6 +160,7 @@ describe('SecurityNavControlService', () => {
|
|||
navControlService.setup({
|
||||
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
|
||||
authc: securityMock.createSetup().authc,
|
||||
logoutUrl: '/some/logout/url',
|
||||
});
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
|
|
|
@ -15,6 +15,7 @@ import { AuthenticationServiceSetup } from '../authentication';
|
|||
interface SetupDeps {
|
||||
securityLicense: SecurityLicense;
|
||||
authc: AuthenticationServiceSetup;
|
||||
logoutUrl: string;
|
||||
}
|
||||
|
||||
interface StartDeps {
|
||||
|
@ -24,14 +25,16 @@ interface StartDeps {
|
|||
export class SecurityNavControlService {
|
||||
private securityLicense!: SecurityLicense;
|
||||
private authc!: AuthenticationServiceSetup;
|
||||
private logoutUrl!: string;
|
||||
|
||||
private navControlRegistered!: boolean;
|
||||
|
||||
private securityFeaturesSubscription?: Subscription;
|
||||
|
||||
public setup({ securityLicense, authc }: SetupDeps) {
|
||||
public setup({ securityLicense, authc, logoutUrl }: SetupDeps) {
|
||||
this.securityLicense = securityLicense;
|
||||
this.authc = authc;
|
||||
this.logoutUrl = logoutUrl;
|
||||
}
|
||||
|
||||
public start({ core }: StartDeps) {
|
||||
|
@ -65,12 +68,10 @@ export class SecurityNavControlService {
|
|||
mount: (el: HTMLElement) => {
|
||||
const I18nContext = core.i18n.Context;
|
||||
|
||||
const logoutUrl = core.injectedMetadata.getInjectedVar('logoutUrl') as string;
|
||||
|
||||
const props = {
|
||||
user: currentUserPromise,
|
||||
editProfileUrl: core.http.basePath.prepend('/app/kibana#/account'),
|
||||
logoutUrl,
|
||||
editProfileUrl: core.http.basePath.prepend('/security/account'),
|
||||
logoutUrl: this.logoutUrl,
|
||||
};
|
||||
ReactDOM.render(
|
||||
<I18nContext>
|
||||
|
|
147
x-pack/plugins/security/public/plugin.test.tsx
Normal file
147
x-pack/plugins/security/public/plugin.test.tsx
Normal file
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import BroadcastChannel from 'broadcast-channel';
|
||||
import { CoreSetup } from 'src/core/public';
|
||||
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
|
||||
import { SessionTimeout } from './session';
|
||||
import { PluginStartDependencies, SecurityPlugin } from './plugin';
|
||||
|
||||
import { coreMock } from '../../../../src/core/public/mocks';
|
||||
import { managementPluginMock } from '../../../../src/plugins/management/public/mocks';
|
||||
import { licensingMock } from '../../licensing/public/mocks';
|
||||
import { ManagementService } from './management';
|
||||
|
||||
describe('Security Plugin', () => {
|
||||
beforeAll(() => {
|
||||
BroadcastChannel.enforceOptions({ type: 'simulate' });
|
||||
});
|
||||
afterAll(() => {
|
||||
BroadcastChannel.enforceOptions(null);
|
||||
});
|
||||
|
||||
describe('#setup', () => {
|
||||
it('should be able to setup if optional plugins are not available', () => {
|
||||
const plugin = new SecurityPlugin(coreMock.createPluginInitializerContext());
|
||||
expect(
|
||||
plugin.setup(
|
||||
coreMock.createSetup({ basePath: '/some-base-path' }) as CoreSetup<
|
||||
PluginStartDependencies
|
||||
>,
|
||||
{ licensing: licensingMock.createSetup() }
|
||||
)
|
||||
).toEqual({
|
||||
__legacyCompat: { logoutUrl: '/some-base-path/logout', tenant: '/some-base-path' },
|
||||
authc: { getCurrentUser: expect.any(Function) },
|
||||
license: {
|
||||
isEnabled: expect.any(Function),
|
||||
getFeatures: expect.any(Function),
|
||||
features$: expect.any(Observable),
|
||||
},
|
||||
sessionTimeout: expect.any(SessionTimeout),
|
||||
});
|
||||
});
|
||||
|
||||
it('setups Management Service if `management` plugin is available', () => {
|
||||
const coreSetupMock = coreMock.createSetup({ basePath: '/some-base-path' });
|
||||
const setupManagementServiceMock = jest
|
||||
.spyOn(ManagementService.prototype, 'setup')
|
||||
.mockImplementation(() => {});
|
||||
const managementSetupMock = managementPluginMock.createSetupContract();
|
||||
|
||||
const plugin = new SecurityPlugin(coreMock.createPluginInitializerContext());
|
||||
|
||||
plugin.setup(coreSetupMock as CoreSetup<PluginStartDependencies>, {
|
||||
licensing: licensingMock.createSetup(),
|
||||
management: managementSetupMock,
|
||||
});
|
||||
|
||||
expect(setupManagementServiceMock).toHaveBeenCalledTimes(1);
|
||||
expect(setupManagementServiceMock).toHaveBeenCalledWith({
|
||||
authc: { getCurrentUser: expect.any(Function) },
|
||||
license: {
|
||||
isEnabled: expect.any(Function),
|
||||
getFeatures: expect.any(Function),
|
||||
features$: expect.any(Observable),
|
||||
},
|
||||
management: managementSetupMock,
|
||||
fatalErrors: coreSetupMock.fatalErrors,
|
||||
getStartServices: coreSetupMock.getStartServices,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#start', () => {
|
||||
it('should be able to setup if optional plugins are not available', () => {
|
||||
const plugin = new SecurityPlugin(coreMock.createPluginInitializerContext());
|
||||
plugin.setup(
|
||||
coreMock.createSetup({ basePath: '/some-base-path' }) as CoreSetup<PluginStartDependencies>,
|
||||
{ licensing: licensingMock.createSetup() }
|
||||
);
|
||||
|
||||
expect(
|
||||
plugin.start(coreMock.createStart({ basePath: '/some-base-path' }), {
|
||||
data: {} as DataPublicPluginStart,
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('starts Management Service if `management` plugin is available', () => {
|
||||
jest.spyOn(ManagementService.prototype, 'setup').mockImplementation(() => {});
|
||||
const startManagementServiceMock = jest
|
||||
.spyOn(ManagementService.prototype, 'start')
|
||||
.mockImplementation(() => {});
|
||||
const managementSetupMock = managementPluginMock.createSetupContract();
|
||||
const managementStartMock = managementPluginMock.createStartContract();
|
||||
|
||||
const plugin = new SecurityPlugin(coreMock.createPluginInitializerContext());
|
||||
|
||||
plugin.setup(
|
||||
coreMock.createSetup({ basePath: '/some-base-path' }) as CoreSetup<PluginStartDependencies>,
|
||||
{
|
||||
licensing: licensingMock.createSetup(),
|
||||
management: managementSetupMock,
|
||||
}
|
||||
);
|
||||
|
||||
plugin.start(coreMock.createStart({ basePath: '/some-base-path' }), {
|
||||
data: {} as DataPublicPluginStart,
|
||||
management: managementStartMock,
|
||||
});
|
||||
|
||||
expect(startManagementServiceMock).toHaveBeenCalledTimes(1);
|
||||
expect(startManagementServiceMock).toHaveBeenCalledWith({ management: managementStartMock });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#stop', () => {
|
||||
it('does not fail if called before `start`.', () => {
|
||||
const plugin = new SecurityPlugin(coreMock.createPluginInitializerContext());
|
||||
plugin.setup(
|
||||
coreMock.createSetup({ basePath: '/some-base-path' }) as CoreSetup<PluginStartDependencies>,
|
||||
{ licensing: licensingMock.createSetup() }
|
||||
);
|
||||
|
||||
expect(() => plugin.stop()).not.toThrow();
|
||||
});
|
||||
|
||||
it('does not fail if called during normal plugin life cycle.', () => {
|
||||
const plugin = new SecurityPlugin(coreMock.createPluginInitializerContext());
|
||||
|
||||
plugin.setup(
|
||||
coreMock.createSetup({ basePath: '/some-base-path' }) as CoreSetup<PluginStartDependencies>,
|
||||
{ licensing: licensingMock.createSetup() }
|
||||
);
|
||||
|
||||
plugin.start(coreMock.createStart({ basePath: '/some-base-path' }), {
|
||||
data: {} as DataPublicPluginStart,
|
||||
});
|
||||
|
||||
expect(() => plugin.stop()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,9 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Plugin, CoreSetup, CoreStart } from 'src/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import {
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
Plugin,
|
||||
PluginInitializerContext,
|
||||
} from '../../../../src/core/public';
|
||||
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
|
||||
import {
|
||||
FeatureCatalogueCategory,
|
||||
|
@ -15,17 +19,18 @@ import {
|
|||
import { LicensingPluginSetup } from '../../licensing/public';
|
||||
import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public';
|
||||
import {
|
||||
ISessionTimeout,
|
||||
SessionExpired,
|
||||
SessionTimeout,
|
||||
ISessionTimeout,
|
||||
SessionTimeoutHttpInterceptor,
|
||||
UnauthorizedResponseHttpInterceptor,
|
||||
} from './session';
|
||||
import { SecurityLicenseService } from '../common/licensing';
|
||||
import { SecurityNavControlService } from './nav_control';
|
||||
import { AccountManagementPage } from './account_management';
|
||||
import { AuthenticationService, AuthenticationServiceSetup } from './authentication';
|
||||
import { ManagementService, UserAPIClient } from './management';
|
||||
import { ConfigType } from './config';
|
||||
import { ManagementService } from './management';
|
||||
import { accountManagementApp } from './account_management';
|
||||
|
||||
export interface PluginSetupDependencies {
|
||||
licensing: LicensingPluginSetup;
|
||||
|
@ -47,23 +52,27 @@ export class SecurityPlugin
|
|||
PluginStartDependencies
|
||||
> {
|
||||
private sessionTimeout!: ISessionTimeout;
|
||||
private readonly authenticationService = new AuthenticationService();
|
||||
private readonly navControlService = new SecurityNavControlService();
|
||||
private readonly securityLicenseService = new SecurityLicenseService();
|
||||
private readonly managementService = new ManagementService();
|
||||
private authc!: AuthenticationServiceSetup;
|
||||
private readonly config: ConfigType;
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<ConfigType>();
|
||||
}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup<PluginStartDependencies>,
|
||||
{ home, licensing, management }: PluginSetupDependencies
|
||||
) {
|
||||
const { http, notifications, injectedMetadata } = core;
|
||||
const { http, notifications } = core;
|
||||
const { anonymousPaths } = http;
|
||||
anonymousPaths.register('/login');
|
||||
anonymousPaths.register('/logout');
|
||||
anonymousPaths.register('/logged_out');
|
||||
|
||||
const tenant = injectedMetadata.getInjectedVar('session.tenant', '') as string;
|
||||
const logoutUrl = injectedMetadata.getInjectedVar('logoutUrl') as string;
|
||||
const logoutUrl = `${core.http.basePath.serverBasePath}/logout`;
|
||||
const tenant = core.http.basePath.serverBasePath;
|
||||
|
||||
const sessionExpired = new SessionExpired(logoutUrl, tenant);
|
||||
http.intercept(new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths));
|
||||
this.sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant);
|
||||
|
@ -71,11 +80,23 @@ export class SecurityPlugin
|
|||
|
||||
const { license } = this.securityLicenseService.setup({ license$: licensing.license$ });
|
||||
|
||||
this.authc = new AuthenticationService().setup({ http: core.http });
|
||||
this.authc = this.authenticationService.setup({
|
||||
application: core.application,
|
||||
config: this.config,
|
||||
getStartServices: core.getStartServices,
|
||||
http: core.http,
|
||||
});
|
||||
|
||||
this.navControlService.setup({
|
||||
securityLicense: license,
|
||||
authc: this.authc,
|
||||
logoutUrl,
|
||||
});
|
||||
|
||||
accountManagementApp.create({
|
||||
authc: this.authc,
|
||||
application: core.application,
|
||||
getStartServices: core.getStartServices,
|
||||
});
|
||||
|
||||
if (management) {
|
||||
|
@ -109,6 +130,7 @@ export class SecurityPlugin
|
|||
authc: this.authc,
|
||||
sessionTimeout: this.sessionTimeout,
|
||||
license,
|
||||
__legacyCompat: { logoutUrl, tenant },
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -119,22 +141,6 @@ export class SecurityPlugin
|
|||
if (management) {
|
||||
this.managementService.start({ management });
|
||||
}
|
||||
|
||||
return {
|
||||
__legacyCompat: {
|
||||
account_management: {
|
||||
AccountManagementPage: () => (
|
||||
<core.i18n.Context>
|
||||
<AccountManagementPage
|
||||
authc={this.authc}
|
||||
notifications={core.notifications}
|
||||
userAPIClient={new UserAPIClient(core.http)}
|
||||
/>
|
||||
</core.i18n.Context>
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {
|
||||
|
|
|
@ -194,7 +194,6 @@ export class Authenticator {
|
|||
logger: this.options.loggers.get('tokens'),
|
||||
}),
|
||||
getServerBaseURL: this.options.getServerBaseURL,
|
||||
isProviderEnabled: this.isProviderEnabled.bind(this),
|
||||
};
|
||||
|
||||
const authProviders = this.options.config.authc.providers;
|
||||
|
|
|
@ -494,7 +494,7 @@ describe('KerberosAuthenticationProvider', () => {
|
|||
mockOptions.tokens.invalidate.mockResolvedValue(undefined);
|
||||
|
||||
await expect(provider.logout(request, tokenPair)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/security/logged_out')
|
||||
);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -91,7 +91,9 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
return DeauthenticationResult.failed(err);
|
||||
}
|
||||
|
||||
return DeauthenticationResult.redirectTo(`${this.options.basePath.serverBasePath}/logged_out`);
|
||||
return DeauthenticationResult.redirectTo(
|
||||
`${this.options.basePath.serverBasePath}/security/logged_out`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -575,7 +575,7 @@ describe('OIDCAuthenticationProvider', () => {
|
|||
mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null });
|
||||
|
||||
await expect(provider.logout(request, { accessToken, refreshToken })).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/security/logged_out')
|
||||
);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -395,7 +395,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
}
|
||||
|
||||
return DeauthenticationResult.redirectTo(
|
||||
`${this.options.basePath.serverBasePath}/logged_out`
|
||||
`${this.options.basePath.serverBasePath}/security/logged_out`
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.debug(`Failed to deauthenticate user: ${err.message}`);
|
||||
|
|
|
@ -511,7 +511,7 @@ describe('PKIAuthenticationProvider', () => {
|
|||
mockOptions.tokens.invalidate.mockResolvedValue(undefined);
|
||||
|
||||
await expect(provider.logout(request, state)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/security/logged_out')
|
||||
);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -98,7 +98,9 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
return DeauthenticationResult.failed(err);
|
||||
}
|
||||
|
||||
return DeauthenticationResult.redirectTo(`${this.options.basePath.serverBasePath}/logged_out`);
|
||||
return DeauthenticationResult.redirectTo(
|
||||
`${this.options.basePath.serverBasePath}/security/logged_out`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -445,7 +445,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
state
|
||||
)
|
||||
).resolves.toEqual(
|
||||
AuthenticationResult.redirectTo('/base-path/overwritten_session', {
|
||||
AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', {
|
||||
state: {
|
||||
username: 'new-user',
|
||||
accessToken: 'new-valid-token',
|
||||
|
@ -509,7 +509,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
state
|
||||
)
|
||||
).resolves.toEqual(
|
||||
AuthenticationResult.redirectTo('/base-path/overwritten_session', {
|
||||
AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', {
|
||||
state: {
|
||||
username: 'user',
|
||||
accessToken: 'new-valid-token',
|
||||
|
@ -1159,7 +1159,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('redirects to /logged_out if `redirect` field in SAML logout response is null.', async () => {
|
||||
it('redirects to /security/logged_out if `redirect` field in SAML logout response is null.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const accessToken = 'x-saml-token';
|
||||
const refreshToken = 'x-saml-refresh-token';
|
||||
|
@ -1168,7 +1168,9 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
|
||||
await expect(
|
||||
provider.logout(request, { username: 'user', accessToken, refreshToken })
|
||||
).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out'));
|
||||
).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/security/logged_out')
|
||||
);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', {
|
||||
|
@ -1176,7 +1178,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('redirects to /logged_out if `redirect` field in SAML logout response is not defined.', async () => {
|
||||
it('redirects to /security/logged_out if `redirect` field in SAML logout response is not defined.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const accessToken = 'x-saml-token';
|
||||
const refreshToken = 'x-saml-refresh-token';
|
||||
|
@ -1185,7 +1187,9 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
|
||||
await expect(
|
||||
provider.logout(request, { username: 'user', accessToken, refreshToken })
|
||||
).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out'));
|
||||
).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/security/logged_out')
|
||||
);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', {
|
||||
|
@ -1204,7 +1208,9 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
|
||||
await expect(
|
||||
provider.logout(request, { username: 'user', accessToken, refreshToken })
|
||||
).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out'));
|
||||
).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/security/logged_out')
|
||||
);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', {
|
||||
|
@ -1223,24 +1229,8 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
accessToken: 'x-saml-token',
|
||||
refreshToken: 'x-saml-refresh-token',
|
||||
})
|
||||
).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out'));
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', {
|
||||
body: {
|
||||
queryString: 'SAMLRequest=xxx%20yyy',
|
||||
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects to /logged_out if `redirect` field in SAML invalidate response is null.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } });
|
||||
|
||||
mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null });
|
||||
|
||||
await expect(provider.logout(request)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')
|
||||
).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/security/logged_out')
|
||||
);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
|
@ -1252,13 +1242,31 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('redirects to /logged_out if `redirect` field in SAML invalidate response is not defined.', async () => {
|
||||
it('redirects to /security/logged_out if `redirect` field in SAML invalidate response is null.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } });
|
||||
|
||||
mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null });
|
||||
|
||||
await expect(provider.logout(request)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/security/logged_out')
|
||||
);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', {
|
||||
body: {
|
||||
queryString: 'SAMLRequest=xxx%20yyy',
|
||||
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects to /security/logged_out if `redirect` field in SAML invalidate response is not defined.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } });
|
||||
|
||||
mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: undefined });
|
||||
|
||||
await expect(provider.logout(request)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/security/logged_out')
|
||||
);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -230,7 +230,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
}
|
||||
|
||||
return DeauthenticationResult.redirectTo(
|
||||
`${this.options.basePath.serverBasePath}/logged_out`
|
||||
`${this.options.basePath.serverBasePath}/security/logged_out`
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.debug(`Failed to deauthenticate user: ${err.message}`);
|
||||
|
@ -392,7 +392,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
'Login initiated by Identity Provider is for a different user than currently authenticated.'
|
||||
);
|
||||
return AuthenticationResult.redirectTo(
|
||||
`${this.options.basePath.get(request)}/overwritten_session`,
|
||||
`${this.options.basePath.serverBasePath}/security/overwritten_session`,
|
||||
{ state: newState }
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,9 @@ describe('config schema', () => {
|
|||
it('generates proper defaults', () => {
|
||||
expect(ConfigSchema.validate({})).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"audit": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"authc": Object {
|
||||
"http": Object {
|
||||
"autoSchemesEnabled": true,
|
||||
|
@ -27,6 +30,7 @@ describe('config schema', () => {
|
|||
],
|
||||
},
|
||||
"cookieName": "sid",
|
||||
"enabled": true,
|
||||
"encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"loginAssistanceMessage": "",
|
||||
"public": Object {},
|
||||
|
@ -40,6 +44,9 @@ describe('config schema', () => {
|
|||
|
||||
expect(ConfigSchema.validate({}, { dist: false })).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"audit": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"authc": Object {
|
||||
"http": Object {
|
||||
"autoSchemesEnabled": true,
|
||||
|
@ -53,6 +60,7 @@ describe('config schema', () => {
|
|||
],
|
||||
},
|
||||
"cookieName": "sid",
|
||||
"enabled": true,
|
||||
"encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"loginAssistanceMessage": "",
|
||||
"public": Object {},
|
||||
|
@ -66,6 +74,9 @@ describe('config schema', () => {
|
|||
|
||||
expect(ConfigSchema.validate({}, { dist: true })).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"audit": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"authc": Object {
|
||||
"http": Object {
|
||||
"autoSchemesEnabled": true,
|
||||
|
@ -79,6 +90,7 @@ describe('config schema', () => {
|
|||
],
|
||||
},
|
||||
"cookieName": "sid",
|
||||
"enabled": true,
|
||||
"loginAssistanceMessage": "",
|
||||
"public": Object {},
|
||||
"secureCookies": false,
|
||||
|
@ -215,23 +227,11 @@ describe('config schema', () => {
|
|||
`"[authc.oidc.realm]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authProviders: ['oidc'] })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[authc.oidc.realm]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authc: { providers: ['oidc'], oidc: {} } })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[authc.oidc.realm]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authProviders: ['oidc'], authc: { oidc: {} } })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[authc.oidc.realm]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
});
|
||||
|
||||
it(`is valid when authc.providers is "['oidc']" and realm is specified`, async () => {
|
||||
|
@ -256,29 +256,6 @@ describe('config schema', () => {
|
|||
],
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
ConfigSchema.validate({
|
||||
authProviders: ['oidc'],
|
||||
authc: { oidc: { realm: 'realm-1' } },
|
||||
}).authc
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"http": Object {
|
||||
"autoSchemesEnabled": true,
|
||||
"enabled": true,
|
||||
"schemes": Array [
|
||||
"apikey",
|
||||
],
|
||||
},
|
||||
"oidc": Object {
|
||||
"realm": "realm-1",
|
||||
},
|
||||
"providers": Array [
|
||||
"oidc",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it(`returns a validation error when authc.providers is "['oidc', 'basic']" and realm is unspecified`, async () => {
|
||||
|
@ -287,12 +264,6 @@ describe('config schema', () => {
|
|||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[authc.oidc.realm]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authProviders: ['oidc', 'basic'] })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[authc.oidc.realm]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
});
|
||||
|
||||
it(`is valid when authc.providers is "['oidc', 'basic']" and realm is specified`, async () => {
|
||||
|
@ -318,40 +289,12 @@ describe('config schema', () => {
|
|||
],
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
ConfigSchema.validate({
|
||||
authProviders: ['oidc', 'basic'],
|
||||
authc: { oidc: { realm: 'realm-1' } },
|
||||
}).authc
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"http": Object {
|
||||
"autoSchemesEnabled": true,
|
||||
"enabled": true,
|
||||
"schemes": Array [
|
||||
"apikey",
|
||||
],
|
||||
},
|
||||
"oidc": Object {
|
||||
"realm": "realm-1",
|
||||
},
|
||||
"providers": Array [
|
||||
"oidc",
|
||||
"basic",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it(`realm is not allowed when authc.providers is "['basic']"`, async () => {
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authc: { providers: ['basic'], oidc: { realm: 'realm-1' } } })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"[authc.oidc]: a value wasn't expected to be present"`);
|
||||
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authProviders: ['basic'], authc: { oidc: { realm: 'realm-1' } } })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"[authc.oidc]: a value wasn't expected to be present"`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -378,26 +321,6 @@ describe('config schema', () => {
|
|||
}
|
||||
`);
|
||||
|
||||
expect(ConfigSchema.validate({ authProviders: ['saml'] }).authc).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"http": Object {
|
||||
"autoSchemesEnabled": true,
|
||||
"enabled": true,
|
||||
"schemes": Array [
|
||||
"apikey",
|
||||
],
|
||||
},
|
||||
"providers": Array [
|
||||
"saml",
|
||||
],
|
||||
"saml": Object {
|
||||
"maxRedirectURLSize": ByteSizeValue {
|
||||
"valueInBytes": 2048,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(ConfigSchema.validate({ authc: { providers: ['saml'], saml: {} } }).authc)
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -419,27 +342,6 @@ describe('config schema', () => {
|
|||
}
|
||||
`);
|
||||
|
||||
expect(ConfigSchema.validate({ authProviders: ['saml'], authc: { saml: {} } }).authc)
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"http": Object {
|
||||
"autoSchemesEnabled": true,
|
||||
"enabled": true,
|
||||
"schemes": Array [
|
||||
"apikey",
|
||||
],
|
||||
},
|
||||
"providers": Array [
|
||||
"saml",
|
||||
],
|
||||
"saml": Object {
|
||||
"maxRedirectURLSize": ByteSizeValue {
|
||||
"valueInBytes": 2048,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
ConfigSchema.validate({
|
||||
authc: { providers: ['saml'], saml: { realm: 'realm-1' } },
|
||||
|
@ -464,42 +366,12 @@ describe('config schema', () => {
|
|||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
ConfigSchema.validate({
|
||||
authProviders: ['saml'],
|
||||
authc: { saml: { realm: 'realm-1' } },
|
||||
}).authc
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"http": Object {
|
||||
"autoSchemesEnabled": true,
|
||||
"enabled": true,
|
||||
"schemes": Array [
|
||||
"apikey",
|
||||
],
|
||||
},
|
||||
"providers": Array [
|
||||
"saml",
|
||||
],
|
||||
"saml": Object {
|
||||
"maxRedirectURLSize": ByteSizeValue {
|
||||
"valueInBytes": 2048,
|
||||
},
|
||||
"realm": "realm-1",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('`realm` is not allowed if saml provider is not enabled', async () => {
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authc: { providers: ['basic'], saml: { realm: 'realm-1' } } })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"[authc.saml]: a value wasn't expected to be present"`);
|
||||
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authProviders: ['basic'], authc: { saml: { realm: 'realm-1' } } })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"[authc.saml]: a value wasn't expected to be present"`);
|
||||
});
|
||||
|
||||
it('`maxRedirectURLSize` accepts any positive value that can coerce to `ByteSizeValue`', async () => {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue