diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/DirectoryObjectService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/DirectoryObjectService.java index 5a67b24b6..abe5ce1c3 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/DirectoryObjectService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/DirectoryObjectService.java @@ -363,6 +363,47 @@ public abstract class DirectoryObjectService getImplicitPermissions(AuthenticatedUser user, + ModelType model) { + + // Build list of implicit permissions + Collection implicitPermissions = + new ArrayList(IMPLICIT_OBJECT_PERMISSIONS.length); + + UserModel userModel = user.getUser().getModel(); + for (ObjectPermission.Type permission : IMPLICIT_OBJECT_PERMISSIONS) { + + // Create model which grants this permission to the current user + ObjectPermissionModel permissionModel = new ObjectPermissionModel(); + permissionModel.setUserID(userModel.getObjectID()); + permissionModel.setUsername(userModel.getIdentifier()); + permissionModel.setType(permission); + permissionModel.setObjectIdentifier(model.getIdentifier()); + + // Add permission + implicitPermissions.add(permissionModel); + + } + + return implicitPermissions; + + } + /** * Creates the given object within the database. If the object already * exists, an error will be thrown. The internal model object will be @@ -390,27 +431,8 @@ public abstract class DirectoryObjectService implicitPermissions = - new ArrayList(IMPLICIT_OBJECT_PERMISSIONS.length); - - UserModel userModel = user.getUser().getModel(); - for (ObjectPermission.Type permission : IMPLICIT_OBJECT_PERMISSIONS) { - - // Create model which grants this permission to the current user - ObjectPermissionModel permissionModel = new ObjectPermissionModel(); - permissionModel.setUserID(userModel.getObjectID()); - permissionModel.setUsername(userModel.getIdentifier()); - permissionModel.setType(permission); - permissionModel.setObjectIdentifier(model.getIdentifier()); - - // Add permission - implicitPermissions.add(permissionModel); - - } - // Add implicit permissions - getPermissionMapper().insert(implicitPermissions); + getPermissionMapper().insert(getImplicitPermissions(user, model)); return getObjectInstance(user, model); diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserService.java index 2db7f3d96..f552ce7a6 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserService.java @@ -34,9 +34,11 @@ import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleUnsupportedException; import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionMapper; +import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionModel; import org.glyptodon.guacamole.auth.jdbc.permission.UserPermissionMapper; import org.glyptodon.guacamole.auth.jdbc.security.PasswordEncryptionService; import org.glyptodon.guacamole.net.auth.User; +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; @@ -48,7 +50,16 @@ import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; * @author Michael Jumper, James Muehlner */ public class UserService extends DirectoryObjectService { - + + /** + * All user permissions which are implicitly granted to the new user upon + * creation. + */ + private static final ObjectPermission.Type[] IMPLICIT_USER_PERMISSIONS = { + ObjectPermission.Type.READ, + ObjectPermission.Type.UPDATE + }; + /** * Mapper for accessing users. */ @@ -165,6 +176,30 @@ public class UserService extends DirectoryObjectService + getImplicitPermissions(AuthenticatedUser user, UserModel model) { + + // Get original set of implicit permissions + Collection implicitPermissions = super.getImplicitPermissions(user, model); + + // Grant implicit permissions to the new user + for (ObjectPermission.Type permissionType : IMPLICIT_USER_PERMISSIONS) { + + ObjectPermissionModel permissionModel = new ObjectPermissionModel(); + permissionModel.setUserID(model.getObjectID()); + permissionModel.setUsername(model.getIdentifier()); + permissionModel.setType(permissionType); + permissionModel.setObjectIdentifier(model.getIdentifier()); + + // Add new permission to implicit permission set + implicitPermissions.add(permissionModel); + + } + + return implicitPermissions; + } + @Override protected void beforeDelete(AuthenticatedUser user, String identifier) throws GuacamoleException { diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.6.sql b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.6.sql new file mode 100644 index 000000000..6d6777d08 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.6.sql @@ -0,0 +1,53 @@ +-- +-- 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. +-- + +-- +-- Explicitly add permission for each user to READ and UPDATE him/herself +-- + +INSERT INTO guacamole_user_permission + (user_id, affected_user_id, permission) +SELECT user_id, user_id, 'UPDATE' +FROM guacamole_user +WHERE + user_id NOT IN ( + SELECT user_id + FROM guacamole_user_permission + WHERE + user_id = affected_user_id + AND permission = 'UPDATE' + ); + + +INSERT INTO guacamole_user_permission + (user_id, affected_user_id, permission) +SELECT user_id, user_id, 'READ' +FROM guacamole_user +WHERE + user_id NOT IN ( + SELECT user_id + FROM guacamole_user_permission + WHERE + user_id = affected_user_id + AND permission = 'READ' + ); + diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUserPasswordUpdate.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUserPasswordUpdate.java new file mode 100644 index 000000000..7bdb60150 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUserPasswordUpdate.java @@ -0,0 +1,82 @@ +/* + * 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. + */ +package org.glyptodon.guacamole.net.basic.rest.user; + +/** + * All the information necessary for the password update operation on a user. + * + * @author James Muehlner + */ +public class APIUserPasswordUpdate { + + /** + * The old (current) password of this user. + */ + private String oldPassword; + + /** + * The new password of this user. + */ + private String newPassword; + + /** + * Returns the old password for this user. This password must match the + * user's current password for the password update operation to succeed. + * + * @return + * The old password for this user. + */ + public String getOldPassword() { + return oldPassword; + } + + /** + * Set the old password for this user. This password must match the + * user's current password for the password update operation to succeed. + * + * @param oldPassword + * The old password for this user. + */ + public void setOldPassword(String oldPassword) { + this.oldPassword = oldPassword; + } + + /** + * Returns the new password that will be assigned to this user. + * + * @return + * The new password for this user. + */ + public String getNewPassword() { + return newPassword; + } + + /** + * Set the new password that will be assigned to this user. + * + * @param newPassword + * The new password for this user. + */ + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} 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 d8fc5f15a..0167cf860 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 @@ -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 @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.UUID; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; @@ -36,11 +37,14 @@ 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.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; 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; import org.glyptodon.guacamole.net.auth.UserContext; @@ -112,6 +116,12 @@ public class UserRESTService { @Inject private ObjectRetrievalService retrievalService; + /** + * The authentication provider used to authenticating a user. + */ + @Inject + private AuthenticationProvider authProvider; + /** * Gets a list of users in the system, filtering the returned list by the * given permission, if specified. @@ -262,6 +272,12 @@ public class UserRESTService { if (!user.getUsername().equals(username)) throw new HTTPException(Response.Status.BAD_REQUEST, "Username in path does not match username provided JSON data."); + + // A user may not use this endpoint to modify himself + if (userContext.self().getIdentifier().equals(user.getUsername())) { + throw new HTTPException(Response.Status.FORBIDDEN, + "Permission denied."); + } // Get the user User existingUser = retrievalService.retrieveUser(userContext, username); @@ -275,6 +291,63 @@ public class UserRESTService { } + /** + * Updates the password for an individual existing user. + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param username + * The username of the user to update. + * + * @param userPasswordUpdate + * The object containing the old password for the user, as well as the + * new password to set for that user. + * + * @param request + * The HttpServletRequest associated with the password update attempt. + * + * @throws GuacamoleException + * If an error occurs while updating the user's password. + */ + @PUT + @Path("/{username}/password") + @AuthProviderRESTExposure + public void updatePassword(@QueryParam("token") String authToken, + @PathParam("username") String username, + APIUserPasswordUpdate userPasswordUpdate, + @Context HttpServletRequest request) throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Build credentials + Credentials credentials = new Credentials(); + credentials.setUsername(username); + credentials.setPassword(userPasswordUpdate.getOldPassword()); + credentials.setRequest(request); + credentials.setSession(request.getSession(true)); + + // Verify that the old password was correct + if (authProvider.getUserContext(credentials) == null) { + throw new HTTPException(Response.Status.FORBIDDEN, + "Permission denied."); + } + + // Get the user directory + Directory userDirectory = userContext.getUserDirectory(); + + // Get the user that we want to updates + User user = retrievalService.retrieveUser(userContext, username); + + // Set password to the newly provided one + user.setPassword(userPasswordUpdate.getNewPassword()); + + // Update the user + userDirectory.update(user); + + } + /** * Deletes an individual existing user. * diff --git a/guacamole/src/main/webapp/app/home/controllers/homeController.js b/guacamole/src/main/webapp/app/home/controllers/homeController.js index 70051971c..601b18ae5 100644 --- a/guacamole/src/main/webapp/app/home/controllers/homeController.js +++ b/guacamole/src/main/webapp/app/home/controllers/homeController.js @@ -34,6 +34,7 @@ angular.module('home').controller('homeController', ['$scope', '$injector', var authenticationService = $injector.get("authenticationService"); var connectionGroupService = $injector.get("connectionGroupService"); var permissionService = $injector.get("permissionService"); + var userService = $injector.get("userService"); /** * The root connection group, or null if the connection group hierarchy has @@ -52,6 +53,37 @@ angular.module('home').controller('homeController', ['$scope', '$injector', */ $scope.canManageGuacamole = null; + /** + * Whether the current user has sufficient permissions to change + * his/her own password. If permissions have not yet been loaded, this will + * be null. + * + * @type Boolean + */ + $scope.canChangePassword = null; + + /** + * Whether the password edit dialog should be shown. + * + * @type Boolean + */ + $scope.showPasswordDialog = false; + + /** + * The new password for the user. + * + * @type String + */ + $scope.newPassword = null; + + /** + * The password match for the user. The update password action will fail if + * $scope.newPassword !== $scope.passwordMatch. + * + * @type String + */ + $scope.newPasswordMatch = null; + /** * Returns whether critical data has completed being loaded. * @@ -62,7 +94,8 @@ angular.module('home').controller('homeController', ['$scope', '$injector', $scope.isLoaded = function isLoaded() { return $scope.rootConnectionGroup !== null - && $scope.canManageGuacamole !== null; + && $scope.canManageGuacamole !== null + && $scope.canChangePassword !== null; }; @@ -71,13 +104,24 @@ angular.module('home').controller('homeController', ['$scope', '$injector', .success(function rootGroupRetrieved(rootConnectionGroup) { $scope.rootConnectionGroup = rootConnectionGroup; }); + + // Identifier of the current user + var currentUserID = authenticationService.getCurrentUserID(); // Retrieve current permissions - permissionService.getPermissions(authenticationService.getCurrentUserID()) + permissionService.getPermissions(currentUserID) .success(function permissionsRetrieved(permissions) { + + // Determine whether the current user can change his/her own password + $scope.canChangePassword = + PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, currentUserID) + && PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.READ, currentUserID); // Ignore permission to update root group PermissionSet.removeConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER); + + // Ignore permission to update self + PermissionSet.removeUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, currentUserID); // Determine whether the current user needs access to the management UI $scope.canManageGuacamole = @@ -105,4 +149,91 @@ angular.module('home').controller('homeController', ['$scope', '$injector', }); + /** + * An action to be provided along with the object sent to showStatus which + * closes the currently-shown status dialog. + */ + var ACKNOWLEDGE_ACTION = { + name : "HOME.ACTION_ACKNOWLEDGE", + // Handle action + callback : function acknowledgeCallback() { + $scope.showStatus(false); + } + }; + + /** + * Show the password update dialog. + */ + $scope.showPasswordUpdate = function showPasswordUpdate() { + + // Show the dialog + $scope.showPasswordDialog = true; + }; + + /** + * Close the password update dialog. + */ + $scope.closePasswordUpdate = function closePasswordUpdate() { + + // Clear the password fields and close the dialog + $scope.oldPassword = null; + $scope.newPassword = null; + $scope.newPasswordMatch = null; + $scope.showPasswordDialog = false; + }; + + /** + * Update the current user's password to the password currently set within + * the password change dialog. + */ + $scope.updatePassword = function updatePassword() { + + // Verify passwords match + if ($scope.newPasswordMatch !== $scope.newPassword) { + $scope.showStatus({ + className : 'error', + title : 'HOME.DIALOG_HEADER_ERROR', + text : 'HOME.ERROR_PASSWORD_MISMATCH', + actions : [ ACKNOWLEDGE_ACTION ] + }); + return; + } + + // Verify that the new password is not blank + if (!$scope.newPassword) { + $scope.showStatus({ + className : 'error', + title : 'HOME.DIALOG_HEADER_ERROR', + text : 'HOME.ERROR_PASSWORD_BLANK', + actions : [ ACKNOWLEDGE_ACTION ] + }); + return; + } + + // Save the user with the new password + userService.updateUserPassword(currentUserID, $scope.oldPassword, $scope.newPassword) + .success(function passwordUpdated() { + + // Close the password update dialog + $scope.closePasswordUpdate(); + + // Indicate that the password has been changed + $scope.showStatus({ + text : 'HOME.PASSWORD_CHANGED', + actions : [ ACKNOWLEDGE_ACTION ] + }); + }) + + // Notify of any errors + .error(function passwordUpdateFailed(error) { + $scope.showStatus({ + className : 'error', + title : 'HOME.DIALOG_HEADER_ERROR', + 'text' : error.message, + actions : [ ACKNOWLEDGE_ACTION ] + }); + }); + + }; + }]); diff --git a/guacamole/src/main/webapp/app/home/styles/home.css b/guacamole/src/main/webapp/app/home/styles/home.css index 6ba463b8a..9754d74af 100644 --- a/guacamole/src/main/webapp/app/home/styles/home.css +++ b/guacamole/src/main/webapp/app/home/styles/home.css @@ -69,3 +69,36 @@ div.recent-connections div.connection { max-width: 75%; overflow: hidden; } + +.password-dialog { + visibility: hidden; + opacity: 0; + -webkit-transition: visibility 0.125s, opacity 0.125s; + -moz-transition: visibility 0.125s, opacity 0.125s; + -ms-transition: visibility 0.125s, opacity 0.125s; + -o-transition: visibility 0.125s, opacity 0.125s; + transition: visibility 0.125s, opacity 0.125s; + position: absolute; + background: white; + padding: 1em; + border: 1px solid rgba(0, 0, 0, 0.25); + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25); + margin: 1em; + right: 0; + top: 0; + z-index: 1; +} + +.password-dialog.shown { + visibility: visible; + opacity: 1; + -webkit-transition: opacity 0.125s; + -moz-transition: opacity 0.125s; + -ms-transition: opacity 0.125s; + -o-transition: opacity 0.125s; + transition: opacity 0.125s; +} + +.password-dialog .fields { + width: 100%; +} \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/home/templates/home.html b/guacamole/src/main/webapp/app/home/templates/home.html index 4396625b1..2fb88ed51 100644 --- a/guacamole/src/main/webapp/app/home/templates/home.html +++ b/guacamole/src/main/webapp/app/home/templates/home.html @@ -25,6 +25,33 @@
+ {{'HOME.ACTION_CHANGE_PASSWORD' | translate}} +
+ +
+ + + + + + + + + + + + + +
{{'HOME.FIELD_HEADER_PASSWORD_OLD' | translate}}
{{'HOME.FIELD_HEADER_PASSWORD_NEW' | translate}}
{{'HOME.FIELD_HEADER_PASSWORD_NEW_AGAIN' | translate}}
+
+ + +
+ + +
+
+ {{'HOME.ACTION_MANAGE' | translate}} {{'HOME.ACTION_LOGOUT' | translate}}
diff --git a/guacamole/src/main/webapp/app/index/services/authenticationInterceptor.js b/guacamole/src/main/webapp/app/index/services/authenticationInterceptor.js index 744e08924..4e524741d 100644 --- a/guacamole/src/main/webapp/app/index/services/authenticationInterceptor.js +++ b/guacamole/src/main/webapp/app/index/services/authenticationInterceptor.js @@ -29,9 +29,9 @@ angular.module('index').factory('authenticationInterceptor', ['$location', '$q', }, 'responseError': function(rejection) { - // Do not redirect failed login requests to the login page. + // Do not redirect failed api requests. if ((rejection.status === 401 || rejection.status === 403) - && rejection.config.url.search('api/token') === -1) { + && rejection.config.url.search('api/') === -1) { $location.path('/login'); } return $q.reject(rejection); diff --git a/guacamole/src/main/webapp/app/index/styles/buttons.css b/guacamole/src/main/webapp/app/index/styles/buttons.css index bc7648906..748d0910c 100644 --- a/guacamole/src/main/webapp/app/index/styles/buttons.css +++ b/guacamole/src/main/webapp/app/index/styles/buttons.css @@ -73,7 +73,7 @@ button.danger:active, a.button.danger:active { background: #932; } -.button.logout, .button.manage, .button.back, .button.home { +.button.logout, .button.manage, .button.back, .button.home, .button.change-password { background-repeat: no-repeat; background-size: 1em; background-position: 0.5em 0.45em; @@ -95,3 +95,7 @@ button.danger:active, a.button.danger:active { .button.home { background-image: url('images/action-icons/guac-home.png'); } + +.button.change-password { + background-image: url('images/action-icons/guac-key.png'); +} diff --git a/guacamole/src/main/webapp/app/index/styles/status.css b/guacamole/src/main/webapp/app/index/styles/status.css index 9af6c463b..2b079cd0b 100644 --- a/guacamole/src/main/webapp/app/index/styles/status.css +++ b/guacamole/src/main/webapp/app/index/styles/status.css @@ -28,7 +28,7 @@ left: 0; top: 0; background: rgba(0, 0, 0, 0.5); - z-index: 1; + z-index: 10; } .status-middle { diff --git a/guacamole/src/main/webapp/app/rest/services/userService.js b/guacamole/src/main/webapp/app/rest/services/userService.js index c4173fd1e..07dd87453 100644 --- a/guacamole/src/main/webapp/app/rest/services/userService.js +++ b/guacamole/src/main/webapp/app/rest/services/userService.js @@ -23,8 +23,15 @@ /** * Service for operating on users via the REST API. */ -angular.module('rest').factory('userService', ['$http', 'authenticationService', - function userService($http, authenticationService) { +angular.module('rest').factory('userService', ['$injector', + function userService($injector) { + + // Get required types + var UserPasswordUpdate = $injector.get("UserPasswordUpdate"); + + // Get required services + var $http = $injector.get("$http"); + var authenticationService = $injector.get("authenticationService"); var service = {}; @@ -173,6 +180,44 @@ angular.module('rest').factory('userService', ['$http', 'authenticationService', }; + /** + * 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} username + * The username of the user to update. + * + * @param {String} oldPassword + * The exiting password of the user to update. + * + * @param {String} newPassword + * The new password of the user to update. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * password update operation is successful. + */ + service.updateUserPassword = function updateUserPassword(username, + oldPassword, newPassword) { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Retrieve user + return $http({ + method : 'PUT', + url : 'api/users/' + encodeURIComponent(username) + '/password', + params : httpParameters, + data : new UserPasswordUpdate({ + oldPassword : oldPassword, + newPassword : newPassword + }) + }); + + }; + return service; }]); diff --git a/guacamole/src/main/webapp/app/rest/types/UserPasswordUpdate.js b/guacamole/src/main/webapp/app/rest/types/UserPasswordUpdate.js new file mode 100644 index 000000000..14c7e2cff --- /dev/null +++ b/guacamole/src/main/webapp/app/rest/types/UserPasswordUpdate.js @@ -0,0 +1,61 @@ +/* + * 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 defines the UserPasswordUpdate class. + */ +angular.module('rest').factory('UserPasswordUpdate', [function defineUserPasswordUpdate() { + + /** + * The object sent to the REST API when representing the data + * associated with a user password update. + * + * @constructor + * @param {UserPasswordUpdate|Object} [template={}] + * The object whose properties should be copied within the new + * UserPasswordUpdate. + */ + var UserPasswordUpdate = function UserPasswordUpdate(template) { + + // Use empty object by default + template = template || {}; + + /** + * This user's current password. Required for authenticating the user + * as part of to the password update operation. + * + * @type String + */ + this.oldPassword = template.oldPassword; + + /** + * The new password to set for the user. + * + * @type String + */ + this.newPassword = template.newPassword; + + }; + + return UserPasswordUpdate; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/webapp/images/action-icons/guac-key.png b/guacamole/src/main/webapp/images/action-icons/guac-key.png new file mode 100644 index 000000000..89ea62b6b Binary files /dev/null and b/guacamole/src/main/webapp/images/action-icons/guac-key.png differ diff --git a/guacamole/src/main/webapp/translations/en_US.json b/guacamole/src/main/webapp/translations/en_US.json index 64859ed5d..de751955e 100644 --- a/guacamole/src/main/webapp/translations/en_US.json +++ b/guacamole/src/main/webapp/translations/en_US.json @@ -2,16 +2,24 @@ "APP" : { - "ACTION_ACKNOWLEDGE" : "OK", - "ACTION_CANCEL" : "Cancel", - "ACTION_CLONE" : "Clone", - "ACTION_DELETE" : "Delete", - "ACTION_LOGIN" : "Login", - "ACTION_LOGOUT" : "Logout", - "ACTION_MANAGE" : "Manage", - "ACTION_NAVIGATE_BACK" : "Back", - "ACTION_NAVIGATE_HOME" : "Home", - "ACTION_SAVE" : "Save", + "ACTION_ACKNOWLEDGE" : "OK", + "ACTION_CANCEL" : "Cancel", + "ACTION_CHANGE_PASSWORD" : "Change Password", + "ACTION_CLONE" : "Clone", + "ACTION_DELETE" : "Delete", + "ACTION_LOGIN" : "Login", + "ACTION_LOGOUT" : "Logout", + "ACTION_MANAGE" : "Manage", + "ACTION_NAVIGATE_BACK" : "Back", + "ACTION_NAVIGATE_HOME" : "Home", + "ACTION_SAVE" : "Save", + + "DIALOG_HEADER_ERROR" : "Error", + + "ERROR_PASSWORD_MISMATCH" : "The provided passwords do not match.", + + "FIELD_HEADER_PASSWORD" : "Password:", + "FIELD_HEADER_PASSWORD_AGAIN" : "Re-enter Password:", "INFO_ACTIVE_USER_COUNT" : "Currently in use by {USERS} {USERS, plural, one{user} other{users}}.", @@ -106,12 +114,27 @@ "HOME" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE" : "@:APP.ACTION_MANAGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CHANGE_PASSWORD" : "@:APP.ACTION_CHANGE_PASSWORD", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", + "ACTION_MANAGE" : "@:APP.ACTION_MANAGE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", + + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "ERROR_PASSWORD_BLANK" : "Your password cannot be blank.", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + + "FIELD_HEADER_PASSWORD_OLD" : "Current Password:", + "FIELD_HEADER_PASSWORD_NEW" : "New Password:", + "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Confirm New Password:", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", "INFO_NO_RECENT_CONNECTIONS" : "No recent connections.", + + "PASSWORD_CHANGED" : "Password changed.", "SECTION_HEADER_ALL_CONNECTIONS" : "All Connections", "SECTION_HEADER_RECENT_CONNECTIONS" : "Recent Connections" @@ -224,14 +247,14 @@ "DIALOG_HEADER_CONFIRM_DELETE" : "Delete User", "DIALOG_HEADER_ERROR" : "Error", - "ERROR_PASSWORD_MISMATCH" : "The provided passwords do not match.", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Administer system:", "FIELD_HEADER_CREATE_NEW_USERS" : "Create new users:", "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Create new connections:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Create new connection groups:", - "FIELD_HEADER_PASSWORD" : "Password:", - "FIELD_HEADER_PASSWORD_AGAIN" : "Re-enter Password:", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", "FIELD_HEADER_USERNAME" : "Username:", "SECTION_HEADER_CONNECTIONS" : "Connections",