From 6f8ae83ca54e6ba8226b3be64794870b275afaa6 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 27 Aug 2015 23:18:37 -0700 Subject: [PATCH 01/47] GUAC-586: Add convenience methods for retrieving objects directly from session. --- .../basic/rest/ObjectRetrievalService.java | 107 +++++++++++++++++- .../rest/auth/APIAuthenticationResponse.java | 105 +++++++++++++++++ 2 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/APIAuthenticationResponse.java diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/ObjectRetrievalService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/ObjectRetrievalService.java index 981233eb5..792c05f61 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/ObjectRetrievalService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/ObjectRetrievalService.java @@ -25,6 +25,7 @@ package org.glyptodon.guacamole.net.basic.rest; import java.util.List; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; +import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.Directory; @@ -48,7 +49,7 @@ public class ObjectRetrievalService { * @param session * The GuacamoleSession to retrieve the UserContext from. * - * @param identifier + * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider that created the * UserContext being retrieved. Only one UserContext per * AuthenticationProvider can exist. @@ -62,7 +63,7 @@ public class ObjectRetrievalService { * UserContext does not exist. */ public UserContext retrieveUserContext(GuacamoleSession session, - String identifier) throws GuacamoleException { + String authProviderIdentifier) throws GuacamoleException { // Get list of UserContexts List userContexts = session.getUserContexts(); @@ -70,11 +71,17 @@ public class ObjectRetrievalService { // Locate and return the UserContext associated with the // AuthenticationProvider having the given identifier, if any for (UserContext userContext : userContexts) { - if (userContext.getAuthenticationProvider().getIdentifier().equals(identifier)) + + // Get AuthenticationProvider associated with current UserContext + AuthenticationProvider authProvider = userContext.getAuthenticationProvider(); + + // If AuthenticationProvider identifier matches, done + if (authProvider.getIdentifier().equals(authProviderIdentifier)) return userContext; + } - throw new GuacamoleResourceNotFoundException("Session not associated with authentication provider \"" + identifier + "\"."); + throw new GuacamoleResourceNotFoundException("Session not associated with authentication provider \"" + authProviderIdentifier + "\"."); } @@ -109,6 +116,35 @@ public class ObjectRetrievalService { } + /** + * Retrieves a single user from the given GuacamoleSession. + * + * @param session + * The GuacamoleSession to retrieve the user from. + * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider that created the + * UserContext from which the user should be retrieved. Only one + * UserContext per AuthenticationProvider can exist. + * + * @param identifier + * The identifier of the user to retrieve. + * + * @return + * The user having the given identifier. + * + * @throws GuacamoleException + * If an error occurs while retrieving the user, or if the + * user does not exist. + */ + public User retrieveUser(GuacamoleSession session, String authProviderIdentifier, + String identifier) throws GuacamoleException { + + UserContext userContext = retrieveUserContext(session, authProviderIdentifier); + return retrieveUser(userContext, identifier); + + } + /** * Retrieves a single connection from the given user context. * @@ -140,6 +176,36 @@ public class ObjectRetrievalService { } + /** + * Retrieves a single connection from the given GuacamoleSession. + * + * @param session + * The GuacamoleSession to retrieve the connection from. + * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider that created the + * UserContext from which the connection should be retrieved. Only one + * UserContext per AuthenticationProvider can exist. + * + * @param identifier + * The identifier of the connection to retrieve. + * + * @return + * The connection having the given identifier. + * + * @throws GuacamoleException + * If an error occurs while retrieving the connection, or if the + * connection does not exist. + */ + public Connection retrieveConnection(GuacamoleSession session, + String authProviderIdentifier, String identifier) + throws GuacamoleException { + + UserContext userContext = retrieveUserContext(session, authProviderIdentifier); + return retrieveConnection(userContext, identifier); + + } + /** * Retrieves a single connection group from the given user context. If * the given identifier the REST API root identifier, the root connection @@ -178,4 +244,37 @@ public class ObjectRetrievalService { } + /** + * Retrieves a single connection group from the given GuacamoleSession. If + * the given identifier the REST API root identifier, the root connection + * group will be returned. The underlying authentication provider may + * additionally use a different identifier for root. + * + * @param session + * The GuacamoleSession to retrieve the connection group from. + * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider that created the + * UserContext from which the connection group should be retrieved. + * Only one UserContext per AuthenticationProvider can exist. + * + * @param identifier + * The identifier of the connection group to retrieve. + * + * @return + * The connection group having the given identifier, or the root + * connection group if the identifier the root identifier. + * + * @throws GuacamoleException + * If an error occurs while retrieving the connection group, or if the + * connection group does not exist. + */ + public ConnectionGroup retrieveConnectionGroup(GuacamoleSession session, + String authProviderIdentifier, String identifier) throws GuacamoleException { + + UserContext userContext = retrieveUserContext(session, authProviderIdentifier); + return retrieveConnectionGroup(userContext, identifier); + + } + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/APIAuthenticationResponse.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/APIAuthenticationResponse.java new file mode 100644 index 000000000..a7c7a73c3 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/APIAuthenticationResponse.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.auth; + +/** + * A simple object to represent an auth token/username pair in the API. + * + * @author James Muehlner + */ +public class APIAuthenticationResponse { + + /** + * The auth token. + */ + private final String authToken; + + + /** + * The username of the user that authenticated. + */ + private final String username; + + /** + * The unique identifier of the data source from which this user account + * came. Although this user account may exist across several data sources + * (AuthenticationProviders), this will be the unique identifier of the + * AuthenticationProvider that authenticated this user for the current + * session. + */ + private final String dataSource; + + /** + * Returns the unique authentication token which identifies the current + * session. + * + * @return + * The user's authentication token. + */ + public String getAuthToken() { + return authToken; + } + + /** + * Returns the user identified by the authentication token associated with + * the current session. + * + * @return + * The user identified by this authentication token. + */ + public String getUsername() { + return username; + } + + /** + * Returns the unique identifier of the data source associated with the user + * account associated with this auth token. + * + * @return + * The unique identifier of the data source associated with the user + * account associated with this auth token. + */ + public String getDataSource() { + return dataSource; + } + + /** + * Create a new APIAuthToken Object with the given auth token. + * + * @param dataSource + * The unique identifier of the AuthenticationProvider which + * authenticated the user. + * + * @param authToken + * The auth token to create the new APIAuthToken with. + * + * @param username + * The username of the user owning the given token. + */ + public APIAuthenticationResponse(String dataSource, String authToken, String username) { + this.dataSource = dataSource; + this.authToken = authToken; + this.username = username; + } + +} From e75ab6ebd50df506415e22a0a972bffdd6e1e45b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 27 Aug 2015 23:23:19 -0700 Subject: [PATCH 02/47] GUAC-586: Add data source to user and permissions services. --- .../net/basic/rest/user/UserRESTService.java | 132 ++++++++++++------ .../app/index/config/indexRouteConfig.js | 2 +- .../controllers/manageUserController.js | 22 ++- .../app/rest/services/permissionService.js | 36 +++-- .../webapp/app/rest/services/userService.js | 56 ++++++-- .../settings/directives/guacSettingsUsers.js | 19 ++- .../app/settings/templates/settingsUsers.html | 2 +- 7 files changed, 196 insertions(+), 73 deletions(-) diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java index 37fda2bff..3f290c5c1 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java @@ -41,6 +41,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; +import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.User; @@ -51,6 +52,7 @@ import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.Permission; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; +import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.APIError; import org.glyptodon.guacamole.net.basic.rest.APIPatch; import static org.glyptodon.guacamole.net.basic.rest.APIPatch.Operation.add; @@ -68,8 +70,9 @@ import org.slf4j.LoggerFactory; * A REST Service for handling user CRUD operations. * * @author James Muehlner + * @author Michael Jumper */ -@Path("/users") +@Path("/data/{dataSource}/users") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class UserRESTService { @@ -120,42 +123,44 @@ public class UserRESTService { */ @Inject private ObjectRetrievalService retrievalService; - + /** - * Gets a list of users in the system, filtering the returned list by the - * given permission, if specified. - * + * Gets a list of users in the given data source (UserContext), filtering + * the returned list by the given permission, if specified. + * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The index of the UserContext within the overall List of available + * UserContexts in which the requested user is to be created. + * * @param permissions * The set of permissions to filter with. A user must have one or more - * of these permissions for a user to appear in the result. + * of these permissions for a user to appear in the result. * If null, no filtering will be performed. - * + * * @return * A list of all visible users. If a permission was specified, this * list will contain only those users for whom the current user has * that permission. - * + * * @throws GuacamoleException * If an error is encountered while retrieving users. */ @GET @AuthProviderRESTExposure public List getUsers(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, @QueryParam("permission") List permissions) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); - User self = userContext.self(); - - // Do not filter on permissions if no permissions are specified - if (permissions != null && permissions.isEmpty()) - permissions = null; + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // An admin user has access to any user + User self = userContext.self(); SystemPermissionSet systemPermissions = self.getSystemPermissions(); boolean isAdmin = systemPermissions.hasPermission(SystemPermission.Type.ADMINISTER); @@ -174,7 +179,6 @@ public class UserRESTService { for (User user : userDirectory.getAll(userIdentifiers)) apiUsers.add(new APIUser(user)); - // Return the converted user list return apiUsers; } @@ -186,6 +190,10 @@ public class UserRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The index of the UserContext within the overall List of available + * UserContexts in which the requested user is to be created. + * * @param username * The username of the user to retrieve. * @@ -198,34 +206,49 @@ public class UserRESTService { @GET @Path("/{username}") @AuthProviderRESTExposure - public APIUser getUser(@QueryParam("token") String authToken, @PathParam("username") String username) + public APIUser getUser(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("username") String username) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); // Retrieve the requested user - User user = retrievalService.retrieveUser(userContext, username); + User user = retrievalService.retrieveUser(session, authProviderIdentifier, username); return new APIUser(user); } /** * Creates a new user and returns the username. - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param user The new user to create. - * @throws GuacamoleException If a problem is encountered while creating the user. - * - * @return The username of the newly created user. + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param authProviderIdentifier + * The index of the UserContext within the overall List of available + * UserContexts in which the requested user is to be created. + * + * @param user + * The new user to create. + * + * @throws GuacamoleException + * If a problem is encountered while creating the user. + * + * @return + * The username of the newly created user. */ @POST @Produces(MediaType.TEXT_PLAIN) @AuthProviderRESTExposure - public String createUser(@QueryParam("token") String authToken, APIUser user) + public String createUser(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, APIUser user) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); - + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); + // Get the directory Directory userDirectory = userContext.getUserDirectory(); @@ -247,6 +270,10 @@ public class UserRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The index of the UserContext within the overall List of available + * UserContexts in which the requested user is to be found. + * * @param username * The username of the user to update. * @@ -260,11 +287,13 @@ public class UserRESTService { @Path("/{username}") @AuthProviderRESTExposure public void updateUser(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username, APIUser user) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); - + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); + // Get the directory Directory userDirectory = userContext.getUserDirectory(); @@ -301,6 +330,10 @@ public class UserRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The index of the UserContext within the overall List of available + * UserContexts in which the requested user is to be found. + * * @param username * The username of the user to update. * @@ -318,12 +351,14 @@ public class UserRESTService { @Path("/{username}/password") @AuthProviderRESTExposure public void updatePassword(@QueryParam("token") String authToken, - @PathParam("username") String username, + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("username") String username, APIUserPasswordUpdate userPasswordUpdate, @Context HttpServletRequest request) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); - + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); + // Build credentials Credentials credentials = new Credentials(); credentials.setUsername(username); @@ -333,7 +368,8 @@ public class UserRESTService { // Verify that the old password was correct try { - if (userContext.getAuthenticationProvider().authenticateUser(credentials) == null) { + AuthenticationProvider authProvider = userContext.getAuthenticationProvider(); + if (authProvider.authenticateUser(credentials) == null) { throw new APIException(APIError.Type.PERMISSION_DENIED, "Permission denied."); } @@ -366,6 +402,10 @@ public class UserRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The index of the UserContext within the overall List of available + * UserContexts in which the requested user is to be found. + * * @param username * The username of the user to delete. * @@ -376,11 +416,13 @@ public class UserRESTService { @Path("/{username}") @AuthProviderRESTExposure public void deleteUser(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); - + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); + // Get the directory Directory userDirectory = userContext.getUserDirectory(); @@ -401,6 +443,10 @@ public class UserRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The index of the UserContext within the overall List of available + * UserContexts in which the requested user is to be found. + * * @param username * The username of the user to retrieve permissions for. * @@ -414,10 +460,12 @@ public class UserRESTService { @Path("/{username}/permissions") @AuthProviderRESTExposure public APIPermissionSet getPermissions(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); User user; @@ -489,7 +537,11 @@ public class UserRESTService { * @param authToken * The authentication token that is used to authenticate the user * performing the operation. - * + * + * @param authProviderIdentifier + * The index of the UserContext within the overall List of available + * UserContexts in which the requested user is to be found. + * * @param username * The username of the user to modify the permissions of. * @@ -503,11 +555,13 @@ public class UserRESTService { @Path("/{username}/permissions") @AuthProviderRESTExposure public void patchPermissions(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username, List> patches) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); - + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); + // Get the user User user = userContext.getUserDirectory().get(username); if (user == null) diff --git a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js index ad350a1ef..e578828b1 100644 --- a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js @@ -150,7 +150,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider', }) // User editor - .when('/manage/users/:id', { + .when('/manage/:dataSource/users/:id', { title : 'APP.NAME', bodyClassName : 'manage', templateUrl : 'app/manage/templates/manageUser.html', diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index 00c09c5e1..cf5625349 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -53,6 +53,14 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto } }; + /** + * The unique identifier of the data source containing the user being + * edited. + * + * @type String + */ + var dataSource = $routeParams.dataSource; + /** * The username of the user being edited. * @@ -137,12 +145,12 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }); // Pull user data - userService.getUser(username).success(function userReceived(user) { + userService.getUser(dataSource, username).success(function userReceived(user) { $scope.user = user; }); // Pull user permissions - permissionService.getPermissions(username).success(function gotPermissions(permissions) { + permissionService.getPermissions(dataSource, username).success(function gotPermissions(permissions) { $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions); }); @@ -152,8 +160,8 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.rootGroup = rootGroup; }); - // Query the user's permissions for the current connection - permissionService.getPermissions(authenticationService.getCurrentUsername()) + // Query the user's permissions for the current user + permissionService.getPermissions(dataSource, authenticationService.getCurrentUsername()) .success(function permissionsReceived(permissions) { $scope.permissions = permissions; @@ -508,11 +516,11 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto } // Save the user - userService.saveUser($scope.user) + userService.saveUser(dataSource, $scope.user) .success(function savedUser() { // Upon success, save any changed permissions - permissionService.patchPermissions($scope.user.username, permissionsAdded, permissionsRemoved) + permissionService.patchPermissions(dataSource, $scope.user.username, permissionsAdded, permissionsRemoved) .success(function patchedUserPermissions() { $location.path('/settings/users'); }) @@ -574,7 +582,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto var deleteUserImmediately = function deleteUserImmediately() { // Delete the user - userService.deleteUser($scope.user) + userService.deleteUser(dataSource, $scope.user) .success(function deletedUser() { $location.path('/settings/users'); }) diff --git a/guacamole/src/main/webapp/app/rest/services/permissionService.js b/guacamole/src/main/webapp/app/rest/services/permissionService.js index c60bfd2dc..ee15804a8 100644 --- a/guacamole/src/main/webapp/app/rest/services/permissionService.js +++ b/guacamole/src/main/webapp/app/rest/services/permissionService.js @@ -41,6 +41,11 @@ angular.module('rest').factory('permissionService', ['$injector', * given user, returning a promise that provides an array of * @link{Permission} objects if successful. * + * @param {String} dataSource + * The unique identifier of the data source containing the user whose + * permissions should be retrieved. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * * @param {String} userID * The ID of the user to retrieve the permissions for. * @@ -48,7 +53,7 @@ angular.module('rest').factory('permissionService', ['$injector', * A promise which will resolve with a @link{PermissionSet} upon * success. */ - service.getPermissions = function getPermissions(userID) { + service.getPermissions = function getPermissions(dataSource, userID) { // Build HTTP parameters set var httpParameters = { @@ -59,7 +64,7 @@ angular.module('rest').factory('permissionService', ['$injector', return $http({ cache : cacheService.users, method : 'GET', - url : 'api/users/' + encodeURIComponent(userID) + '/permissions', + url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(userID) + '/permissions', params : httpParameters }); @@ -70,6 +75,11 @@ angular.module('rest').factory('permissionService', ['$injector', * returning a promise that can be used for processing the results of the * call. * + * @param {String} dataSource + * The unique identifier of the data source containing the user whose + * permissions should be modified. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * * @param {String} userID * The ID of the user to modify the permissions of. * @@ -80,8 +90,8 @@ angular.module('rest').factory('permissionService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * add operation is successful. */ - service.addPermissions = function addPermissions(userID, permissions) { - return service.patchPermissions(userID, permissions, null); + service.addPermissions = function addPermissions(dataSource, userID, permissions) { + return service.patchPermissions(dataSource, userID, permissions, null); }; /** @@ -89,6 +99,11 @@ angular.module('rest').factory('permissionService', ['$injector', * returning a promise that can be used for processing the results of the * call. * + * @param {String} dataSource + * The unique identifier of the data source containing the user whose + * permissions should be modified. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * * @param {String} userID * The ID of the user to modify the permissions of. * @@ -99,8 +114,8 @@ angular.module('rest').factory('permissionService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * remove operation is successful. */ - service.removePermissions = function removePermissions(userID, permissions) { - return service.patchPermissions(userID, null, permissions); + service.removePermissions = function removePermissions(dataSource, userID, permissions) { + return service.patchPermissions(dataSource, userID, null, permissions); }; /** @@ -186,6 +201,11 @@ angular.module('rest').factory('permissionService', ['$injector', * user, returning a promise that can be used for processing the results of * the call. * + * @param {String} dataSource + * The unique identifier of the data source containing the user whose + * permissions should be modified. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * * @param {String} userID * The ID of the user to modify the permissions of. * @@ -199,7 +219,7 @@ angular.module('rest').factory('permissionService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * patch operation is successful. */ - service.patchPermissions = function patchPermissions(userID, permissionsToAdd, permissionsToRemove) { + service.patchPermissions = function patchPermissions(dataSource, userID, permissionsToAdd, permissionsToRemove) { var permissionPatch = []; @@ -217,7 +237,7 @@ angular.module('rest').factory('permissionService', ['$injector', // Patch user permissions return $http({ method : 'PATCH', - url : 'api/users/' + encodeURIComponent(userID) + '/permissions', + url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(userID) + '/permissions', params : httpParameters, data : permissionPatch }) diff --git a/guacamole/src/main/webapp/app/rest/services/userService.js b/guacamole/src/main/webapp/app/rest/services/userService.js index a81916e19..55eab2ed3 100644 --- a/guacamole/src/main/webapp/app/rest/services/userService.js +++ b/guacamole/src/main/webapp/app/rest/services/userService.js @@ -41,6 +41,11 @@ angular.module('rest').factory('userService', ['$injector', * returning a promise that provides an array of @link{User} objects if * successful. * + * @param {String} dataSource + * The unique identifier of the data source containing the users to be + * retrieved. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * * @param {String[]} [permissionTypes] * The set of permissions to filter with. A user must have one or more * of these permissions for a user to appear in the result. @@ -51,7 +56,7 @@ angular.module('rest').factory('userService', ['$injector', * A promise which will resolve with an array of @link{User} objects * upon success. */ - service.getUsers = function getUsers(permissionTypes) { + service.getUsers = function getUsers(dataSource, permissionTypes) { // Build HTTP parameters set var httpParameters = { @@ -66,7 +71,7 @@ angular.module('rest').factory('userService', ['$injector', return $http({ cache : cacheService.users, method : 'GET', - url : 'api/users', + url : 'api/data/' + encodeURIComponent(dataSource) + '/users', params : httpParameters }); @@ -76,14 +81,19 @@ angular.module('rest').factory('userService', ['$injector', * Makes a request to the REST API to get the user having the given * username, returning a promise that provides the corresponding * @link{User} if successful. - * + * + * @param {String} dataSource + * The unique identifier of the data source containing the user to be + * retrieved. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * * @param {String} username * The username of the user to retrieve. * * @returns {Promise.} * A promise which will resolve with a @link{User} upon success. */ - service.getUser = function getUser(username) { + service.getUser = function getUser(dataSource, username) { // Build HTTP parameters set var httpParameters = { @@ -94,7 +104,7 @@ angular.module('rest').factory('userService', ['$injector', return $http({ cache : cacheService.users, method : 'GET', - url : 'api/users/' + encodeURIComponent(username), + url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username), params : httpParameters }); @@ -104,6 +114,11 @@ angular.module('rest').factory('userService', ['$injector', * Makes a request to the REST API to delete a user, returning a promise * that can be used for processing the results of the call. * + * @param {String} dataSource + * The unique identifier of the data source containing the user to be + * deleted. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * * @param {User} user * The user to delete. * @@ -111,7 +126,7 @@ angular.module('rest').factory('userService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * delete operation is successful. */ - service.deleteUser = function deleteUser(user) { + service.deleteUser = function deleteUser(dataSource, user) { // Build HTTP parameters set var httpParameters = { @@ -121,7 +136,7 @@ angular.module('rest').factory('userService', ['$injector', // Delete user return $http({ method : 'DELETE', - url : 'api/users/' + encodeURIComponent(user.username), + url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(user.username), params : httpParameters }) @@ -137,6 +152,11 @@ angular.module('rest').factory('userService', ['$injector', * Makes a request to the REST API to create a user, returning a promise * that can be used for processing the results of the call. * + * @param {String} dataSource + * The unique identifier of the data source in which the user should be + * created. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * * @param {User} user * The user to create. * @@ -144,7 +164,7 @@ angular.module('rest').factory('userService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * create operation is successful. */ - service.createUser = function createUser(user) { + service.createUser = function createUser(dataSource, user) { // Build HTTP parameters set var httpParameters = { @@ -154,7 +174,7 @@ angular.module('rest').factory('userService', ['$injector', // Create user return $http({ method : 'POST', - url : 'api/users', + url : 'api/data/' + encodeURIComponent(dataSource) + '/users', params : httpParameters, data : user }) @@ -170,6 +190,11 @@ angular.module('rest').factory('userService', ['$injector', * Makes a request to the REST API to save a user, returning a promise that * can be used for processing the results of the call. * + * @param {String} dataSource + * The unique identifier of the data source containing the user to be + * updated. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * * @param {User} user * The user to update. * @@ -177,7 +202,7 @@ angular.module('rest').factory('userService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * save operation is successful. */ - service.saveUser = function saveUser(user) { + service.saveUser = function saveUser(dataSource, user) { // Build HTTP parameters set var httpParameters = { @@ -187,7 +212,7 @@ angular.module('rest').factory('userService', ['$injector', // Update user return $http({ method : 'PUT', - url : 'api/users/' + encodeURIComponent(user.username), + url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(user.username), params : httpParameters, data : user }) @@ -203,6 +228,11 @@ angular.module('rest').factory('userService', ['$injector', * Makes a request to the REST API to update the password for a user, * returning a promise that can be used for processing the results of the call. * + * @param {String} dataSource + * The unique identifier of the data source containing the user to be + * updated. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * * @param {String} username * The username of the user to update. * @@ -216,7 +246,7 @@ angular.module('rest').factory('userService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * password update operation is successful. */ - service.updateUserPassword = function updateUserPassword(username, + service.updateUserPassword = function updateUserPassword(dataSource, username, oldPassword, newPassword) { // Build HTTP parameters set @@ -227,7 +257,7 @@ angular.module('rest').factory('userService', ['$injector', // Update user password return $http({ method : 'PUT', - url : 'api/users/' + encodeURIComponent(username) + '/password', + url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username) + '/password', params : httpParameters, data : new UserPasswordUpdate({ oldPassword : oldPassword, diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js index 1d0f411fb..420409196 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js @@ -62,6 +62,15 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings } }; + /** + * The data source from which the list of users should be pulled. + * For the time being, this is just the data source which + * authenticated the current user. + * + * @type String + */ + $scope.dataSource = authenticationService.getDataSource(); + /** * All visible users. * @@ -118,7 +127,7 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings }; // Retrieve current permissions - permissionService.getPermissions(currentUsername) + permissionService.getPermissions($scope.dataSource, currentUsername) .success(function permissionsRetrieved(permissions) { $scope.permissions = permissions; @@ -141,8 +150,10 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings }); // Retrieve all users for whom we have UPDATE or DELETE permission - userService.getUsers([PermissionSet.ObjectPermissionType.UPDATE, - PermissionSet.ObjectPermissionType.DELETE]) + userService.getUsers($scope.dataSource, [ + PermissionSet.ObjectPermissionType.UPDATE, + PermissionSet.ObjectPermissionType.DELETE + ]) .success(function usersReceived(users) { // Display only other users, not self @@ -164,7 +175,7 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings }); // Create specified user - userService.createUser(user) + userService.createUser($scope.dataSource, user) // Add user to visible list upon success .success(function userCreated() { diff --git a/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html b/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html index 79290cb07..430f57912 100644 --- a/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html +++ b/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html @@ -33,7 +33,7 @@
- +
{{user.username}} From f892446e03fe9b70bc896d843d069ae1311021fd Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 27 Aug 2015 23:50:16 -0700 Subject: [PATCH 03/47] GUAC-586: Only provide password change interface for the data source that authenticated the current user. --- .../settings/directives/guacSettingsPreferences.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js index 30a52e265..f13c04baa 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js @@ -66,6 +66,14 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe */ var username = authenticationService.getCurrentUsername(); + /** + * The identifier of the data source which authenticated the + * current user. + * + * @type String + */ + var dataSource = authenticationService.getDataSource(); + /** * All currently-set preferences, or their defaults if not yet set. * @@ -140,7 +148,7 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe } // Save the user with the new password - userService.updateUserPassword(username, $scope.oldPassword, $scope.newPassword) + userService.updateUserPassword(dataSource, username, $scope.oldPassword, $scope.newPassword) .success(function passwordUpdated() { // Clear the password fields @@ -174,7 +182,7 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe }); // Retrieve current permissions - permissionService.getPermissions(username) + permissionService.getPermissions(dataSource, username) .success(function permissionsRetrieved(permissions) { // Add action for changing password if permission is granted From a3f8888a27f50ab87e62044bf42aef80f7007b09 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 27 Aug 2015 23:55:42 -0700 Subject: [PATCH 04/47] GUAC-586: Remove unused services from active sessions page. --- .../directives/guacSettingsSessions.js | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js index 3635d38cb..1c5e0fb0e 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js @@ -45,18 +45,8 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti var $filter = $injector.get('$filter'); var $translate = $injector.get('$translate'); var activeConnectionService = $injector.get('activeConnectionService'); - var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); var guacNotification = $injector.get('guacNotification'); - var permissionService = $injector.get('permissionService'); - - /** - * All permissions associated with the current user, or null if the - * user's permissions have not yet been loaded. - * - * @type PermissionSet - */ - $scope.permissions = null; /** * The ActiveConnectionWrappers of all active sessions accessible @@ -186,12 +176,6 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti }; - // Query the user's permissions - permissionService.getPermissions(authenticationService.getCurrentUsername()) - .success(function permissionsReceived(retrievedPermissions) { - $scope.permissions = retrievedPermissions; - }); - // Retrieve all connections connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER) .success(function connectionGroupReceived(retrievedRootGroup) { @@ -237,8 +221,7 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti $scope.isLoaded = function isLoaded() { return $scope.wrappers !== null - && $scope.sessionDateFormat !== null - && $scope.permissions !== null; + && $scope.sessionDateFormat !== null; }; From d6139bb02e65e055b37080d3e79eb15a57c44510 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 28 Aug 2015 14:17:55 -0700 Subject: [PATCH 05/47] GUAC-586: Fix getAvailableDataSources(). --- .../main/webapp/app/auth/service/authenticationService.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/guacamole/src/main/webapp/app/auth/service/authenticationService.js b/guacamole/src/main/webapp/app/auth/service/authenticationService.js index 8c17cbea2..6493cc2a5 100644 --- a/guacamole/src/main/webapp/app/auth/service/authenticationService.js +++ b/guacamole/src/main/webapp/app/auth/service/authenticationService.js @@ -99,9 +99,10 @@ angular.module('auth').factory('authenticationService', ['$injector', // Store auth data $cookieStore.put(AUTH_COOKIE_ID, { - authToken : data.authToken, - username : data.username, - dataSource : data.dataSource + 'authToken' : data.authToken, + 'username' : data.username, + 'dataSource' : data.dataSource, + 'availableDataSources' : data.availableDataSources }); // Process is complete From f55f388667315a7c3be406811ff75559b52909ac Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 28 Aug 2015 14:49:44 -0700 Subject: [PATCH 06/47] GUAC-586: Add multi-source retrieval of permissions to permissionService. Use multiple sources to determine user pages. --- .../navigation/services/userPageService.js | 115 +++++++++++------- .../app/rest/services/permissionService.js | 60 ++++++++- 2 files changed, 129 insertions(+), 46 deletions(-) diff --git a/guacamole/src/main/webapp/app/navigation/services/userPageService.js b/guacamole/src/main/webapp/app/navigation/services/userPageService.js index 9489581b0..b64556bf3 100644 --- a/guacamole/src/main/webapp/app/navigation/services/userPageService.js +++ b/guacamole/src/main/webapp/app/navigation/services/userPageService.js @@ -140,64 +140,79 @@ angular.module('navigation').factory('userPageService', ['$injector', * Returns all settings pages that the current user can visit. This can * include any of the various manage pages. * - * @param {PermissionSet} permissions - * The permissions for the current user. + * @param {Object.} permissionSets + * A map of all permissions granted to the current user, where each + * key is the identifier of the corresponding data source. * * @returns {Page[]} * An array of all settings pages that the current user can visit. */ - var generateSettingsPages = function generateSettingsPages(permissions) { + var generateSettingsPages = function generateSettingsPages(permissionSets) { var pages = []; - permissions = angular.copy(permissions); + var canManageUsers = false; + var canManageConnections = false; + var canManageSessions = false; - // Ignore permission to update root group - PermissionSet.removeConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER); + // Inspect the contents of each provided permission set + angular.forEach(permissionSets, function inspectPermissions(permissions) { - // Ignore permission to update self - PermissionSet.removeUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, authenticationService.getCurrentUsername()); + permissions = angular.copy(permissions); - // Determine whether the current user needs access to the user management UI - var canManageUsers = + // Ignore permission to update root group + PermissionSet.removeConnectionGroupPermission(permissions, + PermissionSet.ObjectPermissionType.UPDATE, + ConnectionGroup.ROOT_IDENTIFIER); - // System permissions - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER) + // Ignore permission to update self + PermissionSet.removeUserPermission(permissions, + PermissionSet.ObjectPermissionType.UPDATE, + authenticationService.getCurrentUsername()); - // Permission to update users - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) + // Determine whether the current user needs access to the user management UI + canManageUsers = canManageUsers || - // Permission to delete users - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) + // System permissions + PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER) - // Permission to administer users - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER); + // Permission to update users + || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) - // Determine whether the current user needs access to the connection management UI - var canManageConnections = + // Permission to delete users + || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) - // System permissions - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP) + // Permission to administer users + || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER); - // Permission to update connections or connection groups - || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) - || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) + // Determine whether the current user needs access to the connection management UI + canManageConnections = canManageConnections || - // Permission to delete connections or connection groups - || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) - || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) + // System permissions + PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION) + || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP) - // Permission to administer connections or connection groups - || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) - || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER); + // Permission to update connections or connection groups + || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) + || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) - var canManageSessions = + // Permission to delete connections or connection groups + || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) + || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) - // A user must be a system administrator to manage sessions - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER); + // Permission to administer connections or connection groups + || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) + || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER); + + // Determine whether the current user needs access to the session management UI + canManageSessions = canManageSessions || + + // A user must be a system administrator to manage sessions + PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER); + + }); // If user can manage sessions, add link to sessions management page if (canManageSessions) { @@ -245,10 +260,14 @@ angular.module('navigation').factory('userPageService', ['$injector', var deferred = $q.defer(); - // Retrieve current permissions, resolving main pages if possible + // Retrieve current permissions + permissionService.getAllPermissions( + authenticationService.getAvailableDataSources(), + authenticationService.getCurrentUsername() + ) + // Resolve promise using settings pages derived from permissions - permissionService.getPermissions(authenticationService.getCurrentUsername()) - .success(function permissionsRetrieved(permissions) { + .then(function permissionsRetrieved(permissions) { deferred.resolve(generateSettingsPages(permissions)); }); @@ -264,8 +283,9 @@ angular.module('navigation').factory('userPageService', ['$injector', * @param {ConnectionGroup} rootGroup * The root of the connection group tree for the current user. * - * @param {PermissionSet} permissions - * The permissions for the current user. + * @param {Object.} permissions + * A map of all permissions granted to the current user, where each + * key is the identifier of the corresponding data source. * * @returns {Page[]} * An array of all main pages that the current user can visit. @@ -327,9 +347,14 @@ angular.module('navigation').factory('userPageService', ['$injector', resolveMainPages(); }); - // Retrieve current permissions, resolving main pages if possible - permissionService.getPermissions(authenticationService.getCurrentUsername()) - .success(function permissionsRetrieved(retrievedPermissions) { + // Retrieve current permissions + permissionService.getAllPermissions( + authenticationService.getAvailableDataSources(), + authenticationService.getCurrentUsername() + ) + + // Resolving main pages if possible + .then(function permissionsRetrieved(retrievedPermissions) { permissions = retrievedPermissions; resolveMainPages(); }); diff --git a/guacamole/src/main/webapp/app/rest/services/permissionService.js b/guacamole/src/main/webapp/app/rest/services/permissionService.js index ee15804a8..ca9a2ef89 100644 --- a/guacamole/src/main/webapp/app/rest/services/permissionService.js +++ b/guacamole/src/main/webapp/app/rest/services/permissionService.js @@ -28,6 +28,7 @@ angular.module('rest').factory('permissionService', ['$injector', // Required services var $http = $injector.get('$http'); + var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); @@ -69,7 +70,64 @@ angular.module('rest').factory('permissionService', ['$injector', }); }; - + + /** + * Returns a promise which resolves with all permissions available to the + * given user, as a map of all PermissionSet objects by the identifier of + * their corresponding data source. All given data sources are queried. If + * an error occurs while retrieving any PermissionSet, the promise will be + * rejected. + * + * @param {String[]} dataSources + * The unique identifier of the data sources containing the user whose + * permissions should be retrieved. These identifiers corresponds to + * AuthenticationProviders within the Guacamole web application. + * + * @param {String} username + * The username of the user to retrieve the permissions for. + * + * @returns {Promise.>} + * A promise which resolves with all permissions available to the + * current user, as a map of app PermissionSet objects by the + * identifier of their corresponding data source. + */ + service.getAllPermissions = function getAllPermissions(dataSources, username) { + + var deferred = $q.defer(); + + var permissionSetRequests = []; + var permissionSets = {}; + + // Retrieve all permissions from all data sources + angular.forEach(dataSources, function retrievePermissions(dataSource) { + permissionSetRequests.push( + service.getPermissions(dataSource, username) + .success(function permissionsRetrieved(permissions) { + permissionSets[dataSource] = permissions; + }) + ); + }); + + // Resolve when all requests are completed + $q.all(permissionSetRequests) + .then( + + // All requests completed successfully + function allPermissionsRetrieved() { + deferred.resolve(permissionSets); + }, + + // At least one request failed + function permissionRetrievalFailed(e) { + deferred.reject(e); + } + + ); + + return deferred.promise; + + }; + /** * Makes a request to the REST API to add permissions for a given user, * returning a promise that can be used for processing the results of the From 28092b9b23a5091e9d34dec9428a2b0271621528 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 28 Aug 2015 16:33:55 -0700 Subject: [PATCH 07/47] GUAC-586: Make PageDefinition class public. --- .../navigation/services/userPageService.js | 32 +++++----- .../app/navigation/types/PageDefinition.js | 59 +++++++++++++++++++ 2 files changed, 74 insertions(+), 17 deletions(-) create mode 100644 guacamole/src/main/webapp/app/navigation/types/PageDefinition.js diff --git a/guacamole/src/main/webapp/app/navigation/services/userPageService.js b/guacamole/src/main/webapp/app/navigation/services/userPageService.js index b64556bf3..e9239d796 100644 --- a/guacamole/src/main/webapp/app/navigation/services/userPageService.js +++ b/guacamole/src/main/webapp/app/navigation/services/userPageService.js @@ -28,6 +28,7 @@ angular.module('navigation').factory('userPageService', ['$injector', // Get required types var ConnectionGroup = $injector.get('ConnectionGroup'); + var PageDefinition = $injector.get('PageDefinition'); var PermissionSet = $injector.get('PermissionSet'); // Get required services @@ -39,19 +40,16 @@ angular.module('navigation').factory('userPageService', ['$injector', var service = {}; /** - * Construct a new Page object with the given name and url. + * Construct a new PageDefinition object with the given name and url. + * * @constructor - * * @param {String} name * The i18n key for the name of the page. * * @param {String} url - * The url to the page. - * - * @returns {PageDefinition} - * The newly created PageDefinition object. + * The URL of the page. */ - var Page = function Page(name, url) { + var PageDefinition = function PageDefinition(name, url) { this.name = name; this.url = url; }; @@ -60,9 +58,9 @@ angular.module('navigation').factory('userPageService', ['$injector', * The home page to assign to a user if they can navigate to more than one * page. * - * @type Page + * @type PageDefinition */ - var SYSTEM_HOME_PAGE = new Page( + var SYSTEM_HOME_PAGE = new PageDefinition( 'USER_MENU.ACTION_NAVIGATE_HOME', '/' ); @@ -73,7 +71,7 @@ angular.module('navigation').factory('userPageService', ['$injector', * @param {ConnectionGroup} rootGroup * The root of the connection group tree for the current user. * - * @returns {Page} + * @returns {PageDefinition} * The user's home page. */ var generateHomePage = function generateHomePage(rootGroup) { @@ -91,7 +89,7 @@ angular.module('navigation').factory('userPageService', ['$injector', // Only one connection present, use as home page if (connection) { - return new Page( + return new PageDefinition( connection.name, '/client/c/' + connection.identifier ); @@ -102,7 +100,7 @@ angular.module('navigation').factory('userPageService', ['$injector', && connectionGroup.type === ConnectionGroup.Type.BALANCING && _.isEmpty(connectionGroup.childConnections) && _.isEmpty(connectionGroup.childConnectionGroups)) { - return new Page( + return new PageDefinition( connectionGroup.name, '/client/g/' + connectionGroup.identifier ); @@ -216,7 +214,7 @@ angular.module('navigation').factory('userPageService', ['$injector', // If user can manage sessions, add link to sessions management page if (canManageSessions) { - pages.push(new Page( + pages.push(new PageDefinition( 'USER_MENU.ACTION_MANAGE_SESSIONS', '/settings/sessions' )); @@ -224,7 +222,7 @@ angular.module('navigation').factory('userPageService', ['$injector', // If user can manage users, add link to user management page if (canManageUsers) { - pages.push(new Page( + pages.push(new PageDefinition( 'USER_MENU.ACTION_MANAGE_USERS', '/settings/users' )); @@ -232,14 +230,14 @@ angular.module('navigation').factory('userPageService', ['$injector', // If user can manage connections, add link to connections management page if (canManageConnections) { - pages.push(new Page( + pages.push(new PageDefinition( 'USER_MENU.ACTION_MANAGE_CONNECTIONS', '/settings/connections' )); } // Add link to user preferences (always accessible) - pages.push(new Page( + pages.push(new PageDefinition( 'USER_MENU.ACTION_MANAGE_PREFERENCES', '/settings/preferences' )); @@ -305,7 +303,7 @@ angular.module('navigation').factory('userPageService', ['$injector', // Add generic link to the first-available settings page if (settingsPages.length) { - pages.push(new Page( + pages.push(new PageDefinition( 'USER_MENU.ACTION_MANAGE_SETTINGS', settingsPages[0].url )); diff --git a/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js b/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js new file mode 100644 index 000000000..6b58849fd --- /dev/null +++ b/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Provides the PageDefinition class definition. + */ +angular.module('navigation').factory('PageDefinition', [function definePageDefinition() { + + /** + * Creates a new PageDefinition object which pairs the URL of a page with + * an arbitrary, human-readable name. + * + * @constructor + * @param {String} name + * The the name of the page, which should be a translation table key. + * + * @param {String} url + * The URL of the page. + */ + var PageDefinition = function PageDefinition(name, url) { + + /** + * The the name of the page, which should be a translation table key. + * + * @type String + */ + this.name = name; + + /** + * The URL of the page. + * + * @type String + */ + this.url = url; + + }; + + return PageDefinition; + +}]); From 40ca19fb3a092e4b5433a05fd64170dcdc4c5955 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 28 Aug 2015 16:34:57 -0700 Subject: [PATCH 08/47] GUAC-586: Dynamic user page URI components should be encoded. --- .../main/webapp/app/navigation/services/userPageService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/webapp/app/navigation/services/userPageService.js b/guacamole/src/main/webapp/app/navigation/services/userPageService.js index e9239d796..daefb4707 100644 --- a/guacamole/src/main/webapp/app/navigation/services/userPageService.js +++ b/guacamole/src/main/webapp/app/navigation/services/userPageService.js @@ -91,7 +91,7 @@ angular.module('navigation').factory('userPageService', ['$injector', if (connection) { return new PageDefinition( connection.name, - '/client/c/' + connection.identifier + '/client/c/' + encodeURIComponent(connection.identifier) ); } @@ -102,7 +102,7 @@ angular.module('navigation').factory('userPageService', ['$injector', && _.isEmpty(connectionGroup.childConnectionGroups)) { return new PageDefinition( connectionGroup.name, - '/client/g/' + connectionGroup.identifier + '/client/g/' + encodeURIComponent(connectionGroup.identifier) ); } From 70485286d677cd039617b5fb7a59ee6821cbd4ce Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 28 Aug 2015 16:50:53 -0700 Subject: [PATCH 09/47] GUAC-586: Add localized data source names. Display data sources as tabs within user edit screen. --- .../src/main/resources/translations/en.json | 8 ++++ .../src/main/resources/guac-manifest.json | 4 ++ .../src/main/resources/translations/en.json | 7 ++++ .../src/main/resources/guac-manifest.json | 4 ++ .../src/main/resources/translations/en.json | 7 ++++ .../controllers/manageUserController.js | 40 +++++++++++++++---- .../webapp/app/manage/styles/manage-user.css | 25 ++++++++++++ .../app/manage/templates/manageUser.html | 19 ++++----- .../src/main/webapp/translations/en.json | 4 ++ 9 files changed, 99 insertions(+), 19 deletions(-) create mode 100644 extensions/guacamole-auth-ldap/src/main/resources/translations/en.json create mode 100644 extensions/guacamole-auth-noauth/src/main/resources/translations/en.json create mode 100644 guacamole/src/main/webapp/app/manage/styles/manage-user.css diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/en.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/en.json index 1b631c993..189018b88 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/en.json +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/en.json @@ -33,6 +33,14 @@ }, + "DATA_SOURCE_MYSQL" : { + "NAME" : "MySQL" + }, + + "DATA_SOURCE_POSTGRESQL" : { + "NAME" : "PostgreSQL" + }, + "USER_ATTRIBUTES" : { "FIELD_HEADER_DISABLED" : "Login disabled:", diff --git a/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json index c1bd1af80..5aed8a65c 100644 --- a/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json @@ -7,6 +7,10 @@ "authProviders" : [ "net.sourceforge.guacamole.net.auth.ldap.LDAPAuthenticationProvider" + ], + + "translations" : [ + "translations/en.json" ] } diff --git a/extensions/guacamole-auth-ldap/src/main/resources/translations/en.json b/extensions/guacamole-auth-ldap/src/main/resources/translations/en.json new file mode 100644 index 000000000..a1d6ae949 --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/resources/translations/en.json @@ -0,0 +1,7 @@ +{ + + "DATA_SOURCE_LDAP" : { + "NAME" : "LDAP" + } + +} diff --git a/extensions/guacamole-auth-noauth/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-noauth/src/main/resources/guac-manifest.json index 0a7b4478e..565455432 100644 --- a/extensions/guacamole-auth-noauth/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-auth-noauth/src/main/resources/guac-manifest.json @@ -7,6 +7,10 @@ "authProviders" : [ "net.sourceforge.guacamole.net.auth.noauth.NoAuthenticationProvider" + ], + + "translations" : [ + "translations/en.json" ] } diff --git a/extensions/guacamole-auth-noauth/src/main/resources/translations/en.json b/extensions/guacamole-auth-noauth/src/main/resources/translations/en.json new file mode 100644 index 000000000..f755bd7b9 --- /dev/null +++ b/extensions/guacamole-auth-noauth/src/main/resources/translations/en.json @@ -0,0 +1,7 @@ +{ + + "DATA_SOURCE_NOAUTH" : { + "NAME" : "NoAuth" + } + +} diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index cf5625349..e493164ab 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -28,18 +28,20 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto // Required types var ConnectionGroup = $injector.get('ConnectionGroup'); + var PageDefinition = $injector.get('PageDefinition'); var PermissionFlagSet = $injector.get('PermissionFlagSet'); var PermissionSet = $injector.get('PermissionSet'); // Required services - var $location = $injector.get('$location'); - var $routeParams = $injector.get('$routeParams'); - var authenticationService = $injector.get('authenticationService'); - var connectionGroupService = $injector.get('connectionGroupService'); - var guacNotification = $injector.get('guacNotification'); - var permissionService = $injector.get('permissionService'); - var schemaService = $injector.get('schemaService'); - var userService = $injector.get('userService'); + var $location = $injector.get('$location'); + var $routeParams = $injector.get('$routeParams'); + var authenticationService = $injector.get('authenticationService'); + var connectionGroupService = $injector.get('connectionGroupService'); + var guacNotification = $injector.get('guacNotification'); + var permissionService = $injector.get('permissionService'); + var schemaService = $injector.get('schemaService'); + var translationStringService = $injector.get('translationStringService'); + var userService = $injector.get('userService'); /** * An action to be provided along with the object sent to showStatus which @@ -120,6 +122,28 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto */ $scope.attributes = null; + /** + * The pages associated with each user account having the given username. + * Each user account will be associated with a particular data source. + * + * @type PageDefinition[] + */ + $scope.accountPages = (function getAccountPages(dataSources) { + + var accountPages = []; + + // Add an account page for each applicable data source + angular.forEach(dataSources, function addAccountPage(dataSource) { + accountPages.push(new PageDefinition( + translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', + '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username) + )); + }); + + return accountPages; + + })(authenticationService.getAvailableDataSources()); + /** * Returns whether critical data has completed being loaded. * diff --git a/guacamole/src/main/webapp/app/manage/styles/manage-user.css b/guacamole/src/main/webapp/app/manage/styles/manage-user.css new file mode 100644 index 000000000..540689fa6 --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/styles/manage-user.css @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +.manage-user .username.header { + margin-bottom: 0; +} diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index 4e0903f2d..909ea4f3c 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -1,5 +1,5 @@ -
+
-
-

{{'MANAGE_USER.SECTION_HEADER_EDIT_USER' | translate}}

+
+

{{user.username}}

+
+ +
+
- - - - - - -
{{'MANAGE_USER.FIELD_HEADER_USERNAME' | translate}}{{user.username}}
{{'MANAGE_USER.FIELD_HEADER_PASSWORD' | translate}}
{{'MANAGE_USER.FIELD_HEADER_PASSWORD_AGAIN' | translate}}
diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json index 8e24f5269..2d968a1f6 100644 --- a/guacamole/src/main/webapp/translations/en.json +++ b/guacamole/src/main/webapp/translations/en.json @@ -125,6 +125,10 @@ }, + "DATA_SOURCE_DEFAULT" : { + "NAME" : "Default (XML)" + }, + "FORM" : { "FIELD_PLACEHOLDER_DATE" : "YYYY-MM-DD", From 6f0079d7788ba9df2ae200272c47cac996928bc3 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 28 Aug 2015 17:14:51 -0700 Subject: [PATCH 10/47] GUAC-586: Only show available accounts if there are more than one. --- .../app/manage/controllers/manageUserController.js | 11 +++++++++++ .../main/webapp/app/manage/templates/manageUser.html | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index e493164ab..a31a5c707 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -144,6 +144,17 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto })(authenticationService.getAvailableDataSources()); + /** + * Returns whether the list of all available account tabs should be shown. + * + * @returns {Boolean} + * true if the list of available account tabs should be shown, false + * otherwise. + */ + $scope.showAccountTabs = function showAccountTabs() { + return !!$scope.accountPages && $scope.accountPages.length > 1; + }; + /** * Returns whether critical data has completed being loaded. * diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index 909ea4f3c..b0b84f496 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -28,7 +28,7 @@ THE SOFTWARE.
- +
From cff2b7a85746a7932cba5c4b43ae31ce61d92c06 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 28 Aug 2015 17:48:23 -0700 Subject: [PATCH 11/47] GUAC-586: Associate CSS class names with page definitions. --- .../app/navigation/templates/guacPageList.html | 2 +- .../webapp/app/navigation/types/PageDefinition.js | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html b/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html index d303c8efc..35d12c31e 100644 --- a/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html +++ b/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html @@ -22,7 +22,7 @@ --> -
  • +
  • {{page.name | translate}} diff --git a/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js b/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js index 6b58849fd..0b5d00b1e 100644 --- a/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js +++ b/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js @@ -35,8 +35,11 @@ angular.module('navigation').factory('PageDefinition', [function definePageDefin * * @param {String} url * The URL of the page. + * + * @param {String} [className=''] + * The CSS class name to associate with this page, if any. */ - var PageDefinition = function PageDefinition(name, url) { + var PageDefinition = function PageDefinition(name, url, className) { /** * The the name of the page, which should be a translation table key. @@ -52,6 +55,13 @@ angular.module('navigation').factory('PageDefinition', [function definePageDefin */ this.url = url; + /** + * The CSS class name to associate with this page, if any. + * + * @type String + */ + this.className = className || ''; + }; return PageDefinition; From 83318d9c685a08c1b1ba2a10eeacb15caab96f41 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 28 Aug 2015 22:57:57 -0700 Subject: [PATCH 12/47] GUAC-586: Ensure model is set prior to region in time zone field. --- .../app/form/controllers/timeZoneFieldController.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/guacamole/src/main/webapp/app/form/controllers/timeZoneFieldController.js b/guacamole/src/main/webapp/app/form/controllers/timeZoneFieldController.js index 85483b195..0410567ee 100644 --- a/guacamole/src/main/webapp/app/form/controllers/timeZoneFieldController.js +++ b/guacamole/src/main/webapp/app/form/controllers/timeZoneFieldController.js @@ -697,15 +697,15 @@ angular.module('form').controller('timeZoneFieldController', ['$scope', '$inject */ $scope.region = ''; - // Restore time zone selection when region changes - $scope.$watch('region', function restoreSelection(region) { - $scope.model = selectedTimeZone[region] || null; - }); - // Ensure corresponding region is selected $scope.$watch('model', function setModel(model) { $scope.region = timeZoneRegions[model] || ''; selectedTimeZone[$scope.region] = model; }); + // Restore time zone selection when region changes + $scope.$watch('region', function restoreSelection(region) { + $scope.model = selectedTimeZone[region] || null; + }); + }]); From 6cbe8be35469c25107e6f18b0e082a2151b26055 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 28 Aug 2015 23:04:27 -0700 Subject: [PATCH 13/47] GUAC-586: Username header should respect case. --- guacamole/src/main/webapp/app/manage/styles/manage-user.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/guacamole/src/main/webapp/app/manage/styles/manage-user.css b/guacamole/src/main/webapp/app/manage/styles/manage-user.css index 540689fa6..48a99a4c6 100644 --- a/guacamole/src/main/webapp/app/manage/styles/manage-user.css +++ b/guacamole/src/main/webapp/app/manage/styles/manage-user.css @@ -23,3 +23,7 @@ .manage-user .username.header { margin-bottom: 0; } + +.manage-user .username.header h2 { + text-transform: none; +} From 379229dee5514a6f4e782dc109d637084af4637f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 29 Aug 2015 22:36:01 -0700 Subject: [PATCH 14/47] GUAC-586: Set attributes during object creation. --- .../guacamole/auth/jdbc/connection/ConnectionService.java | 1 + .../auth/jdbc/connectiongroup/ConnectionGroupService.java | 1 + .../java/org/glyptodon/guacamole/auth/jdbc/user/UserService.java | 1 + 3 files changed, 3 insertions(+) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionService.java index 53c2d3b5d..c15e5e067 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionService.java @@ -123,6 +123,7 @@ public class ConnectionService extends ModeledGroupedDirectoryObjectService Date: Sat, 29 Aug 2015 23:27:29 -0700 Subject: [PATCH 15/47] GUAC-586: Only use cached configurations within SimpleAuthenticationProvider if truly from same instance. --- .../guacamole/net/auth/simple/SimpleAuthenticationProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleAuthenticationProvider.java b/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleAuthenticationProvider.java index efc804c14..3ee2342dc 100644 --- a/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleAuthenticationProvider.java +++ b/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleAuthenticationProvider.java @@ -202,7 +202,7 @@ public abstract class SimpleAuthenticationProvider throws GuacamoleException { // Pull cached configurations, if any - if (authenticatedUser instanceof SimpleAuthenticatedUser) + if (authenticatedUser instanceof SimpleAuthenticatedUser && authenticatedUser.getAuthenticationProvider() == this) return ((SimpleAuthenticatedUser) authenticatedUser).getAuthorizedConfigurations(); // Otherwise, pull using credentials From b6607ac21e1f420658c43f0ae13ec4130c163729 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 29 Aug 2015 23:33:15 -0700 Subject: [PATCH 16/47] GUAC-586: Read extension files in lexicographical order. --- .../guacamole/net/basic/extension/ExtensionModule.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java index ac72a34ae..dbf8bfc1e 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java @@ -333,7 +333,10 @@ public class ExtensionModule extends ServletModule { logger.warn("Although GUACAMOLE_HOME/" + EXTENSIONS_DIRECTORY + " exists, its contents cannot be read."); return; } - + + // Sort files lexicographically + Arrays.sort(extensionFiles); + // Load each extension within the extension directory for (File extensionFile : extensionFiles) { From fc211628218d4ae8eba3d6bd528f7fba4d37c0c3 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 30 Aug 2015 22:20:07 -0700 Subject: [PATCH 17/47] GUAC-586: Expand handling of permissions within user editor. Allow users to be created through editor. Display tabs for each possible account. --- .../controllers/manageUserController.js | 339 +++++++++++++++--- .../webapp/app/manage/styles/manage-user.css | 36 ++ .../app/manage/templates/manageUser.html | 103 +++--- .../src/main/webapp/images/checkmark.png | Bin 0 -> 569 bytes guacamole/src/main/webapp/images/plus.png | Bin 0 -> 299 bytes .../src/main/webapp/translations/en.json | 4 +- 6 files changed, 382 insertions(+), 100 deletions(-) create mode 100644 guacamole/src/main/webapp/images/checkmark.png create mode 100644 guacamole/src/main/webapp/images/plus.png diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index a31a5c707..76394900f 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -31,10 +31,12 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto var PageDefinition = $injector.get('PageDefinition'); var PermissionFlagSet = $injector.get('PermissionFlagSet'); var PermissionSet = $injector.get('PermissionSet'); + var User = $injector.get('User'); // Required services var $location = $injector.get('$location'); var $routeParams = $injector.get('$routeParams'); + var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); var guacNotification = $injector.get('guacNotification'); @@ -55,6 +57,21 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto } }; + /** + * The identifiers of all data sources currently available to the + * authenticated user. + * + * @type String[] + */ + var dataSources = authenticationService.getAvailableDataSources(); + + /** + * The username of the current, authenticated user. + * + * @type String + */ + var currentUsername = authenticationService.getCurrentUsername(); + /** * The unique identifier of the data source containing the user being * edited. @@ -70,6 +87,16 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto */ var username = $routeParams.id; + /** + * Whether the user being modified actually exists. If the user does not + * yet exist, a different REST service call must be made to create that + * user rather than update an existing user. If the user has not yet been + * loaded, this will be null. + * + * @type Boolean + */ + var userExists = null; + /** * The user being modified. * @@ -85,26 +112,13 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.permissionFlags = null; /** - * The root connection group of the connection group hierarchy. + * The root connection group of the connection group hierarchy within the + * data source containing the user being edited/created. * * @type ConnectionGroup */ $scope.rootGroup = null; - /** - * Whether the authenticated user has UPDATE permission for the user being edited. - * - * @type Boolean - */ - $scope.hasUpdatePermission = null; - - /** - * Whether the authenticated user has DELETE permission for the user being edited. - * - * @type Boolean - */ - $scope.hasDeletePermission = null; - /** * All permissions associated with the current user, or null if the user's * permissions have not yet been loaded. @@ -128,21 +142,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto * * @type PageDefinition[] */ - $scope.accountPages = (function getAccountPages(dataSources) { - - var accountPages = []; - - // Add an account page for each applicable data source - angular.forEach(dataSources, function addAccountPage(dataSource) { - accountPages.push(new PageDefinition( - translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', - '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username) - )); - }); - - return accountPages; - - })(authenticationService.getAvailableDataSources()); + $scope.accountPages = []; /** * Returns whether the list of all available account tabs should be shown. @@ -168,25 +168,262 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto && $scope.permissionFlags !== null && $scope.rootGroup !== null && $scope.permissions !== null - && $scope.attributes !== null - && $scope.canSaveUser !== null - && $scope.canDeleteUser !== null; + && $scope.attributes !== null; }; + /** + * Returns whether the current user can change attributes associated with + * the user being edited. + * + * @returns {Boolean} + * true if the current user can change attributes associated with the + * user being edited, false otherwise. + */ + $scope.canChangeAttributes = function canChangeAttributes() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // Attributes can always be set if we are creating the user + if (!userExists) + return true; + + // The administrator can always change attributes + if (PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.ADMINISTER)) + return true; + + // Otherwise, can change attributes if we have permission to update this user + return PermissionSet.hasUserPermission($scope.permissions, + PermissionSet.ObjectPermissionType.UPDATE, username); + + }; + + /** + * Returns whether the current user can change permissions of any kind + * which are associated with the user being edited. + * + * @returns {Boolean} + * true if the current user can grant or revoke permissions of any kind + * which are associated with the user being edited, false otherwise. + */ + $scope.canChangePermissions = function canChangePermissions() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // Permissions can always be set if we are creating the user + if (!userExists) + return true; + + // The administrator can always modify permissions + if (PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.ADMINISTER)) + return true; + + // Otherwise, can only modify permissions if we have explicit ADMINSTER permission + return PermissionSet.hasUserPermission($scope.permissions, + PermissionSet.ObjectPermissionType.ADMINISTER, username); + + }; + + /** + * Returns whether the current user can change the system permissions + * granted to the user being edited. + * + * @returns {Boolean} + * true if the current user can grant or revoke system permissions to + * the user being edited, false otherwise. + */ + $scope.canChangeSystemPermissions = function canChangeSystemPermissions() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // Only the administrator can modify system permissions + return PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.ADMINISTER); + + }; + + /** + * Returns whether the current user can save the user being edited, + * creating or updating that user, depending on whether the user already + * exists. + * + * @returns {Boolean} + * true if the current user can save changes to the user being edited, + * false otherwise. + */ + $scope.canSaveUser = function canSaveUser() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // The administrator can always save users + if (PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.ADMINISTER)) + return true; + + // If user does not exist, can only save if we have permission to create users + if (!userExists) + return PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.CREATE_USER); + + // Otherwise, can only save if we have permission to update this user + return PermissionSet.hasUserPermission($scope.permissions, + PermissionSet.ObjectPermissionType.UPDATE, username); + + }; + + /** + * Returns whether the current user can delete the user being edited. + * + * @returns {Boolean} + * true if the current user can delete the user being edited, false + * otherwise. + */ + $scope.canDeleteUser = function canDeleteUser() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // Can't delete what doesn't exist + if (!userExists) + return false; + + // The administrator can always delete users + if (PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.ADMINISTER)) + return true; + + // Otherwise, require explicit DELETE permission on the user + return PermissionSet.hasUserPermission($scope.permissions, + PermissionSet.ObjectPermissionType.DELETE, username); + + }; + + /** + * Returns whether the user being edited is read-only, and thus cannot be + * modified by the current user. + * + * @returns {Boolean} + * true if the user being edited is actually read-only and cannot be + * edited at all, false otherwise. + */ + $scope.isReadOnly = function isReadOnly() { + return !$scope.canSaveUser(); + }; + // Pull user attribute schema schemaService.getUserAttributes().success(function attributesReceived(attributes) { $scope.attributes = attributes; }); + /** + * Retrieves all user objects having the given username from each of the + * given data sources, returning a promise which resolves to a map of those + * users by data source identifier. If any data source returns an error, + * that data source is omitted from the results. + * + * @param {String[]} dataSources + * The identifiers of the data sources to query. + * + * @param {String} username + * The username of the user to retrieve from each of the given data + * sources. + * + * @returns {Promise.>} + * A promise which resolves to a map of user objects by data source + * identifier. + */ + var getUserAccounts = function getUserAccounts(dataSources, username) { + + var deferred = $q.defer(); + + var userRequests = []; + var accounts = {}; + + // Retrieve the requested user account from all data sources + angular.forEach(dataSources, function retrieveUser(dataSource) { + + // Add promise to list of pending requests + var deferredUserRequest = $q.defer(); + userRequests.push(deferredUserRequest.promise); + + // Retrieve user from data source + userService.getUser(dataSource, username) + .success(function userRetrieved(user) { + accounts[dataSource] = user; + deferredUserRequest.resolve(); + }) + + // Ignore any errors + .error(function userRetrievalFailed() { + deferredUserRequest.resolve(); + }); + + }); + + // Resolve when all requests are completed + $q.all(userRequests) + .then(function accountsRetrieved() { + deferred.resolve(accounts); + }); + + return deferred.promise; + + }; + // Pull user data - userService.getUser(dataSource, username).success(function userReceived(user) { - $scope.user = user; + getUserAccounts(dataSources, username) + .then(function usersReceived(users) { + + // Get user for currently-selected data source + $scope.user = users[dataSource]; + + // Create skeleton user if user does not exist + if (!$scope.user) { + userExists = false; + $scope.user = new User({ + 'username' : username + }); + } + else + userExists = true; + + // Generate pages for each applicable data source + $scope.accountPages = []; + angular.forEach(dataSources, function addAccountPage(dataSource) { + + // Determine whether data source contains this user + var linked = dataSource in users; + + // Add page entry + $scope.accountPages.push(new PageDefinition( + translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', + '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username), + linked ? 'linked' : 'unlinked' + )); + + }); + }); // Pull user permissions permissionService.getPermissions(dataSource, username).success(function gotPermissions(permissions) { $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions); + }) + + // If permissions cannot be retrieved, use empty permissions + .error(function permissionRetrievalFailed() { + $scope.permissionFlags = new PermissionFlagSet(); }); // Retrieve all connections for which we have ADMINISTER permission @@ -196,24 +433,9 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }); // Query the user's permissions for the current user - permissionService.getPermissions(dataSource, authenticationService.getCurrentUsername()) - .success(function permissionsReceived(permissions) { - + permissionService.getPermissions(dataSource, currentUsername) + .success(function permissionsReceived(permissions) { $scope.permissions = permissions; - - // Check if the user is new or if the user has UPDATE permission - $scope.canSaveUser = - !username - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, username); - - // Check if user is not new and the user has DELETE permission - $scope.canDeleteUser = - !!username && ( - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE, username) - ); - }); /** @@ -550,9 +772,14 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto return; } - // Save the user - userService.saveUser(dataSource, $scope.user) - .success(function savedUser() { + // Save or create the user, depending on whether the user exists + var saveUserPromise; + if (userExists) + saveUserPromise = userService.saveUser(dataSource, $scope.user); + else + saveUserPromise = userService.createUser(dataSource, $scope.user); + + saveUserPromise.success(function savedUser() { // Upon success, save any changed permissions permissionService.patchPermissions(dataSource, $scope.user.username, permissionsAdded, permissionsRemoved) diff --git a/guacamole/src/main/webapp/app/manage/styles/manage-user.css b/guacamole/src/main/webapp/app/manage/styles/manage-user.css index 48a99a4c6..a24d3e569 100644 --- a/guacamole/src/main/webapp/app/manage/styles/manage-user.css +++ b/guacamole/src/main/webapp/app/manage/styles/manage-user.css @@ -27,3 +27,39 @@ .manage-user .username.header h2 { text-transform: none; } + +.manage-user .settings-tabs .page-list li.unlinked a[href], +.manage-user .settings-tabs .page-list li.linked a[href] { + padding-right: 2.5em; + position: relative; +} + +.manage-user .settings-tabs .page-list li.unlinked a[href]:before, +.manage-user .settings-tabs .page-list li.linked a[href]:before { + content: ' '; + position: absolute; + right: 0; + bottom: 0; + top: 0; + width: 2.5em; + background-size: 1.25em; + background-repeat: no-repeat; + background-position: center; +} + +.manage-user .settings-tabs .page-list li.unlinked a[href]:before { + background-image: url('images/plus.png'); +} + +.manage-user .settings-tabs .page-list li.unlinked a[href] { + opacity: 0.5; +} + +.manage-user .settings-tabs .page-list li.unlinked a[href]:hover, +.manage-user .settings-tabs .page-list li.unlinked a[href].current { + opacity: 1; +} + +.manage-user .settings-tabs .page-list li.linked a[href]:before { + background-image: url('images/checkmark.png'); +} diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index b0b84f496..2f28b3335 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -22,7 +22,7 @@ THE SOFTWARE.
    - +

    {{user.username}}

    @@ -31,56 +31,73 @@ THE SOFTWARE.
    -
    - - - - - - - - - -
    {{'MANAGE_USER.FIELD_HEADER_PASSWORD' | translate}}
    {{'MANAGE_USER.FIELD_HEADER_PASSWORD_AGAIN' | translate}}
    + +
    +

    {{'MANAGE_USER.INFO_READ_ONLY' | translate}}

    - -
    - -
    + +
    + + +
    + + + + + + + + + +
    {{'MANAGE_USER.FIELD_HEADER_PASSWORD' | translate}}
    {{'MANAGE_USER.FIELD_HEADER_PASSWORD_AGAIN' | translate}}
    +
    + + +
    + +
    + + +
    +

    {{'MANAGE_USER.SECTION_HEADER_PERMISSIONS' | translate}}

    +
    + + + + + + + + + +
    {{systemPermissionType.label | translate}}
    {{'MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD' | translate}}
    +
    +
    + + +
    +

    {{'MANAGE_USER.SECTION_HEADER_CONNECTIONS' | translate}}

    +
    + +
    +
    - -

    {{'MANAGE_USER.SECTION_HEADER_PERMISSIONS' | translate}}

    -
    - - - - - - - - - -
    {{systemPermissionType.label | translate}}
    {{'MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD' | translate}}
    -
    - -

    {{'MANAGE_USER.SECTION_HEADER_CONNECTIONS' | translate}}

    -
    -
    - + - +
    diff --git a/guacamole/src/main/webapp/images/checkmark.png b/guacamole/src/main/webapp/images/checkmark.png new file mode 100644 index 0000000000000000000000000000000000000000..c54feb2656d98aad13cbfc8fb290def723db2fda GIT binary patch literal 569 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=EX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4)&QRetZ{njVTg1wHP>X{NcbE>17TO$G!rOtu+P8Jxr;u*g4W#Y)(E^ejs_`lxuOmSn*Geh)Lbg;?xV; zcmC16pg8SZv*?@f`Y63w)sYW4i+^(O%&2>lq!-V2?8%~?<~&mj_&4Z_pY0Dm^R)ZG zf*6zUY8&f6<^oMxFT3Md%s<8#g5J*$Dt%kMUqSrrUY-vfJOA*0RDTkm#r#ZaPdfMS ztMiK_FKn%RB=oxNv*iQP;-A9&l5=0OafO^#=`Cp3S;HyuY0{O6ysx~k_8Bz&jI(Dd zt7-F%mjB>XwDAK?3-@>`EWZn(AvMpb(uAge+ zB>7@G&(quA1*861#!ouM;rhtlv8jY@jaPdBEKr%gaSFJI^D~8>j|E0EgQu&X%Q~lo FCIF|C{#O71 literal 0 HcmV?d00001 diff --git a/guacamole/src/main/webapp/images/plus.png b/guacamole/src/main/webapp/images/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..14bedbe4892bdf33ca399407b0d1ceb2ec292210 GIT binary patch literal 299 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=EX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4J!Gv`Q^Ab%3Sp{FlmkgO)9_${wOJ`jC_17-+YVq@1QK8zopr0B{{;?f?J) literal 0 HcmV?d00001 diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json index 2d968a1f6..270db30f7 100644 --- a/guacamole/src/main/webapp/translations/en.json +++ b/guacamole/src/main/webapp/translations/en.json @@ -244,7 +244,9 @@ "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", "FIELD_HEADER_USERNAME" : "Username:", - + + "INFO_READ_ONLY" : "Sorry, but this user account cannot be edited.", + "SECTION_HEADER_CONNECTIONS" : "Connections", "SECTION_HEADER_EDIT_USER" : "Edit User", "SECTION_HEADER_PERMISSIONS" : "Permissions", From 738f3bb02647a27feb6fbb438c3728dd34dc1453 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 30 Aug 2015 22:21:06 -0700 Subject: [PATCH 18/47] GUAC-586: Add getAllUsers() to userService. --- .../webapp/app/rest/services/userService.js | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/guacamole/src/main/webapp/app/rest/services/userService.js b/guacamole/src/main/webapp/app/rest/services/userService.js index 55eab2ed3..87dc9f915 100644 --- a/guacamole/src/main/webapp/app/rest/services/userService.js +++ b/guacamole/src/main/webapp/app/rest/services/userService.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -28,6 +28,7 @@ angular.module('rest').factory('userService', ['$injector', // Required services var $http = $injector.get('$http'); + var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); @@ -77,6 +78,66 @@ angular.module('rest').factory('userService', ['$injector', }; + /** + * Returns a promise which resolves with all users accessible by the + * given user, as a map of @link{User} arrays by the identifier of their + * corresponding data source. All given data sources are queried. If an + * error occurs while retrieving any PermissionSet, the promise will be + * rejected. + * + * @param {String[]} dataSources + * The unique identifier of the data sources containing the user to be + * retrieved. These identifiers correspond to AuthenticationProviders + * within the Guacamole web application. + * + * @param {String[]} [permissionTypes] + * The set of permissions to filter with. A user must have one or more + * of these permissions for a user to appear in the result. + * If null, no filtering will be performed. Valid values are listed + * within PermissionSet.ObjectType. + * + * @returns {Promise.>} + * A promise which resolves with all user objects available to the + * current user, as a map of @link{User} arrays grouped by the + * identifier of their corresponding data source. + */ + service.getAllUsers = function getAllUsers(dataSources, permissionTypes) { + + var deferred = $q.defer(); + + var userRequests = []; + var userArrays = {}; + + // Retrieve all users from all data sources + angular.forEach(dataSources, function retrieveUsers(dataSource) { + userRequests.push( + service.getUsers(dataSource, permissionTypes) + .success(function usersRetrieved(users) { + userArrays[dataSource] = users; + }) + ); + }); + + // Resolve when all requests are completed + $q.all(userRequests) + .then( + + // All requests completed successfully + function allUsersRetrieved() { + deferred.resolve(userArrays); + }, + + // At least one request failed + function UserRetrievalFailed(e) { + deferred.reject(e); + } + + ); + + return deferred.promise; + + }; + /** * Makes a request to the REST API to get the user having the given * username, returning a promise that provides the corresponding From d03cfbe9db84e4ebe86f703127438ca7ea3a9dc0 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 30 Aug 2015 23:06:30 -0700 Subject: [PATCH 19/47] GUAC-586: Return an empty array for getAvailableDataSources() if no auth data is present. --- .../src/main/webapp/app/auth/service/authenticationService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/webapp/app/auth/service/authenticationService.js b/guacamole/src/main/webapp/app/auth/service/authenticationService.js index 6493cc2a5..5dd549a34 100644 --- a/guacamole/src/main/webapp/app/auth/service/authenticationService.js +++ b/guacamole/src/main/webapp/app/auth/service/authenticationService.js @@ -321,7 +321,7 @@ angular.module('auth').factory('authenticationService', ['$injector', * * @returns {String[]} * The identifiers of all data sources availble to the current user, - * or null if no authentication data is present. + * or an empty array if no authentication data is present. */ service.getAvailableDataSources = function getAvailableDataSources() { @@ -331,7 +331,7 @@ angular.module('auth').factory('authenticationService', ['$injector', return authData.availableDataSources; // No auth data present - return null; + return []; }; From 7a9b3a5cabec457775485bacfa839a0ea481bb0c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 30 Aug 2015 23:44:01 -0700 Subject: [PATCH 20/47] GUAC-586: Display ALL users in the user list. Navigate to the first data source with creation permission when "New User" is clicked. --- .../webapp/app/manage/types/ManageableUser.js | 56 +++++ .../settings/directives/guacSettingsUsers.js | 206 +++++++++++------- .../app/settings/templates/settingsUsers.html | 11 +- 3 files changed, 188 insertions(+), 85 deletions(-) create mode 100644 guacamole/src/main/webapp/app/manage/types/ManageableUser.js diff --git a/guacamole/src/main/webapp/app/manage/types/ManageableUser.js b/guacamole/src/main/webapp/app/manage/types/ManageableUser.js new file mode 100644 index 000000000..8f2e43aef --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/types/ManageableUser.js @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * A service for defining the ManageableUser class. + */ +angular.module('manage').factory('ManageableUser', [function defineManageableUser() { + + /** + * A pairing of an @link{User} with the identifier of its corresponding + * data source. + * + * @constructor + * @param {Object|ManageableUser} template + */ + var ManageableUser = function ManageableUser(template) { + + /** + * The unique identifier of the data source containing this user. + * + * @type String + */ + this.dataSource = template.dataSource; + + /** + * The @link{User} object represented by this ManageableUser and + * contained within the associated data source. + * + * @type User + */ + this.user = template.user; + + }; + + return ManageableUser; + +}]); diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js index 420409196..494148888 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js @@ -37,8 +37,8 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings controller: ['$scope', '$injector', function settingsUsersController($scope, $injector) { // Required types + var ManageableUser = $injector.get('ManageableUser'); var PermissionSet = $injector.get('PermissionSet'); - var User = $injector.get('User'); // Required services var $location = $injector.get('$location'); @@ -63,36 +63,19 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings }; /** - * The data source from which the list of users should be pulled. - * For the time being, this is just the data source which - * authenticated the current user. + * The identifiers of all data sources accessible by the current + * user. * - * @type String + * @type String[] */ - $scope.dataSource = authenticationService.getDataSource(); + var dataSources = authenticationService.getAvailableDataSources(); /** - * All visible users. + * All visible users, along with their corresponding data sources. * - * @type User[] + * @type ManageableUser[] */ - $scope.users = null; - - /** - * Whether the current user can manage users. If the current - * permissions have not yet been loaded, this will be null. - * - * @type Boolean - */ - $scope.canManageUsers = null; - - /** - * Whether the current user can create new users. If the current - * permissions have not yet been loaded, this will be null. - * - * @type Boolean - */ - $scope.canCreateUsers = null; + $scope.manageableUsers = null; /** * The name of the new user to create, if any, when user creation @@ -103,10 +86,11 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings $scope.newUsername = ""; /** - * All permissions associated with the current user, or null if the + * Map of data source identifiers to all permissions associated + * with the current user within that data source, or null if the * user's permissions have not yet been loaded. * - * @type PermissionSet + * @type Object. */ $scope.permissions = null; @@ -119,82 +103,144 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings */ $scope.isLoaded = function isLoaded() { - return $scope.users !== null - && $scope.permissions !== null - && $scope.canManageUsers !== null - && $scope.canCreateUsers !== null; + return $scope.manageableUsers !== null + && $scope.permissions !== null; + + }; + + /** + * Returns the identifier of the data source that should be used by + * default when creating a new user. + * + * @return {String} + * The identifier of the data source that should be used by + * default when creating a new user, or null if user creation + * is not allowed. + */ + var getDefaultDataSource = function getDefaultDataSource() { + + // Abort if permissions have not yet loaded + if (!$scope.permissions) + return null; + + // For each data source + for (var dataSource in $scope.permissions) { + + // Retrieve corresponding permission set + var permissionSet = $scope.permissions[dataSource]; + + // Can create users if adminstrator or have explicit permission + if (PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.CREATE_USER)) + return dataSource; + + } + + // No data sources allow user creation + return null; + + }; + + /** + * Returns whether the current user can create new users within at + * least one data source. + * + * @return {Boolean} + * true if the current user can create new users within at + * least one data source, false otherwise. + */ + $scope.canCreateUsers = function canCreateUsers() { + return getDefaultDataSource() !== null; + }; + + /** + * Returns whether the current user can create new users or make + * changes to existing users within at least one data source. The + * user management interface as a whole is useless if this function + * returns false. + * + * @return {Boolean} + * true if the current user can create new users or make + * changes to existing users within at least one data source, + * false otherwise. + */ + var canManageUsers = function canManageUsers() { + + // Abort if permissions have not yet loaded + if (!$scope.permissions) + return false; + + // Creating users counts as management + if ($scope.canCreateUsers()) + return true; + + // For each data source + for (var dataSource in $scope.permissions) { + + // Retrieve corresponding permission set + var permissionSet = $scope.permissions[dataSource]; + + // Can manage users if granted explicit update or delete + if (PermissionSet.hasUserPermission(permissionSet, PermissionSet.ObjectPermissionType.UPDATE) + || PermissionSet.hasUserPermission(permissionSet, PermissionSet.ObjectPermissionType.DELETE)) + return true; + + } + + // No data sources allow management of users + return false; }; // Retrieve current permissions - permissionService.getPermissions($scope.dataSource, currentUsername) - .success(function permissionsRetrieved(permissions) { + permissionService.getAllPermissions(dataSources, currentUsername) + .then(function permissionsRetrieved(permissions) { + // Store retrieved permissions $scope.permissions = permissions; - // Determine whether the current user can create new users - $scope.canCreateUsers = - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER); - - // Determine whether the current user can manage other users - $scope.canManageUsers = - $scope.canCreateUsers - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE); - // Return to home if there's nothing to do here - if (!$scope.canManageUsers) + if (!canManageUsers()) $location.path('/'); - + }); // Retrieve all users for whom we have UPDATE or DELETE permission - userService.getUsers($scope.dataSource, [ + userService.getAllUsers(dataSources, [ PermissionSet.ObjectPermissionType.UPDATE, PermissionSet.ObjectPermissionType.DELETE ]) - .success(function usersReceived(users) { + .then(function usersReceived(userArrays) { - // Display only other users, not self - $scope.users = users.filter(function isNotSelf(user) { - return user.username !== currentUsername; + var addedUsers = {}; + $scope.manageableUsers = []; + + // For each user in each data source + angular.forEach(dataSources, function addUserList(dataSource) { + angular.forEach(userArrays[dataSource], function addUser(user) { + + // Do not add the same user twice + if (addedUsers[user.username]) + return; + + // Add user to overall list + addedUsers[user.username] = user; + $scope.manageableUsers.push(new ManageableUser ({ + 'dataSource' : dataSource, + 'user' : user + })); + + }); }); }); /** - * Creates a new user having the username specified in the user - * creation interface. + * Navigates to an interface for creating a new user having the + * username specified. */ $scope.newUser = function newUser() { - - // Create user skeleton - var user = new User({ - username: $scope.newUsername || '' - }); - - // Create specified user - userService.createUser($scope.dataSource, user) - - // Add user to visible list upon success - .success(function userCreated() { - $scope.users.push(user); - }) - - // Notify of any errors - .error(function userCreationFailed(error) { - guacNotification.showStatus({ - 'className' : 'error', - 'title' : 'SETTINGS_USERS.DIALOG_HEADER_ERROR', - 'text' : error.message, - 'actions' : [ ACKNOWLEDGE_ACTION ] - }); - }); - - // Reset username - $scope.newUsername = ""; - + $location.url('/manage/' + encodeURIComponent(getDefaultDataSource()) + '/users/' + encodeURIComponent($scope.newUsername)); }; }] diff --git a/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html b/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html index 430f57912..e1932bfb4 100644 --- a/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html +++ b/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html @@ -25,24 +25,25 @@

    {{'SETTINGS_USERS.HELP_USERS' | translate}}

    -
    +
    \ No newline at end of file From ce064cfc68bb17876b57135886e3320a790806fc Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 31 Aug 2015 00:05:53 -0700 Subject: [PATCH 21/47] GUAC-586: Fix comments incorrectly referring to the auth provider identifier as an "index". --- .../net/basic/rest/user/UserRESTService.java | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java index 3f290c5c1..29f6b3f1c 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java @@ -133,8 +133,8 @@ public class UserRESTService { * performing the operation. * * @param authProviderIdentifier - * The index of the UserContext within the overall List of available - * UserContexts in which the requested user is to be created. + * The unique identifier of the AuthenticationProvider associated with + * the UserContext from which the users are to be retrieved. * * @param permissions * The set of permissions to filter with. A user must have one or more @@ -191,8 +191,8 @@ public class UserRESTService { * performing the operation. * * @param authProviderIdentifier - * The index of the UserContext within the overall List of available - * UserContexts in which the requested user is to be created. + * The unique identifier of the AuthenticationProvider associated with + * the UserContext from which the requested user is to be retrieved. * * @param username * The username of the user to retrieve. @@ -227,8 +227,8 @@ public class UserRESTService { * performing the operation. * * @param authProviderIdentifier - * The index of the UserContext within the overall List of available - * UserContexts in which the requested user is to be created. + * The unique identifier of the AuthenticationProvider associated with + * the UserContext in which the requested user is to be created. * * @param user * The new user to create. @@ -271,8 +271,8 @@ public class UserRESTService { * performing the operation. * * @param authProviderIdentifier - * The index of the UserContext within the overall List of available - * UserContexts in which the requested user is to be found. + * The unique identifier of the AuthenticationProvider associated with + * the UserContext in which the requested user is to be updated. * * @param username * The username of the user to update. @@ -331,8 +331,8 @@ public class UserRESTService { * performing the operation. * * @param authProviderIdentifier - * The index of the UserContext within the overall List of available - * UserContexts in which the requested user is to be found. + * The unique identifier of the AuthenticationProvider associated with + * the UserContext in which the requested user is to be updated. * * @param username * The username of the user to update. @@ -403,8 +403,8 @@ public class UserRESTService { * performing the operation. * * @param authProviderIdentifier - * The index of the UserContext within the overall List of available - * UserContexts in which the requested user is to be found. + * The unique identifier of the AuthenticationProvider associated with + * the UserContext from which the requested user is to be deleted. * * @param username * The username of the user to delete. @@ -444,8 +444,8 @@ public class UserRESTService { * performing the operation. * * @param authProviderIdentifier - * The index of the UserContext within the overall List of available - * UserContexts in which the requested user is to be found. + * The unique identifier of the AuthenticationProvider associated with + * the UserContext in which the requested user is to be found. * * @param username * The username of the user to retrieve permissions for. @@ -539,8 +539,8 @@ public class UserRESTService { * performing the operation. * * @param authProviderIdentifier - * The index of the UserContext within the overall List of available - * UserContexts in which the requested user is to be found. + * The unique identifier of the AuthenticationProvider associated with + * the UserContext in which the requested user is to be found. * * @param username * The username of the user to modify the permissions of. From 0f661dfec3678549706995605d00d9bdc587533f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 31 Aug 2015 00:14:22 -0700 Subject: [PATCH 22/47] GUAC-586: Use auth provider identifiers within schema REST service. --- .../basic/rest/schema/SchemaRESTService.java | 61 ++++++++++++++++--- .../webapp/app/rest/services/schemaService.js | 39 +++++++++--- 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/SchemaRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/SchemaRESTService.java index 05d0c6d77..6a09cba32 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/SchemaRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/SchemaRESTService.java @@ -28,6 +28,7 @@ import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; @@ -36,7 +37,9 @@ import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.LocalEnvironment; import org.glyptodon.guacamole.form.Form; import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; +import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.protocols.ProtocolInfo; @@ -46,7 +49,7 @@ import org.glyptodon.guacamole.protocols.ProtocolInfo; * * @author Michael Jumper */ -@Path("/schema") +@Path("/schema/{dataSource}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class SchemaRESTService { @@ -57,6 +60,12 @@ public class SchemaRESTService { @Inject private AuthenticationService authenticationService; + /** + * Service for convenient retrieval of objects. + */ + @Inject + private ObjectRetrievalService retrievalService; + /** * Retrieves the possible attributes of a user object. * @@ -64,6 +73,10 @@ public class SchemaRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext dictating the available user attributes. + * * @return * A collection of forms which describe the possible attributes of a * user object. @@ -74,10 +87,13 @@ public class SchemaRESTService { @GET @Path("/users/attributes") @AuthProviderRESTExposure - public Collection
    getUserAttributes(@QueryParam("token") String authToken) throws GuacamoleException { + public Collection getUserAttributes(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier) + throws GuacamoleException { // Retrieve all possible user attributes - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); return userContext.getUserAttributes(); } @@ -89,6 +105,10 @@ public class SchemaRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext dictating the available connection attributes. + * * @return * A collection of forms which describe the possible attributes of a * connection object. @@ -99,10 +119,13 @@ public class SchemaRESTService { @GET @Path("/connections/attributes") @AuthProviderRESTExposure - public Collection getConnectionAttributes(@QueryParam("token") String authToken) throws GuacamoleException { + public Collection getConnectionAttributes(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier) + throws GuacamoleException { // Retrieve all possible connection attributes - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); return userContext.getConnectionAttributes(); } @@ -114,6 +137,11 @@ public class SchemaRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext dictating the available connection group + * attributes. + * * @return * A collection of forms which describe the possible attributes of a * connection group object. @@ -124,10 +152,13 @@ public class SchemaRESTService { @GET @Path("/connectionGroups/attributes") @AuthProviderRESTExposure - public Collection getConnectionGroupAttributes(@QueryParam("token") String authToken) throws GuacamoleException { + public Collection getConnectionGroupAttributes(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier) + throws GuacamoleException { // Retrieve all possible connection group attributes - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); return userContext.getConnectionGroupAttributes(); } @@ -139,6 +170,13 @@ public class SchemaRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext dictating the protocols available. Currently, the + * UserContext actually does not dictate this, the the same set of + * protocols will be retrieved for all users, though the identifier + * given here will be validated. + * * @return * A map of protocol information, where each key is the unique name * associated with that protocol. @@ -149,10 +187,13 @@ public class SchemaRESTService { @GET @Path("/protocols") @AuthProviderRESTExposure - public Map getProtocols(@QueryParam("token") String authToken) throws GuacamoleException { + public Map getProtocols(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier) + throws GuacamoleException { - // Verify the given auth token is valid - authenticationService.getUserContext(authToken); + // Verify the given auth token and auth provider identifier are valid + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get and return a map of all protocols. Environment env = new LocalEnvironment(); diff --git a/guacamole/src/main/webapp/app/rest/services/schemaService.js b/guacamole/src/main/webapp/app/rest/services/schemaService.js index 0ca3596c5..2c3041f85 100644 --- a/guacamole/src/main/webapp/app/rest/services/schemaService.js +++ b/guacamole/src/main/webapp/app/rest/services/schemaService.js @@ -39,12 +39,18 @@ angular.module('rest').factory('schemaService', ['$injector', * @link{Form} objects if successful. Each element of the array describes * a logical grouping of possible attributes. * + * @param {String} dataSource + * The unique identifier of the data source containing the users whose + * available attributes are to be retrieved. This identifier + * corresponds to an AuthenticationProvider within the Guacamole web + * application. + * * @returns {Promise.} * A promise which will resolve with an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ - service.getUserAttributes = function getUserAttributes() { + service.getUserAttributes = function getUserAttributes(dataSource) { // Build HTTP parameters set var httpParameters = { @@ -55,7 +61,7 @@ angular.module('rest').factory('schemaService', ['$injector', return $http({ cache : cacheService.schema, method : 'GET', - url : 'api/schema/users/attributes', + url : 'api/schema/' + encodeURIComponent(dataSource) + '/users/attributes', params : httpParameters }); @@ -67,12 +73,18 @@ angular.module('rest').factory('schemaService', ['$injector', * @link{Form} objects if successful. Each element of the array describes * a logical grouping of possible attributes. * + * @param {String} dataSource + * The unique identifier of the data source containing the connections + * whose available attributes are to be retrieved. This identifier + * corresponds to an AuthenticationProvider within the Guacamole web + * application. + * * @returns {Promise.} * A promise which will resolve with an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ - service.getConnectionAttributes = function getConnectionAttributes() { + service.getConnectionAttributes = function getConnectionAttributes(dataSource) { // Build HTTP parameters set var httpParameters = { @@ -83,7 +95,7 @@ angular.module('rest').factory('schemaService', ['$injector', return $http({ cache : cacheService.schema, method : 'GET', - url : 'api/schema/connections/attributes', + url : 'api/schema/' + encodeURIComponent(dataSource) + '/connections/attributes', params : httpParameters }); @@ -95,12 +107,18 @@ angular.module('rest').factory('schemaService', ['$injector', * of @link{Form} objects if successful. Each element of the array * a logical grouping of possible attributes. * + * @param {String} dataSource + * The unique identifier of the data source containing the connection + * groups whose available attributes are to be retrieved. This + * identifier corresponds to an AuthenticationProvider within the + * Guacamole web application. + * * @returns {Promise.} * A promise which will resolve with an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ - service.getConnectionGroupAttributes = function getConnectionGroupAttributes() { + service.getConnectionGroupAttributes = function getConnectionGroupAttributes(dataSource) { // Build HTTP parameters set var httpParameters = { @@ -111,7 +129,7 @@ angular.module('rest').factory('schemaService', ['$injector', return $http({ cache : cacheService.schema, method : 'GET', - url : 'api/schema/connectionGroups/attributes', + url : 'api/schema/' + encodeURIComponent(dataSource) + '/connectionGroups/attributes', params : httpParameters }); @@ -122,11 +140,16 @@ angular.module('rest').factory('schemaService', ['$injector', * a promise that provides a map of @link{Protocol} objects by protocol * name if successful. * + * @param {String} dataSource + * The unique identifier of the data source defining available + * protocols. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * * @returns {Promise.>} * A promise which will resolve with a map of @link{Protocol} * objects by protocol name upon success. */ - service.getProtocols = function getProtocols() { + service.getProtocols = function getProtocols(dataSource) { // Build HTTP parameters set var httpParameters = { @@ -137,7 +160,7 @@ angular.module('rest').factory('schemaService', ['$injector', return $http({ cache : cacheService.schema, method : 'GET', - url : 'api/schema/protocols', + url : 'api/schema/' + encodeURIComponent(dataSource) + '/protocols', params : httpParameters }); From 09fd512571dcb6ad5f3fe4f890f40a39fb9288a1 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 31 Aug 2015 00:14:44 -0700 Subject: [PATCH 23/47] GUAC-586: Specify data source when querying available user attributes. --- .../main/webapp/app/manage/controllers/manageUserController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index 76394900f..c302f0669 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -322,7 +322,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }; // Pull user attribute schema - schemaService.getUserAttributes().success(function attributesReceived(attributes) { + schemaService.getUserAttributes(dataSource).success(function attributesReceived(attributes) { $scope.attributes = attributes; }); From 8f39671c6b16da3b57b5d5d54b1fa8093490d60d Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 31 Aug 2015 12:32:36 -0700 Subject: [PATCH 24/47] GUAC-586: Fix typos surrounding getAllUsers() in userService. --- guacamole/src/main/webapp/app/rest/services/userService.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/guacamole/src/main/webapp/app/rest/services/userService.js b/guacamole/src/main/webapp/app/rest/services/userService.js index 87dc9f915..268e00e69 100644 --- a/guacamole/src/main/webapp/app/rest/services/userService.js +++ b/guacamole/src/main/webapp/app/rest/services/userService.js @@ -80,10 +80,9 @@ angular.module('rest').factory('userService', ['$injector', /** * Returns a promise which resolves with all users accessible by the - * given user, as a map of @link{User} arrays by the identifier of their + * current user, as a map of @link{User} arrays by the identifier of their * corresponding data source. All given data sources are queried. If an - * error occurs while retrieving any PermissionSet, the promise will be - * rejected. + * error occurs while retrieving any user, the promise will be rejected. * * @param {String[]} dataSources * The unique identifier of the data sources containing the user to be @@ -128,7 +127,7 @@ angular.module('rest').factory('userService', ['$injector', }, // At least one request failed - function UserRetrievalFailed(e) { + function userRetrievalFailed(e) { deferred.reject(e); } From 7235ed980fd78b329d73deeebb8d82678bafe9b5 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 31 Aug 2015 14:08:11 -0700 Subject: [PATCH 25/47] GUAC-586: Invoke REST service functions across multiple data sources using dataSourceService.apply(). --- .../controllers/manageUserController.js | 60 +-------- .../navigation/services/userPageService.js | 11 +- .../app/rest/services/dataSourceService.js | 125 ++++++++++++++++++ .../app/rest/services/permissionService.js | 57 -------- .../webapp/app/rest/services/userService.js | 59 --------- .../settings/directives/guacSettingsUsers.js | 5 +- 6 files changed, 138 insertions(+), 179 deletions(-) create mode 100644 guacamole/src/main/webapp/app/rest/services/dataSourceService.js diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index c302f0669..5e8eaf1a3 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -39,6 +39,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); + var dataSourceService = $injector.get('dataSourceService'); var guacNotification = $injector.get('guacNotification'); var permissionService = $injector.get('permissionService'); var schemaService = $injector.get('schemaService'); @@ -326,63 +327,8 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.attributes = attributes; }); - /** - * Retrieves all user objects having the given username from each of the - * given data sources, returning a promise which resolves to a map of those - * users by data source identifier. If any data source returns an error, - * that data source is omitted from the results. - * - * @param {String[]} dataSources - * The identifiers of the data sources to query. - * - * @param {String} username - * The username of the user to retrieve from each of the given data - * sources. - * - * @returns {Promise.>} - * A promise which resolves to a map of user objects by data source - * identifier. - */ - var getUserAccounts = function getUserAccounts(dataSources, username) { - - var deferred = $q.defer(); - - var userRequests = []; - var accounts = {}; - - // Retrieve the requested user account from all data sources - angular.forEach(dataSources, function retrieveUser(dataSource) { - - // Add promise to list of pending requests - var deferredUserRequest = $q.defer(); - userRequests.push(deferredUserRequest.promise); - - // Retrieve user from data source - userService.getUser(dataSource, username) - .success(function userRetrieved(user) { - accounts[dataSource] = user; - deferredUserRequest.resolve(); - }) - - // Ignore any errors - .error(function userRetrievalFailed() { - deferredUserRequest.resolve(); - }); - - }); - - // Resolve when all requests are completed - $q.all(userRequests) - .then(function accountsRetrieved() { - deferred.resolve(accounts); - }); - - return deferred.promise; - - }; - // Pull user data - getUserAccounts(dataSources, username) + dataSourceService.apply(userService.getUser, dataSources, username) .then(function usersReceived(users) { // Get user for currently-selected data source @@ -427,7 +373,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }); // Retrieve all connections for which we have ADMINISTER permission - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER]) + connectionGroupService.getConnectionGroupTree(dataSource, ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER]) .success(function connectionGroupReceived(rootGroup) { $scope.rootGroup = rootGroup; }); diff --git a/guacamole/src/main/webapp/app/navigation/services/userPageService.js b/guacamole/src/main/webapp/app/navigation/services/userPageService.js index daefb4707..574307104 100644 --- a/guacamole/src/main/webapp/app/navigation/services/userPageService.js +++ b/guacamole/src/main/webapp/app/navigation/services/userPageService.js @@ -34,8 +34,9 @@ angular.module('navigation').factory('userPageService', ['$injector', // Get required services var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); - var connectionGroupService = $injector.get("connectionGroupService"); - var permissionService = $injector.get("permissionService"); + var connectionGroupService = $injector.get('connectionGroupService'); + var dataSourceService = $injector.get('dataSourceService'); + var permissionService = $injector.get('permissionService'); var service = {}; @@ -259,7 +260,8 @@ angular.module('navigation').factory('userPageService', ['$injector', var deferred = $q.defer(); // Retrieve current permissions - permissionService.getAllPermissions( + dataSourceService.apply( + permissionService.getPermissions, authenticationService.getAvailableDataSources(), authenticationService.getCurrentUsername() ) @@ -346,7 +348,8 @@ angular.module('navigation').factory('userPageService', ['$injector', }); // Retrieve current permissions - permissionService.getAllPermissions( + dataSourceService.apply( + permissionService.getPermissions, authenticationService.getAvailableDataSources(), authenticationService.getCurrentUsername() ) diff --git a/guacamole/src/main/webapp/app/rest/services/dataSourceService.js b/guacamole/src/main/webapp/app/rest/services/dataSourceService.js new file mode 100644 index 000000000..70e7ad48d --- /dev/null +++ b/guacamole/src/main/webapp/app/rest/services/dataSourceService.js @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Service which contains all REST API response caches. + */ +angular.module('rest').factory('dataSourceService', ['$injector', + function dataSourceService($injector) { + + // Required services + var $q = $injector.get('$q'); + + // Service containing all caches + var service = {}; + + /** + * Invokes the given function once for each of the given data sources, + * passing that data source as the first argument to each invocation, + * followed by any additional arguments passed to apply(). The results of + * each invocation are aggregated into a map by data source identifier, + * and handled through a single promise which is resolved or rejected + * depending on the success/failure of each resulting REST call. Any error + * results in rejection of the entire apply() operation, except 404 ("NOT + * FOUND") errors, which are ignored. + * + * @param {Function} fn + * The function to call for each of the given data sources. The data + * source identifier will be given as the first argument, followed by + * the rest of the arguments given to apply(), in order. The function + * must return a Promise which is resolved or rejected depending on the + * result of the REST call. + * + * @param {String[]} dataSources + * The array or data source identifiers against which the given + * function should be called. + * + * @param {...*} args + * Any additional arguments to pass to the given function each time it + * is called. + * + * @returns {Promise.>} + * A Promise which resolves with a map of data source identifier to + * corresponding result. The result will be the exact object or value + * provided as the resolution to the Promise returned by calls to the + * given function. + */ + service.apply = function apply(fn, dataSources) { + + var deferred = $q.defer(); + + var requests = []; + var results = {}; + + // Build array of arguments to pass to the given function + var args = []; + for (var i = 2; i < arguments.length; i++) + args.push(arguments[i]); + + // Retrieve the root group from all data sources + angular.forEach(dataSources, function invokeAgainstDataSource(dataSource) { + + // Add promise to list of pending requests + var deferredRequest = $q.defer(); + requests.push(deferredRequest.promise); + + // Retrieve root group from data source + fn.apply(this, [dataSource].concat(args)) + + // Store result on success + .then(function immediateRequestSucceeded(response) { + results[dataSource] = response.data; + deferredRequest.resolve(); + }, + + // Fail on any errors (except "NOT FOUND") + function immediateRequestFailed(response) { + + // Ignore "NOT FOUND" errors + if (response.status === 404) + deferredRequest.resolve(); + + // Explicitly abort for all other errors + else + deferredRequest.reject(response); + + }); + + }); + + // Resolve if all requests succeed + $q.all(requests).then(function requestsSucceeded() { + deferred.resolve(results); + }, + + // Reject if at least one request fails + function requestFailed(response) { + deferred.reject(response); + }); + + return deferred.promise; + + }; + + return service; + +}]); diff --git a/guacamole/src/main/webapp/app/rest/services/permissionService.js b/guacamole/src/main/webapp/app/rest/services/permissionService.js index ca9a2ef89..3cef2b9bc 100644 --- a/guacamole/src/main/webapp/app/rest/services/permissionService.js +++ b/guacamole/src/main/webapp/app/rest/services/permissionService.js @@ -71,63 +71,6 @@ angular.module('rest').factory('permissionService', ['$injector', }; - /** - * Returns a promise which resolves with all permissions available to the - * given user, as a map of all PermissionSet objects by the identifier of - * their corresponding data source. All given data sources are queried. If - * an error occurs while retrieving any PermissionSet, the promise will be - * rejected. - * - * @param {String[]} dataSources - * The unique identifier of the data sources containing the user whose - * permissions should be retrieved. These identifiers corresponds to - * AuthenticationProviders within the Guacamole web application. - * - * @param {String} username - * The username of the user to retrieve the permissions for. - * - * @returns {Promise.>} - * A promise which resolves with all permissions available to the - * current user, as a map of app PermissionSet objects by the - * identifier of their corresponding data source. - */ - service.getAllPermissions = function getAllPermissions(dataSources, username) { - - var deferred = $q.defer(); - - var permissionSetRequests = []; - var permissionSets = {}; - - // Retrieve all permissions from all data sources - angular.forEach(dataSources, function retrievePermissions(dataSource) { - permissionSetRequests.push( - service.getPermissions(dataSource, username) - .success(function permissionsRetrieved(permissions) { - permissionSets[dataSource] = permissions; - }) - ); - }); - - // Resolve when all requests are completed - $q.all(permissionSetRequests) - .then( - - // All requests completed successfully - function allPermissionsRetrieved() { - deferred.resolve(permissionSets); - }, - - // At least one request failed - function permissionRetrievalFailed(e) { - deferred.reject(e); - } - - ); - - return deferred.promise; - - }; - /** * Makes a request to the REST API to add permissions for a given user, * returning a promise that can be used for processing the results of the diff --git a/guacamole/src/main/webapp/app/rest/services/userService.js b/guacamole/src/main/webapp/app/rest/services/userService.js index 268e00e69..e7b3a359e 100644 --- a/guacamole/src/main/webapp/app/rest/services/userService.js +++ b/guacamole/src/main/webapp/app/rest/services/userService.js @@ -78,65 +78,6 @@ angular.module('rest').factory('userService', ['$injector', }; - /** - * Returns a promise which resolves with all users accessible by the - * current user, as a map of @link{User} arrays by the identifier of their - * corresponding data source. All given data sources are queried. If an - * error occurs while retrieving any user, the promise will be rejected. - * - * @param {String[]} dataSources - * The unique identifier of the data sources containing the user to be - * retrieved. These identifiers correspond to AuthenticationProviders - * within the Guacamole web application. - * - * @param {String[]} [permissionTypes] - * The set of permissions to filter with. A user must have one or more - * of these permissions for a user to appear in the result. - * If null, no filtering will be performed. Valid values are listed - * within PermissionSet.ObjectType. - * - * @returns {Promise.>} - * A promise which resolves with all user objects available to the - * current user, as a map of @link{User} arrays grouped by the - * identifier of their corresponding data source. - */ - service.getAllUsers = function getAllUsers(dataSources, permissionTypes) { - - var deferred = $q.defer(); - - var userRequests = []; - var userArrays = {}; - - // Retrieve all users from all data sources - angular.forEach(dataSources, function retrieveUsers(dataSource) { - userRequests.push( - service.getUsers(dataSource, permissionTypes) - .success(function usersRetrieved(users) { - userArrays[dataSource] = users; - }) - ); - }); - - // Resolve when all requests are completed - $q.all(userRequests) - .then( - - // All requests completed successfully - function allUsersRetrieved() { - deferred.resolve(userArrays); - }, - - // At least one request failed - function userRetrievalFailed(e) { - deferred.reject(e); - } - - ); - - return deferred.promise; - - }; - /** * Makes a request to the REST API to get the user having the given * username, returning a promise that provides the corresponding diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js index 494148888..e6f6dd709 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js @@ -43,6 +43,7 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings // Required services var $location = $injector.get('$location'); var authenticationService = $injector.get('authenticationService'); + var dataSourceService = $injector.get('dataSourceService'); var guacNotification = $injector.get('guacNotification'); var permissionService = $injector.get('permissionService'); var userService = $injector.get('userService'); @@ -193,7 +194,7 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings }; // Retrieve current permissions - permissionService.getAllPermissions(dataSources, currentUsername) + dataSourceService.apply(permissionService.getPermissions, dataSources, currentUsername) .then(function permissionsRetrieved(permissions) { // Store retrieved permissions @@ -206,7 +207,7 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings }); // Retrieve all users for whom we have UPDATE or DELETE permission - userService.getAllUsers(dataSources, [ + dataSourceService.apply(userService.getUsers, dataSources, [ PermissionSet.ObjectPermissionType.UPDATE, PermissionSet.ObjectPermissionType.DELETE ]) From 16cd2ab49bdbe6dbd0ecc840aa9a90f24e7bc1ce Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 31 Aug 2015 14:30:00 -0700 Subject: [PATCH 26/47] GUAC-586: Use auth provider identifiers within connection REST service. --- .../connection/ConnectionRESTService.java | 72 +++++++++++++++---- .../app/rest/services/connectionService.js | 24 +++---- 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java index 2f29fbe81..270e3c302 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java @@ -48,6 +48,7 @@ import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; +import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; @@ -60,7 +61,7 @@ import org.slf4j.LoggerFactory; * * @author James Muehlner */ -@Path("/connections") +@Path("/data/{dataSource}/connections") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ConnectionRESTService { @@ -89,6 +90,10 @@ public class ConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection to be retrieved. + * * @param connectionID * The identifier of the connection to retrieve. * @@ -102,12 +107,14 @@ public class ConnectionRESTService { @Path("/{connectionID}") @AuthProviderRESTExposure public APIConnection getConnection(@QueryParam("token") String authToken, - @PathParam("connectionID") String connectionID) throws GuacamoleException { + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionID") String connectionID) + throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); // Retrieve the requested connection - return new APIConnection(retrievalService.retrieveConnection(userContext, connectionID)); + return new APIConnection(retrievalService.retrieveConnection(session, authProviderIdentifier, connectionID)); } @@ -118,6 +125,11 @@ public class ConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection whose parameters are to be + * retrieved. + * * @param connectionID * The identifier of the connection. * @@ -131,9 +143,12 @@ public class ConnectionRESTService { @Path("/{connectionID}/parameters") @AuthProviderRESTExposure public Map getConnectionParameters(@QueryParam("token") String authToken, - @PathParam("connectionID") String connectionID) throws GuacamoleException { + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionID") String connectionID) + throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); User self = userContext.self(); // Retrieve permission sets @@ -163,6 +178,11 @@ public class ConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection whose history is to be + * retrieved. + * * @param connectionID * The identifier of the connection. * @@ -177,12 +197,14 @@ public class ConnectionRESTService { @Path("/{connectionID}/history") @AuthProviderRESTExposure public List getConnectionHistory(@QueryParam("token") String authToken, - @PathParam("connectionID") String connectionID) throws GuacamoleException { + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionID") String connectionID) + throws GuacamoleException { + + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); - UserContext userContext = authenticationService.getUserContext(authToken); - // Retrieve the requested connection - Connection connection = retrievalService.retrieveConnection(userContext, connectionID); + Connection connection = retrievalService.retrieveConnection(session, authProviderIdentifier, connectionID); // Retrieve the requested connection's history List apiRecords = new ArrayList(); @@ -201,6 +223,10 @@ public class ConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection to be deleted. + * * @param connectionID * The identifier of the connection to delete. * @@ -210,10 +236,13 @@ public class ConnectionRESTService { @DELETE @Path("/{connectionID}") @AuthProviderRESTExposure - public void deleteConnection(@QueryParam("token") String authToken, @PathParam("connectionID") String connectionID) + public void deleteConnection(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionID") String connectionID) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get the connection directory Directory connectionDirectory = userContext.getConnectionDirectory(); @@ -231,6 +260,10 @@ public class ConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext in which the connection is to be created. + * * @param connection * The connection to create. * @@ -244,9 +277,11 @@ public class ConnectionRESTService { @Produces(MediaType.TEXT_PLAIN) @AuthProviderRESTExposure public String createConnection(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, APIConnection connection) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Validate that connection data was provided if (connection == null) @@ -270,6 +305,10 @@ public class ConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection to be updated. + * * @param connectionID * The identifier of the connection to update. * @@ -283,9 +322,12 @@ public class ConnectionRESTService { @Path("/{connectionID}") @AuthProviderRESTExposure public void updateConnection(@QueryParam("token") String authToken, - @PathParam("connectionID") String connectionID, APIConnection connection) throws GuacamoleException { + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionID") String connectionID, + APIConnection connection) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Validate that connection data was provided if (connection == null) diff --git a/guacamole/src/main/webapp/app/rest/services/connectionService.js b/guacamole/src/main/webapp/app/rest/services/connectionService.js index 2afba2489..cbf40eb81 100644 --- a/guacamole/src/main/webapp/app/rest/services/connectionService.js +++ b/guacamole/src/main/webapp/app/rest/services/connectionService.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -48,7 +48,7 @@ angular.module('rest').factory('connectionService', ['$injector', * // Do something with the connection * }); */ - service.getConnection = function getConnection(id) { + service.getConnection = function getConnection(dataSource, id) { // Build HTTP parameters set var httpParameters = { @@ -59,7 +59,7 @@ angular.module('rest').factory('connectionService', ['$injector', return $http({ cache : cacheService.connections, method : 'GET', - url : 'api/connections/' + encodeURIComponent(id), + url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id), params : httpParameters }); @@ -77,7 +77,7 @@ angular.module('rest').factory('connectionService', ['$injector', * A promise which will resolve with an array of * @link{ConnectionHistoryEntry} objects upon success. */ - service.getConnectionHistory = function getConnectionHistory(id) { + service.getConnectionHistory = function getConnectionHistory(dataSource, id) { // Build HTTP parameters set var httpParameters = { @@ -87,7 +87,7 @@ angular.module('rest').factory('connectionService', ['$injector', // Retrieve connection history return $http({ method : 'GET', - url : 'api/connections/' + encodeURIComponent(id) + '/history', + url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id) + '/history', params : httpParameters }); @@ -105,7 +105,7 @@ angular.module('rest').factory('connectionService', ['$injector', * A promise which will resolve with an map of parameter name/value * pairs upon success. */ - service.getConnectionParameters = function getConnectionParameters(id) { + service.getConnectionParameters = function getConnectionParameters(dataSource, id) { // Build HTTP parameters set var httpParameters = { @@ -116,7 +116,7 @@ angular.module('rest').factory('connectionService', ['$injector', return $http({ cache : cacheService.connections, method : 'GET', - url : 'api/connections/' + encodeURIComponent(id) + '/parameters', + url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id) + '/parameters', params : httpParameters }); @@ -135,7 +135,7 @@ angular.module('rest').factory('connectionService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * save operation is successful. */ - service.saveConnection = function saveConnection(connection) { + service.saveConnection = function saveConnection(dataSource, connection) { // Build HTTP parameters set var httpParameters = { @@ -146,7 +146,7 @@ angular.module('rest').factory('connectionService', ['$injector', if (!connection.identifier) { return $http({ method : 'POST', - url : 'api/connections', + url : 'api/data/' + encodeURIComponent(dataSource) + '/connections', params : httpParameters, data : connection }) @@ -162,7 +162,7 @@ angular.module('rest').factory('connectionService', ['$injector', else { return $http({ method : 'PUT', - url : 'api/connections/' + encodeURIComponent(connection.identifier), + url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(connection.identifier), params : httpParameters, data : connection }) @@ -185,7 +185,7 @@ angular.module('rest').factory('connectionService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * delete operation is successful. */ - service.deleteConnection = function deleteConnection(connection) { + service.deleteConnection = function deleteConnection(dataSource, connection) { // Build HTTP parameters set var httpParameters = { @@ -195,7 +195,7 @@ angular.module('rest').factory('connectionService', ['$injector', // Delete connection return $http({ method : 'DELETE', - url : 'api/connections/' + encodeURIComponent(connection.identifier), + url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(connection.identifier), params : httpParameters }) From a6cab24983aab04397601b1b9202a75c8d5dbcce Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 31 Aug 2015 14:30:09 -0700 Subject: [PATCH 27/47] GUAC-586: Use auth provider identifiers within connection group REST service. --- .../ConnectionGroupRESTService.java | 55 +++++++++++++++---- .../rest/services/connectionGroupService.js | 23 ++++---- 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java index bb3b843cd..5899d32d7 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java @@ -40,6 +40,7 @@ import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; +import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; @@ -51,7 +52,7 @@ import org.slf4j.LoggerFactory; * * @author James Muehlner */ -@Path("/connectionGroups") +@Path("/data/{dataSource}/connectionGroups") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ConnectionGroupRESTService { @@ -80,6 +81,10 @@ public class ConnectionGroupRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection group to be retrieved. + * * @param connectionGroupID * The ID of the connection group to retrieve. * @@ -92,13 +97,15 @@ public class ConnectionGroupRESTService { @GET @Path("/{connectionGroupID}") @AuthProviderRESTExposure - public APIConnectionGroup getConnectionGroup(@QueryParam("token") String authToken, - @PathParam("connectionGroupID") String connectionGroupID) throws GuacamoleException { + public APIConnectionGroup getConnectionGroup(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionGroupID") String connectionGroupID) + throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); // Retrieve the requested connection group - return new APIConnectionGroup(retrievalService.retrieveConnectionGroup(userContext, connectionGroupID)); + return new APIConnectionGroup(retrievalService.retrieveConnectionGroup(session, authProviderIdentifier, connectionGroupID)); } @@ -109,6 +116,10 @@ public class ConnectionGroupRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection group to be retrieved. + * * @param connectionGroupID * The ID of the connection group to retrieve. * @@ -129,11 +140,13 @@ public class ConnectionGroupRESTService { @Path("/{connectionGroupID}/tree") @AuthProviderRESTExposure public APIConnectionGroup getConnectionGroupTree(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionGroupID") String connectionGroupID, @QueryParam("permission") List permissions) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Retrieve the requested tree, filtering by the given permissions ConnectionGroup treeRoot = retrievalService.retrieveConnectionGroup(userContext, connectionGroupID); @@ -151,6 +164,10 @@ public class ConnectionGroupRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection group to be deleted. + * * @param connectionGroupID * The identifier of the connection group to delete. * @@ -161,9 +178,12 @@ public class ConnectionGroupRESTService { @Path("/{connectionGroupID}") @AuthProviderRESTExposure public void deleteConnectionGroup(@QueryParam("token") String authToken, - @PathParam("connectionGroupID") String connectionGroupID) throws GuacamoleException { + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionGroupID") String connectionGroupID) + throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get the connection group directory Directory connectionGroupDirectory = userContext.getConnectionGroupDirectory(); @@ -183,6 +203,10 @@ public class ConnectionGroupRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext in which the connection group is to be created. + * * @param connectionGroup * The connection group to create. * @@ -196,9 +220,11 @@ public class ConnectionGroupRESTService { @Produces(MediaType.TEXT_PLAIN) @AuthProviderRESTExposure public String createConnectionGroup(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, APIConnectionGroup connectionGroup) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Validate that connection group data was provided if (connectionGroup == null) @@ -222,6 +248,10 @@ public class ConnectionGroupRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection group to be updated. + * * @param connectionGroupID * The identifier of the existing connection group to update. * @@ -235,10 +265,13 @@ public class ConnectionGroupRESTService { @Path("/{connectionGroupID}") @AuthProviderRESTExposure public void updateConnectionGroup(@QueryParam("token") String authToken, - @PathParam("connectionGroupID") String connectionGroupID, APIConnectionGroup connectionGroup) + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionGroupID") String connectionGroupID, + APIConnectionGroup connectionGroup) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Validate that connection group data was provided if (connectionGroup == null) diff --git a/guacamole/src/main/webapp/app/rest/services/connectionGroupService.js b/guacamole/src/main/webapp/app/rest/services/connectionGroupService.js index d43c58f52..1d48c160d 100644 --- a/guacamole/src/main/webapp/app/rest/services/connectionGroupService.js +++ b/guacamole/src/main/webapp/app/rest/services/connectionGroupService.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -28,6 +28,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', // Required services var $http = $injector.get('$http'); + var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); @@ -57,7 +58,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', * A promise which will resolve with a @link{ConnectionGroup} upon * success. */ - service.getConnectionGroupTree = function getConnectionGroupTree(connectionGroupID, permissionTypes) { + service.getConnectionGroupTree = function getConnectionGroupTree(dataSource, connectionGroupID, permissionTypes) { // Use the root connection group ID if no ID is passed in connectionGroupID = connectionGroupID || ConnectionGroup.ROOT_IDENTIFIER; @@ -75,12 +76,12 @@ angular.module('rest').factory('connectionGroupService', ['$injector', return $http({ cache : cacheService.connections, method : 'GET', - url : 'api/connectionGroups/' + encodeURIComponent(connectionGroupID) + '/tree', + url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroupID) + '/tree', params : httpParameters }); }; - + /** * Makes a request to the REST API to get an individual connection group, * returning a promise that provides the corresponding @@ -94,7 +95,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', * A promise which will resolve with a @link{ConnectionGroup} upon * success. */ - service.getConnectionGroup = function getConnectionGroup(connectionGroupID) { + service.getConnectionGroup = function getConnectionGroup(dataSource, connectionGroupID) { // Use the root connection group ID if no ID is passed in connectionGroupID = connectionGroupID || ConnectionGroup.ROOT_IDENTIFIER; @@ -108,7 +109,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', return $http({ cache : cacheService.connections, method : 'GET', - url : 'api/connectionGroups/' + encodeURIComponent(connectionGroupID), + url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroupID), params : httpParameters }); @@ -127,7 +128,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * save operation is successful. */ - service.saveConnectionGroup = function saveConnectionGroup(connectionGroup) { + service.saveConnectionGroup = function saveConnectionGroup(dataSource, connectionGroup) { // Build HTTP parameters set var httpParameters = { @@ -138,7 +139,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', if (!connectionGroup.identifier) { return $http({ method : 'POST', - url : 'api/connectionGroups', + url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups', params : httpParameters, data : connectionGroup }) @@ -154,7 +155,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', else { return $http({ method : 'PUT', - url : 'api/connectionGroups/' + encodeURIComponent(connectionGroup.identifier), + url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroup.identifier), params : httpParameters, data : connectionGroup }) @@ -177,7 +178,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * delete operation is successful. */ - service.deleteConnectionGroup = function deleteConnectionGroup(connectionGroup) { + service.deleteConnectionGroup = function deleteConnectionGroup(dataSource, connectionGroup) { // Build HTTP parameters set var httpParameters = { @@ -187,7 +188,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', // Delete connection group return $http({ method : 'DELETE', - url : 'api/connectionGroups/' + encodeURIComponent(connectionGroup.identifier), + url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroup.identifier), params : httpParameters }) From e0a4fc325797a599a245d97700970edfc879aaed Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 31 Aug 2015 14:30:21 -0700 Subject: [PATCH 28/47] GUAC-586: Use auth provider identifiers within active connection REST service. --- .../ActiveConnectionRESTService.java | 27 ++++++- .../rest/services/activeConnectionService.js | 80 +++++++++++++++++-- 2 files changed, 97 insertions(+), 10 deletions(-) diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java index 107f97ef8..42ae1c15f 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java @@ -30,6 +30,7 @@ import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; @@ -44,8 +45,10 @@ import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; +import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.APIPatch; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; +import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.PATCH; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.slf4j.Logger; @@ -56,7 +59,7 @@ import org.slf4j.LoggerFactory; * * @author Michael Jumper */ -@Path("/activeConnections") +@Path("/data/{dataSource}/activeConnections") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ActiveConnectionRESTService { @@ -72,6 +75,12 @@ public class ActiveConnectionRESTService { @Inject private AuthenticationService authenticationService; + /** + * Service for convenient retrieval of objects. + */ + @Inject + private ObjectRetrievalService retrievalService; + /** * Gets a list of active connections in the system, filtering the returned * list by the given permissions, if specified. @@ -80,6 +89,10 @@ public class ActiveConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the active connections to be retrieved. + * * @param permissions * The set of permissions to filter with. A user must have one or more * of these permissions for a user to appear in the result. @@ -96,10 +109,12 @@ public class ActiveConnectionRESTService { @GET @AuthProviderRESTExposure public Map getActiveConnections(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, @QueryParam("permission") List permissions) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); User self = userContext.self(); // Do not filter on permissions if no permissions are specified @@ -140,6 +155,10 @@ public class ActiveConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the active connections to be deleted. + * * @param patches * The active connection patches to apply for this request. * @@ -149,9 +168,11 @@ public class ActiveConnectionRESTService { @PATCH @AuthProviderRESTExposure public void patchTunnels(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, List> patches) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get the directory Directory activeConnectionDirectory = userContext.getActiveConnectionDirectory(); diff --git a/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js b/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js index a96ebdf3f..99428a91c 100644 --- a/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js +++ b/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js @@ -23,8 +23,13 @@ /** * Service for operating on active connections via the REST API. */ -angular.module('rest').factory('activeConnectionService', ['$http', 'authenticationService', - function activeConnectionService($http, authenticationService) { +angular.module('rest').factory('activeConnectionService', ['$injector', + function activeConnectionService($injector) { + + // Required services + var $http = $injector.get('$http'); + var $q = $injector.get('$q'); + var authenticationService = $injector.get('authenticationService'); var service = {}; @@ -39,13 +44,12 @@ angular.module('rest').factory('activeConnectionService', ['$http', 'authenticat * result. If null, no filtering will be performed. Valid values are * listed within PermissionSet.ObjectType. * - * @returns {Promise.>} * A promise which will resolve with a map of @link{ActiveConnection} * objects, where each key is the identifier of the corresponding * active connection. */ - service.getActiveConnections = function getActiveConnections(permissionTypes) { + service.getActiveConnections = function getActiveConnections(dataSource, permissionTypes) { // Build HTTP parameters set var httpParameters = { @@ -59,12 +63,74 @@ angular.module('rest').factory('activeConnectionService', ['$http', 'authenticat // Retrieve tunnels return $http({ method : 'GET', - url : 'api/activeConnections', + url : 'api/data/' + encodeURIComponent(dataSource) + '/activeConnections', params : httpParameters }); }; + /** + * Returns a promise which resolves with all active connections accessible + * by the current user, as a map of @link{ActiveConnection} maps, as would + * be returned by getActiveConnections(), grouped by the identifier of + * their corresponding data source. All given data sources are queried. If + * an error occurs while retrieving any ActiveConnection map, the promise + * will be rejected. + * + * @param {String[]} dataSources + * The unique identifier of the data sources containing the active + * connections to be retrieved. These identifiers correspond to + * AuthenticationProviders within the Guacamole web application. + * + * @param {String[]} [permissionTypes] + * The set of permissions to filter with. A user must have one or more + * of these permissions for an active connection to appear in the + * result. If null, no filtering will be performed. Valid values are + * listed within PermissionSet.ObjectType. + * + * @returns {Promise.>>} + * A promise which resolves with all active connections available to + * the current user, as a map of ActiveConnection maps, as would be + * returned by getActiveConnections(), grouped by the identifier of + * their corresponding data source. + */ + service.getAllActiveConnections = function getAllActiveConnections(dataSources, permissionTypes) { + + var deferred = $q.defer(); + + var activeConnectionRequests = []; + var activeConnectionMaps = {}; + + // Retrieve all active connections from all data sources + angular.forEach(dataSources, function retrieveActiveConnections(dataSource) { + activeConnectionRequests.push( + service.getActiveConnections(dataSource, permissionTypes) + .success(function activeConnectionsRetrieved(activeConnections) { + activeConnectionMaps[dataSource] = activeConnections; + }) + ); + }); + + // Resolve when all requests are completed + $q.all(activeConnectionRequests) + .then( + + // All requests completed successfully + function allActiveConnectionsRetrieved() { + deferred.resolve(userArrays); + }, + + // At least one request failed + function activeConnectionRetrievalFailed(e) { + deferred.reject(e); + } + + ); + + return deferred.promise; + + }; + /** * Makes a request to the REST API to delete the active connections having * the given identifiers, effectively disconnecting them, returning a @@ -77,7 +143,7 @@ angular.module('rest').factory('activeConnectionService', ['$http', 'authenticat * A promise for the HTTP call which will succeed if and only if the * delete operation is successful. */ - service.deleteActiveConnections = function deleteActiveConnections(identifiers) { + service.deleteActiveConnections = function deleteActiveConnections(dataSource, identifiers) { // Build HTTP parameters set var httpParameters = { @@ -96,7 +162,7 @@ angular.module('rest').factory('activeConnectionService', ['$http', 'authenticat // Perform active connection deletion via PATCH return $http({ method : 'PATCH', - url : 'api/activeConnections', + url : 'api/data/' + encodeURIComponent(dataSource) + '/activeConnections', params : httpParameters, data : activeConnectionPatch }); From a68243765af123ce501b1359772feb9225406ec9 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 31 Aug 2015 14:30:55 -0700 Subject: [PATCH 29/47] GUAC-586: Pull connection groups from multiple data sources when determining home page. --- .../navigation/services/userPageService.js | 114 +++++++++++------- 1 file changed, 71 insertions(+), 43 deletions(-) diff --git a/guacamole/src/main/webapp/app/navigation/services/userPageService.js b/guacamole/src/main/webapp/app/navigation/services/userPageService.js index 574307104..43dfaf5f9 100644 --- a/guacamole/src/main/webapp/app/navigation/services/userPageService.js +++ b/guacamole/src/main/webapp/app/navigation/services/userPageService.js @@ -69,48 +69,67 @@ angular.module('navigation').factory('userPageService', ['$injector', /** * Returns an appropriate home page for the current user. * - * @param {ConnectionGroup} rootGroup - * The root of the connection group tree for the current user. + * @param {Object.} rootGroups + * A map of all root connection groups visible to the current user, + * where each key is the identifier of the corresponding data source. * * @returns {PageDefinition} * The user's home page. */ - var generateHomePage = function generateHomePage(rootGroup) { + var generateHomePage = function generateHomePage(rootGroups) { - // Get children - var connections = rootGroup.childConnections || []; - var connectionGroups = rootGroup.childConnectionGroups || []; + var homePage = null; - // Use main connection list screen as home if multiple connections - // are available - if (connections.length + connectionGroups.length === 1) { + // Determine whether a connection or balancing group should serve as + // the home page + for (var dataSource in rootGroups) { - var connection = connections[0]; - var connectionGroup = connectionGroups[0]; + // Get corresponding root group + var rootGroup = rootGroups[dataSource]; + + // Get children + var connections = rootGroup.childConnections || []; + var connectionGroups = rootGroup.childConnectionGroups || []; + + // If exactly one connection or balancing group is available, use + // that as the home page + if (homePage === null && connections.length + connectionGroups.length === 1) { + + var connection = connections[0]; + var connectionGroup = connectionGroups[0]; + + // Only one connection present, use as home page + if (connection) { + homePage = new PageDefinition( + connection.name, + '/client/c/' + encodeURIComponent(connection.identifier) + ); + } + + // Only one balancing group present, use as home page + if (connectionGroup + && connectionGroup.type === ConnectionGroup.Type.BALANCING + && _.isEmpty(connectionGroup.childConnections) + && _.isEmpty(connectionGroup.childConnectionGroups)) { + homePage = new PageDefinition( + connectionGroup.name, + '/client/g/' + encodeURIComponent(connectionGroup.identifier) + ); + } - // Only one connection present, use as home page - if (connection) { - return new PageDefinition( - connection.name, - '/client/c/' + encodeURIComponent(connection.identifier) - ); } - // Only one connection present, use as home page - if (connectionGroup - && connectionGroup.type === ConnectionGroup.Type.BALANCING - && _.isEmpty(connectionGroup.childConnections) - && _.isEmpty(connectionGroup.childConnectionGroups)) { - return new PageDefinition( - connectionGroup.name, - '/client/g/' + encodeURIComponent(connectionGroup.identifier) - ); + // Otherwise, a connection or balancing group cannot serve as the + // home page + else { + homePage = null; + break; } - } + } // end for each data source - // Resolve promise with default home page - return SYSTEM_HOME_PAGE; + // Use default home page if no other is available + return homePage || SYSTEM_HOME_PAGE; }; @@ -125,10 +144,14 @@ angular.module('navigation').factory('userPageService', ['$injector', var deferred = $q.defer(); - // Resolve promise using home page derived from root connection group - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER) - .success(function rootConnectionGroupRetrieved(rootGroup) { - deferred.resolve(generateHomePage(rootGroup)); + // Resolve promise using home page derived from root connection groups + dataSourceService.apply( + connectionGroupService.getConnectionGroupTree, + authenticationService.getAvailableDataSources(), + ConnectionGroup.ROOT_IDENTIFIER + ) + .then(function rootConnectionGroupsRetrieved(rootGroups) { + deferred.resolve(generateHomePage(rootGroups)); }); return deferred.promise; @@ -280,8 +303,9 @@ angular.module('navigation').factory('userPageService', ['$injector', * include the home page, manage pages, etc. In the case that there are no * applicable pages of this sort, it may return a client page. * - * @param {ConnectionGroup} rootGroup - * The root of the connection group tree for the current user. + * @param {Object.} rootGroups + * A map of all root connection groups visible to the current user, + * where each key is the identifier of the corresponding data source. * * @param {Object.} permissions * A map of all permissions granted to the current user, where each @@ -290,12 +314,12 @@ angular.module('navigation').factory('userPageService', ['$injector', * @returns {Page[]} * An array of all main pages that the current user can visit. */ - var generateMainPages = function generateMainPages(rootGroup, permissions) { + var generateMainPages = function generateMainPages(rootGroups, permissions) { var pages = []; // Get home page and settings pages - var homePage = generateHomePage(rootGroup); + var homePage = generateHomePage(rootGroups); var settingsPages = generateSettingsPages(permissions); // Only include the home page in the list of main pages if the user @@ -328,7 +352,7 @@ angular.module('navigation').factory('userPageService', ['$injector', var deferred = $q.defer(); - var rootGroup = null; + var rootGroups = null; var permissions = null; /** @@ -336,14 +360,18 @@ angular.module('navigation').factory('userPageService', ['$injector', * insufficient data is available, this function does nothing. */ var resolveMainPages = function resolveMainPages() { - if (rootGroup && permissions) - deferred.resolve(generateMainPages(rootGroup, permissions)); + if (rootGroups && permissions) + deferred.resolve(generateMainPages(rootGroups, permissions)); }; // Retrieve root group, resolving main pages if possible - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER) - .success(function rootConnectionGroupRetrieved(retrievedRootGroup) { - rootGroup = retrievedRootGroup; + dataSourceService.apply( + connectionGroupService.getConnectionGroupTree, + authenticationService.getAvailableDataSources(), + ConnectionGroup.ROOT_IDENTIFIER + ) + .then(function rootConnectionGroupsRetrieved(retrievedRootGroups) { + rootGroups = retrievedRootGroups; resolveMainPages(); }); From fb39588db56dd8a853085f7b3bb5ba8761e42fec Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 31 Aug 2015 16:55:23 -0700 Subject: [PATCH 30/47] GUAC-586: Add data source support to guacGroupList. --- .../app/groupList/directives/guacGroupList.js | 44 ++++++++++--------- .../groupList/templates/guacGroupList.html | 2 +- .../app/groupList/types/GroupListItem.js | 33 +++++++++++--- 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js index d5ba313d4..bbfd3c8b7 100644 --- a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js +++ b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js @@ -32,11 +32,12 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() scope: { /** - * The connection group to display. + * The connection groups to display as a map of data source + * identifier to corresponding root group. * - * @type ConnectionGroup|Object + * @type Object. */ - connectionGroup : '=', + connectionGroups : '=', /** * Arbitrary object which shall be made available to the connection @@ -106,6 +107,8 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() */ var connectionCount = {}; + $scope.rootItems = []; + // Count active connections by connection identifier activeConnectionService.getActiveConnections() .success(function activeConnectionsRetrieved(activeConnections) { @@ -173,29 +176,30 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() }; // Set contents whenever the connection group is assigned or changed - $scope.$watch("connectionGroup", function setContents(connectionGroup) { + $scope.$watch('connectionGroups', function setContents(connectionGroups) { - if (connectionGroup) { + $scope.rootItems = []; - // Create item hierarchy, including connections only if they will be visible - var rootItem = GroupListItem.fromConnectionGroup(connectionGroup, - !!$scope.connectionTemplate, countActiveConnections); + // If connection groups are given, add them to the interface + if (connectionGroups) { + angular.forEach(connectionGroups, function addConnectionGroup(connectionGroup, dataSource) { - // If root group is to be shown, wrap that group as the child of a fake root group - if ($scope.showRootGroup) - $scope.rootItem = new GroupListItem({ - isConnectionGroup : true, - isBalancing : false, - children : [ rootItem ] - }); + var rootItem = GroupListItem.fromConnectionGroup(dataSource, connectionGroup, + !!$scope.connectionTemplate, countActiveConnections); - // If not wrapped, only the descendants of the root will be shown - else - $scope.rootItem = rootItem; + // If root group is to be shown, add it as a root item + if ($scope.showRootGroup) + $scope.rootItems.push(rootItem); + // Otherwise, add its children as root items + else { + angular.forEach(rootItem.children, function addRootItem(child) { + $scope.rootItems.push(child); + }); + } + + }); } - else - $scope.rootItem = null; }); diff --git a/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html b/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html index ae1ef679f..9059ab633 100644 --- a/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html +++ b/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html @@ -57,7 +57,7 @@
    -
    diff --git a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js index aa9a0f0de..b73be67c4 100644 --- a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js +++ b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js @@ -39,6 +39,14 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio // Use empty object by default template = template || {}; + /** + * The identifier of the data source associated with the connection or + * connection group this item represents. + * + * @type String + */ + this.dataSource = template.dataSource; + /** * The unique identifier associated with the connection or connection * group this item represents. @@ -124,6 +132,10 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio /** * Creates a new GroupListItem using the contents of the given connection. * + * @param {String} dataSource + * The identifier of the data source containing the given connection + * group. + * * @param {ConnectionGroup} connection * The connection whose contents should be represented by the new * GroupListItem. @@ -136,7 +148,8 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio * @returns {GroupListItem} * A new GroupListItem which represents the given connection. */ - GroupListItem.fromConnection = function fromConnection(connection, countActiveConnections) { + GroupListItem.fromConnection = function fromConnection(dataSource, + connection, countActiveConnections) { // Return item representing the given connection return new GroupListItem({ @@ -145,6 +158,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio name : connection.name, identifier : connection.identifier, protocol : connection.protocol, + dataSource : dataSource, // Type information isConnection : true, @@ -172,6 +186,10 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio * Creates a new GroupListItem using the contents and descendants of the * given connection group. * + * @param {String} dataSource + * The identifier of the data source containing the given connection + * group. + * * @param {ConnectionGroup} connectionGroup * The connection group whose contents and descendants should be * represented by the new GroupListItem and its descendants. @@ -195,22 +213,26 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio * A new GroupListItem which represents the given connection group, * including all descendants. */ - GroupListItem.fromConnectionGroup = function fromConnectionGroup(connectionGroup, - includeConnections, countActiveConnections, countActiveConnectionGroups) { + GroupListItem.fromConnectionGroup = function fromConnectionGroup(dataSource, + connectionGroup, includeConnections, countActiveConnections, + countActiveConnectionGroups) { var children = []; // Add any child connections if (connectionGroup.childConnections && includeConnections !== false) { connectionGroup.childConnections.forEach(function addChildConnection(child) { - children.push(GroupListItem.fromConnection(child, countActiveConnections)); + children.push(GroupListItem.fromConnection(dataSource, child, + countActiveConnections)); }); } // Add any child groups if (connectionGroup.childConnectionGroups) { connectionGroup.childConnectionGroups.forEach(function addChildGroup(child) { - children.push(GroupListItem.fromConnectionGroup(child, includeConnections, countActiveConnections, countActiveConnectionGroups)); + children.push(GroupListItem.fromConnectionGroup(dataSource, + child, includeConnections, countActiveConnections, + countActiveConnectionGroups)); }); } @@ -220,6 +242,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio // Identifying information name : connectionGroup.name, identifier : connectionGroup.identifier, + dataSource : dataSource, // Type information isConnection : false, From 2ea4b609bb40e1193e697b67745126f967b27153 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 31 Aug 2015 16:56:14 -0700 Subject: [PATCH 31/47] GUAC-586: Migrate user management UI to data source version of guacGroupList. --- .../controllers/manageUserController.js | 22 ++++++++++++------- .../app/manage/templates/manageUser.html | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index 5e8eaf1a3..983ae71ad 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -113,12 +113,13 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.permissionFlags = null; /** - * The root connection group of the connection group hierarchy within the - * data source containing the user being edited/created. + * A map of data source identifiers to the root connection groups within + * thost data sources. As only one data source is applicable to any one + * user being edited/created, this will only contain a single key. * - * @type ConnectionGroup + * @type Object. */ - $scope.rootGroup = null; + $scope.rootGroups = null; /** * All permissions associated with the current user, or null if the user's @@ -167,7 +168,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto return $scope.user !== null && $scope.permissionFlags !== null - && $scope.rootGroup !== null + && $scope.rootGroups !== null && $scope.permissions !== null && $scope.attributes !== null; @@ -373,9 +374,14 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }); // Retrieve all connections for which we have ADMINISTER permission - connectionGroupService.getConnectionGroupTree(dataSource, ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER]) - .success(function connectionGroupReceived(rootGroup) { - $scope.rootGroup = rootGroup; + dataSourceService.apply( + connectionGroupService.getConnectionGroupTree, + [dataSource], + ConnectionGroup.ROOT_IDENTIFIER, + [PermissionSet.ObjectPermissionType.ADMINISTER] + ) + .then(function connectionGroupReceived(rootGroups) { + $scope.rootGroups = rootGroups; }); // Query the user's permissions for the current user diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index 2f28b3335..442ee5116 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -84,7 +84,7 @@ THE SOFTWARE.
    From a72cc118f45b9206cfd616671b1b73cf5b10ced1 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 31 Aug 2015 17:02:53 -0700 Subject: [PATCH 32/47] GUAC-586: Support data sources within home screen. --- .../app/home/controllers/homeController.js | 30 +++++++----- .../home/directives/guacRecentConnections.js | 47 +++++++++++++------ .../main/webapp/app/home/templates/home.html | 4 +- 3 files changed, 52 insertions(+), 29 deletions(-) diff --git a/guacamole/src/main/webapp/app/home/controllers/homeController.js b/guacamole/src/main/webapp/app/home/controllers/homeController.js index 89f88e25a..1e0573ebc 100644 --- a/guacamole/src/main/webapp/app/home/controllers/homeController.js +++ b/guacamole/src/main/webapp/app/home/controllers/homeController.js @@ -27,19 +27,21 @@ angular.module('home').controller('homeController', ['$scope', '$injector', function homeController($scope, $injector) { // Get required types - var ConnectionGroup = $injector.get("ConnectionGroup"); + var ConnectionGroup = $injector.get('ConnectionGroup'); // Get required services - var authenticationService = $injector.get("authenticationService"); - var connectionGroupService = $injector.get("connectionGroupService"); - + var authenticationService = $injector.get('authenticationService'); + var connectionGroupService = $injector.get('connectionGroupService'); + var dataSourceService = $injector.get('dataSourceService'); + /** - * The root connection group, or null if the connection group hierarchy has - * not yet been loaded. + * Map of data source identifier to the root connection group of that data + * source, or null if the connection group hierarchy has not yet been + * loaded. * - * @type ConnectionGroup + * @type Object. */ - $scope.rootConnectionGroup = null; + $scope.rootConnectionGroups = null; /** * Returns whether critical data has completed being loaded. @@ -54,10 +56,14 @@ angular.module('home').controller('homeController', ['$scope', '$injector', }; - // Retrieve root group and all descendants - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER) - .success(function rootGroupRetrieved(rootConnectionGroup) { - $scope.rootConnectionGroup = rootConnectionGroup; + // Retrieve root groups and all descendants + dataSourceService.apply( + connectionGroupService.getConnectionGroupTree, + authenticationService.getAvailableDataSources(), + ConnectionGroup.ROOT_IDENTIFIER + ) + .then(function rootGroupsRetrieved(rootConnectionGroups) { + $scope.rootConnectionGroups = rootConnectionGroups; }); }]); diff --git a/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js b/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js index 5dddd9717..c6e8443ba 100644 --- a/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js +++ b/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js @@ -31,13 +31,15 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo scope: { /** - * The root connection group, and all visible descendants. - * Recent connections will only be shown if they exist within this - * hierarchy, regardless of their existence within the history. + * The root connection groups to display, and all visible + * descendants, as a map of data source identifier to the root + * connection group within that data source. Recent connections + * will only be shown if they exist within this hierarchy, + * regardless of their existence within the history. * - * @type ConnectionGroup + * @type Object. */ - rootGroup : '=' + rootGroups : '=' }, @@ -91,11 +93,15 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo /** * Adds the given connection to the internal set of visible * objects. - * + * + * @param {String} dataSource + * The identifier of the data source associated with the + * given connection group. + * * @param {Connection} connection * The connection to add to the internal set of visible objects. */ - var addVisibleConnection = function addVisibleConnection(connection) { + var addVisibleConnection = function addVisibleConnection(dataSource, connection) { // Add given connection to set of visible objects visibleObjects['c/' + connection.identifier] = connection; @@ -105,28 +111,36 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo /** * Adds the given connection group to the internal set of visible * objects, along with any descendants. - * + * + * @param {String} dataSource + * The identifier of the data source associated with the + * given connection group. + * * @param {ConnectionGroup} connectionGroup * The connection group to add to the internal set of visible * objects, along with any descendants. */ - var addVisibleConnectionGroup = function addVisibleConnectionGroup(connectionGroup) { + var addVisibleConnectionGroup = function addVisibleConnectionGroup(dataSource, connectionGroup) { // Add given connection group to set of visible objects visibleObjects['g/' + connectionGroup.identifier] = connectionGroup; // Add all child connections if (connectionGroup.childConnections) - connectionGroup.childConnections.forEach(addVisibleConnection); + connectionGroup.childConnections.forEach(function addChildConnection(child) { + addVisibleConnection(dataSource, child); + }); // Add all child connection groups if (connectionGroup.childConnectionGroups) - connectionGroup.childConnectionGroups.forEach(addVisibleConnectionGroup); + connectionGroup.childConnectionGroups.forEach(function addChildConnectionGroup(child) { + addVisibleConnectionGroup(dataSource, child); + }); }; - // Update visible objects when root group is set - $scope.$watch("rootGroup", function setRootGroup(rootGroup) { + // Update visible objects when root groups are set + $scope.$watch("rootGroups", function setRootGroups(rootGroups) { // Clear connection arrays $scope.activeConnections = []; @@ -134,8 +148,11 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo // Produce collection of visible objects visibleObjects = {}; - if (rootGroup) - addVisibleConnectionGroup(rootGroup); + if (rootGroups) { + angular.forEach(rootGroups, function addConnectionGroup(rootGroup, dataSource) { + addVisibleConnectionGroup(dataSource, rootGroup); + }); + } var managedClients = guacClientManager.getManagedClients(); diff --git a/guacamole/src/main/webapp/app/home/templates/home.html b/guacamole/src/main/webapp/app/home/templates/home.html index be41ada77..c5e2df696 100644 --- a/guacamole/src/main/webapp/app/home/templates/home.html +++ b/guacamole/src/main/webapp/app/home/templates/home.html @@ -30,14 +30,14 @@
    - +

    {{'HOME.SECTION_HEADER_ALL_CONNECTIONS' | translate}}

    From 3c46dda5bc9920d124aa8b6b09570f9dd5712c34 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 Sep 2015 01:18:59 -0700 Subject: [PATCH 33/47] GUAC-586: List readable users if applicable to management. --- .../net/basic/rest/user/UserRESTService.java | 2 +- .../settings/directives/guacSettingsUsers.js | 50 +++++++++++-------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java index 29f6b3f1c..687d4ff85 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java @@ -169,7 +169,7 @@ public class UserRESTService { // Filter users, if requested Collection userIdentifiers = userDirectory.getIdentifiers(); - if (!isAdmin && permissions != null) { + if (!isAdmin && permissions != null && !permissions.isEmpty()) { ObjectPermissionSet userPermissions = self.getUserPermissions(); userIdentifiers = userPermissions.getAccessibleObjects(permissions, userIdentifiers); } diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js index e6f6dd709..af52ee3bf 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js @@ -204,34 +204,42 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings if (!canManageUsers()) $location.path('/'); - }); + var userPromise; - // Retrieve all users for whom we have UPDATE or DELETE permission - dataSourceService.apply(userService.getUsers, dataSources, [ - PermissionSet.ObjectPermissionType.UPDATE, - PermissionSet.ObjectPermissionType.DELETE - ]) - .then(function usersReceived(userArrays) { + // If users can be created, list all readable users + if ($scope.canCreateUsers()) + userPromise = dataSourceService.apply(userService.getUsers, dataSources); - var addedUsers = {}; - $scope.manageableUsers = []; + // Otherwise, list only updateable/deletable users + else + userPromise = dataSourceService.apply(userService.getUsers, dataSources, [ + PermissionSet.ObjectPermissionType.UPDATE, + PermissionSet.ObjectPermissionType.DELETE + ]); - // For each user in each data source - angular.forEach(dataSources, function addUserList(dataSource) { - angular.forEach(userArrays[dataSource], function addUser(user) { + userPromise.then(function usersReceived(userArrays) { - // Do not add the same user twice - if (addedUsers[user.username]) - return; + var addedUsers = {}; + $scope.manageableUsers = []; - // Add user to overall list - addedUsers[user.username] = user; - $scope.manageableUsers.push(new ManageableUser ({ - 'dataSource' : dataSource, - 'user' : user - })); + // For each user in each data source + angular.forEach(dataSources, function addUserList(dataSource) { + angular.forEach(userArrays[dataSource], function addUser(user) { + // Do not add the same user twice + if (addedUsers[user.username]) + return; + + // Add user to overall list + addedUsers[user.username] = user; + $scope.manageableUsers.push(new ManageableUser ({ + 'dataSource' : dataSource, + 'user' : user + })); + + }); }); + }); }); From d2924a5e79b29432c1762da3c57d6ef8b9b611f9 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 Sep 2015 13:01:56 -0700 Subject: [PATCH 34/47] GUAC-586: Use data source when determining active user count. --- .../app/groupList/directives/guacGroupList.js | 83 +++++++++++++------ .../app/groupList/types/GroupListItem.js | 15 ++-- 2 files changed, 66 insertions(+), 32 deletions(-) diff --git a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js index bbfd3c8b7..2241ce11a 100644 --- a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js +++ b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js @@ -93,45 +93,38 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() // Required services var activeConnectionService = $injector.get('activeConnectionService'); + var dataSourceService = $injector.get('dataSourceService'); // Required types var GroupListItem = $injector.get('GroupListItem'); /** - * The number of active connections associated with a given - * connection identifier. If this information is unknown, or there - * are no active connections for a given identifier, no number will - * be stored. + * Map of data source identifier to the number of active + * connections associated with a given connection identifier. + * If this information is unknown, or there are no active + * connections for a given identifier, no number will be stored. * - * @type Object. + * @type Object.> */ var connectionCount = {}; + /** + * A list of all items which should appear at the root level. As + * connections and connection groups from multiple data sources may + * be included in a guacGroupList, there may be multiple root + * items, even if the root connection group is shown. + * + * @type GroupListItem[] + */ $scope.rootItems = []; - // Count active connections by connection identifier - activeConnectionService.getActiveConnections() - .success(function activeConnectionsRetrieved(activeConnections) { - - // Count each active connection by identifier - angular.forEach(activeConnections, function addActiveConnection(activeConnection) { - - // If counter already exists, increment - var identifier = activeConnection.connectionIdentifier; - if (connectionCount[identifier]) - connectionCount[identifier]++; - - // Otherwise, initialize counter to 1 - else - connectionCount[identifier] = 1; - - }); - - }); - /** * Returns the number of active usages of a given connection. * + * @param {String} dataSource + * The identifier of the data source containing the given + * connection. + * * @param {Connection} connection * The connection whose active connections should be counted. * @@ -139,8 +132,8 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() * The number of currently-active usages of the given * connection. */ - var countActiveConnections = function countActiveConnections(connection) { - return connectionCount[connection.identifier]; + var countActiveConnections = function countActiveConnections(dataSource, connection) { + return connectionCount[dataSource][connection.identifier]; }; /** @@ -178,12 +171,22 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() // Set contents whenever the connection group is assigned or changed $scope.$watch('connectionGroups', function setContents(connectionGroups) { + // Reset stored data + var dataSources = []; $scope.rootItems = []; + connectionCount = {}; // If connection groups are given, add them to the interface if (connectionGroups) { + + // Add each provided connection group angular.forEach(connectionGroups, function addConnectionGroup(connectionGroup, dataSource) { + // Prepare data source for active connection counting + dataSources.push(dataSource); + connectionCount[dataSource] = {}; + + // Create root item for current connection group var rootItem = GroupListItem.fromConnectionGroup(dataSource, connectionGroup, !!$scope.connectionTemplate, countActiveConnections); @@ -199,6 +202,32 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() } }); + + // Count active connections by connection identifier + dataSourceService.apply( + activeConnectionService.getActiveConnections, + dataSources + ) + .then(function activeConnectionsRetrieved(activeConnectionMap) { + + // Within each data source, count each active connection by identifier + angular.forEach(activeConnectionMap, function addActiveConnections(activeConnections, dataSource) { + angular.forEach(activeConnections, function addActiveConnection(activeConnection) { + + // If counter already exists, increment + var identifier = activeConnection.connectionIdentifier; + if (connectionCount[dataSource][identifier]) + connectionCount[dataSource][identifier]++; + + // Otherwise, initialize counter to 1 + else + connectionCount[dataSource][identifier] = 1; + + }); + }); + + }); + } }); diff --git a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js index b73be67c4..09d639082 100644 --- a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js +++ b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js @@ -143,7 +143,9 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio * @param {Function} [countActiveConnections] * A getter which returns the current number of active connections for * the given connection. If omitted, the number of active connections - * known at the time this function was called is used instead. + * known at the time this function was called is used instead. This + * function will be passed, in order, the data source identifier and + * the connection in question. * * @returns {GroupListItem} * A new GroupListItem which represents the given connection. @@ -169,7 +171,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio // Use getter, if provided if (countActiveConnections) - return countActiveConnections(connection); + return countActiveConnections(dataSource, connection); return connection.activeConnections; @@ -201,13 +203,16 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio * @param {Function} [countActiveConnections] * A getter which returns the current number of active connections for * the given connection. If omitted, the number of active connections - * known at the time this function was called is used instead. + * known at the time this function was called is used instead. This + * function will be passed, in order, the data source identifier and + * the connection group in question. * * @param {Function} [countActiveConnectionGroups] * A getter which returns the current number of active connections for * the given connection group. If omitted, the number of active * connections known at the time this function was called is used - * instead. + * instead. This function will be passed, in order, the data source + * identifier and the connection group in question. * * @returns {GroupListItem} * A new GroupListItem which represents the given connection group, @@ -257,7 +262,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio // Use getter, if provided if (countActiveConnectionGroups) - return countActiveConnectionGroups(connectionGroup); + return countActiveConnectionGroups(dataSource, connectionGroup); return connectionGroup.activeConnections; From b3614aef58e103a00dce2703d06c70b3b9745b30 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 Sep 2015 18:22:25 -0700 Subject: [PATCH 35/47] GUAC-586: Use data source when connecting to connections or groups. Remove deprecated getUserContext() from GuacamoleSession and related classes. Use identifiers which embed the data source for client URLs. --- .../guacamole/net/basic/GuacamoleSession.java | 34 -- .../net/basic/HTTPTunnelRequest.java | 2 +- .../guacamole/net/basic/TunnelRequest.java | 353 +++++++++++++++--- .../net/basic/TunnelRequestService.java | 85 ++--- .../rest/auth/AuthenticationService.java | 13 - .../websocket/WebSocketTunnelRequest.java | 2 +- .../jetty9/WebSocketTunnelRequest.java | 2 +- .../client/controllers/clientController.js | 14 +- .../webapp/app/client/types/ManagedClient.js | 43 ++- .../app/home/controllers/homeController.js | 48 ++- .../home/directives/guacRecentConnections.js | 13 +- .../webapp/app/home/templates/connection.html | 2 +- .../app/home/templates/connectionGroup.html | 2 +- .../main/webapp/app/home/templates/home.html | 1 + .../app/index/config/indexRouteConfig.js | 2 +- .../webapp/app/navigation/navigationModule.js | 6 +- .../navigation/services/userPageService.js | 19 +- .../app/navigation/types/ClientIdentifier.js | 157 ++++++++ 18 files changed, 611 insertions(+), 187 deletions(-) create mode 100644 guacamole/src/main/webapp/app/navigation/types/ClientIdentifier.js diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java index 0816ae2c5..dab83953e 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java @@ -115,40 +115,6 @@ public class GuacamoleSession { this.authenticatedUser = authenticatedUser; } - /** - * Returns the UserContext associated with this session. - * - * @return The UserContext associated with this session. - */ - public UserContext getUserContext() { - - // Warn of deprecation - logger.debug( - "\n****************************************************************" - + "\n" - + "\n !!!! PLEASE DO NOT USE getUserContext() !!!!" - + "\n" - + "\n getUserContext() has been replaced by getUserContexts(), which" - + "\n properly handles multiple authentication providers. All use of" - + "\n the old getUserContext() must be removed before GUAC-586 can" - + "\n be considered complete." - + "\n" - + "\n****************************************************************" - ); - - // Return the UserContext associated with the AuthenticationProvider - // that authenticated the current user. - String authProviderIdentifier = authenticatedUser.getAuthenticationProvider().getIdentifier(); - for (UserContext userContext : userContexts) { - if (userContext.getAuthenticationProvider().getIdentifier().equals(authProviderIdentifier)) - return userContext; - } - - // If not found, return null - return null; - - } - /** * Returns a list of all UserContexts associated with this session. Each * AuthenticationProvider currently loaded by Guacamole may provide its own diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/HTTPTunnelRequest.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/HTTPTunnelRequest.java index e36d7e6e3..3aab28006 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/HTTPTunnelRequest.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/HTTPTunnelRequest.java @@ -31,7 +31,7 @@ import javax.servlet.http.HttpServletRequest; * * @author Michael Jumper */ -public class HTTPTunnelRequest implements TunnelRequest { +public class HTTPTunnelRequest extends TunnelRequest { /** * The wrapped HttpServletRequest. diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequest.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequest.java index b172cb107..a18375a71 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequest.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,88 +23,331 @@ package org.glyptodon.guacamole.net.basic; import java.util.List; +import org.glyptodon.guacamole.GuacamoleClientException; +import org.glyptodon.guacamole.GuacamoleException; /** - * Request interface which provides only the functions absolutely required - * to retrieve and connect to a tunnel. + * A request object which provides only the functions absolutely required to + * retrieve and connect to a tunnel. * * @author Michael Jumper */ -public interface TunnelRequest { +public abstract class TunnelRequest { /** - * All supported identifier types. + * The name of the request parameter containing the user's authentication + * token. */ - public static enum IdentifierType { + public static final String AUTH_TOKEN_PARAMETER = "token"; + + /** + * The name of the parameter containing the identifier of the + * AuthenticationProvider associated with the UserContext containing the + * object to which a tunnel is being requested. + */ + public static final String AUTH_PROVIDER_IDENTIFIER_PARAMETER = "GUAC_DATA_SOURCE"; + + /** + * The name of the parameter specifying the type of object to which a + * tunnel is being requested. Currently, this may be "c" for a Guacamole + * connection, or "g" for a Guacamole connection group. + */ + public static final String TYPE_PARAMETER = "GUAC_TYPE"; + + /** + * The name of the parameter containing the unique identifier of the object + * to which a tunnel is being requested. + */ + public static final String IDENTIFIER_PARAMETER = "GUAC_ID"; + + /** + * The name of the parameter containing the desired display width, in + * pixels. + */ + public static final String WIDTH_PARAMETER = "GUAC_WIDTH"; + + /** + * The name of the parameter containing the desired display height, in + * pixels. + */ + public static final String HEIGHT_PARAMETER = "GUAC_HEIGHT"; + + /** + * The name of the parameter containing the desired display resolution, in + * DPI. + */ + public static final String DPI_PARAMETER = "GUAC_DPI"; + + /** + * The name of the parameter specifying one supported audio mimetype. This + * will normally appear multiple times within a single tunnel request - + * once for each mimetype. + */ + public static final String AUDIO_PARAMETER = "GUAC_AUDIO"; + + /** + * The name of the parameter specifying one supported video mimetype. This + * will normally appear multiple times within a single tunnel request - + * once for each mimetype. + */ + public static final String VIDEO_PARAMETER = "GUAC_VIDEO"; + + /** + * All supported object types that can be used as the destination of a + * tunnel. + */ + public static enum Type { /** - * The unique identifier of a connection. + * A Guacamole connection. */ - CONNECTION("c/"), + CONNECTION("c"), /** - * The unique identifier of a connection group. + * A Guacamole connection group. */ - CONNECTION_GROUP("g/"); + CONNECTION_GROUP("g"); + + /** + * The parameter value which denotes a destination object of this type. + */ + final String PARAMETER_VALUE; /** - * The prefix which precedes an identifier of this type. + * Defines a Type having the given corresponding parameter value. + * + * @param value + * The parameter value which denotes a destination object of this + * type. */ - final String PREFIX; - - /** - * Defines an IdentifierType having the given prefix. - * @param prefix The prefix which will precede any identifier of this - * type, thus differentiating it from other identifier - * types. - */ - IdentifierType(String prefix) { - PREFIX = prefix; + Type(String value) { + PARAMETER_VALUE = value; } - /** - * Given an identifier, determines the corresponding identifier type. - * - * @param identifier The identifier whose type should be identified. - * @return The identified identifier type. - */ - static IdentifierType getType(String identifier) { - - // If null, no known identifier - if (identifier == null) - return null; - - // Connection identifiers - if (identifier.startsWith(CONNECTION.PREFIX)) - return CONNECTION; - - // Connection group identifiers - if (identifier.startsWith(CONNECTION_GROUP.PREFIX)) - return CONNECTION_GROUP; - - // Otherwise, unknown - return null; - - } - }; /** * Returns the value of the parameter having the given name. * - * @param name The name of the parameter to return. - * @return The value of the parameter having the given name, or null - * if no such parameter was specified. + * @param name + * The name of the parameter to return. + * + * @return + * The value of the parameter having the given name, or null if no such + * parameter was specified. */ - public String getParameter(String name); + public abstract String getParameter(String name); /** * Returns a list of all values specified for the given parameter. * - * @param name The name of the parameter to return. - * @return All values of the parameter having the given name , or null - * if no such parameter was specified. + * @param name + * The name of the parameter to return. + * + * @return + * All values of the parameter having the given name , or null if no + * such parameter was specified. */ - public List getParameterValues(String name); - + public abstract List getParameterValues(String name); + + /** + * Returns the value of the parameter having the given name, throwing an + * exception if the parameter is missing. + * + * @param name + * The name of the parameter to return. + * + * @return + * The value of the parameter having the given name. + * + * @throws GuacamoleException + * If the parameter is not present in the request. + */ + public String getRequiredParameter(String name) throws GuacamoleException { + + // Pull requested parameter, aborting if absent + String value = getParameter(name); + if (value == null) + throw new GuacamoleClientException("Parameter \"" + name + "\" is required."); + + return value; + + } + + /** + * Returns the integer value of the parameter having the given name, + * throwing an exception if the parameter cannot be parsed. + * + * @param name + * The name of the parameter to return. + * + * @return + * The integer value of the parameter having the given name, or null if + * the parameter is missing. + * + * @throws GuacamoleException + * If the parameter is not a valid integer. + */ + public Integer getIntegerParameter(String name) throws GuacamoleException { + + // Pull requested parameter + String value = getParameter(name); + if (value == null) + return null; + + // Attempt to parse as an integer + try { + return Integer.parseInt(value); + } + + // Rethrow any parsing error as a GuacamoleClientException + catch (NumberFormatException e) { + throw new GuacamoleClientException("Parameter \"" + name + "\" must be a valid integer.", e); + } + + } + + /** + * Returns the authentication token associated with this tunnel request. + * + * @return + * The authentication token associated with this tunnel request, or + * null if no authentication token is present. + */ + public String getAuthenticationToken() { + return getParameter(AUTH_TOKEN_PARAMETER); + } + + /** + * Returns the identifier of the AuthenticationProvider associated with the + * UserContext from which the connection or connection group is to be + * retrieved when the tunnel is created. In the context of the REST API and + * the JavaScript side of the web application, this is referred to as the + * data source identifier. + * + * @return + * The identifier of the AuthenticationProvider associated with the + * UserContext from which the connection or connection group is to be + * retrieved when the tunnel is created. + * + * @throws GuacamoleException + * If the identifier was not present in the request. + */ + public String getAuthenticationProviderIdentifier() + throws GuacamoleException { + return getRequiredParameter(AUTH_PROVIDER_IDENTIFIER_PARAMETER); + } + + /** + * Returns the type of object for which the tunnel is being requested. + * + * @return + * The type of object for which the tunnel is being requested. + * + * @throws GuacamoleException + * If the type was not present in the request, or if the type requested + * is in the wrong format. + */ + public Type getType() throws GuacamoleException { + + String type = getRequiredParameter(TYPE_PARAMETER); + + // For each possible object type + for (Type possibleType : Type.values()) { + + // Match against defined parameter value + if (type.equals(possibleType.PARAMETER_VALUE)) + return possibleType; + + } + + throw new GuacamoleClientException("Illegal identifier - unknown type."); + + } + + /** + * Returns the identifier of the destination of the tunnel being requested. + * As there are multiple types of destination objects available, and within + * multiple data sources, the associated object type and data source are + * also necessary to determine what this identifier refers to. + * + * @return + * The identifier of the destination of the tunnel being requested. + * + * @throws GuacamoleException + * If the identifier was not present in the request. + */ + public String getIdentifier() throws GuacamoleException { + return getRequiredParameter(IDENTIFIER_PARAMETER); + } + + /** + * Returns the display width desired for the Guacamole session over the + * tunnel being requested. + * + * @return + * The display width desired for the Guacamole session over the tunnel + * being requested, or null if no width was given. + * + * @throws GuacamoleException + * If the width specified was not a valid integer. + */ + public Integer getWidth() throws GuacamoleException { + return getIntegerParameter(WIDTH_PARAMETER); + } + + /** + * Returns the display height desired for the Guacamole session over the + * tunnel being requested. + * + * @return + * The display height desired for the Guacamole session over the tunnel + * being requested, or null if no width was given. + * + * @throws GuacamoleException + * If the height specified was not a valid integer. + */ + public Integer getHeight() throws GuacamoleException { + return getIntegerParameter(HEIGHT_PARAMETER); + } + + /** + * Returns the display resolution desired for the Guacamole session over + * the tunnel being requested, in DPI. + * + * @return + * The display resolution desired for the Guacamole session over the + * tunnel being requested, or null if no resolution was given. + * + * @throws GuacamoleException + * If the resolution specified was not a valid integer. + */ + public Integer getDPI() throws GuacamoleException { + return getIntegerParameter(DPI_PARAMETER); + } + + /** + * Returns a list of all audio mimetypes declared as supported within the + * tunnel request. + * + * @return + * A list of all audio mimetypes declared as supported within the + * tunnel request, or null if no mimetypes were specified. + */ + public List getAudioMimetypes() { + return getParameterValues(AUDIO_PARAMETER); + } + + /** + * Returns a list of all video mimetypes declared as supported within the + * tunnel request. + * + * @return + * A list of all video mimetypes declared as supported within the + * tunnel request, or null if no mimetypes were specified. + */ + public List getVideoMimetypes() { + return getParameterValues(VIDEO_PARAMETER); + } + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java index 9c20eedeb..5953a31ed 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java @@ -25,16 +25,15 @@ package org.glyptodon.guacamole.net.basic; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.List; -import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; -import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.net.DelegatingGuacamoleTunnel; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; import org.slf4j.Logger; @@ -53,12 +52,6 @@ import org.slf4j.LoggerFactory; @Singleton public class TunnelRequestService { - /** - * The Guacamole server environment. - */ - @Inject - private Environment environment; - /** * Logger for this class. */ @@ -70,6 +63,12 @@ public class TunnelRequestService { @Inject private AuthenticationService authenticationService; + /** + * Service for convenient retrieval of objects. + */ + @Inject + private ObjectRetrievalService retrievalService; + /** * Reads and returns the client information provided within the given * request. @@ -80,35 +79,40 @@ public class TunnelRequestService { * @return GuacamoleClientInformation * An object containing information about the client sending the tunnel * request. + * + * @throws GuacamoleException + * If the parameters of the tunnel request are invalid. */ - protected GuacamoleClientInformation getClientInformation(TunnelRequest request) { + protected GuacamoleClientInformation getClientInformation(TunnelRequest request) + throws GuacamoleException { + // Get client information GuacamoleClientInformation info = new GuacamoleClientInformation(); // Set width if provided - String width = request.getParameter("width"); + Integer width = request.getWidth(); if (width != null) - info.setOptimalScreenWidth(Integer.parseInt(width)); + info.setOptimalScreenWidth(width); // Set height if provided - String height = request.getParameter("height"); + Integer height = request.getHeight(); if (height != null) - info.setOptimalScreenHeight(Integer.parseInt(height)); + info.setOptimalScreenHeight(height); // Set resolution if provided - String dpi = request.getParameter("dpi"); + Integer dpi = request.getDPI(); if (dpi != null) - info.setOptimalResolution(Integer.parseInt(dpi)); + info.setOptimalResolution(dpi); // Add audio mimetypes - List audio_mimetypes = request.getParameterValues("audio"); - if (audio_mimetypes != null) - info.getAudioMimetypes().addAll(audio_mimetypes); + List audioMimetypes = request.getAudioMimetypes(); + if (audioMimetypes != null) + info.getAudioMimetypes().addAll(audioMimetypes); // Add video mimetypes - List video_mimetypes = request.getParameterValues("video"); - if (video_mimetypes != null) - info.getVideoMimetypes().addAll(video_mimetypes); + List videoMimetypes = request.getVideoMimetypes(); + if (videoMimetypes != null) + info.getVideoMimetypes().addAll(videoMimetypes); return info; } @@ -122,7 +126,7 @@ public class TunnelRequestService { * The UserContext associated with the user for whom the tunnel is * being created. * - * @param idType + * @param type * The type of object being connected to (connection or group). * * @param id @@ -138,13 +142,13 @@ public class TunnelRequestService { * If an error occurs while creating the tunnel. */ protected GuacamoleTunnel createConnectedTunnel(UserContext context, - final TunnelRequest.IdentifierType idType, String id, + final TunnelRequest.Type type, String id, GuacamoleClientInformation info) throws GuacamoleException { // Create connected tunnel from identifier GuacamoleTunnel tunnel = null; - switch (idType) { + switch (type) { // Connection identifiers case CONNECTION: { @@ -205,7 +209,7 @@ public class TunnelRequestService { * @param session * The Guacamole session to associate the tunnel with. * - * @param idType + * @param type * The type of object being connected to (connection or group). * * @param id @@ -220,7 +224,7 @@ public class TunnelRequestService { * If an error occurs while obtaining the tunnel. */ protected GuacamoleTunnel createAssociatedTunnel(final GuacamoleSession session, - GuacamoleTunnel tunnel, final TunnelRequest.IdentifierType idType, + GuacamoleTunnel tunnel, final TunnelRequest.Type type, final String id) throws GuacamoleException { // Monitor tunnel closure and data @@ -239,7 +243,7 @@ public class TunnelRequestService { long duration = connectionEndTime - connectionStartTime; // Log closure - switch (idType) { + switch (type) { // Connection identifiers case CONNECTION: @@ -289,27 +293,20 @@ public class TunnelRequestService { public GuacamoleTunnel createTunnel(TunnelRequest request) throws GuacamoleException { - // Get auth token and session - final String authToken = request.getParameter("authToken"); - final GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); - - // Get client information and connection ID from request - String id = request.getParameter("id"); - final GuacamoleClientInformation info = getClientInformation(request); - - // Determine ID type - TunnelRequest.IdentifierType idType = TunnelRequest.IdentifierType.getType(id); - if (idType == null) - throw new GuacamoleClientException("Illegal identifier - unknown type."); - - // Remove prefix - id = id.substring(idType.PREFIX.length()); + // Parse request parameters + String authToken = request.getAuthenticationToken(); + String id = request.getIdentifier(); + TunnelRequest.Type type = request.getType(); + String authProviderIdentifier = request.getAuthenticationProviderIdentifier(); + GuacamoleClientInformation info = getClientInformation(request); // Create connected tunnel using provided connection ID and client information - final GuacamoleTunnel tunnel = createConnectedTunnel(session.getUserContext(), idType, id, info); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); + GuacamoleTunnel tunnel = createConnectedTunnel(userContext, type, id, info); // Associate tunnel with session - return createAssociatedTunnel(session, tunnel, idType, id); + return createAssociatedTunnel(session, tunnel, type, id); } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthenticationService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthenticationService.java index b730a7163..7667bb43c 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthenticationService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthenticationService.java @@ -66,19 +66,6 @@ public class AuthenticationService { } - /** - * Finds the UserContext for a given auth token, if the auth token represents - * a currently logged in user. Throws an unauthorized error otherwise. - * - * @param authToken The auth token to check against the map of logged in users. - * @return The user context that corresponds to the provided auth token. - * @throws GuacamoleException If the auth token does not correspond to any - * logged in user. - */ - public UserContext getUserContext(String authToken) throws GuacamoleException { - return getGuacamoleSession(authToken).getUserContext(); - } - /** * Returns all UserContexts associated with a given auth token, if the auth * token represents a currently logged in user. Throws an unauthorized diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketTunnelRequest.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketTunnelRequest.java index cf6922157..6a687f8f0 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketTunnelRequest.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketTunnelRequest.java @@ -32,7 +32,7 @@ import org.glyptodon.guacamole.net.basic.TunnelRequest; * * @author Michael Jumper */ -public class WebSocketTunnelRequest implements TunnelRequest { +public class WebSocketTunnelRequest extends TunnelRequest { /** * All parameters passed via HTTP to the WebSocket handshake. diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/WebSocketTunnelRequest.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/WebSocketTunnelRequest.java index 1c4c96bd8..5e71b3824 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/WebSocketTunnelRequest.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/WebSocketTunnelRequest.java @@ -33,7 +33,7 @@ import org.glyptodon.guacamole.net.basic.TunnelRequest; * * @author Michael Jumper */ -public class WebSocketTunnelRequest implements TunnelRequest { +public class WebSocketTunnelRequest extends TunnelRequest { /** * All parameters passed via HTTP to the WebSocket handshake. diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 692051bc1..2fab8e37b 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -158,7 +158,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams var RECONNECT_ACTION = { name : "CLIENT.ACTION_RECONNECT", callback : function reconnectCallback() { - $scope.client = guacClientManager.replaceManagedClient(uniqueId, $routeParams.params); + $scope.client = guacClientManager.replaceManagedClient($routeParams.id, $routeParams.params); guacNotification.showStatus(false); } }; @@ -219,13 +219,13 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.$on('guacClientClipboard', function clientClipboardListener(event, client, mimetype, clipboardData) { $scope.clipboardData = clipboardData; }); - - /* - * Parse the type, name, and id out of the url paramteres, - * as well as any extra parameters if set. + + /** + * The client which should be attached to the client UI. + * + * @type ManagedClient */ - var uniqueId = $routeParams.type + '/' + $routeParams.id; - $scope.client = guacClientManager.getManagedClient(uniqueId, $routeParams.params); + $scope.client = guacClientManager.getManagedClient($routeParams.id, $routeParams.params); var keysCurrentlyPressed = {}; diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js index 822a80870..c2a3bf035 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -28,6 +28,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Required types var ClientProperties = $injector.get('ClientProperties'); + var ClientIdentifier = $injector.get('ClientIdentifier'); var ManagedClientState = $injector.get('ManagedClientState'); var ManagedDisplay = $injector.get('ManagedDisplay'); var ManagedFileDownload = $injector.get('ManagedFileDownload'); @@ -153,8 +154,8 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', * desired connection ID, display resolution, and supported audio/video * codecs. * - * @param {String} id - * The ID of the connection or group to connect to. + * @param {ClientIdentifier} identifier + * The identifier representing the connection or group to connect to. * * @param {String} [connectionParameters] * Any additional HTTP parameters to pass while connecting. @@ -163,7 +164,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', * The string of connection parameters to be passed to the Guacamole * client. */ - var getConnectString = function getConnectString(id, connectionParameters) { + var getConnectString = function getConnectString(identifier, connectionParameters) { // Calculate optimal width/height for display var pixel_density = $window.devicePixelRatio || 1; @@ -173,21 +174,23 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Build base connect string var connectString = - "id=" + encodeURIComponent(id) - + "&authToken=" + encodeURIComponent(authenticationService.getCurrentToken()) - + "&width=" + Math.floor(optimal_width) - + "&height=" + Math.floor(optimal_height) - + "&dpi=" + Math.floor(optimal_dpi) + "token=" + encodeURIComponent(authenticationService.getCurrentToken()) + + "&GUAC_DATA_SOURCE=" + encodeURIComponent(identifier.dataSource) + + "&GUAC_ID=" + encodeURIComponent(identifier.id) + + "&GUAC_TYPE=" + encodeURIComponent(identifier.type) + + "&GUAC_WIDTH=" + Math.floor(optimal_width) + + "&GUAC_HEIGHT=" + Math.floor(optimal_height) + + "&GUAC_DPI=" + Math.floor(optimal_dpi) + (connectionParameters ? '&' + connectionParameters : ''); // Add audio mimetypes to connect_string guacAudio.supported.forEach(function(mimetype) { - connectString += "&audio=" + encodeURIComponent(mimetype); + connectString += "&GUAC_AUDIO=" + encodeURIComponent(mimetype); }); // Add video mimetypes to connect_string guacVideo.supported.forEach(function(mimetype) { - connectString += "&video=" + encodeURIComponent(mimetype); + connectString += "&GUAC_VIDEO=" + encodeURIComponent(mimetype); }); return connectString; @@ -238,7 +241,9 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', * or group. * * @param {String} id - * The ID of the connection or group to connect to. + * The ID of the connection or group to connect to. This String must be + * a valid ClientIdentifier string, as would be generated by + * ClientIdentifier.toString(). * * @param {String} [connectionParameters] * Any additional HTTP parameters to pass while connecting. @@ -402,23 +407,23 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Manage the client display managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay()); - // Connect the Guacamole client - client.connect(getConnectString(id, connectionParameters)); + // Parse connection details from ID + var clientIdentifier = ClientIdentifier.fromString(id); - // Determine type of connection - var typePrefix = id.substring(0, 2); + // Connect the Guacamole client + client.connect(getConnectString(clientIdentifier, connectionParameters)); // If using a connection, pull connection name - if (typePrefix === 'c/') { - connectionService.getConnection(id.substring(2)) + if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) { + connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id) .success(function connectionRetrieved(connection) { managedClient.name = connection.name; }); } // If using a connection group, pull connection name - else if (typePrefix === 'g/') { - connectionGroupService.getConnectionGroup(id.substring(2)) + else if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION_GROUP) { + connectionGroupService.getConnectionGroup(clientIdentifier.dataSource, clientIdentifier.id) .success(function connectionGroupRetrieved(group) { managedClient.name = group.name; }); diff --git a/guacamole/src/main/webapp/app/home/controllers/homeController.js b/guacamole/src/main/webapp/app/home/controllers/homeController.js index 1e0573ebc..12eb1c25e 100644 --- a/guacamole/src/main/webapp/app/home/controllers/homeController.js +++ b/guacamole/src/main/webapp/app/home/controllers/homeController.js @@ -27,7 +27,8 @@ angular.module('home').controller('homeController', ['$scope', '$injector', function homeController($scope, $injector) { // Get required types - var ConnectionGroup = $injector.get('ConnectionGroup'); + var ConnectionGroup = $injector.get('ConnectionGroup'); + var ClientIdentifier = $injector.get('ClientIdentifier'); // Get required services var authenticationService = $injector.get('authenticationService'); @@ -56,6 +57,51 @@ angular.module('home').controller('homeController', ['$scope', '$injector', }; + /** + * Object passed to the guacGroupList directive, providing context-specific + * functions or data. + */ + $scope.context = { + + /** + * Returns the unique string identifier which must be used when + * connecting to a connection or connection group represented by the + * given GroupListItem. + * + * @param {GroupListItem} item + * The GroupListItem to determine the client identifier of. + * + * @returns {String} + * The client identifier associated with the connection or + * connection group represented by the given GroupListItem, or null + * if the GroupListItem cannot have an associated client + * identifier. + */ + getClientIdentifier : function getClientIdentifier(item) { + + // If the item is a connection, generate a connection identifier + if (item.isConnection) + return ClientIdentifier.toString({ + dataSource : item.dataSource, + type : ClientIdentifier.Types.CONNECTION, + id : item.identifier + }); + + // If the item is a connection, generate a connection group identifier + if (item.isConnectionGroup) + return ClientIdentifier.toString({ + dataSource : item.dataSource, + type : ClientIdentifier.Types.CONNECTION_GROUP, + id : item.identifier + }); + + // Otherwise, no such identifier can exist + return null; + + } + + }; + // Retrieve root groups and all descendants dataSourceService.apply( connectionGroupService.getConnectionGroupTree, diff --git a/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js b/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js index c6e8443ba..a5d913a02 100644 --- a/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js +++ b/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js @@ -48,6 +48,7 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo // Required types var ActiveConnection = $injector.get('ActiveConnection'); + var ClientIdentifier = $injector.get('ClientIdentifier'); var RecentConnection = $injector.get('RecentConnection'); // Required services @@ -104,7 +105,11 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo var addVisibleConnection = function addVisibleConnection(dataSource, connection) { // Add given connection to set of visible objects - visibleObjects['c/' + connection.identifier] = connection; + visibleObjects[ClientIdentifier.toString({ + dataSource : dataSource, + type : ClientIdentifier.Types.CONNECTION, + id : connection.identifier + })] = connection; }; @@ -123,7 +128,11 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo var addVisibleConnectionGroup = function addVisibleConnectionGroup(dataSource, connectionGroup) { // Add given connection group to set of visible objects - visibleObjects['g/' + connectionGroup.identifier] = connectionGroup; + visibleObjects[ClientIdentifier.toString({ + dataSource : dataSource, + type : ClientIdentifier.Types.CONNECTION_GROUP, + id : connectionGroup.identifier + })] = connectionGroup; // Add all child connections if (connectionGroup.childConnections) diff --git a/guacamole/src/main/webapp/app/home/templates/connection.html b/guacamole/src/main/webapp/app/home/templates/connection.html index 33c5aec7e..87f101af7 100644 --- a/guacamole/src/main/webapp/app/home/templates/connection.html +++ b/guacamole/src/main/webapp/app/home/templates/connection.html @@ -1,4 +1,4 @@ - + - {{item.name}} + {{item.name}} {{item.name}} diff --git a/guacamole/src/main/webapp/app/home/templates/home.html b/guacamole/src/main/webapp/app/home/templates/home.html index c5e2df696..a2c362ac5 100644 --- a/guacamole/src/main/webapp/app/home/templates/home.html +++ b/guacamole/src/main/webapp/app/home/templates/home.html @@ -37,6 +37,7 @@

    {{'HOME.SECTION_HEADER_ALL_CONNECTIONS' | translate}}

    + */ + ClientIdentifier.Types = { + + /** + * The type string for a Guacamole connection. + * + * @type String + */ + CONNECTION : 'c', + + /** + * The type string for a Guacamole connection group. + * + * @type String + */ + CONNECTION_GROUP : 'g' + + }; + + /** + * Converts the given ClientIdentifier or ClientIdentifier-like object to + * a String representation. Any object having the same properties as + * ClientIdentifier may be used, but only those properties will be taken + * into account when producing the resulting String. + * + * @param {ClientIdentifier|Object} id + * The ClientIdentifier or ClientIdentifier-like object to convert to + * a String representation. + * + * @returns {String} + * A deterministic String representation of the given ClientIdentifier + * or ClientIdentifier-like object. + */ + ClientIdentifier.toString = function toString(id) { + return $window.btoa([ + id.id, + id.type, + id.dataSource + ].join('\0')); + }; + + /** + * Converts the given String into the corresponding ClientIdentifier. If + * the provided String is not a valid identifier, it will be interpreted + * as the identifier of a connection within the data source that + * authenticated the current user. + * + * @param {String} str + * The String to convert to a ClientIdentifier. + * + * @returns {ClientIdentifier} + * The ClientIdentifier represented by the given String. + */ + ClientIdentifier.fromString = function fromString(str) { + + try { + var values = $window.atob(str).split('\0'); + return new ClientIdentifier({ + id : values[0], + type : values[1], + dataSource : values[2] + }); + } + + // If the provided string is invalid, transform into a reasonable guess + catch (e) { + return new ClientIdentifier({ + id : str, + type : ClientIdentifier.Types.CONNECTION, + dataSource : authenticationService.getDataSource() || 'default' + }); + } + + }; + + return ClientIdentifier; + +}]); From 36c1c853f946faf3306a4848334131b3bf089f44 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 2 Sep 2015 15:29:35 -0700 Subject: [PATCH 36/47] GUAC-586: Implement generic and hierarchical page tabbed page lists. --- .../controllers/manageUserController.js | 10 +- .../webapp/app/manage/styles/manage-user.css | 18 +- .../app/manage/templates/manageUser.html | 2 +- .../app/navigation/directives/guacPageList.js | 167 +++++++++++++++++- .../navigation/services/userPageService.js | 79 ++++----- .../app/navigation/styles/page-tabs.css | 58 ++++++ .../navigation/templates/guacPageList.html | 18 +- .../app/navigation/types/PageDefinition.js | 37 ++-- .../webapp/app/settings/styles/settings.css | 33 ---- .../app/settings/templates/settings.html | 2 +- 10 files changed, 301 insertions(+), 123 deletions(-) create mode 100644 guacamole/src/main/webapp/app/navigation/styles/page-tabs.css diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index 983ae71ad..524b897f2 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -353,11 +353,11 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto var linked = dataSource in users; // Add page entry - $scope.accountPages.push(new PageDefinition( - translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', - '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username), - linked ? 'linked' : 'unlinked' - )); + $scope.accountPages.push(new PageDefinition({ + name : translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', + url : '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username), + className : linked ? 'linked' : 'unlinked' + })); }); diff --git a/guacamole/src/main/webapp/app/manage/styles/manage-user.css b/guacamole/src/main/webapp/app/manage/styles/manage-user.css index a24d3e569..7d6f077db 100644 --- a/guacamole/src/main/webapp/app/manage/styles/manage-user.css +++ b/guacamole/src/main/webapp/app/manage/styles/manage-user.css @@ -28,14 +28,14 @@ text-transform: none; } -.manage-user .settings-tabs .page-list li.unlinked a[href], -.manage-user .settings-tabs .page-list li.linked a[href] { +.manage-user .page-tabs .page-list li.unlinked a[href], +.manage-user .page-tabs .page-list li.linked a[href] { padding-right: 2.5em; position: relative; } -.manage-user .settings-tabs .page-list li.unlinked a[href]:before, -.manage-user .settings-tabs .page-list li.linked a[href]:before { +.manage-user .page-tabs .page-list li.unlinked a[href]:before, +.manage-user .page-tabs .page-list li.linked a[href]:before { content: ' '; position: absolute; right: 0; @@ -47,19 +47,19 @@ background-position: center; } -.manage-user .settings-tabs .page-list li.unlinked a[href]:before { +.manage-user .page-tabs .page-list li.unlinked a[href]:before { background-image: url('images/plus.png'); } -.manage-user .settings-tabs .page-list li.unlinked a[href] { +.manage-user .page-tabs .page-list li.unlinked a[href] { opacity: 0.5; } -.manage-user .settings-tabs .page-list li.unlinked a[href]:hover, -.manage-user .settings-tabs .page-list li.unlinked a[href].current { +.manage-user .page-tabs .page-list li.unlinked a[href]:hover, +.manage-user .page-tabs .page-list li.unlinked a[href].current { opacity: 1; } -.manage-user .settings-tabs .page-list li.linked a[href]:before { +.manage-user .page-tabs .page-list li.linked a[href]:before { background-image: url('images/checkmark.png'); } diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index 442ee5116..04cd593a6 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -27,7 +27,7 @@ THE SOFTWARE.

    {{user.username}}

    -
    +
    diff --git a/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js b/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js index bd5f909c5..04ecf7a77 100644 --- a/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js +++ b/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js @@ -33,7 +33,7 @@ angular.module('navigation').directive('guacPageList', [function guacPageList() /** * The array of pages to display. * - * @type Page[] + * @type PageDefinition[] */ pages : '=' @@ -42,13 +42,119 @@ angular.module('navigation').directive('guacPageList', [function guacPageList() templateUrl: 'app/navigation/templates/guacPageList.html', controller: ['$scope', '$injector', function guacPageListController($scope, $injector) { - // Get required services + // Required types + var PageDefinition = $injector.get('PageDefinition'); + + // Required services var $location = $injector.get('$location'); + /** + * The URL of the currently-displayed page. + * + * @type String + */ + var currentURL = $location.url(); + + /** + * The names associated with the current page, if the current page + * is known. The value of this property corresponds to the value of + * PageDefinition.name. Though PageDefinition.name may be a String, + * this will always be an Array. + * + * @type String[] + */ + var currentPageName = []; + + /** + * Array of each level of the page list, where a level is defined + * by a mapping of names (translation strings) to the + * PageDefinitions corresponding to those names. + * + * @type Object.[] + */ + $scope.levels = []; + + /** + * Returns the names associated with the given page, in + * hierarchical order. If the page is only associated with a single + * name, and that name is not stored as an array, it will be still + * be returned as an array containing a single item. + * + * @param {PageDefinition} page + * The page to return the names of. + * + * @return {String[]} + * An array of all names associated with the given page, in + * hierarchical order. + */ + var getPageNames = function getPageNames(page) { + + // If already an array, simply return the name + if (angular.isArray(page.name)) + return page.name; + + // Otherwise, transform into array + return [page.name]; + + }; + + /** + * Adds the given PageDefinition to the overall set of pages + * displayed by this guacPageList, automatically updating the + * available levels ($scope.levels) and the contents of those + * levels. + * + * @param {PageDefinition} page + * The PageDefinition to add. + * + * @param {Number} weight + * The sorting weight to use for the page if it does not + * already have an associated weight. + */ + var addPage = function addPage(page, weight) { + + // Pull all names for page + var names = getPageNames(page); + + // Copy the hierarchy of this page into the displayed levels + // as far as is relevant for the currently-displayed page + for (var i = 0; i < names.length; i++) { + + // Create current level, if it doesn't yet exist + var pages = $scope.levels[i]; + if (!pages) + pages = $scope.levels[i] = {}; + + // Get the name at the current level + var name = names[i]; + + // Determine whether this page definition is part of the + // hierarchy containing the current page + var isCurrentPage = (currentPageName[i] === name); + + // Store new page if it doesn't yet exist at this level + if (!pages[name]) { + pages[name] = new PageDefinition({ + name : name, + url : isCurrentPage ? currentURL : page.url, + className : page.className, + weight : page.weight || weight + }); + } + + // If the name at this level no longer matches the + // hierarchy of the current page, do not go any deeper + if (currentPageName[i] !== name) + break; + + } + + }; + /** * Navigate to the given page. * - * @param {Page} page + * @param {PageDefinition} page * The page to navigate to. */ $scope.navigateToPage = function navigateToPage(page) { @@ -58,16 +164,67 @@ angular.module('navigation').directive('guacPageList', [function guacPageList() /** * Tests whether the given page is the page currently being viewed. * - * @param {Page} page + * @param {PageDefinition} page * The page to test. * * @returns {Boolean} * true if the given page is the current page, false otherwise. */ $scope.isCurrentPage = function isCurrentPage(page) { - return $location.url() === page.url; + return currentURL === page.url; }; + /** + * Given an arbitrary map of PageDefinitions, returns an array of + * those PageDefinitions, sorted by weight. + * + * @param {Object.<*, PageDefinition>} level + * A map of PageDefinitions with arbitrary keys. The value of + * each key is ignored. + * + * @returns {PageDefinition[]} + * An array of all PageDefinitions in the given map, sorted by + * weight. + */ + $scope.getPages = function getPages(level) { + + var pages = []; + + // Convert contents of level to a flat array of pages + angular.forEach(level, function addPageFromLevel(page) { + pages.push(page); + }); + + // Sort page array by weight + pages.sort(function comparePages(a, b) { + return a.weight - b.weight; + }); + + return pages; + + }; + + // Update page levels whenever pages changes + $scope.$watch('pages', function setPages(pages) { + + // Determine current page name + currentPageName = []; + angular.forEach(pages, function findCurrentPageName(page) { + + // If page is current page, store its names + if ($scope.isCurrentPage(page)) + currentPageName = getPageNames(page); + + }); + + // Reset contents of levels + $scope.levels = []; + + // Add all page definitions + angular.forEach(pages, addPage); + + }); + }] // end controller }; diff --git a/guacamole/src/main/webapp/app/navigation/services/userPageService.js b/guacamole/src/main/webapp/app/navigation/services/userPageService.js index 0cb9f530c..ef2280d37 100644 --- a/guacamole/src/main/webapp/app/navigation/services/userPageService.js +++ b/guacamole/src/main/webapp/app/navigation/services/userPageService.js @@ -41,31 +41,16 @@ angular.module('navigation').factory('userPageService', ['$injector', var service = {}; - /** - * Construct a new PageDefinition object with the given name and url. - * - * @constructor - * @param {String} name - * The i18n key for the name of the page. - * - * @param {String} url - * The URL of the page. - */ - var PageDefinition = function PageDefinition(name, url) { - this.name = name; - this.url = url; - }; - /** * The home page to assign to a user if they can navigate to more than one * page. * * @type PageDefinition */ - var SYSTEM_HOME_PAGE = new PageDefinition( - 'USER_MENU.ACTION_NAVIGATE_HOME', - '/' - ); + var SYSTEM_HOME_PAGE = new PageDefinition({ + name : 'USER_MENU.ACTION_NAVIGATE_HOME', + url : '/' + }); /** * Returns an appropriate home page for the current user. @@ -101,14 +86,14 @@ angular.module('navigation').factory('userPageService', ['$injector', // Only one connection present, use as home page if (connection) { - homePage = new PageDefinition( - connection.name, - '/client/' + ClientIdentifier.toString({ + homePage = new PageDefinition({ + name : connection.name, + url : '/client/' + ClientIdentifier.toString({ dataSource : dataSource, type : ClientIdentifier.Types.CONNECTION, id : connection.identifier }) - ); + }); } // Only one balancing group present, use as home page @@ -116,14 +101,14 @@ angular.module('navigation').factory('userPageService', ['$injector', && connectionGroup.type === ConnectionGroup.Type.BALANCING && _.isEmpty(connectionGroup.childConnections) && _.isEmpty(connectionGroup.childConnectionGroups)) { - homePage = new PageDefinition( - connectionGroup.name, - '/client/' + ClientIdentifier.toString({ + homePage = new PageDefinition({ + name : connectionGroup.name, + url : '/client/' + ClientIdentifier.toString({ dataSource : dataSource, type : ClientIdentifier.Types.CONNECTION_GROUP, id : connectionGroup.identifier }) - ); + }); } } @@ -247,33 +232,33 @@ angular.module('navigation').factory('userPageService', ['$injector', // If user can manage sessions, add link to sessions management page if (canManageSessions) { - pages.push(new PageDefinition( - 'USER_MENU.ACTION_MANAGE_SESSIONS', - '/settings/sessions' - )); + pages.push(new PageDefinition({ + name : 'USER_MENU.ACTION_MANAGE_SESSIONS', + url : '/settings/sessions' + })); } // If user can manage users, add link to user management page if (canManageUsers) { - pages.push(new PageDefinition( - 'USER_MENU.ACTION_MANAGE_USERS', - '/settings/users' - )); + pages.push(new PageDefinition({ + name : 'USER_MENU.ACTION_MANAGE_USERS', + url : '/settings/users' + })); } // If user can manage connections, add link to connections management page if (canManageConnections) { - pages.push(new PageDefinition( - 'USER_MENU.ACTION_MANAGE_CONNECTIONS', - '/settings/connections' - )); + pages.push(new PageDefinition({ + name : 'USER_MENU.ACTION_MANAGE_CONNECTIONS', + url : '/settings/connections' + })); } // Add link to user preferences (always accessible) - pages.push(new PageDefinition( - 'USER_MENU.ACTION_MANAGE_PREFERENCES', - '/settings/preferences' - )); + pages.push(new PageDefinition({ + name : 'USER_MENU.ACTION_MANAGE_PREFERENCES', + url : '/settings/preferences' + })); return pages; }; @@ -338,10 +323,10 @@ angular.module('navigation').factory('userPageService', ['$injector', // Add generic link to the first-available settings page if (settingsPages.length) { - pages.push(new PageDefinition( - 'USER_MENU.ACTION_MANAGE_SETTINGS', - settingsPages[0].url - )); + pages.push(new PageDefinition({ + name : 'USER_MENU.ACTION_MANAGE_SETTINGS', + url : settingsPages[0].url + })); } return pages; diff --git a/guacamole/src/main/webapp/app/navigation/styles/page-tabs.css b/guacamole/src/main/webapp/app/navigation/styles/page-tabs.css new file mode 100644 index 000000000..28b4de263 --- /dev/null +++ b/guacamole/src/main/webapp/app/navigation/styles/page-tabs.css @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +.page-tabs .page-list ul { + margin: 0; + padding: 0; + background: rgba(0, 0, 0, 0.0125); + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.page-tabs .page-list ul + ul { + font-size: 0.75em; +} + +.page-tabs .page-list li { + display: inline-block; + list-style: none; +} + +.page-tabs .page-list li a[href] { + display: block; + color: black; + text-decoration: none; + padding: 0.75em 1em; +} + +.page-tabs .page-list li a[href]:visited { + color: black; +} + +.page-tabs .page-list li a[href]:hover { + background-color: #CDA; +} + +.page-tabs .page-list li a[href].current, +.page-tabs .page-list li a[href].current:hover { + background: rgba(0,0,0,0.3); + cursor: default; +} diff --git a/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html b/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html index 35d12c31e..c5d131e9e 100644 --- a/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html +++ b/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html @@ -1,4 +1,4 @@ - +
    diff --git a/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js b/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js index 0b5d00b1e..ba702b014 100644 --- a/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js +++ b/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js @@ -30,37 +30,46 @@ angular.module('navigation').factory('PageDefinition', [function definePageDefin * an arbitrary, human-readable name. * * @constructor - * @param {String} name - * The the name of the page, which should be a translation table key. - * - * @param {String} url - * The URL of the page. - * - * @param {String} [className=''] - * The CSS class name to associate with this page, if any. + * @param {PageDefinition|Object} template + * The object whose properties should be copied within the new + * PageDefinition. */ - var PageDefinition = function PageDefinition(name, url, className) { + var PageDefinition = function PageDefinition(template) { /** * The the name of the page, which should be a translation table key. + * Alternatively, this may also be a list of names, where the final + * name represents the page and earlier names represent categorization. + * Those categorical names may be rendered hierarchically as a system + * of menus, tabs, etc. * - * @type String + * @type String|String[] */ - this.name = name; + this.name = template.name; /** * The URL of the page. * * @type String */ - this.url = url; + this.url = template.url; /** - * The CSS class name to associate with this page, if any. + * The CSS class name to associate with this page, if any. This will be + * an empty string by default. * * @type String */ - this.className = className || ''; + this.className = template.className || ''; + + /** + * A numeric value denoting the relative sort order when compared to + * other sibling PageDefinitions. If unspecified, sort order is + * determined by the system using the PageDefinition. + * + * @type Number + */ + this.weight = template.weight; }; diff --git a/guacamole/src/main/webapp/app/settings/styles/settings.css b/guacamole/src/main/webapp/app/settings/styles/settings.css index 67453a266..deb4b8cc1 100644 --- a/guacamole/src/main/webapp/app/settings/styles/settings.css +++ b/guacamole/src/main/webapp/app/settings/styles/settings.css @@ -34,36 +34,3 @@ text-align: center; margin: 1em 0; } - -.settings-tabs .page-list { - margin: 0; - padding: 0; - background: rgba(0, 0, 0, 0.0125); - border-bottom: 1px solid rgba(0, 0, 0, 0.05); -} - -.settings-tabs .page-list li { - display: inline-block; - list-style: none; -} - -.settings-tabs .page-list li a[href] { - display: block; - color: black; - text-decoration: none; - padding: 0.75em 1em; -} - -.settings-tabs .page-list li a[href]:visited { - color: black; -} - -.settings-tabs .page-list li a[href]:hover { - background-color: #CDA; -} - -.settings-tabs .page-list li a[href].current, -.settings-tabs .page-list li a[href].current:hover { - background: rgba(0,0,0,0.3); - cursor: default; -} diff --git a/guacamole/src/main/webapp/app/settings/templates/settings.html b/guacamole/src/main/webapp/app/settings/templates/settings.html index 3c7f26e84..97142467f 100644 --- a/guacamole/src/main/webapp/app/settings/templates/settings.html +++ b/guacamole/src/main/webapp/app/settings/templates/settings.html @@ -28,7 +28,7 @@ THE SOFTWARE.
    -
    +
    From 361e985ae1f817c4ec5bc6743a6a76d51bbc0661 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 2 Sep 2015 15:46:59 -0700 Subject: [PATCH 37/47] GUAC-586: Do not show tab levels which have only one tab. --- .../manage/controllers/manageUserController.js | 11 ----------- .../app/manage/templates/manageUser.html | 2 +- .../app/navigation/directives/guacPageList.js | 18 ++++++++++++++++++ .../app/navigation/templates/guacPageList.html | 2 +- .../settings/controllers/settingsController.js | 11 ----------- .../app/settings/templates/settings.html | 2 +- 6 files changed, 21 insertions(+), 25 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index 524b897f2..bf7cf6f51 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -146,17 +146,6 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto */ $scope.accountPages = []; - /** - * Returns whether the list of all available account tabs should be shown. - * - * @returns {Boolean} - * true if the list of available account tabs should be shown, false - * otherwise. - */ - $scope.showAccountTabs = function showAccountTabs() { - return !!$scope.accountPages && $scope.accountPages.length > 1; - }; - /** * Returns whether critical data has completed being loaded. * diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index 04cd593a6..71a6d93cf 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -28,7 +28,7 @@ THE SOFTWARE.
    - +
    diff --git a/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js b/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js index 04ecf7a77..ef2b05e61 100644 --- a/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js +++ b/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js @@ -223,6 +223,24 @@ angular.module('navigation').directive('guacPageList', [function guacPageList() // Add all page definitions angular.forEach(pages, addPage); + // Filter to only relevant levels + $scope.levels = $scope.levels.filter(function isRelevant(level) { + + // Determine relevancy by counting the number of pages + var pageCount = 0; + for (var name in level) { + + // Level is relevant if it has two or more pages + if (++pageCount === 2) + return true; + + } + + // Otherwise, the level is not relevant + return false; + + }); + }); }] // end controller diff --git a/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html b/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html index c5d131e9e..2f3b8aab8 100644 --- a/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html +++ b/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html @@ -1,4 +1,4 @@ -
    +
    - +
    From ddd144fc47c0ca344a7fab2f00d57348dd66a4e2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 2 Sep 2015 16:09:29 -0700 Subject: [PATCH 38/47] GUAC-586: Add support for data sources to connection and connection group management. --- .../app/index/config/indexRouteConfig.js | 6 +- .../controllers/manageConnectionController.js | 54 ++++++++++++------ .../manageConnectionGroupController.js | 34 +++++++---- .../navigation/services/userPageService.js | 57 +++++++++++-------- .../directives/guacSettingsConnections.js | 57 +++++++++++++------ .../app/settings/templates/connection.html | 2 +- .../settings/templates/connectionGroup.html | 2 +- .../templates/settingsConnections.html | 6 +- 8 files changed, 139 insertions(+), 79 deletions(-) diff --git a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js index f0a7f97f6..39808ea22 100644 --- a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js @@ -123,7 +123,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider', }) // Management screen - .when('/settings/:tab', { + .when('/settings/:dataSource?/:tab', { title : 'APP.NAME', bodyClassName : 'settings', templateUrl : 'app/settings/templates/settings.html', @@ -132,7 +132,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider', }) // Connection editor - .when('/manage/connections/:id?', { + .when('/manage/:dataSource/connections/:id?', { title : 'APP.NAME', bodyClassName : 'manage', templateUrl : 'app/manage/templates/manageConnection.html', @@ -141,7 +141,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider', }) // Connection group editor - .when('/manage/connectionGroups/:id?', { + .when('/manage/:dataSource/connectionGroups/:id?', { title : 'APP.NAME', bodyClassName : 'manage', templateUrl : 'app/manage/templates/manageConnectionGroup.html', diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js index 04e1066d4..1f1dc5ab5 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js @@ -55,7 +55,15 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i guacNotification.showStatus(false); } }; - + + /** + * The unique identifier of the data source containing the connection being + * edited. + * + * @type String + */ + var dataSource = $routeParams.dataSource; + /** * The identifier of the original connection from which this connection is * being cloned. Only valid if this is a new connection. @@ -178,20 +186,24 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i }; // Pull connection attribute schema - schemaService.getConnectionAttributes().success(function attributesReceived(attributes) { + schemaService.getConnectionAttributes(dataSource) + .success(function attributesReceived(attributes) { $scope.attributes = attributes; }); // Pull connection group hierarchy - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER, - [PermissionSet.ObjectPermissionType.ADMINISTER]) + connectionGroupService.getConnectionGroupTree( + dataSource, + ConnectionGroup.ROOT_IDENTIFIER, + [PermissionSet.ObjectPermissionType.ADMINISTER] + ) .success(function connectionGroupReceived(rootGroup) { $scope.rootGroup = rootGroup; }); // Query the user's permissions for the current connection - permissionService.getPermissions(authenticationService.getCurrentUsername()) - .success(function permissionsReceived(permissions) { + permissionService.getPermissions(dataSource, authenticationService.getCurrentUsername()) + .success(function permissionsReceived(permissions) { $scope.permissions = permissions; @@ -220,7 +232,8 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i }); // Get protocol metadata - schemaService.getProtocols().success(function protocolsReceived(protocols) { + schemaService.getProtocols(dataSource) + .success(function protocolsReceived(protocols) { $scope.protocols = protocols; }); @@ -233,12 +246,14 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i if (identifier) { // Pull data from existing connection - connectionService.getConnection(identifier).success(function connectionRetrieved(connection) { + connectionService.getConnection(dataSource, identifier) + .success(function connectionRetrieved(connection) { $scope.connection = connection; }); // Pull connection history - connectionService.getConnectionHistory(identifier).success(function historyReceived(historyEntries) { + connectionService.getConnectionHistory(dataSource, identifier) + .success(function historyReceived(historyEntries) { // Wrap all history entries for sake of display $scope.historyEntryWrappers = []; @@ -249,7 +264,8 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i }); // Pull connection parameters - connectionService.getConnectionParameters(identifier).success(function parametersReceived(parameters) { + connectionService.getConnectionParameters(dataSource, identifier) + .success(function parametersReceived(parameters) { $scope.parameters = parameters; }); } @@ -258,7 +274,8 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i else if (cloneSourceIdentifier) { // Pull data from cloned connection - connectionService.getConnection(cloneSourceIdentifier).success(function connectionRetrieved(connection) { + connectionService.getConnection(dataSource, cloneSourceIdentifier) + .success(function connectionRetrieved(connection) { $scope.connection = connection; // Clear the identifier field because this connection is new @@ -269,7 +286,8 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i $scope.historyEntryWrappers = []; // Pull connection parameters from cloned connection - connectionService.getConnectionParameters(cloneSourceIdentifier).success(function parametersReceived(parameters) { + connectionService.getConnectionParameters(dataSource, cloneSourceIdentifier) + .success(function parametersReceived(parameters) { $scope.parameters = parameters; }); } @@ -332,7 +350,7 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i * Cancels all pending edits, returning to the management page. */ $scope.cancel = function cancel() { - $location.path('/settings/connections'); + $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections'); }; /** @@ -340,7 +358,7 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i * which is prepopulated with the data from the connection currently being edited. */ $scope.cloneConnection = function cloneConnection() { - $location.path('/manage/connections').search('clone', identifier); + $location.path('/manage/' + encodeURIComponent(dataSource) + '/connections').search('clone', identifier); }; /** @@ -352,9 +370,9 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i $scope.connection.parameters = $scope.parameters; // Save the connection - connectionService.saveConnection($scope.connection) + connectionService.saveConnection(dataSource, $scope.connection) .success(function savedConnection() { - $location.path('/settings/connections'); + $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections'); }) // Notify of any errors @@ -402,9 +420,9 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i var deleteConnectionImmediately = function deleteConnectionImmediately() { // Delete the connection - connectionService.deleteConnection($scope.connection) + connectionService.deleteConnection(dataSource, $scope.connection) .success(function deletedConnection() { - $location.path('/settings/connections'); + $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections'); }) // Notify of any errors diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js index 64fb03b08..1de6c7127 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js @@ -51,6 +51,14 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' } }; + /** + * The unique identifier of the data source containing the connection group + * being edited. + * + * @type String + */ + var dataSource = $routeParams.dataSource; + /** * The identifier of the connection group being edited. If a new connection * group is being created, this will not be defined. @@ -123,13 +131,14 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' }; // Pull connection group attribute schema - schemaService.getConnectionGroupAttributes().success(function attributesReceived(attributes) { + schemaService.getConnectionGroupAttributes(dataSource) + .success(function attributesReceived(attributes) { $scope.attributes = attributes; }); // Query the user's permissions for the current connection group - permissionService.getPermissions(authenticationService.getCurrentUsername()) - .success(function permissionsReceived(permissions) { + permissionService.getPermissions(dataSource, authenticationService.getCurrentUsername()) + .success(function permissionsReceived(permissions) { $scope.permissions = permissions; @@ -150,14 +159,19 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' // Pull connection group hierarchy - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER]) + connectionGroupService.getConnectionGroupTree( + dataSource, + ConnectionGroup.ROOT_IDENTIFIER, + [PermissionSet.ObjectPermissionType.ADMINISTER] + ) .success(function connectionGroupReceived(rootGroup) { $scope.rootGroup = rootGroup; }); // If we are editing an existing connection group, pull its data if (identifier) { - connectionGroupService.getConnectionGroup(identifier).success(function connectionGroupReceived(connectionGroup) { + connectionGroupService.getConnectionGroup(dataSource, identifier) + .success(function connectionGroupReceived(connectionGroup) { $scope.connectionGroup = connectionGroup; }); } @@ -187,7 +201,7 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' * Cancels all pending edits, returning to the management page. */ $scope.cancel = function cancel() { - $location.path('/settings/connections'); + $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections'); }; /** @@ -197,9 +211,9 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' $scope.saveConnectionGroup = function saveConnectionGroup() { // Save the connection - connectionGroupService.saveConnectionGroup($scope.connectionGroup) + connectionGroupService.saveConnectionGroup(dataSource, $scope.connectionGroup) .success(function savedConnectionGroup() { - $location.path('/settings/connections'); + $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections'); }) // Notify of any errors @@ -247,9 +261,9 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' var deleteConnectionGroupImmediately = function deleteConnectionGroupImmediately() { // Delete the connection group - connectionGroupService.deleteConnectionGroup($scope.connectionGroup) + connectionGroupService.deleteConnectionGroup(dataSource, $scope.connectionGroup) .success(function deletedConnectionGroup() { - $location.path('/settings/connections'); + $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections'); }) // Notify of any errors diff --git a/guacamole/src/main/webapp/app/navigation/services/userPageService.js b/guacamole/src/main/webapp/app/navigation/services/userPageService.js index ef2280d37..27878ceb8 100644 --- a/guacamole/src/main/webapp/app/navigation/services/userPageService.js +++ b/guacamole/src/main/webapp/app/navigation/services/userPageService.js @@ -33,11 +33,12 @@ angular.module('navigation').factory('userPageService', ['$injector', var PermissionSet = $injector.get('PermissionSet'); // Get required services - var $q = $injector.get('$q'); - var authenticationService = $injector.get('authenticationService'); - var connectionGroupService = $injector.get('connectionGroupService'); - var dataSourceService = $injector.get('dataSourceService'); - var permissionService = $injector.get('permissionService'); + var $q = $injector.get('$q'); + var authenticationService = $injector.get('authenticationService'); + var connectionGroupService = $injector.get('connectionGroupService'); + var dataSourceService = $injector.get('dataSourceService'); + var permissionService = $injector.get('permissionService'); + var translationStringService = $injector.get('translationStringService'); var service = {}; @@ -167,12 +168,12 @@ angular.module('navigation').factory('userPageService', ['$injector', var pages = []; - var canManageUsers = false; - var canManageConnections = false; - var canManageSessions = false; + var canManageUsers = []; + var canManageConnections = []; + var canManageSessions = []; // Inspect the contents of each provided permission set - angular.forEach(permissionSets, function inspectPermissions(permissions) { + angular.forEach(permissionSets, function inspectPermissions(permissions, dataSource) { permissions = angular.copy(permissions); @@ -187,8 +188,7 @@ angular.module('navigation').factory('userPageService', ['$injector', authenticationService.getCurrentUsername()); // Determine whether the current user needs access to the user management UI - canManageUsers = canManageUsers || - + if ( // System permissions PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER) @@ -200,11 +200,12 @@ angular.module('navigation').factory('userPageService', ['$injector', || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) // Permission to administer users - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER); + || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) + ) + canManageUsers.push(dataSource); // Determine whether the current user needs access to the connection management UI - canManageConnections = canManageConnections || - + if ( // System permissions PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION) @@ -220,18 +221,21 @@ angular.module('navigation').factory('userPageService', ['$injector', // Permission to administer connections or connection groups || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) - || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER); + || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) + ) + canManageConnections.push(dataSource); // Determine whether the current user needs access to the session management UI - canManageSessions = canManageSessions || - + if ( // A user must be a system administrator to manage sessions - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER); + PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) + ) + canManageSessions.push(dataSource); }); // If user can manage sessions, add link to sessions management page - if (canManageSessions) { + if (canManageSessions.length) { pages.push(new PageDefinition({ name : 'USER_MENU.ACTION_MANAGE_SESSIONS', url : '/settings/sessions' @@ -239,20 +243,23 @@ angular.module('navigation').factory('userPageService', ['$injector', } // If user can manage users, add link to user management page - if (canManageUsers) { + if (canManageUsers.length) { pages.push(new PageDefinition({ name : 'USER_MENU.ACTION_MANAGE_USERS', url : '/settings/users' })); } - // If user can manage connections, add link to connections management page - if (canManageConnections) { + // If user can manage connections, add links for connection management pages + angular.forEach(canManageConnections, function addConnectionManagementLink(dataSource) { pages.push(new PageDefinition({ - name : 'USER_MENU.ACTION_MANAGE_CONNECTIONS', - url : '/settings/connections' + name : [ + 'USER_MENU.ACTION_MANAGE_CONNECTIONS', + translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME' + ], + url : '/settings/' + encodeURIComponent(dataSource) + '/connections' })); - } + }); // Add link to user preferences (always accessible) pages.push(new PageDefinition({ diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js index e1b77893f..640cb69a9 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js @@ -41,13 +41,18 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe var PermissionSet = $injector.get('PermissionSet'); // Required services - var $location = $injector.get('$location'); + var $routeParams = $injector.get('$routeParams'); var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); + var dataSourceService = $injector.get('dataSourceService'); var guacNotification = $injector.get('guacNotification'); var permissionService = $injector.get('permissionService'); - // Identifier of the current user + /** + * The identifier of the current user. + * + * @type String + */ var currentUsername = authenticationService.getCurrentUsername(); /** @@ -62,12 +67,19 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe } }; + /** + * The identifier of the currently-selected data source. + * + * @type String + */ + $scope.dataSource = $routeParams.dataSource; + /** * The root connection group of the connection group hierarchy. * - * @type ConnectionGroup + * @type Object. */ - $scope.rootGroup = null; + $scope.rootGroups = null; /** * Whether the current user can manage connections. If the current @@ -98,7 +110,7 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe * All permissions associated with the current user, or null if the * user's permissions have not yet been loaded. * - * @type PermissionSet + * @type Object. */ $scope.permissions = null; @@ -111,20 +123,25 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe */ $scope.isLoaded = function isLoaded() { - return $scope.rootGroup !== null - && $scope.permissions !== null - && $scope.canManageConnections !== null - && $scope.canCreateConnections !== null - && $scope.canCreateConnectionGroups !== null; + return $scope.rootGroup !== null + && $scope.permissions !== null; }; + $scope.canManageConnections = true; + $scope.canCreateConnections = true; + $scope.canCreateConnectionGroups = true; + // Retrieve current permissions - permissionService.getPermissions(currentUsername) - .success(function permissionsRetrieved(permissions) { + dataSourceService.apply( + permissionService.getPermissions, + [$scope.dataSource], + currentUsername + ) + .then(function permissionsRetrieved(permissions) { $scope.permissions = permissions; - +/* // Ignore permission to update root group PermissionSet.removeConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER); @@ -154,14 +171,18 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe // Return to home if there's nothing to do here if (!$scope.canManageConnections) $location.path('/'); - +*/ }); // Retrieve all connections for which we have UPDATE or DELETE permission - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER, - [PermissionSet.ObjectPermissionType.UPDATE, PermissionSet.ObjectPermissionType.DELETE]) - .success(function connectionGroupReceived(rootGroup) { - $scope.rootGroup = rootGroup; + dataSourceService.apply( + connectionGroupService.getConnectionGroupTree, + [$scope.dataSource], + ConnectionGroup.ROOT_IDENTIFIER, + [PermissionSet.ObjectPermissionType.UPDATE, PermissionSet.ObjectPermissionType.DELETE] + ) + .then(function connectionGroupsReceived(rootGroups) { + $scope.rootGroups = rootGroups; }); }] diff --git a/guacamole/src/main/webapp/app/settings/templates/connection.html b/guacamole/src/main/webapp/app/settings/templates/connection.html index f2d1a5550..7514ade8e 100644 --- a/guacamole/src/main/webapp/app/settings/templates/connection.html +++ b/guacamole/src/main/webapp/app/settings/templates/connection.html @@ -1,4 +1,4 @@ - +