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 fc2bdb331..0a16e031e 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 @@ -338,6 +338,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 @@ -369,27 +410,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 98114434d..fa4c376d0 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 @@ -33,9 +33,11 @@ import org.glyptodon.guacamole.auth.jdbc.base.DirectoryObjectService; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; 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; @@ -47,7 +49,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. */ @@ -160,6 +171,31 @@ 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; + + } + /** * Retrieves the user corresponding to the given credentials from the * database. 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/webapp/app/home/controllers/homeController.js b/guacamole/src/main/webapp/app/home/controllers/homeController.js index 70051971c..c7276ee48 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.password = null; + + /** + * The password match for the user. The update password action will fail if + * $scope.password !== $scope.passwordMatch. + * + * @type String + */ + $scope.passwordMatch = 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,68 @@ 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 : "MANAGE_USER.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.password = null; + $scope.passwordMatch = 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.passwordMatch !== $scope.password) { + $scope.showStatus({ + '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 + }); + + $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/styles/home.css b/guacamole/src/main/webapp/app/home/styles/home.css index 6ba463b8a..10d228d00 100644 --- a/guacamole/src/main/webapp/app/home/styles/home.css +++ b/guacamole/src/main/webapp/app/home/styles/home.css @@ -69,3 +69,13 @@ div.recent-connections div.connection { max-width: 75%; overflow: hidden; } + +.password-dialog { + position: absolute; + background: white; + border: 1px solid rgba(0, 0, 0, 0.25); + margin: 1em; + width: 5in; + right: 0; + z-index: 1; +} \ 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..83dfd97eb 100644 --- a/guacamole/src/main/webapp/app/home/templates/home.html +++ b/guacamole/src/main/webapp/app/home/templates/home.html @@ -25,6 +25,31 @@
+ {{'HOME.ACTION_CHANGE_PASSWORD' | translate}} +
+ +
+ + + + + + + + + + + +
{{'HOME.FIELD_HEADER_PASSWORD' | translate}}
{{'HOME.FIELD_HEADER_PASSWORD_AGAIN' | translate}}
+
+ + +
+ + +
+
+ {{'HOME.ACTION_MANAGE' | translate}} {{'HOME.ACTION_LOGOUT' | translate}}
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/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..7921e58e5 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,25 @@ "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_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", "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 +245,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",