diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/user/SharedAuthenticatedUser.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/user/SharedAuthenticatedUser.java index a50b62535..701ffd5ba 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/user/SharedAuthenticatedUser.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/user/SharedAuthenticatedUser.java @@ -19,7 +19,6 @@ package org.apache.guacamole.auth.jdbc.sharing.user; -import java.util.UUID; import org.apache.guacamole.auth.jdbc.user.RemoteAuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; @@ -61,7 +60,8 @@ public class SharedAuthenticatedUser extends RemoteAuthenticatedUser { /** * Creates a new SharedAuthenticatedUser associating the given user with * their corresponding credentials and share key. The identifier (username) - * of the user will be randomly generated. + * of the user will be the standard identifier for anonymous users as + * defined by the Guacamole extension API. * * @param authenticationProvider * The AuthenticationProvider that has authenticated the given user. @@ -77,7 +77,7 @@ public class SharedAuthenticatedUser extends RemoteAuthenticatedUser { Credentials credentials, String shareKey) { super(authenticationProvider, credentials); this.shareKey = shareKey; - this.identifier = UUID.randomUUID().toString(); + this.identifier = AuthenticatedUser.ANONYMOUS_IDENTIFIER; } /** diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AuthenticatedUser.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AuthenticatedUser.java index 514e65a52..b121abff4 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AuthenticatedUser.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AuthenticatedUser.java @@ -28,6 +28,12 @@ package org.apache.guacamole.net.auth; */ public interface AuthenticatedUser extends Identifiable { + /** + * The identifier reserved for representing a user that has authenticated + * anonymously. + */ + public static final String ANONYMOUS_IDENTIFIER = ""; + /** * Returns the AuthenticationProvider that authenticated this user. * diff --git a/guacamole/src/main/webapp/app/auth/service/authenticationService.js b/guacamole/src/main/webapp/app/auth/service/authenticationService.js index 2bd6a9f9c..74f0570b4 100644 --- a/guacamole/src/main/webapp/app/auth/service/authenticationService.js +++ b/guacamole/src/main/webapp/app/auth/service/authenticationService.js @@ -42,7 +42,8 @@ angular.module('auth').factory('authenticationService', ['$injector', function authenticationService($injector) { // Required types - var Error = $injector.get('Error'); + var AuthenticationResult = $injector.get('AuthenticationResult'); + var Error = $injector.get('Error'); // Required services var $cookieStore = $injector.get('$cookieStore'); @@ -53,13 +54,84 @@ angular.module('auth').factory('authenticationService', ['$injector', var service = {}; /** - * The unique identifier of the local cookie which stores the user's - * current authentication token and username. + * The most recent authentication result, or null if no authentication + * result is cached. + * + * @type AuthenticationResult + */ + var cachedResult = null; + + /** + * The unique identifier of the local cookie which stores the result of the + * last authentication attempt. * * @type String */ var AUTH_COOKIE_ID = "GUAC_AUTH"; + /** + * Retrieves the last successful authentication result. If the user has not + * yet authenticated, the user has logged out, or the last authentication + * attempt failed, null is returned. + * + * @returns {AuthenticationResult} + * The last successful authentication result, or null if the user is not + * currently authenticated. + */ + var getAuthenticationResult = function getAuthenticationResult() { + + // Use cached result, if any + if (cachedResult) + return cachedResult; + + // Return explicit null if no auth data is currently stored + var data = $cookieStore.get(AUTH_COOKIE_ID); + if (!data) + return null; + + // Update cache and return retrieved auth result + return (cachedResult = new AuthenticationResult(data)); + + }; + + /** + * Stores the given authentication result for future retrieval. The given + * result MUST be the result of the most recent authentication attempt. + * + * @param {AuthenticationResult} data + * The last successful authentication result, or null if the last + * authentication attempt failed. + */ + var setAuthenticationResult = function setAuthenticationResult(data) { + + // Clear the currently-stored result if the last attempt failed + if (!data) { + cachedResult = null; + $cookieStore.remove(AUTH_COOKIE_ID); + } + + // Otherwise store the authentication attempt directly + else { + + // Always store in cache + cachedResult = data; + + // Store cookie ONLY if not anonymous + if (data.username !== AuthenticationResult.ANONYMOUS_USERNAME) + $cookieStore.put(AUTH_COOKIE_ID, data); + + } + + }; + + /** + * Clears the stored authentication result, if any. If no authentication + * result is currently stored, this function has no effect. + */ + var clearAuthenticationResult = function clearAuthenticationResult() { + setAuthenticationResult(null); + }; + /** * Makes a request to authenticate a user using the token REST API endpoint * and given arbitrary parameters, returning a promise that succeeds only @@ -95,12 +167,7 @@ angular.module('auth').factory('authenticationService', ['$injector', var completeAuthentication = function completeAuthentication(data) { // Store auth data - $cookieStore.put(AUTH_COOKIE_ID, { - 'authToken' : data.authToken, - 'username' : data.username, - 'dataSource' : data.dataSource, - 'availableDataSources' : data.availableDataSources - }); + setAuthenticationResult(new AuthenticationResult(data)); // Process is complete authenticationProcess.resolve(); @@ -114,7 +181,7 @@ angular.module('auth').factory('authenticationService', ['$injector', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - data: $.param(parameters), + data: $.param(parameters) }) // If authentication succeeds, handle received auth data @@ -243,7 +310,7 @@ angular.module('auth').factory('authenticationService', ['$injector', // Clear authentication data var token = service.getCurrentToken(); - $cookieStore.remove(AUTH_COOKIE_ID); + clearAuthenticationResult(); // Notify listeners that a token is being destroyed $rootScope.$broadcast('guacLogout', token); @@ -256,6 +323,19 @@ angular.module('auth').factory('authenticationService', ['$injector', }; + /** + * Returns whether the current user has authenticated anonymously. An + * anonymous user is denoted by the identifier reserved by the Guacamole + * extension API for anonymous users (the empty string). + * + * @returns {Boolean} + * true if the current user has authenticated anonymously, false + * otherwise. + */ + service.isAnonymous = function isAnonymous() { + return service.getCurrentUsername() === ''; + }; + /** * Returns the username of the current user. If the current user is not * logged in, this value may not be valid. @@ -267,7 +347,7 @@ angular.module('auth').factory('authenticationService', ['$injector', service.getCurrentUsername = function getCurrentUsername() { // Return username, if available - var authData = $cookieStore.get(AUTH_COOKIE_ID); + var authData = getAuthenticationResult(); if (authData) return authData.username; @@ -287,7 +367,7 @@ angular.module('auth').factory('authenticationService', ['$injector', service.getCurrentToken = function getCurrentToken() { // Return auth token, if available - var authData = $cookieStore.get(AUTH_COOKIE_ID); + var authData = getAuthenticationResult(); if (authData) return authData.authToken; @@ -307,7 +387,7 @@ angular.module('auth').factory('authenticationService', ['$injector', service.getDataSource = function getDataSource() { // Return data source, if available - var authData = $cookieStore.get(AUTH_COOKIE_ID); + var authData = getAuthenticationResult(); if (authData) return authData.dataSource; @@ -327,7 +407,7 @@ angular.module('auth').factory('authenticationService', ['$injector', service.getAvailableDataSources = function getAvailableDataSources() { // Return data sources, if available - var authData = $cookieStore.get(AUTH_COOKIE_ID); + var authData = getAuthenticationResult(); if (authData) return authData.availableDataSources; diff --git a/guacamole/src/main/webapp/app/auth/types/AuthenticationResult.js b/guacamole/src/main/webapp/app/auth/types/AuthenticationResult.js new file mode 100644 index 000000000..e2774619f --- /dev/null +++ b/guacamole/src/main/webapp/app/auth/types/AuthenticationResult.js @@ -0,0 +1,81 @@ +/* + * 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. + */ + +/** + * Service which defines the AuthenticationResult class. + */ +angular.module('auth').factory('AuthenticationResult', [function defineAuthenticationResult() { + + /** + * The object returned by REST API calls when representing the successful + * result of an authentication attempt. + * + * @constructor + * @param {AuthenticationResult|Object} [template={}] + * The object whose properties should be copied within the new + * AuthenticationResult. + */ + var AuthenticationResult = function AuthenticationResult(template) { + + // Use empty object by default + template = template || {}; + + /** + * The unique token generated for the user that authenticated. + * + * @type String + */ + this.authToken = template.authToken; + + /** + * The name which uniquely identifies the user that authenticated. + * + * @type String + */ + this.username = template.username; + + /** + * The unique identifier of the data source which authenticated the + * user. + * + * @type String + */ + this.dataSource = template.dataSource; + + /** + * The identifiers of all data sources available to the user that + * authenticated. + * + * @type String[] + */ + this.availableDataSources = template.availableDataSources; + + }; + + /** + * The username reserved by the Guacamole extension API for users which have + * authenticated anonymously. + * + * @type String + */ + AuthenticationResult.ANONYMOUS_USERNAME = ''; + + return AuthenticationResult; + +}]); diff --git a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js index 904ab85b4..dd2ec6d68 100644 --- a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js @@ -88,7 +88,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider', // Otherwise, reject and reroute else { - $location.url(homePage.url); + $location.path(homePage.url); route.reject(); } diff --git a/guacamole/src/main/webapp/app/list/directives/guacUserItem.js b/guacamole/src/main/webapp/app/list/directives/guacUserItem.js new file mode 100644 index 000000000..5c5d26d06 --- /dev/null +++ b/guacamole/src/main/webapp/app/list/directives/guacUserItem.js @@ -0,0 +1,92 @@ +/* + * 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 which graphically represents an individual user. + */ +angular.module('list').directive('guacUserItem', [function guacUserItem() { + + return { + restrict: 'E', + replace: true, + scope: { + + /** + * The username of the user represented by this guacUserItem. + * + * @type String + */ + username : '=' + + }, + + templateUrl: 'app/list/templates/guacUserItem.html', + controller: ['$scope', '$injector', + function guacUserItemController($scope, $injector) { + + // Required types + var AuthenticationResult = $injector.get('AuthenticationResult'); + + // Required services + var $translate = $injector.get('$translate'); + + /** + * The string to display when listing the user having the provided + * username. Generally, this will be the username itself, but can + * also be an arbitrary human-readable representation of the user, + * or null if the display name is not yet determined. + * + * @type String + */ + $scope.displayName = null; + + /** + * Returns whether the username provided to this directive denotes + * a user that authenticated anonymously. + * + * @returns {Boolean} + * true if the username provided represents an anonymous user, + * false otherwise. + */ + $scope.isAnonymous = function isAnonymous() { + return $scope.username === AuthenticationResult.ANONYMOUS_USERNAME; + }; + + // Update display name whenever provided username changes + $scope.$watch('username', function updateDisplayName(username) { + + // If the user is anonymous, pull the display name for anonymous + // users from the translation service + if ($scope.isAnonymous()) { + $translate('LIST.TEXT_ANONYMOUS_USER') + .then(function retrieveAnonymousDisplayName(anonymousDisplayName) { + $scope.displayName = anonymousDisplayName; + }); + } + + // For all other users, use the username verbatim + else + $scope.displayName = username; + + }); + + }] // end controller + + }; +}]); diff --git a/guacamole/src/main/webapp/app/list/listModule.js b/guacamole/src/main/webapp/app/list/listModule.js index b3e65810e..845c8c968 100644 --- a/guacamole/src/main/webapp/app/list/listModule.js +++ b/guacamole/src/main/webapp/app/list/listModule.js @@ -21,4 +21,6 @@ * Module for displaying, sorting, and filtering the contents of a list, split * into multiple pages. */ -angular.module('list', []); +angular.module('list', [ + 'auth' +]); diff --git a/guacamole/src/main/webapp/app/list/styles/user-item.css b/guacamole/src/main/webapp/app/list/styles/user-item.css new file mode 100644 index 000000000..ffedd3902 --- /dev/null +++ b/guacamole/src/main/webapp/app/list/styles/user-item.css @@ -0,0 +1,23 @@ +/* + * 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. + */ + +.user-item.anonymous { + font-style: italic; + opacity: 0.5; +} diff --git a/guacamole/src/main/webapp/app/list/templates/guacUserItem.html b/guacamole/src/main/webapp/app/list/templates/guacUserItem.html new file mode 100644 index 000000000..e6ea670a6 --- /dev/null +++ b/guacamole/src/main/webapp/app/list/templates/guacUserItem.html @@ -0,0 +1,3 @@ +