GUACAMOLE-680: Merge do not immediately re-authenticate after logout.

This commit is contained in:
Virtually Nick
2021-06-15 17:22:29 -04:00
committed by GitHub
16 changed files with 246 additions and 172 deletions

View File

@@ -175,7 +175,7 @@ angular.module('auth').factory('authenticationService', ['$injector',
// If an old token existed, request that the token be revoked // If an old token existed, request that the token be revoked
if (currentToken) { if (currentToken) {
service.logout().catch(angular.noop) service.revokeToken(currentToken).catch(angular.noop);
} }
// Notify of login and new token // Notify of login and new token
@@ -252,6 +252,24 @@ angular.module('auth').factory('authenticationService', ['$injector',
}; };
/**
* Makes a request to revoke an authentication token using the token REST
* API endpoint, returning a promise that succeeds only if the token was
* successfully revoked.
*
* @param {string} token
* The authentication token to revoke.
*
* @returns {Promise}
* A promise which succeeds only if the token was successfully revoked.
*/
service.revokeToken = function revokeToken(token) {
return requestService({
method: 'DELETE',
url: 'api/tokens/' + token
});
};
/** /**
* Makes a request to authenticate a user using the token REST API endpoint * Makes a request to authenticate a user using the token REST API endpoint
* with a username and password, ignoring any currently-stored token, * with a username and password, ignoring any currently-stored token,
@@ -276,8 +294,8 @@ angular.module('auth').factory('authenticationService', ['$injector',
}; };
/** /**
* Makes a request to logout a user using the login REST API endpoint, * Makes a request to logout a user using the token REST API endpoint,
* returning a promise succeeds only if the logout operation was * returning a promise that succeeds only if the logout operation was
* successful. * successful.
* *
* @returns {Promise} * @returns {Promise}
@@ -294,10 +312,7 @@ angular.module('auth').factory('authenticationService', ['$injector',
$rootScope.$broadcast('guacLogout', token); $rootScope.$broadcast('guacLogout', token);
// Delete old token // Delete old token
return requestService({ return service.revokeToken(token);
method: 'DELETE',
url: 'api/tokens/' + token
});
}; };

View File

@@ -167,10 +167,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
className : "logout button", className : "logout button",
callback : function logoutCallback() { callback : function logoutCallback() {
authenticationService.logout() authenticationService.logout()
['catch'](requestService.IGNORE) ['catch'](requestService.IGNORE);
['finally'](function logoutComplete() {
$location.url('/');
});
} }
}; };

View File

@@ -25,6 +25,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
// Required services // Required services
var $document = $injector.get('$document'); var $document = $injector.get('$document');
var $route = $injector.get('$route');
var $window = $injector.get('$window'); var $window = $injector.get('$window');
var clipboardService = $injector.get('clipboardService'); var clipboardService = $injector.get('clipboardService');
var guacNotification = $injector.get('guacNotification'); var guacNotification = $injector.get('guacNotification');
@@ -50,6 +51,13 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
*/ */
$scope.loginHelpText = null; $scope.loginHelpText = null;
/**
* Whether the user has selected to log back in after having logged out.
*
* @type boolean
*/
$scope.reAuthenticating = false;
/** /**
* The credentials that the authentication service is has already accepted, * The credentials that the authentication service is has already accepted,
* pending additional credentials, if any. If the user is logged in, or no * pending additional credentials, if any. If the user is logged in, or no
@@ -69,6 +77,51 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
*/ */
$scope.expectedCredentials = null; $scope.expectedCredentials = null;
/**
* Possible overall states of the client side of the web application.
*
* @enum {string}
*/
var ApplicationState = {
/**
* The application has fully loaded but is awaiting credentials from
* the user before proceeding.
*/
AWAITING_CREDENTIALS : 'awaitingCredentials',
/**
* A fatal error has occurred that will prevent the client side of the
* application from functioning properly.
*/
FATAL_ERROR : 'fatalError',
/**
* The application has just started within the user's browser and has
* not yet settled into any specific state.
*/
LOADING : 'loading',
/**
* The user has manually logged out.
*/
LOGGED_OUT : 'loggedOut',
/**
* The application has fully loaded and the user has logged in
*/
READY : 'ready'
};
/**
* The current overall state of the client side of the application.
* Possible values are defined by {@link ApplicationState}.
*
* @type string
*/
$scope.applicationState = ApplicationState.LOADING;
/** /**
* Basic page-level information. * Basic page-level information.
*/ */
@@ -103,7 +156,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
// Do not handle key events if not logged in or if a notification is // Do not handle key events if not logged in or if a notification is
// shown // shown
if ($scope.expectedCredentials || guacNotification.getStatus()) if ($scope.applicationState !== ApplicationState.READY || guacNotification.getStatus())
return true; return true;
// Warn of pending keydown // Warn of pending keydown
@@ -122,7 +175,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
// Do not handle key events if not logged in or if a notification is // Do not handle key events if not logged in or if a notification is
// shown // shown
if ($scope.expectedCredentials || guacNotification.getStatus()) if ($scope.applicationState !== ApplicationState.READY || guacNotification.getStatus())
return; return;
// Warn of pending keyup // Warn of pending keyup
@@ -168,32 +221,59 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
}, true); }, true);
/**
* Reloads the current route and controller, effectively forcing
* reauthentication. If the user is not logged in, this will result in
* the login screen appearing.
*/
$scope.reAuthenticate = function reAuthenticate() {
$scope.reAuthenticating = true;
$route.reload();
};
// Display login screen if a whole new set of credentials is needed // Display login screen if a whole new set of credentials is needed
$scope.$on('guacInvalidCredentials', function loginInvalid(event, parameters, error) { $scope.$on('guacInvalidCredentials', function loginInvalid(event, parameters, error) {
$scope.applicationState = ApplicationState.AWAITING_CREDENTIALS;
$scope.page.title = 'APP.NAME'; $scope.page.title = 'APP.NAME';
$scope.page.bodyClassName = ''; $scope.page.bodyClassName = '';
$scope.loginHelpText = null; $scope.loginHelpText = null;
$scope.acceptedCredentials = {}; $scope.acceptedCredentials = {};
$scope.expectedCredentials = error.expected; $scope.expectedCredentials = error.expected;
$scope.fatalError = null;
}); });
// Prompt for remaining credentials if provided credentials were not enough // Prompt for remaining credentials if provided credentials were not enough
$scope.$on('guacInsufficientCredentials', function loginInsufficient(event, parameters, error) { $scope.$on('guacInsufficientCredentials', function loginInsufficient(event, parameters, error) {
$scope.applicationState = ApplicationState.AWAITING_CREDENTIALS;
$scope.page.title = 'APP.NAME'; $scope.page.title = 'APP.NAME';
$scope.page.bodyClassName = ''; $scope.page.bodyClassName = '';
$scope.loginHelpText = error.translatableMessage; $scope.loginHelpText = error.translatableMessage;
$scope.acceptedCredentials = parameters; $scope.acceptedCredentials = parameters;
$scope.expectedCredentials = error.expected; $scope.expectedCredentials = error.expected;
$scope.fatalError = null;
}); });
// Replace absolutely all content with an error message if the page itself // Replace absolutely all content with an error message if the page itself
// cannot be displayed due to an error // cannot be displayed due to an error
$scope.$on('guacFatalPageError', function fatalPageError(error) { $scope.$on('guacFatalPageError', function fatalPageError(error) {
$scope.applicationState = ApplicationState.FATAL_ERROR;
$scope.page.title = 'APP.NAME'; $scope.page.title = 'APP.NAME';
$scope.page.bodyClassName = ''; $scope.page.bodyClassName = '';
$scope.fatalError = error; $scope.fatalError = error;
});
// Replace the overall user interface with an informational message if the
// user has manually logged out
$scope.$on('guacLogout', function loggedOut() {
$scope.applicationState = ApplicationState.LOGGED_OUT;
$scope.reAuthenticating = false;
}); });
// Ensure new pages always start with clear keyboard state // Ensure new pages always start with clear keyboard state
@@ -209,10 +289,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
// Clear login screen if route change was successful (and thus // Clear login screen if route change was successful (and thus
// login was either successful or not required) // login was either successful or not required)
$scope.loginHelpText = null; $scope.applicationState = ApplicationState.READY;
$scope.acceptedCredentials = null;
$scope.expectedCredentials = null;
$scope.fatalError = null;
// Set title // Set title
var title = current.$$route.title; var title = current.$$route.title;

View File

@@ -0,0 +1,27 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* Hide portions of DOM by default until Angular has finished loading,
* compiling, etc.
*/
*[ng-cloak], .translate-cloak {
display: none !important;
}

View File

@@ -17,23 +17,10 @@
* under the License. * under the License.
*/ */
.fatal-page-error-outer { .fatal-page-error-modal guac-modal {
display: table;
height: 100%;
width: 100%;
position: fixed;
left: 0;
top: 0;
z-index: 30; z-index: 30;
} }
.fatal-page-error-middle {
width: 100%;
text-align: center;
display: table-cell;
vertical-align: middle;
}
.fatal-page-error { .fatal-page-error {
display: inline-block; display: inline-block;
width: 100%; width: 100%;

View File

@@ -0,0 +1,29 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.logged-out-modal guac-modal {
background: white;
z-index: 20;
}
.logged-out-modal .notification {
display: inline-block;
max-width: 3in;
width: 100%;
}

View File

@@ -17,25 +17,11 @@
* under the License. * under the License.
*/ */
.status-outer { .global-status-modal guac-modal {
display: table;
height: 100%;
width: 100%;
position: fixed;
left: 0;
top: 0;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
z-index: 10;
} }
.status-middle { .global-status-modal .notification {
width: 100%;
text-align: center;
display: table-cell;
vertical-align: middle;
}
.status-middle .notification {
width: 75%; width: 75%;
max-width: 5in; max-width: 5in;
@@ -47,34 +33,10 @@
} }
.status-middle .notification .body { .global-status-modal .notification .body {
margin: 1.25em; margin: 1.25em;
} }
.status-middle .notification .buttons { .global-status-modal .notification .buttons {
margin: 1em; margin: 1em;
} }
/* Fade entire status area in/out based on shown status */
.status-outer {
visibility: hidden;
opacity: 0;
transition: opacity, visibility;
transition-duration: 0.25s;
}
.shown.status-outer {
visibility: visible;
opacity: 1;
}
/* Hide dialog immediately based on status */
.status-middle .notification {
visibility: hidden;
}
.shown .status-middle .notification {
visibility: visible;
}

View File

@@ -36,8 +36,6 @@
max-width: 3in; max-width: 3in;
text-align: left; text-align: left;
padding: 1em; padding: 1em;
border: 1px solid rgba(0, 0, 0, 0.25);
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25);
font-size: 1.25em; font-size: 1.25em;
display: inline-block; display: inline-block;

View File

@@ -6,7 +6,7 @@
<div class="login-dialog-middle"> <div class="login-dialog-middle">
<div class="login-dialog"> <div class="login-dialog notification">
<form class="login-form" ng-submit="login()"> <form class="login-form" ng-submit="login()">

View File

@@ -140,13 +140,7 @@ angular.module('navigation').directive('guacUserMenu', [function guacUserMenu()
*/ */
$scope.logout = function logout() { $scope.logout = function logout() {
authenticationService.logout() authenticationService.logout()
['catch'](requestService.IGNORE) ['catch'](requestService.IGNORE);
['finally'](function logoutComplete() {
if ($location.path() !== '/')
$location.url('/');
else
$route.reload();
});
}; };
/** /**

View File

@@ -0,0 +1,29 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive for displaying arbitrary modal content.
*/
angular.module('notification').directive('guacModal', [function guacModal() {
return {
restrict: 'E',
templateUrl: 'app/notification/templates/guacModal.html',
transclude: true
};
}]);

View File

@@ -17,78 +17,25 @@
* under the License. * under the License.
*/ */
.dialog-container { guac-modal {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: rgba(0, 0, 0, 0.5);
padding: 1em;
}
.dialog-outer {
display: table; display: table;
height: 100%; height: 100%;
width: 100%; width: 100%;
position: fixed; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
background: rgba(0, 0, 0, 0.5); z-index: 10;
} }
.dialog-middle { guac-modal .modal-contents {
width: 100%; width: 100%;
text-align: center; text-align: center;
display: table-cell; display: table-cell;
vertical-align: middle; vertical-align: middle;
} }
.dialog.edit { guac-modal {
max-height: 100%; animation: fadein 0.125s linear;
} -moz-animation: fadein 0.125s linear;
-webkit-animation: fadein 0.125s linear;
.dialog {
max-width: 100%;
width: 8in;
margin-left: auto;
margin-right: auto;
overflow: auto;
border: 1px solid rgba(0, 0, 0, 0.5);
background: #E7E7E7;
-moz-border-radius: 0.2em;
-webkit-border-radius: 0.2em;
-khtml-border-radius: 0.2em;
border-radius: 0.2em;
box-shadow: 0.1em 0.1em 0.2em rgba(0, 0, 0, 0.6);
}
.dialog > * {
margin: 1em;
}
.dialog .header {
margin: 0;
}
.dialog td {
position: relative;
}
.dialog .overlay {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1;
}
.dialog .footer {
text-align: center;
} }

View File

@@ -18,8 +18,8 @@
*/ */
.notification { .notification {
border: 1px solid rgba(0, 0, 0, 0.125); border: 1px solid rgba(0, 0, 0, 0.25);
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.125); box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25);
background: white; background: white;
color: black; color: black;
} }

View File

@@ -0,0 +1,3 @@
<div class="modal-contents">
<ng-transclude></ng-transclude>
</div>

View File

@@ -38,41 +38,48 @@
<title ng-bind="page.title | translate"></title> <title ng-bind="page.title | translate"></title>
</head> </head>
<body ng-class="page.bodyClassName"> <body ng-cloak translate-cloak ng-class="page.bodyClassName" ng-switch="applicationState">
<div ng-if="!fatalError"> <!-- Manually logged-out -->
<div class="logged-out-modal" ng-switch-when="loggedOut">
<!-- Content for logged-in users --> <guac-modal>
<div ng-if="!expectedCredentials"> <div class="notification">
<p translate="APP.INFO_LOGGED_OUT"></p>
<!-- Global status/error dialog --> <p>
<div ng-class="{shown: guacNotification.getStatus()}" class="status-outer"> <button translate="APP.ACTION_LOGIN_AGAIN" ng-disabled="reAuthenticating"
<div class="status-middle"> ng-click="reAuthenticate()"></button>
<guac-notification notification="guacNotification.getStatus()"></guac-notification> </p>
</div>
</div> </div>
</guac-modal>
<div id="content" ng-view>
</div>
</div>
<!-- Login screen for logged-out users -->
<guac-login ng-show="expectedCredentials"
help-text="loginHelpText"
form="expectedCredentials"
values="acceptedCredentials"></guac-login>
</div> </div>
<!-- Absolute fatal error --> <!-- Absolute fatal error -->
<div ng-if="fatalError" ng-class="{shown: fatalError}" class="fatal-page-error-outer"> <div class="fatal-page-error-modal" ng-switch-when="fatalError">
<div class="fatal-page-error-middle"> <guac-modal>
<div class="fatal-page-error"> <div class="fatal-page-error">
<h1 translate="APP.DIALOG_HEADER_ERROR"></h1> <h1 translate="APP.DIALOG_HEADER_ERROR"></h1>
<p translate="APP.ERROR_PAGE_UNAVAILABLE"></p> <p translate="APP.ERROR_PAGE_UNAVAILABLE"></p>
</div> </div>
</guac-modal>
</div>
<!-- Login screen for logged-out users -->
<guac-login ng-switch-when="awaitingCredentials"
help-text="loginHelpText"
form="expectedCredentials"
values="acceptedCredentials"></guac-login>
<!-- Content for logged-in users -->
<div ng-switch-default>
<!-- Global status/error dialog -->
<guac-modal class="global-status-modal" ng-if="guacNotification.getStatus()">
<guac-notification notification="guacNotification.getStatus()"></guac-notification>
</guac-modal>
<div id="content" ng-view>
</div> </div>
</div> </div>
<!-- Polyfills --> <!-- Polyfills -->

View File

@@ -15,6 +15,7 @@
"ACTION_DELETE_SESSIONS" : "Kill Sessions", "ACTION_DELETE_SESSIONS" : "Kill Sessions",
"ACTION_DOWNLOAD" : "Download", "ACTION_DOWNLOAD" : "Download",
"ACTION_LOGIN" : "Login", "ACTION_LOGIN" : "Login",
"ACTION_LOGIN_AGAIN" : "Re-login",
"ACTION_LOGOUT" : "Logout", "ACTION_LOGOUT" : "Logout",
"ACTION_MANAGE_CONNECTIONS" : "Connections", "ACTION_MANAGE_CONNECTIONS" : "Connections",
"ACTION_MANAGE_PREFERENCES" : "Preferences", "ACTION_MANAGE_PREFERENCES" : "Preferences",
@@ -44,6 +45,7 @@
"FORMAT_DATE_TIME_PRECISE" : "yyyy-MM-dd HH:mm:ss", "FORMAT_DATE_TIME_PRECISE" : "yyyy-MM-dd HH:mm:ss",
"INFO_ACTIVE_USER_COUNT" : "Currently in use by {USERS} {USERS, plural, one{user} other{users}}.", "INFO_ACTIVE_USER_COUNT" : "Currently in use by {USERS} {USERS, plural, one{user} other{users}}.",
"INFO_LOGGED_OUT" : "You have been logged out.",
"TEXT_ANONYMOUS_USER" : "Anonymous", "TEXT_ANONYMOUS_USER" : "Anonymous",
"TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{{VALUE, plural, one{second} other{seconds}}} minute{{VALUE, plural, one{minute} other{minutes}}} hour{{VALUE, plural, one{hour} other{hours}}} day{{VALUE, plural, one{day} other{days}}} other{}}", "TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{{VALUE, plural, one{second} other{seconds}}} minute{{VALUE, plural, one{minute} other{minutes}}} hour{{VALUE, plural, one{hour} other{hours}}} day{{VALUE, plural, one{day} other{days}}} other{}}",