mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* Adding very basic place for the logged out page * Redirecting to logged_out when we aren't using SLO * Basing styles on the login styles * Fixing linting errors * Responding to PR feedback * Fixing issue with the basepath and the login link * Adding proper i18n prefix * Updating unit tests
This commit is contained in:
parent
84dfa9b21f
commit
1ef606e4cc
22 changed files with 227 additions and 26 deletions
|
@ -25,7 +25,7 @@ import { downloadReport } from '../lib/download_report';
|
|||
uiModules.get('kibana')
|
||||
.run((Private, reportingPollConfig) => {
|
||||
// Don't show users any reporting toasts until they're logged in.
|
||||
if (Private(PathProvider).isLoginOrLogout()) {
|
||||
if (Private(PathProvider).isUnauthenticated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { initPublicRolesApi } from './server/routes/api/public/roles';
|
|||
import { initIndicesApi } from './server/routes/api/v1/indices';
|
||||
import { initLoginView } from './server/routes/views/login';
|
||||
import { initLogoutView } from './server/routes/views/logout';
|
||||
import { initLoggedOutView } from './server/routes/views/logged_out';
|
||||
import { validateConfig } from './server/lib/validate_config';
|
||||
import { authenticateFactory } from './server/lib/auth_redirect';
|
||||
import { checkLicense } from './server/lib/check_license';
|
||||
|
@ -67,6 +68,11 @@ export const security = (kibana) => new kibana.Plugin({
|
|||
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',
|
||||
|
@ -165,6 +171,7 @@ export const security = (kibana) => new kibana.Plugin({
|
|||
initIndicesApi(server);
|
||||
initLoginView(server, xpackMainPlugin);
|
||||
initLogoutView(server);
|
||||
initLoggedOutView(server);
|
||||
|
||||
server.injectUiAppVars('login', () => {
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ const SESSION_TIMEOUT_GRACE_PERIOD_MS = 5000;
|
|||
const module = uiModules.get('security', []);
|
||||
module.config(($httpProvider) => {
|
||||
$httpProvider.interceptors.push(($timeout, $window, $q, $injector, sessionTimeout, Notifier, Private, autoLogout) => {
|
||||
const isLoginOrLogout = Private(PathProvider).isLoginOrLogout();
|
||||
const isUnauthenticated = Private(PathProvider).isUnauthenticated();
|
||||
const notifier = new Notifier();
|
||||
const notificationLifetime = 60 * 1000;
|
||||
const notificationOptions = {
|
||||
|
@ -61,7 +61,7 @@ module.config(($httpProvider) => {
|
|||
|
||||
function interceptorFactory(responseHandler) {
|
||||
return function interceptor(response) {
|
||||
if (!isLoginOrLogout && !isSystemApiRequest(response.config) && sessionTimeout !== null) {
|
||||
if (!isUnauthenticated && !isSystemApiRequest(response.config) && sessionTimeout !== null) {
|
||||
clearNotifications();
|
||||
scheduleNotification();
|
||||
}
|
||||
|
|
|
@ -21,10 +21,10 @@ function isUnauthorizedResponseAllowed(response) {
|
|||
|
||||
const module = uiModules.get('security');
|
||||
module.factory('onUnauthorizedResponse', ($q, $window, $injector, Private, autoLogout) => {
|
||||
const isLoginOrLogout = Private(PathProvider).isLoginOrLogout();
|
||||
const isUnauthenticated = Private(PathProvider).isUnauthenticated();
|
||||
function interceptorFactory(responseHandler) {
|
||||
return function interceptor(response) {
|
||||
if (response.status === 401 && !isUnauthorizedResponseAllowed(response) && !isLoginOrLogout) return autoLogout();
|
||||
if (response.status === 401 && !isUnauthorizedResponseAllowed(response) && !isUnauthenticated) return autoLogout();
|
||||
return responseHandler(response);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
@import 'ui/public/styles/styling_constants';
|
||||
|
||||
// Logged out styles
|
||||
@import './views/logged_out/index';
|
||||
|
||||
// Login styles
|
||||
@import './views/login/index';
|
||||
@import './views/login/index';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
@import 'logged_out';
|
|
@ -0,0 +1,64 @@
|
|||
|
||||
.loggedOut {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: $euiZLevel9 + 1000;
|
||||
background: inherit;
|
||||
background-image: linear-gradient(0deg, $euiColorLightestShade 0%, $euiColorEmptyShade 100%);
|
||||
opacity: 0;
|
||||
overflow: auto;
|
||||
animation: loggedOut_FadeIn $euiAnimSpeedExtraSlow $euiAnimSlightResistance 0s forwards;
|
||||
}
|
||||
|
||||
.loggedOut::before {
|
||||
// SASSTODO: webpack pipeline isn't setup to handle image urls in SASS yet
|
||||
// content: url(../../assets/bg_top_branded.svg);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.loggedOut::after {
|
||||
// SASSTODO: webpack pipeline isn't setup to handle image urls in SASS yet
|
||||
// content: url(../../assets/bg_bottom_branded.svg);
|
||||
position: fixed;
|
||||
bottom: -2px; // Hides an odd space at the bottom of the svg
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.loggedOut__header {
|
||||
position: relative;
|
||||
padding: $euiSizeXL;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loggedOut__logo {
|
||||
margin-bottom: $euiSizeXL;
|
||||
@include kibanaCircleLogo;
|
||||
@include euiBottomShadowMedium;
|
||||
}
|
||||
|
||||
.loggedOut__content {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
max-width: 460px;
|
||||
padding-left: $euiSizeXL;
|
||||
padding-right: $euiSizeXL;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@keyframes loggedOut_FadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(200px), scale(0.75);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0), scale(1);
|
||||
}
|
||||
}
|
|
@ -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 { LoggedOutPage } from './logged_out_page';
|
|
@ -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 { EuiButton, EuiIcon, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
interface Props {
|
||||
addBasePath: (path: string) => string;
|
||||
}
|
||||
|
||||
export class LoggedOutPage extends Component<Props, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<I18nProvider>
|
||||
<div className="loggedOut">
|
||||
<header className="loggedOut__header">
|
||||
<div className="loggedOut__content eui-textCenter">
|
||||
<EuiSpacer size="xxl" />
|
||||
<span className="loggedOut__logo">
|
||||
<EuiIcon type="logoKibana" size="xxl" />
|
||||
</span>
|
||||
<EuiTitle size="l" className="loggedOut__title">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.security.loggedOut.title"
|
||||
defaultMessage="Successfully logged out"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xl" />
|
||||
</div>
|
||||
</header>
|
||||
<div className="loggedOut__content eui-textCenter">
|
||||
<EuiButton href={this.props.addBasePath('/login')}>
|
||||
<FormattedMessage id="xpack.security.loggedOut.login" defaultMessage="Login" />
|
||||
</EuiButton>
|
||||
</div>
|
||||
</div>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
}
|
7
x-pack/plugins/security/public/views/logged_out/index.js
Normal file
7
x-pack/plugins/security/public/views/logged_out/index.js
Normal file
|
@ -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.
|
||||
*/
|
||||
|
||||
import './logged_out';
|
|
@ -0,0 +1 @@
|
|||
<div id="reactLoggedOutRoot" />
|
|
@ -0,0 +1,7 @@
|
|||
.loggedOut::before {
|
||||
content: url(../../assets/bg_top_branded.svg);
|
||||
}
|
||||
|
||||
.loggedOut::after {
|
||||
content: url(../../assets/bg_bottom_branded.svg);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import template from 'plugins/security/views/logged_out/logged_out.html';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import 'ui/autoload/styles';
|
||||
import chrome from 'ui/chrome';
|
||||
import './logged_out.less';
|
||||
|
||||
import { LoggedOutPage } from './components';
|
||||
|
||||
chrome
|
||||
.setVisible(false)
|
||||
.setRootTemplate(template)
|
||||
.setRootController('logout', ($scope: any) => {
|
||||
$scope.$$postDigest(() => {
|
||||
const domNode = document.getElementById('reactLoggedOutRoot');
|
||||
render(<LoggedOutPage addBasePath={chrome.addBasePath} />, domNode);
|
||||
});
|
||||
});
|
|
@ -31,7 +31,7 @@ const module = uiModules.get('security', ['kibana']);
|
|||
module.controller('securityNavController', ($scope, ShieldUser, globalNavState, kbnBaseUrl, Private, esDataIsTribe) => {
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
const showSecurityLinks = xpackInfo.get('features.security.showLinks');
|
||||
if (Private(PathProvider).isLoginOrLogout() || !showSecurityLinks) return;
|
||||
if (Private(PathProvider).isUnauthenticated() || !showSecurityLinks) return;
|
||||
|
||||
$scope.user = ShieldUser.getCurrent();
|
||||
$scope.route = `${kbnBaseUrl}#/account`;
|
||||
|
@ -70,7 +70,7 @@ chromeHeaderNavControlsRegistry.register((ShieldUser, kbnBaseUrl, Private) => ({
|
|||
render(el) {
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
const showSecurityLinks = xpackInfo.get('features.security.showLinks');
|
||||
if (Private(PathProvider).isLoginOrLogout() || !showSecurityLinks) return null;
|
||||
if (Private(PathProvider).isUnauthenticated() || !showSecurityLinks) return null;
|
||||
|
||||
const props = {
|
||||
user: ShieldUser.getCurrent(),
|
||||
|
|
|
@ -564,7 +564,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
expect(authenticationResult.error).to.be(failureReason);
|
||||
});
|
||||
|
||||
it('does not redirect if `redirect` field in SAML logout response is null.', async () => {
|
||||
it('redirects to /logged_out if `redirect` field in SAML logout response is null.', async () => {
|
||||
const request = requestFixture();
|
||||
const accessToken = 'x-saml-token';
|
||||
const refreshToken = 'x-saml-refresh-token';
|
||||
|
@ -582,10 +582,11 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
{ body: { token: accessToken, refresh_token: refreshToken } }
|
||||
);
|
||||
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.redirected()).to.be(true);
|
||||
expect(authenticationResult.redirectURL).to.be('/logged_out');
|
||||
});
|
||||
|
||||
it('does not redirect if `redirect` field in SAML logout response is not defined.', async () => {
|
||||
it('redirects to /logged_out if `redirect` field in SAML logout response is not defined.', async () => {
|
||||
const request = requestFixture();
|
||||
const accessToken = 'x-saml-token';
|
||||
const refreshToken = 'x-saml-refresh-token';
|
||||
|
@ -603,7 +604,8 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
{ body: { token: accessToken, refresh_token: refreshToken } }
|
||||
);
|
||||
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.redirected()).to.be(true);
|
||||
expect(authenticationResult.redirectURL).to.be('/logged_out');
|
||||
});
|
||||
|
||||
it('relies on SAML logout if query string is not empty, but does not include SAMLRequest.', async () => {
|
||||
|
@ -624,7 +626,8 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
{ body: { token: accessToken, refresh_token: refreshToken } }
|
||||
);
|
||||
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.redirected()).to.be(true);
|
||||
expect(authenticationResult.redirectURL).to.be('/logged_out');
|
||||
});
|
||||
|
||||
it('relies SAML invalidate call even if access token is presented.', async () => {
|
||||
|
@ -651,10 +654,11 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
}
|
||||
);
|
||||
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.redirected()).to.be(true);
|
||||
expect(authenticationResult.redirectURL).to.be('/logged_out');
|
||||
});
|
||||
|
||||
it('does not redirect if `redirect` field in SAML invalidate response is null.', async () => {
|
||||
it('redirects to /logged_out if `redirect` field in SAML invalidate response is null.', async () => {
|
||||
const request = requestFixture({ search: '?SAMLRequest=xxx%20yyy' });
|
||||
|
||||
callWithInternalUser
|
||||
|
@ -675,10 +679,11 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
}
|
||||
);
|
||||
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.redirected()).to.be(true);
|
||||
expect(authenticationResult.redirectURL).to.be('/logged_out');
|
||||
});
|
||||
|
||||
it('does not redirect if `redirect` field in SAML invalidate response is not defined.', async () => {
|
||||
it('redirects to /logged_out if `redirect` field in SAML invalidate response is not defined.', async () => {
|
||||
const request = requestFixture({ search: '?SAMLRequest=xxx%20yyy' });
|
||||
|
||||
callWithInternalUser
|
||||
|
@ -699,7 +704,8 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
}
|
||||
);
|
||||
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.redirected()).to.be(true);
|
||||
expect(authenticationResult.redirectURL).to.be('/logged_out');
|
||||
});
|
||||
|
||||
it('redirects user to the IdP if SLO is supported by IdP in case of SP initiated logout.', async () => {
|
||||
|
|
|
@ -410,7 +410,7 @@ export class SAMLAuthenticationProvider {
|
|||
return DeauthenticationResult.redirectTo(redirect);
|
||||
}
|
||||
|
||||
return DeauthenticationResult.succeeded();
|
||||
return DeauthenticationResult.redirectTo('/logged_out');
|
||||
} catch(err) {
|
||||
this._options.log(['debug', 'security', 'saml'], `Failed to deauthenticate user: ${err.message}`);
|
||||
return DeauthenticationResult.failed(err);
|
||||
|
|
27
x-pack/plugins/security/server/routes/views/logged_out.js
Normal file
27
x-pack/plugins/security/server/routes/views/logged_out.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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(server) {
|
||||
const config = server.config();
|
||||
const loggedOut = server.getHiddenUiAppById('logged_out');
|
||||
const cookieName = config.get('xpack.security.cookieName');
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
|
@ -21,7 +21,7 @@ module.factory('checkXPackInfoChange', ($q, Private) => {
|
|||
const xpackInfo = Private(XPackInfoProvider);
|
||||
const xpackInfoSignature = Private(XPackInfoSignatureProvider);
|
||||
const debounce = Private(DebounceProvider);
|
||||
const isLoginOrLogout = Private(PathProvider).isLoginOrLogout();
|
||||
const isUnauthenticated = Private(PathProvider).isUnauthenticated();
|
||||
let isLicenseExpirationBannerShown = false;
|
||||
|
||||
const notifyIfLicenseIsExpired = debounce(() => {
|
||||
|
@ -59,7 +59,7 @@ module.factory('checkXPackInfoChange', ($q, Private) => {
|
|||
* @return
|
||||
*/
|
||||
function interceptor(response, handleResponse) {
|
||||
if (isLoginOrLogout) {
|
||||
if (isUnauthenticated) {
|
||||
return handleResponse(response);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ function telemetryStart($injector) {
|
|||
if (telemetryEnabled) {
|
||||
const Private = $injector.get('Private');
|
||||
// no telemetry for non-logged in users
|
||||
if (Private(PathProvider).isLoginOrLogout()) { return; }
|
||||
if (Private(PathProvider).isUnauthenticated()) { return; }
|
||||
|
||||
const $http = $injector.get('$http');
|
||||
const sender = new Telemetry($injector, () => fetchTelemetry($http));
|
||||
|
|
|
@ -31,7 +31,7 @@ async function asyncInjectBanner($injector) {
|
|||
}
|
||||
|
||||
// and no banner for non-logged in users
|
||||
if (Private(PathProvider).isLoginOrLogout()) {
|
||||
if (Private(PathProvider).isUnauthenticated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ import chrome from 'ui/chrome';
|
|||
export function PathProvider($window) {
|
||||
const path = chrome.removeBasePath($window.location.pathname);
|
||||
return {
|
||||
isLoginOrLogout() {
|
||||
return path === '/login' || path === '/logout';
|
||||
isUnauthenticated() {
|
||||
return path === '/login' || path === '/logout' || path === '/logged_out';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -28,4 +28,4 @@
|
|||
"jest"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue