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 c7276ee48..c00d4eae4 100644 --- a/guacamole/src/main/webapp/app/home/controllers/homeController.js +++ b/guacamole/src/main/webapp/app/home/controllers/homeController.js @@ -74,15 +74,15 @@ angular.module('home').controller('homeController', ['$scope', '$injector', * * @type String */ - $scope.password = null; + $scope.newPassword = null; /** * The password match for the user. The update password action will fail if - * $scope.password !== $scope.passwordMatch. + * $scope.newPassword !== $scope.passwordMatch. * * @type String */ - $scope.passwordMatch = null; + $scope.newPasswordMatch = null; /** * Returns whether critical data has completed being loaded. @@ -176,8 +176,9 @@ angular.module('home').controller('homeController', ['$scope', '$injector', $scope.closePasswordUpdate = function closePasswordUpdate() { // Clear the password fields and close the dialog - $scope.password = null; - $scope.passwordMatch = null; + $scope.oldPassword = null; + $scope.newPassword = null; + $scope.newPasswordMatch = null; $scope.showPasswordDialog = false; }; @@ -188,29 +189,40 @@ angular.module('home').controller('homeController', ['$scope', '$injector', $scope.updatePassword = function updatePassword() { // Verify passwords match - if ($scope.passwordMatch !== $scope.password) { + if ($scope.newPasswordMatch !== $scope.newPassword) { $scope.showStatus({ - 'className' : 'error', - 'title' : 'HOME.DIALOG_HEADER_ERROR', - 'text' : 'HOME.ERROR_PASSWORD_MISMATCH', - 'actions' : [ ACKNOWLEDGE_ACTION ] + className : 'error', + title : 'HOME.DIALOG_HEADER_ERROR', + text : 'HOME.ERROR_PASSWORD_MISMATCH', + actions : [ ACKNOWLEDGE_ACTION ] }); return; } // Save the user with the new password - userService.saveUser({ - username: currentUserID, - password: $scope.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 ] + }); }); - $scope.closePasswordUpdate(); - - // Indicate that the password has been changed - $scope.showStatus({ - 'text' : 'HOME.PASSWORD_CHANGED', - 'actions' : [ ACKNOWLEDGE_ACTION ] - }); }; }]); diff --git a/guacamole/src/main/webapp/app/home/templates/home.html b/guacamole/src/main/webapp/app/home/templates/home.html index 83dfd97eb..b2fe3a06f 100644 --- a/guacamole/src/main/webapp/app/home/templates/home.html +++ b/guacamole/src/main/webapp/app/home/templates/home.html @@ -31,14 +31,16 @@
- - - + + - - - + + + + + +
{{'HOME.FIELD_HEADER_PASSWORD' | translate}}{{'HOME.FIELD_HEADER_PASSWORD_OLD' | translate}}
{{'HOME.FIELD_HEADER_PASSWORD_AGAIN' | translate}}{{'HOME.FIELD_HEADER_PASSWORD_NEW' | translate}}
{{'HOME.FIELD_HEADER_PASSWORD_NEW_AGAIN' | 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/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/translations/en_US.json b/guacamole/src/main/webapp/translations/en_US.json index 7921e58e5..8111f3737 100644 --- a/guacamole/src/main/webapp/translations/en_US.json +++ b/guacamole/src/main/webapp/translations/en_US.json @@ -125,8 +125,9 @@ "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "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",