diff --git a/extensions/guacamole-auth-jdbc/README b/extensions/guacamole-auth-jdbc/README index d4fa250aa..0fee7bc93 100644 --- a/extensions/guacamole-auth-jdbc/README +++ b/extensions/guacamole-auth-jdbc/README @@ -51,8 +51,8 @@ in the library directory configured in guacamole.properties. created in the target/ subdirectory of the current directory. 4) Extract the .tar.gz file now present in the target/ directory, and - place the .jar files in the extracted lib/ subdirectory in the library - directory specified in guacamole.properties. + place the .jar files from the extracted database-specific subdirectory in + the library directory specified in guacamole.properties. You will likely need to do this as root. @@ -60,10 +60,10 @@ in the library directory configured in guacamole.properties. guacamole.properties, you will need to specify one. The directory is specified using the "lib-directory" property. -5) Set up your MySQL database to authenticate Guacamole users +5) Set up your database to authenticate Guacamole users A schema file is provided in the schema directory for creating - the guacamole authentication tables in your MySQL database. + the guacamole authentication tables in your database of choice. Additionally, a script is provided to create a default admin user with username 'guacadmin' and password 'guacadmin'. This user can @@ -90,6 +90,17 @@ in the library directory configured in guacamole.properties. mysql-disallow-simultaneous-connections: true + For PostgreSQL, the properties are the same, but have different prefixes: + + # Database connection configuration + postgresql-hostname: database.host.name + postgresql-port: 5432 + postgresql-database: guacamole.database.name + postgresql-username: user + postgresql-password: pass + + postgresql-disallow-simultaneous-connections: true + ------------------------------------------------------------ Reporting problems 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 0a16e031e..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 @@ -37,8 +37,8 @@ import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; /** * Service which provides convenience methods for creating, retrieving, and - * manipulating users. This service will automatically enforce the - * permissions of the current user. + * manipulating objects within directories. This service will automatically + * enforce the permissions of the current user. * * @author Michael Jumper * @param @@ -215,52 +215,77 @@ public abstract class DirectoryObjectService + * The type of model object that corresponds to this object. + */ +public abstract class GroupedDirectoryObject + extends DirectoryObject { + + /** + * Returns the identifier of the parent connection group, which cannot be + * null. If the parent is the root connection group, this will be + * RootConnectionGroup.IDENTIFIER. + * + * @return + * The identifier of the parent connection group. + */ + public String getParentIdentifier() { + + // Translate null parent to proper identifier + String parentIdentifier = getModel().getParentIdentifier(); + if (parentIdentifier == null) + return RootConnectionGroup.IDENTIFIER; + + return parentIdentifier; + + } + + /** + * Sets the identifier of the associated parent connection group. If the + * parent is the root connection group, this should be + * RootConnectionGroup.IDENTIFIER. + * + * @param parentIdentifier + * The identifier of the connection group to associate as this object's + * parent. + */ + public void setParentIdentifier(String parentIdentifier) { + + // Translate root identifier back into null + if (parentIdentifier != null + && parentIdentifier.equals(RootConnectionGroup.IDENTIFIER)) + parentIdentifier = null; + + getModel().setParentIdentifier(parentIdentifier); + + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/GroupedDirectoryObjectService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/GroupedDirectoryObjectService.java new file mode 100644 index 000000000..4fb169f35 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/GroupedDirectoryObjectService.java @@ -0,0 +1,196 @@ +/* + * 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.auth.jdbc.base; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleSecurityException; +import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; +import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; +import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; + +/** + * Service which provides convenience methods for creating, retrieving, and + * manipulating objects that can be within connection groups. This service will + * automatically enforce the permissions of the current user. + * + * @author Michael Jumper + * @param + * The specific internal implementation of the type of object this service + * provides access to. + * + * @param + * The external interface or implementation of the type of object this + * service provides access to, as defined by the guacamole-ext API. + * + * @param + * The underlying model object used to represent InternalType in the + * database. + */ +public abstract class GroupedDirectoryObjectService, + ExternalType, ModelType extends GroupedObjectModel> + extends DirectoryObjectService { + + /** + * Returns the set of parent connection groups that are modified by the + * given model object (by virtue of the object changing parent groups). If + * the model is not changing parents, the resulting collection will be + * empty. + * + * @param user + * The user making the given changes to the model. + * + * @param identifier + * The identifier of the object that has been modified, if it exists. + * If the object is being created, this will be null. + * + * @param model + * The model that has been modified, if any. If the object is being + * deleted, this will be null. + * + * @return + * A collection of the identifiers of all parent connection groups + * that will be affected (updated) by the change. + * + * @throws GuacamoleException + * If an error occurs while determining which parent connection groups + * are affected. + */ + protected Collection getModifiedGroups(AuthenticatedUser user, + String identifier, ModelType model) throws GuacamoleException { + + // Get old parent identifier + String oldParentIdentifier = null; + if (identifier != null) { + ModelType current = retrieveObject(user, identifier).getModel(); + oldParentIdentifier = current.getParentIdentifier(); + } + + // Get new parent identifier + String parentIdentifier = null; + if (model != null) { + + parentIdentifier = model.getParentIdentifier(); + + // If both parents have the same identifier, nothing has changed + if (parentIdentifier != null && parentIdentifier.equals(oldParentIdentifier)) + return Collections.EMPTY_LIST; + + } + + // Return collection of all non-root groups involved + Collection groups = new ArrayList(2); + if (oldParentIdentifier != null) groups.add(oldParentIdentifier); + if (parentIdentifier != null) groups.add(parentIdentifier); + return groups; + + } + + /** + * Returns whether the given user has permission to modify the parent + * connection groups affected by the modifications made to the given model + * object. + * + * @param user + * The user who changed the model object. + * + * @param identifier + * The identifier of the object that has been modified, if it exists. + * If the object is being created, this will be null. + * + * @param model + * The model that has been modified, if any. If the object is being + * deleted, this will be null. + * + * @return + * true if the user has update permission for all modified groups, + * false otherwise. + * + * @throws GuacamoleException + * If an error occurs while determining which parent connection groups + * are affected. + */ + protected boolean canUpdateModifiedGroups(AuthenticatedUser user, + String identifier, ModelType model) throws GuacamoleException { + + // If user is an administrator, no need to check + if (user.getUser().isAdministrator()) + return true; + + // Verify that we have permission to modify any modified groups + Collection modifiedGroups = getModifiedGroups(user, identifier, model); + if (!modifiedGroups.isEmpty()) { + + ObjectPermissionSet permissionSet = user.getUser().getConnectionGroupPermissions(); + Collection updateableGroups = permissionSet.getAccessibleObjects( + Collections.singleton(ObjectPermission.Type.UPDATE), + modifiedGroups + ); + + return updateableGroups.size() == modifiedGroups.size(); + + } + + return true; + + } + + @Override + protected void beforeCreate(AuthenticatedUser user, + ModelType model) throws GuacamoleException { + + super.beforeCreate(user, model); + + // Validate that we can update all applicable parent groups + if (!canUpdateModifiedGroups(user, null, model)) + throw new GuacamoleSecurityException("Permission denied."); + + } + + @Override + protected void beforeUpdate(AuthenticatedUser user, + ModelType model) throws GuacamoleException { + + super.beforeUpdate(user, model); + + // Validate that we can update all applicable parent groups + if (!canUpdateModifiedGroups(user, model.getIdentifier(), model)) + throw new GuacamoleSecurityException("Permission denied."); + + } + + @Override + protected void beforeDelete(AuthenticatedUser user, + String identifier) throws GuacamoleException { + + super.beforeDelete(user, identifier); + + // Validate that we can update all applicable parent groups + if (!canUpdateModifiedGroups(user, identifier, null)) + throw new GuacamoleSecurityException("Permission denied."); + + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/GroupedObjectModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/GroupedObjectModel.java new file mode 100644 index 000000000..45b25598a --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/GroupedObjectModel.java @@ -0,0 +1,67 @@ +/* + * 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.auth.jdbc.base; + +/** + * Object representation of a Guacamole object, such as a user or connection, + * as represented in the database. + * + * @author Michael Jumper + */ +public abstract class GroupedObjectModel extends ObjectModel { + + /** + * The unique identifier which identifies the parent of this object. + */ + private String parentIdentifier; + + /** + * Creates a new, empty object. + */ + public GroupedObjectModel() { + } + + /** + * Returns the identifier of the parent connection group, or null if the + * parent connection group is the root connection group. + * + * @return + * The identifier of the parent connection group, or null if the parent + * connection group is the root connection group. + */ + public String getParentIdentifier() { + return parentIdentifier; + } + + /** + * Sets the identifier of the parent connection group. + * + * @param parentIdentifier + * The identifier of the parent connection group, or null if the parent + * connection group is the root connection group. + */ + public void setParentIdentifier(String parentIdentifier) { + this.parentIdentifier = parentIdentifier; + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionModel.java index b4ef2e907..5b3552fe9 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionModel.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionModel.java @@ -22,7 +22,7 @@ package org.glyptodon.guacamole.auth.jdbc.connection; -import org.glyptodon.guacamole.auth.jdbc.base.ObjectModel; +import org.glyptodon.guacamole.auth.jdbc.base.GroupedObjectModel; /** * Object representation of a Guacamole connection, as represented in the @@ -30,14 +30,8 @@ import org.glyptodon.guacamole.auth.jdbc.base.ObjectModel; * * @author Michael Jumper */ -public class ConnectionModel extends ObjectModel { +public class ConnectionModel extends GroupedObjectModel { - /** - * The identifier of the parent connection group in the database, or null - * if the parent connection group is the root group. - */ - private String parentIdentifier; - /** * The human-readable name associated with this connection. */ @@ -95,29 +89,6 @@ public class ConnectionModel extends ObjectModel { this.protocol = protocol; } - /** - * Returns the identifier of the parent connection group, or null if the - * parent connection group is the root connection group. - * - * @return - * The identifier of the parent connection group, or null if the parent - * connection group is the root connection group. - */ - public String getParentIdentifier() { - return parentIdentifier; - } - - /** - * Sets the identifier of the parent connection group. - * - * @param parentIdentifier - * The identifier of the parent connection group, or null if the parent - * connection group is the root connection group. - */ - public void setParentIdentifier(String parentIdentifier) { - this.parentIdentifier = parentIdentifier; - } - @Override public String getIdentifier() { 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 9bf566770..8ac5d5da7 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 @@ -33,11 +33,11 @@ import java.util.Map; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.base.DirectoryObjectMapper; -import org.glyptodon.guacamole.auth.jdbc.base.DirectoryObjectService; import org.glyptodon.guacamole.auth.jdbc.socket.GuacamoleSocketService; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; +import org.glyptodon.guacamole.auth.jdbc.base.GroupedDirectoryObjectService; import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionPermissionMapper; import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionMapper; import org.glyptodon.guacamole.net.GuacamoleSocket; @@ -55,7 +55,7 @@ import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; * * @author Michael Jumper, James Muehlner */ -public class ConnectionService extends DirectoryObjectService { +public class ConnectionService extends GroupedDirectoryObjectService { /** * Mapper for accessing connections. @@ -148,9 +148,11 @@ public class ConnectionService extends DirectoryObjectService +public class ModeledConnection extends GroupedDirectoryObject implements Connection { /** @@ -67,7 +66,7 @@ public class ModeledConnection extends DirectoryObject * The manually-set GuacamoleConfiguration, if any. */ private GuacamoleConfiguration config = null; - + /** * Creates a new, empty ModeledConnection. */ @@ -84,30 +83,6 @@ public class ModeledConnection extends DirectoryObject getModel().setName(name); } - @Override - public String getParentIdentifier() { - - // Translate null parent to proper identifier - String parentIdentifier = getModel().getParentIdentifier(); - if (parentIdentifier == null) - return RootConnectionGroup.IDENTIFIER; - - return parentIdentifier; - - } - - @Override - public void setParentIdentifier(String parentIdentifier) { - - // Translate root identifier back into null - if (parentIdentifier != null - && parentIdentifier.equals(RootConnectionGroup.IDENTIFIER)) - parentIdentifier = null; - - getModel().setParentIdentifier(parentIdentifier); - - } - @Override public GuacamoleConfiguration getConfiguration() { diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupModel.java index 68845df9c..def543ffc 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupModel.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupModel.java @@ -22,7 +22,7 @@ package org.glyptodon.guacamole.auth.jdbc.connectiongroup; -import org.glyptodon.guacamole.auth.jdbc.base.ObjectModel; +import org.glyptodon.guacamole.auth.jdbc.base.GroupedObjectModel; import org.glyptodon.guacamole.net.auth.ConnectionGroup; /** @@ -31,14 +31,8 @@ import org.glyptodon.guacamole.net.auth.ConnectionGroup; * * @author Michael Jumper */ -public class ConnectionGroupModel extends ObjectModel { +public class ConnectionGroupModel extends GroupedObjectModel { - /** - * The identifier of the parent connection group in the database, or null - * if the parent connection group is the root group. - */ - private String parentIdentifier; - /** * The human-readable name associated with this connection group. */ @@ -75,29 +69,6 @@ public class ConnectionGroupModel extends ObjectModel { this.name = name; } - /** - * Returns the identifier of the parent connection group, or null if the - * parent connection group is the root connection group. - * - * @return - * The identifier of the parent connection group, or null if the parent - * connection group is the root connection group. - */ - public String getParentIdentifier() { - return parentIdentifier; - } - - /** - * Sets the identifier of the parent connection group. - * - * @param parentIdentifier - * The identifier of the parent connection group, or null if the parent - * connection group is the root connection group. - */ - public void setParentIdentifier(String parentIdentifier) { - this.parentIdentifier = parentIdentifier; - } - /** * Returns the type of this connection group, such as organizational or * balancing. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupService.java index cfed2edd0..bca97adb6 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupService.java @@ -27,11 +27,12 @@ import com.google.inject.Provider; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.base.DirectoryObjectMapper; -import org.glyptodon.guacamole.auth.jdbc.base.DirectoryObjectService; import org.glyptodon.guacamole.auth.jdbc.socket.GuacamoleSocketService; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; +import org.glyptodon.guacamole.GuacamoleUnsupportedException; +import org.glyptodon.guacamole.auth.jdbc.base.GroupedDirectoryObjectService; import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionGroupPermissionMapper; import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionMapper; import org.glyptodon.guacamole.net.GuacamoleSocket; @@ -48,7 +49,7 @@ import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; * * @author Michael Jumper, James Muehlner */ -public class ConnectionGroupService extends DirectoryObjectService { /** @@ -130,9 +131,11 @@ public class ConnectionGroupService extends DirectoryObjectService +public class ModeledConnectionGroup extends GroupedDirectoryObject implements ConnectionGroup { /** @@ -75,30 +75,6 @@ public class ModeledConnectionGroup extends DirectoryObject retrievePermissions(AuthenticatedUser user, ModeledUser targetUser) throws GuacamoleException { - // Only an admin can read permissions that aren't his own - if (user.getUser().getIdentifier().equals(targetUser.getIdentifier()) - || user.getUser().isAdministrator()) + // Retrieve permissions only if allowed + if (canReadPermissions(user, targetUser)) return getPermissionInstances(getPermissionMapper().select(targetUser.getModel())); // User cannot read this user's permissions diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/SystemPermissionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/SystemPermissionService.java index 52edda6d0..2a7837e58 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/SystemPermissionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/SystemPermissionService.java @@ -29,6 +29,7 @@ import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.user.ModeledUser; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; +import org.glyptodon.guacamole.GuacamoleUnsupportedException; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; /** @@ -112,6 +113,11 @@ public class SystemPermissionService // Only an admin can delete system permissions if (user.getUser().isAdministrator()) { + + // Do not allow users to remove their own admin powers + if (user.getUser().getIdentifier().equals(targetUser.getIdentifier())) + throw new GuacamoleUnsupportedException("Removing your own administrative permissions is not allowed."); + Collection models = getModelInstances(targetUser, permissions); systemPermissionMapper.delete(models); return; diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/security/PasswordEncryptionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/security/PasswordEncryptionService.java index ef3099468..2e78725ef 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/security/PasswordEncryptionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/security/PasswordEncryptionService.java @@ -30,11 +30,16 @@ public interface PasswordEncryptionService { /** * Creates a password hash based on the provided username, password, and - * salt. + * salt. If the provided salt is null, only the password itself is hashed. * - * @param password The password to hash. - * @param salt The salt to use when hashing the password. - * @return The generated password hash. + * @param password + * The password to hash. + * + * @param salt + * The salt to use when hashing the password, if any. + * + * @return + * The generated password hash. */ public byte[] createPasswordHash(String password, byte[] salt); diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/security/SHA256PasswordEncryptionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/security/SHA256PasswordEncryptionService.java index cfe5bc45f..577bdb0ef 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/security/SHA256PasswordEncryptionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/security/SHA256PasswordEncryptionService.java @@ -38,26 +38,26 @@ public class SHA256PasswordEncryptionService implements PasswordEncryptionServic try { - // Build salted password + // Build salted password, if a salt was provided StringBuilder builder = new StringBuilder(); builder.append(password); - builder.append(DatatypeConverter.printHexBinary(salt)); - // Hash UTF-8 bytes of salted password + if (salt != null) + builder.append(DatatypeConverter.printHexBinary(salt)); + + // Hash UTF-8 bytes of possibly-salted password MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(builder.toString().getBytes("UTF-8")); return md.digest(); } - // Should not happen - catch (UnsupportedEncodingException ex) { - throw new RuntimeException(ex); + // Throw hard errors if standard pieces of Java are missing + catch (UnsupportedEncodingException e) { + throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e); } - - // Should not happen - catch (NoSuchAlgorithmException ex) { - throw new RuntimeException(ex); + catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException("Unexpected lack of SHA-256 support.", e); } } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/AbstractGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/AbstractGuacamoleSocketService.java index 084823684..7647ff54d 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/AbstractGuacamoleSocketService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/AbstractGuacamoleSocketService.java @@ -44,7 +44,6 @@ import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionMapper; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.net.GuacamoleSocket; -import org.glyptodon.guacamole.net.InetGuacamoleSocket; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.ConnectionRecord; @@ -139,6 +138,37 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS protected abstract void release(AuthenticatedUser user, ModeledConnection connection); + /** + * Acquires possibly-exclusive access to the given connection group on + * behalf of the given user. If access is denied for any reason, an + * exception is thrown. + * + * @param user + * The user acquiring access. + * + * @param connectionGroup + * The connection group being accessed. + * + * @throws GuacamoleException + * If access is denied to the given user for any reason. + */ + protected abstract void acquire(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) throws GuacamoleException; + + /** + * Releases possibly-exclusive access to the given connection group on + * behalf of the given user. If the given user did not already have access, + * the behavior of this function is undefined. + * + * @param user + * The user releasing access. + * + * @param connectionGroup + * The connection group being released. + */ + protected abstract void release(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup); + /** * Returns a guacamole configuration containing the protocol and parameters * from the given connection. If tokens are used in the connection @@ -183,19 +213,17 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS } /** - * Saves the given ActiveConnectionRecord to the database, associating it - * with the connection having the given identifier. The end date of the - * saved record will be populated with the current time. - * - * @param identifier - * The connection to associate the new record with. + * Saves the given ActiveConnectionRecord to the database. The end date of + * the saved record will be populated with the current time. * * @param record * The record to save. */ - private void saveConnectionRecord(String identifier, - ActiveConnectionRecord record) { + private void saveConnectionRecord(ActiveConnectionRecord record) { + // Get associated connection + ModeledConnection connection = record.getConnection(); + // Get associated models AuthenticatedUser user = record.getUser(); UserModel userModel = user.getUser().getModel(); @@ -204,7 +232,7 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS // Copy user information and timestamps into new record recordModel.setUserID(userModel.getObjectID()); recordModel.setUsername(userModel.getIdentifier()); - recordModel.setConnectionIdentifier(identifier); + recordModel.setConnectionIdentifier(connection.getIdentifier()); recordModel.setStartDate(record.getStartDate()); recordModel.setEndDate(new Date()); @@ -224,24 +252,88 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS * If an error occurs while connecting to guacd, or while parsing * guacd-related properties. */ - private GuacamoleSocket getUnconfiguredGuacamoleSocket() + private GuacamoleSocket getUnconfiguredGuacamoleSocket(Runnable socketClosedCallback) throws GuacamoleException { // Use SSL if requested if (environment.getProperty(Environment.GUACD_SSL, true)) - return new InetGuacamoleSocket( + return new ManagedInetGuacamoleSocket( environment.getRequiredProperty(Environment.GUACD_HOSTNAME), - environment.getRequiredProperty(Environment.GUACD_PORT) + environment.getRequiredProperty(Environment.GUACD_PORT), + socketClosedCallback ); // Otherwise, just use straight TCP - return new InetGuacamoleSocket( + return new ManagedInetGuacamoleSocket( environment.getRequiredProperty(Environment.GUACD_HOSTNAME), - environment.getRequiredProperty(Environment.GUACD_PORT) + environment.getRequiredProperty(Environment.GUACD_PORT), + socketClosedCallback ); } - + + /** + * Task which handles cleanup of a connection associated with some given + * ActiveConnectionRecord. + */ + private class ConnectionCleanupTask implements Runnable { + + /** + * Whether this task has run. + */ + private final AtomicBoolean hasRun = new AtomicBoolean(false); + + /** + * The ActiveConnectionRecord whose connection will be cleaned up once + * this task runs. + */ + private final ActiveConnectionRecord activeConnection; + + /** + * Creates a new task which automatically cleans up after the + * connection associated with the given ActiveConnectionRecord. The + * connection and parent group will be removed from the maps of active + * connections and groups, and exclusive access will be released. + * + * @param activeConnection + * The ActiveConnectionRecord whose associated connection should be + * cleaned up once this task runs. + */ + public ConnectionCleanupTask(ActiveConnectionRecord activeConnection) { + this.activeConnection = activeConnection; + } + + @Override + public void run() { + + // Only run once + if (!hasRun.compareAndSet(false, true)) + return; + + // Get original user and connection + AuthenticatedUser user = activeConnection.getUser(); + ModeledConnection connection = activeConnection.getConnection(); + + // Get associated identifiers + String identifier = connection.getIdentifier(); + String parentIdentifier = connection.getParentIdentifier(); + + // Release connection + activeConnections.remove(identifier, activeConnection); + activeConnectionGroups.remove(parentIdentifier, activeConnection); + release(user, connection); + + // Release any associated group + if (activeConnection.hasBalancingGroup()) + release(user, activeConnection.getBalancingGroup()); + + // Save history record to database + saveConnectionRecord(activeConnection); + + } + + } + /** * Creates a socket for the given user which connects to the given * connection, which MUST already be acquired via acquire(). The given @@ -269,73 +361,77 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS * If an error occurs while the connection is being established, or * while connection configuration information is being retrieved. */ - private GuacamoleSocket connect(final AuthenticatedUser user, - final ModeledConnection connection, GuacamoleClientInformation info) + private GuacamoleSocket getGuacamoleSocket(ActiveConnectionRecord activeConnection, + GuacamoleClientInformation info) throws GuacamoleException { - // Create record for active connection - final ActiveConnectionRecord activeConnection = new ActiveConnectionRecord(user); + ModeledConnection connection = activeConnection.getConnection(); + + // Record new active connection + Runnable cleanupTask = new ConnectionCleanupTask(activeConnection); + activeConnections.put(connection.getIdentifier(), activeConnection); + activeConnectionGroups.put(connection.getParentIdentifier(), activeConnection); - // Get relevant identifiers - final AtomicBoolean released = new AtomicBoolean(false); - final String identifier = connection.getIdentifier(); - final String parentIdentifier = connection.getParentIdentifier(); - // Return new socket try { - - // Record new active connection - activeConnections.put(identifier, activeConnection); - activeConnectionGroups.put(parentIdentifier, activeConnection); - - // Return newly-reserved connection return new ConfiguredGuacamoleSocket( - getUnconfiguredGuacamoleSocket(), - getGuacamoleConfiguration(user, connection), + getUnconfiguredGuacamoleSocket(cleanupTask), + getGuacamoleConfiguration(activeConnection.getUser(), connection), info - ) { - - @Override - public void close() throws GuacamoleException { - - // Attempt to close connection - super.close(); - - // Release connection upon close, if not already released - if (released.compareAndSet(false, true)) { - - // Release connection - activeConnections.remove(identifier, activeConnection); - activeConnectionGroups.remove(parentIdentifier, activeConnection); - release(user, connection); - - // Save record to database - saveConnectionRecord(identifier, activeConnection); - - } - - } // end close() - - }; - + ); } - // Release connection in case of error + // Execute cleanup if socket could not be created catch (GuacamoleException e) { - - // Release connection if not already released - if (released.compareAndSet(false, true)) { - activeConnections.remove(identifier, activeConnection); - activeConnectionGroups.remove(parentIdentifier, activeConnection); - release(user, connection); - } - + cleanupTask.run(); throw e; - } } + /** + * Returns a list of all balanced connections within a given connection + * group. If the connection group is not balancing, or it contains no + * connections, an empty list is returned. + * + * @param user + * The user on whose behalf the balanced connections within the given + * connection group are being retrieved. + * + * @param connectionGroup + * The connection group to retrieve the balanced connections of. + * + * @return + * A list containing all balanced connections within the given group, + * or an empty list if there are no such connections. + */ + private List getBalancedConnections(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) { + + // If not a balancing group, there are no balanced connections + if (connectionGroup.getType() != ConnectionGroup.Type.BALANCING) + return Collections.EMPTY_LIST; + + // If group has no children, there are no balanced connections + Collection identifiers = connectionMapper.selectIdentifiersWithin(connectionGroup.getIdentifier()); + if (identifiers.isEmpty()) + return Collections.EMPTY_LIST; + + // Retrieve all children + Collection models = connectionMapper.select(identifiers); + List connections = new ArrayList(models.size()); + + // Convert each retrieved model to a modeled connection + for (ConnectionModel model : models) { + ModeledConnection connection = connectionProvider.get(); + connection.init(user, model); + connections.add(connection); + } + + return connections; + + } + @Override @Transactional public GuacamoleSocket getGuacamoleSocket(final AuthenticatedUser user, @@ -344,7 +440,7 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS // Acquire and connect to single connection acquire(user, Collections.singletonList(connection)); - return connect(user, connection, info); + return getGuacamoleSocket(new ActiveConnectionRecord(user, connection), info); } @@ -359,29 +455,17 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS ModeledConnectionGroup connectionGroup, GuacamoleClientInformation info) throws GuacamoleException { - // If not a balancing group, cannot connect - if (connectionGroup.getType() != ConnectionGroup.Type.BALANCING) - throw new GuacamoleSecurityException("Permission denied."); - - // If group has no children, cannot connect - Collection identifiers = connectionMapper.selectIdentifiersWithin(connectionGroup.getIdentifier()); - if (identifiers.isEmpty()) + // If group has no associated balanced connections, cannot connect + List connections = getBalancedConnections(user, connectionGroup); + if (connections.isEmpty()) throw new GuacamoleSecurityException("Permission denied."); - // Otherwise, retrieve all children - Collection models = connectionMapper.select(identifiers); - List connections = new ArrayList(models.size()); - - // Convert each retrieved model to a modeled connection - for (ConnectionModel model : models) { - ModeledConnection connection = connectionProvider.get(); - connection.init(user, model); - connections.add(connection); - } + // Acquire group + acquire(user, connectionGroup); // Acquire and connect to any child ModeledConnection connection = acquire(user, connections); - return connect(user, connection, info); + return getGuacamoleSocket(new ActiveConnectionRecord(user, connectionGroup, connection), info); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ActiveConnectionRecord.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ActiveConnectionRecord.java index e6d520b87..5d23a7443 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ActiveConnectionRecord.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ActiveConnectionRecord.java @@ -23,6 +23,8 @@ package org.glyptodon.guacamole.auth.jdbc.socket; import java.util.Date; +import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; +import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.net.auth.ConnectionRecord; @@ -43,21 +45,62 @@ public class ActiveConnectionRecord implements ConnectionRecord { */ private final AuthenticatedUser user; + /** + * The balancing group from which the associated connection was chosen, if + * any. If no balancing group was used, this will be null. + */ + private final ModeledConnectionGroup balancingGroup; + + /** + * The connection associated with this connection record. + */ + private final ModeledConnection connection; + /** * The time this connection record was created. */ private final Date startDate = new Date(); /** - * Creates a new connection record associated with the given user. The - * start date of this connection record will be the time of its creation. + * Creates a new connection record associated with the given user, + * connection, and balancing connection group. The given balancing + * connection group MUST be the connection group from which the given + * connection was chosen. The start date of this connection record will be + * the time of its creation. * * @param user * The user that connected to the connection associated with this * connection record. + * + * @param balancingGroup + * The balancing group from which the given connection was chosen. + * + * @param connection + * The connection to associate with this connection record. */ - public ActiveConnectionRecord(AuthenticatedUser user) { + public ActiveConnectionRecord(AuthenticatedUser user, + ModeledConnectionGroup balancingGroup, + ModeledConnection connection) { this.user = user; + this.balancingGroup = balancingGroup; + this.connection = connection; + } + + /** + * Creates a new connection record associated with the given user and + * connection. The start date of this connection record will be the time of + * its creation. + * + * @param user + * The user that connected to the connection associated with this + * connection record. + * + * @param connection + * The connection to associate with this connection record. + */ + public ActiveConnectionRecord(AuthenticatedUser user, + ModeledConnection connection) { + this(user, null, connection); } /** @@ -72,6 +115,40 @@ public class ActiveConnectionRecord implements ConnectionRecord { return user; } + /** + * Returns the balancing group from which the connection associated with + * this connection record was chosen. + * + * @return + * The balancing group from which the connection associated with this + * connection record was chosen. + */ + public ModeledConnectionGroup getBalancingGroup() { + return balancingGroup; + } + + /** + * Returns the connection associated with this connection record. + * + * @return + * The connection associated with this connection record. + */ + public ModeledConnection getConnection() { + return connection; + } + + /** + * Returns whether the connection associated with this connection record + * was chosen from a balancing group. + * + * @return + * true if the connection associated with this connection record was + * chosen from a balancing group, false otherwise. + */ + public boolean hasBalancingGroup() { + return balancingGroup != null; + } + @Override public Date getStartDate() { return startDate; diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/BalancedGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/BalancedGuacamoleSocketService.java new file mode 100644 index 000000000..118c0cad9 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/BalancedGuacamoleSocketService.java @@ -0,0 +1,88 @@ +/* + * 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.auth.jdbc.socket; + +import com.google.inject.Singleton; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; +import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleResourceConflictException; +import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; + + +/** + * GuacamoleSocketService implementation which allows only one user per + * connection at any time, but does not disallow concurrent use of connection + * groups. If a user attempts to use a connection group multiple times, they + * will receive different underlying connections each time until the group is + * exhausted. + * + * @author Michael Jumper + */ +@Singleton +public class BalancedGuacamoleSocketService + extends AbstractGuacamoleSocketService { + + /** + * The set of all active connection identifiers. + */ + private final Set activeConnections = + Collections.newSetFromMap(new ConcurrentHashMap()); + + @Override + protected ModeledConnection acquire(AuthenticatedUser user, + List connections) throws GuacamoleException { + + // Return the first unused connection + for (ModeledConnection connection : connections) { + if (activeConnections.add(connection.getIdentifier())) + return connection; + } + + // Already in use + throw new GuacamoleResourceConflictException("Cannot connect. This connection is in use."); + + } + + @Override + protected void release(AuthenticatedUser user, ModeledConnection connection) { + activeConnections.remove(connection.getIdentifier()); + } + + @Override + protected void acquire(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) throws GuacamoleException { + // Do nothing + } + + @Override + protected void release(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) { + // Do nothing + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ManagedInetGuacamoleSocket.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ManagedInetGuacamoleSocket.java new file mode 100644 index 000000000..8658bba4c --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ManagedInetGuacamoleSocket.java @@ -0,0 +1,71 @@ +/* + * 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.auth.jdbc.socket; + +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.InetGuacamoleSocket; + +/** + * Implementation of GuacamoleSocket which connects via TCP to a given hostname + * and port. If the socket is closed for any reason, a given task is run. + * + * @author Michael Jumper + */ +public class ManagedInetGuacamoleSocket extends InetGuacamoleSocket { + + /** + * The task to run when the socket is closed. + */ + private final Runnable socketClosedTask; + + /** + * Creates a new socket which connects via TCP to a given hostname and + * port. If the socket is closed for any reason, the given task is run. + * + * @param hostname + * The hostname of the Guacamole proxy server to connect to. + * + * @param port + * The port of the Guacamole proxy server to connect to. + * + * @param socketClosedTask + * The task to run when the socket is closed. This task will NOT be + * run if an exception occurs during connection, and this + * ManagedInetGuacamoleSocket instance is ultimately not created. + * + * @throws GuacamoleException + * If an error occurs while connecting to the Guacamole proxy server. + */ + public ManagedInetGuacamoleSocket(String hostname, int port, + Runnable socketClosedTask) throws GuacamoleException { + super(hostname, port); + this.socketClosedTask = socketClosedTask; + } + + @Override + public void close() throws GuacamoleException { + super.close(); + socketClosedTask.run(); + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java index a55e31780..867c1b5d0 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java @@ -23,20 +23,24 @@ package org.glyptodon.guacamole.auth.jdbc.socket; import com.google.inject.Singleton; +import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.glyptodon.guacamole.GuacamoleClientTooManyException; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceConflictException; +import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; /** * GuacamoleSocketService implementation which restricts concurrency only on a - * per-user basis. Each connection may be used concurrently any number of - * times, but each concurrent use must be associated with a different user. + * per-user basis. Each connection or group may be used concurrently any number + * of times, but each concurrent use must be associated with a different user. * * @author Michael Jumper */ @@ -44,83 +48,40 @@ import org.glyptodon.guacamole.GuacamoleResourceConflictException; public class MultiseatGuacamoleSocketService extends AbstractGuacamoleSocketService { - /** - * A unique pairing of user and connection. - */ - private static class Seat { - - /** - * The user using this seat. - */ - private final String username; - - /** - * The connection associated with this seat. - */ - private final String connectionIdentifier; - - /** - * Creates a new seat which associated the given user with the given - * connection. - * - * @param username - * The username of the user using this seat. - * - * @param connectionIdentifier - * The identifier of the connection associated with this seat. - */ - public Seat(String username, String connectionIdentifier) { - this.username = username; - this.connectionIdentifier = connectionIdentifier; - } - - @Override - public int hashCode() { - - // The various properties will never be null - assert(username != null); - assert(connectionIdentifier != null); - - // Derive hashcode from username and connection identifier - int hash = 5; - hash = 37 * hash + username.hashCode(); - hash = 37 * hash + connectionIdentifier.hashCode(); - return hash; - - } - - @Override - public boolean equals(Object object) { - - // We are only comparing against other seats here - assert(object instanceof Seat); - Seat seat = (Seat) object; - - // The various properties will never be null - assert(seat.username != null); - assert(seat.connectionIdentifier != null); - - return username.equals(seat.username) - && connectionIdentifier.equals(seat.connectionIdentifier); - - } - - } - /** * The set of all active user/connection pairs. */ private final Set activeSeats = Collections.newSetFromMap(new ConcurrentHashMap()); + + /** + * The set of all active user/connection group pairs. + */ + private final Set activeGroupSeats = + Collections.newSetFromMap(new ConcurrentHashMap()); @Override protected ModeledConnection acquire(AuthenticatedUser user, List connections) throws GuacamoleException { String username = user.getUser().getIdentifier(); + + // Sort connections in ascending order of usage + ModeledConnection[] sortedConnections = connections.toArray(new ModeledConnection[connections.size()]); + Arrays.sort(sortedConnections, new Comparator() { + + @Override + public int compare(ModeledConnection a, ModeledConnection b) { + + return getActiveConnections(a).size() + - getActiveConnections(b).size(); + + } + + }); // Return the first unreserved connection - for (ModeledConnection connection : connections) { + for (ModeledConnection connection : sortedConnections) { if (activeSeats.add(new Seat(username, connection.getIdentifier()))) return connection; } @@ -135,4 +96,21 @@ public class MultiseatGuacamoleSocketService activeSeats.remove(new Seat(user.getUser().getIdentifier(), connection.getIdentifier())); } + @Override + protected void acquire(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) throws GuacamoleException { + + // Do not allow duplicate use of connection groups + Seat seat = new Seat(user.getUser().getIdentifier(), connectionGroup.getIdentifier()); + if (!activeGroupSeats.add(seat)) + throw new GuacamoleClientTooManyException("Cannot connect. Connection group already in use by this user."); + + } + + @Override + protected void release(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) { + activeGroupSeats.remove(new Seat(user.getUser().getIdentifier(), connectionGroup.getIdentifier())); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ReservedGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ReservedGuacamoleSocketService.java deleted file mode 100644 index 8cab0b814..000000000 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ReservedGuacamoleSocketService.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * 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.auth.jdbc.socket; - -import com.google.inject.Singleton; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; -import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.GuacamoleResourceConflictException; - - -/** - * GuacamoleSocketService implementation which allows only one user per - * connection at any time, but does not disallow concurrent use. Once - * connected, a user has effectively reserved that connection, and may - * continue to concurrently use that connection any number of times. The - * connection will remain reserved until all associated connections are closed. - * Other users will be denied access to that connection while it is reserved. - * - * @author Michael Jumper - */ -@Singleton -public class ReservedGuacamoleSocketService - extends AbstractGuacamoleSocketService { - - /** - * An arbitrary number of reservations associated with a specific user. - * Initially, each Reservation instance represents exactly one reservation, - * but future calls to acquire() may increase this value. Once the - * reservation count is reduced to zero by calls to release(), a - * Reservation instance is empty and cannot be reused. It must be discarded - * and replaced with a fresh Reservation. - * - * This is necessary as each Reservation will be stored within a Map, and - * the effect of acquire() must be deterministic. If Reservations could be - * reused, the internal count could potentially increase after being - * removed from the map, resulting in a successful acquire() that really - * should have failed. - */ - private static class Reservation { - - /** - * The username of the user associated with this reservation. - */ - private final String username; - - /** - * The number of reservations effectively present under the associated - * username. - */ - private int count = 1; - - /** - * Creates a new reservation which tracks the overall number of - * reservations for a given user. - * @param username - */ - public Reservation(String username) { - this.username = username; - } - - /** - * Attempts to acquire a new reservation under the given username. If - * this reservation is for a different user, or the reservation has - * expired, this will fail. - * - * @param username - * The username of the user to acquire the reservation for. - * - * @return - * true if the reservation was successful, false otherwise. - */ - public boolean acquire(String username) { - - // Acquire always fails if for the wrong user - if (!this.username.equals(username)) - return false; - - // Determine success/failure based on count - synchronized (this) { - - // If already expired, no further reservations are allowed - if (count == 0) - return false; - - // Otherwise, add another reservation, report success - count++; - return true; - - } - - } - - /** - * Releases a previous reservation. The result of calling this function - * without a previous matching call to acquire is undefined. - * - * @return - * true if the last reservation has been released and this - * reservation is now empty, false otherwise. - */ - public boolean release() { - synchronized (this) { - - // Reduce reservation count - count--; - - // Empty if no reservations remain - return count == 0; - - } - } - - } - - /** - * Map of connection identifier to associated reservations. - */ - private final ConcurrentMap reservations = - new ConcurrentHashMap(); - - @Override - protected ModeledConnection acquire(AuthenticatedUser user, - List connections) throws GuacamoleException { - - String username = user.getUser().getIdentifier(); - - // Return the first successfully-reserved connection - for (ModeledConnection connection : connections) { - - String identifier = connection.getIdentifier(); - - // Attempt to reserve connection, return if successful - Reservation reservation = reservations.putIfAbsent(identifier, new Reservation(username)); - if (reservation == null || reservation.acquire(username)) - return connection; - - } - - // Already in use - throw new GuacamoleResourceConflictException("Cannot connect. This connection is in use."); - - } - - @Override - protected void release(AuthenticatedUser user, ModeledConnection connection) { - - String identifier = connection.getIdentifier(); - - // Retrieve active reservation (which must exist) - Reservation reservation = reservations.get(identifier); - assert(reservation != null); - - // Release reservation, remove from map if empty - if (reservation.release()) - reservations.remove(identifier); - - } - -} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/Seat.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/Seat.java new file mode 100644 index 000000000..275536f98 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/Seat.java @@ -0,0 +1,89 @@ +/* + * 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.auth.jdbc.socket; + +/** + * A unique pairing of user and connection or connection group. + * + * @author Michael Jumper + */ +public class Seat { + + /** + * The user using this seat. + */ + private final String username; + + /** + * The connection or connection group associated with this seat. + */ + private final String identifier; + + /** + * Creates a new seat which associated the given user with the given + * connection or connection group. + * + * @param username + * The username of the user using this seat. + * + * @param identifier + * The identifier of the connection or connection group associated with + * this seat. + */ + public Seat(String username, String identifier) { + this.username = username; + this.identifier = identifier; + } + + @Override + public int hashCode() { + + // The various properties will never be null + assert(username != null); + assert(identifier != null); + + // Derive hashcode from username and connection identifier + int hash = 5; + hash = 37 * hash + username.hashCode(); + hash = 37 * hash + identifier.hashCode(); + return hash; + + } + + @Override + public boolean equals(Object object) { + + // We are only comparing against other seats here + assert(object instanceof Seat); + Seat seat = (Seat) object; + + // The various properties will never be null + assert(seat.username != null); + assert(seat.identifier != null); + + return username.equals(seat.username) + && identifier.equals(seat.identifier); + + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/SingleSeatGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/SingleSeatGuacamoleSocketService.java index e3ff6a9a4..383e85f66 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/SingleSeatGuacamoleSocketService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/SingleSeatGuacamoleSocketService.java @@ -27,15 +27,19 @@ import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.glyptodon.guacamole.GuacamoleClientTooManyException; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceConflictException; +import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; /** * GuacamoleSocketService implementation which allows exactly one use - * of any connection at any time. Concurrent usage of any kind is not allowed. + * of any connection at any time. Concurrent usage of connections is not + * allowed, and concurrent usage of connection groups is allowed only between + * different users. * * @author Michael Jumper */ @@ -48,7 +52,13 @@ public class SingleSeatGuacamoleSocketService */ private final Set activeConnections = Collections.newSetFromMap(new ConcurrentHashMap()); - + + /** + * The set of all active user/connection group pairs. + */ + private final Set activeGroupSeats = + Collections.newSetFromMap(new ConcurrentHashMap()); + @Override protected ModeledConnection acquire(AuthenticatedUser user, List connections) throws GuacamoleException { @@ -69,4 +79,21 @@ public class SingleSeatGuacamoleSocketService activeConnections.remove(connection.getIdentifier()); } + @Override + protected void acquire(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) throws GuacamoleException { + + // Do not allow duplicate use of connection groups + Seat seat = new Seat(user.getUser().getIdentifier(), connectionGroup.getIdentifier()); + if (!activeGroupSeats.add(seat)) + throw new GuacamoleClientTooManyException("Cannot connect. Connection group already in use by this user."); + + } + + @Override + protected void release(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) { + activeGroupSeats.remove(new Seat(user.getUser().getIdentifier(), connectionGroup.getIdentifier())); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/UnrestrictedGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/UnrestrictedGuacamoleSocketService.java index c5d2a0503..e5932d8c2 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/UnrestrictedGuacamoleSocketService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/UnrestrictedGuacamoleSocketService.java @@ -27,6 +27,7 @@ import java.util.List; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; /** @@ -66,4 +67,16 @@ public class UnrestrictedGuacamoleSocketService // Do nothing } + @Override + protected void acquire(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) throws GuacamoleException { + // Do nothing + } + + @Override + protected void release(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) { + // Do nothing + } + } 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 fa4c376d0..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 @@ -32,6 +32,7 @@ import org.glyptodon.guacamole.auth.jdbc.base.DirectoryObjectMapper; import org.glyptodon.guacamole.auth.jdbc.base.DirectoryObjectService; 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; @@ -137,9 +138,11 @@ public class UserService extends DirectoryObjectService @@ -83,7 +83,7 @@ #{identifier,jdbcType=VARCHAR} AND guacamole_user_permission.user_id = #{user.objectID,jdbcType=INTEGER} - AND permission = 'read' + AND permission = 'READ' diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/.gitignore b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/.gitignore new file mode 100644 index 000000000..42f4a1a64 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/.gitignore @@ -0,0 +1,2 @@ +target/ +*~ diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml new file mode 100644 index 000000000..2329087cd --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + org.glyptodon.guacamole + guacamole-auth-jdbc-postgresql + jar + guacamole-auth-jdbc-postgresql + http://guac-dev.org/ + + + UTF-8 + + + + org.glyptodon.guacamole + guacamole-auth-jdbc + 0.9.5 + ../../ + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + maven-assembly-plugin + 2.2-beta-5 + + + jar-with-dependencies + package + + single + + + extension/${project.artifactId}-${project.version} + false + + jar-with-dependencies + + + + + + + + + + + + + + org.glyptodon.guacamole + guacamole-ext + provided + + + + + org.glyptodon.guacamole + guacamole-auth-jdbc-base + 0.9.5 + + + + + diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql new file mode 100644 index 000000000..4e4297693 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql @@ -0,0 +1,275 @@ +-- +-- 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. +-- + +-- +-- Connection group types +-- + +CREATE TYPE guacamole_connection_group_type AS ENUM( + 'ORGANIZATIONAL', + 'BALANCING' +); + +-- +-- Object permission types +-- + +CREATE TYPE guacamole_object_permission_type AS ENUM( + 'READ', + 'UPDATE', + 'DELETE', + 'ADMINISTER' +); + +-- +-- System permission types +-- + +CREATE TYPE guacamole_system_permission_type AS ENUM( + 'CREATE_CONNECTION', + 'CREATE_CONNECTION_GROUP', + 'CREATE_USER', + 'ADMINISTER' +); + +-- +-- Table of connection groups. Each connection group has a name. +-- + +CREATE TABLE guacamole_connection_group ( + + connection_group_id serial NOT NULL, + parent_id integer, + connection_group_name varchar(128) NOT NULL, + type guacamole_connection_group_type + NOT NULL DEFAULT 'ORGANIZATIONAL', + + PRIMARY KEY (connection_group_id), + + CONSTRAINT connection_group_name_parent + UNIQUE (connection_group_name, parent_id), + + CONSTRAINT guacamole_connection_group_ibfk_1 + FOREIGN KEY (parent_id) + REFERENCES guacamole_connection_group (connection_group_id) + ON DELETE CASCADE + +); + +CREATE INDEX ON guacamole_connection_group(parent_id); + +-- +-- Table of connections. Each connection has a name, protocol, and +-- associated set of parameters. +-- A connection may belong to a connection group. +-- + +CREATE TABLE guacamole_connection ( + + connection_id serial NOT NULL, + connection_name varchar(128) NOT NULL, + parent_id integer, + protocol varchar(32) NOT NULL, + + PRIMARY KEY (connection_id), + + CONSTRAINT connection_name_parent + UNIQUE (connection_name, parent_id), + + CONSTRAINT guacamole_connection_ibfk_1 + FOREIGN KEY (parent_id) + REFERENCES guacamole_connection_group (connection_group_id) + ON DELETE CASCADE + +); + +CREATE INDEX ON guacamole_connection(parent_id); + +-- +-- Table of users. Each user has a unique username and a hashed password +-- with corresponding salt. Although the authentication system will always set +-- salted passwords, other systems may set unsalted passwords by simply not +-- providing the salt. +-- + +CREATE TABLE guacamole_user ( + + user_id serial NOT NULL, + username varchar(128) NOT NULL, + password_hash bytea NOT NULL, + password_salt bytea, + + PRIMARY KEY (user_id), + + CONSTRAINT username + UNIQUE (username) + +); + +-- +-- Table of connection parameters. Each parameter is simply a name/value pair +-- associated with a connection. +-- + +CREATE TABLE guacamole_connection_parameter ( + + connection_id integer NOT NULL, + parameter_name varchar(128) NOT NULL, + parameter_value varchar(4096) NOT NULL, + + PRIMARY KEY (connection_id,parameter_name), + + CONSTRAINT guacamole_connection_parameter_ibfk_1 + FOREIGN KEY (connection_id) + REFERENCES guacamole_connection (connection_id) ON DELETE CASCADE + +); + +CREATE INDEX ON guacamole_connection_parameter(connection_id); + +-- +-- Table of connection permissions. Each connection permission grants a user +-- specific access to a connection. +-- + +CREATE TABLE guacamole_connection_permission ( + + user_id integer NOT NULL, + connection_id integer NOT NULL, + permission guacamole_object_permission_type NOT NULL, + + PRIMARY KEY (user_id,connection_id,permission), + + CONSTRAINT guacamole_connection_permission_ibfk_1 + FOREIGN KEY (connection_id) + REFERENCES guacamole_connection (connection_id) ON DELETE CASCADE, + + CONSTRAINT guacamole_connection_permission_ibfk_2 + FOREIGN KEY (user_id) + REFERENCES guacamole_user (user_id) ON DELETE CASCADE + +); + +CREATE INDEX ON guacamole_connection_permission(connection_id); +CREATE INDEX ON guacamole_connection_permission(user_id); + +-- +-- Table of connection group permissions. Each group permission grants a user +-- specific access to a connection group. +-- + +CREATE TABLE guacamole_connection_group_permission ( + + user_id integer NOT NULL, + connection_group_id integer NOT NULL, + permission guacamole_object_permission_type NOT NULL, + + PRIMARY KEY (user_id,connection_group_id,permission), + + CONSTRAINT guacamole_connection_group_permission_ibfk_1 + FOREIGN KEY (connection_group_id) + REFERENCES guacamole_connection_group (connection_group_id) ON DELETE CASCADE, + + CONSTRAINT guacamole_connection_group_permission_ibfk_2 + FOREIGN KEY (user_id) + REFERENCES guacamole_user (user_id) ON DELETE CASCADE + +); + +CREATE INDEX ON guacamole_connection_group_permission(connection_group_id); +CREATE INDEX ON guacamole_connection_group_permission(user_id); + +-- +-- Table of system permissions. Each system permission grants a user a +-- system-level privilege of some kind. +-- + +CREATE TABLE guacamole_system_permission ( + + user_id integer NOT NULL, + permission guacamole_system_permission_type NOT NULL, + + PRIMARY KEY (user_id,permission), + + CONSTRAINT guacamole_system_permission_ibfk_1 + FOREIGN KEY (user_id) + REFERENCES guacamole_user (user_id) ON DELETE CASCADE + +); + +CREATE INDEX ON guacamole_system_permission(user_id); + +-- +-- Table of user permissions. Each user permission grants a user access to +-- another user (the "affected" user) for a specific type of operation. +-- + +CREATE TABLE guacamole_user_permission ( + + user_id integer NOT NULL, + affected_user_id integer NOT NULL, + permission guacamole_object_permission_type NOT NULL, + + PRIMARY KEY (user_id,affected_user_id,permission), + + CONSTRAINT guacamole_user_permission_ibfk_1 + FOREIGN KEY (affected_user_id) + REFERENCES guacamole_user (user_id) ON DELETE CASCADE, + + CONSTRAINT guacamole_user_permission_ibfk_2 + FOREIGN KEY (user_id) + REFERENCES guacamole_user (user_id) ON DELETE CASCADE + +); + +CREATE INDEX ON guacamole_user_permission(affected_user_id); +CREATE INDEX ON guacamole_user_permission(user_id); + +-- +-- Table of connection history records. Each record defines a specific user's +-- session, including the connection used, the start time, and the end time +-- (if any). +-- + +CREATE TABLE guacamole_connection_history ( + + history_id serial NOT NULL, + user_id integer NOT NULL, + connection_id integer NOT NULL, + start_date timestamptz NOT NULL, + end_date timestamptz DEFAULT NULL, + + PRIMARY KEY (history_id), + + CONSTRAINT guacamole_connection_history_ibfk_1 + FOREIGN KEY (user_id) + REFERENCES guacamole_user (user_id) ON DELETE CASCADE, + + CONSTRAINT guacamole_connection_history_ibfk_2 + FOREIGN KEY (connection_id) + REFERENCES guacamole_connection (connection_id) ON DELETE CASCADE + +); + +CREATE INDEX ON guacamole_connection_history(user_id); +CREATE INDEX ON guacamole_connection_history(connection_id); + diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/002-create-admin-user.sql b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/002-create-admin-user.sql new file mode 100644 index 000000000..16eafbe73 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/002-create-admin-user.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. +-- + + +-- Create default user "guacadmin" with password "guacadmin" +INSERT INTO guacamole_user (username, password_hash, password_salt) +VALUES ('guacadmin', + E'\\xCA458A7D494E3BE824F5E1E175A1556C0F8EEF2C2D7DF3633BEC4A29C4411960', -- 'guacadmin' + E'\\xFE24ADC5E11E2B25288D1704ABE67A79E342ECC26064CE69C5B3177795A82264'); + +-- Grant this user all system permissions +INSERT INTO guacamole_system_permission +SELECT user_id, permission::guacamole_system_permission_type +FROM ( + VALUES + ('guacadmin', 'CREATE_CONNECTION'), + ('guacadmin', 'CREATE_CONNECTION_GROUP'), + ('guacadmin', 'CREATE_USER'), + ('guacadmin', 'ADMINISTER') +) permissions (username, permission) +JOIN guacamole_user ON permissions.username = guacamole_user.username; + +-- Grant admin permission to read/update/administer self +INSERT INTO guacamole_user_permission +SELECT guacamole_user.user_id, affected.user_id, permission::guacamole_object_permission_type +FROM ( + VALUES + ('guacadmin', 'guacadmin', 'READ'), + ('guacadmin', 'guacadmin', 'UPDATE'), + ('guacadmin', 'guacadmin', 'ADMINISTER') +) permissions (username, affected_username, permission) +JOIN guacamole_user ON permissions.username = guacamole_user.username +JOIN guacamole_user affected ON permissions.affected_username = affected.username; + diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/PostgreSQLAuthenticationProvider.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/PostgreSQLAuthenticationProvider.java new file mode 100644 index 000000000..65bd2270a --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/PostgreSQLAuthenticationProvider.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2013 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.auth.postgresql; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.auth.AuthenticationProvider; +import org.glyptodon.guacamole.net.auth.Credentials; +import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.auth.jdbc.JDBCAuthenticationProviderModule; +import org.glyptodon.guacamole.auth.jdbc.socket.BalancedGuacamoleSocketService; +import org.glyptodon.guacamole.auth.jdbc.socket.GuacamoleSocketService; +import org.glyptodon.guacamole.auth.jdbc.socket.MultiseatGuacamoleSocketService; +import org.glyptodon.guacamole.auth.jdbc.socket.SingleSeatGuacamoleSocketService; +import org.glyptodon.guacamole.auth.jdbc.socket.UnrestrictedGuacamoleSocketService; +import org.glyptodon.guacamole.auth.jdbc.user.UserContextService; +import org.glyptodon.guacamole.environment.Environment; +import org.glyptodon.guacamole.environment.LocalEnvironment; + +/** + * Provides a PostgreSQL-based implementation of the AuthenticationProvider + * functionality. + * + * @author James Muehlner + * @author Michael Jumper + */ +public class PostgreSQLAuthenticationProvider implements AuthenticationProvider { + + /** + * Injector which will manage the object graph of this authentication + * provider. + */ + private final Injector injector; + + /** + * Returns the appropriate socket service class given the Guacamole + * environment. The class is chosen based on configuration options that + * dictate concurrent usage policy. + * + * @param environment + * The environment of the Guacamole server. + * + * @return + * The socket service class that matches the concurrent usage policy + * options set in the Guacamole environment. + * + * @throws GuacamoleException + * If an error occurs while reading the configuration options. + */ + private Class + getSocketServiceClass(Environment environment) + throws GuacamoleException { + + // Read concurrency-related properties + boolean disallowSimultaneous = environment.getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_DISALLOW_SIMULTANEOUS_CONNECTIONS, false); + boolean disallowDuplicate = environment.getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_DISALLOW_DUPLICATE_CONNECTIONS, true); + + if (disallowSimultaneous) { + + // Connections may not be used concurrently + if (disallowDuplicate) + return SingleSeatGuacamoleSocketService.class; + + // Connections are reserved for a single user when in use + else + return BalancedGuacamoleSocketService.class; + + } + + else { + + // Connections may be used concurrently, but only once per user + if (disallowDuplicate) + return MultiseatGuacamoleSocketService.class; + + // Connection use is not restricted + else + return UnrestrictedGuacamoleSocketService.class; + + } + + } + + /** + * Creates a new PostgreSQLAuthenticationProvider that reads and writes + * authentication data to a PostgreSQL database defined by properties in + * guacamole.properties. + * + * @throws GuacamoleException + * If a required property is missing, or an error occurs while parsing + * a property. + */ + public PostgreSQLAuthenticationProvider() throws GuacamoleException { + + // Get local environment + Environment environment = new LocalEnvironment(); + + // Set up Guice injector. + injector = Guice.createInjector( + + // Configure PostgreSQL-specific authentication + new PostgreSQLAuthenticationProviderModule(environment), + + // Configure JDBC authentication core + new JDBCAuthenticationProviderModule(environment, getSocketServiceClass(environment)) + + ); + + } + + @Override + public UserContext getUserContext(Credentials credentials) + throws GuacamoleException { + + // Create UserContext based on credentials, if valid + UserContextService userContextService = injector.getInstance(UserContextService.class); + return userContextService.getUserContext(credentials); + + } + + @Override + public UserContext updateUserContext(UserContext context, + Credentials credentials) throws GuacamoleException { + + // No need to update the context + return context; + + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/PostgreSQLAuthenticationProviderModule.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/PostgreSQLAuthenticationProviderModule.java new file mode 100644 index 000000000..2decdf9a6 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/PostgreSQLAuthenticationProviderModule.java @@ -0,0 +1,99 @@ +/* + * 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.auth.postgresql; + +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.name.Names; +import java.util.Properties; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.environment.Environment; +import org.mybatis.guice.datasource.helper.JdbcHelper; + +/** + * Guice module which configures PostgreSQL-specific injections. + * + * @author James Muehlner + * @author Michael Jumper + */ +public class PostgreSQLAuthenticationProviderModule implements Module { + + /** + * MyBatis-specific configuration properties. + */ + private final Properties myBatisProperties = new Properties(); + + /** + * PostgreSQL-specific driver configuration properties. + */ + private final Properties driverProperties = new Properties(); + + /** + * Creates a new PostgreSQL authentication provider module that configures + * driver and MyBatis properties using the given environment. + * + * @param environment + * The environment to use when configuring MyBatis and the underlying + * JDBC driver. + * + * @throws GuacamoleException + * If a required property is missing, or an error occurs while parsing + * a property. + */ + public PostgreSQLAuthenticationProviderModule(Environment environment) + throws GuacamoleException { + + // Set the PostgreSQL-specific properties for MyBatis. + myBatisProperties.setProperty("mybatis.environment.id", "guacamole"); + myBatisProperties.setProperty("JDBC.host", environment.getRequiredProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_HOSTNAME)); + myBatisProperties.setProperty("JDBC.port", String.valueOf(environment.getRequiredProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_PORT))); + myBatisProperties.setProperty("JDBC.schema", environment.getRequiredProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_DATABASE)); + myBatisProperties.setProperty("JDBC.username", environment.getRequiredProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_USERNAME)); + myBatisProperties.setProperty("JDBC.password", environment.getRequiredProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_PASSWORD)); + myBatisProperties.setProperty("JDBC.autoCommit", "false"); + myBatisProperties.setProperty("mybatis.pooled.pingEnabled", "true"); + myBatisProperties.setProperty("mybatis.pooled.pingQuery", "SELECT 1"); + + // Use UTF-8 in database + driverProperties.setProperty("characterEncoding","UTF-8"); + + + } + + @Override + public void configure(Binder binder) { + + // Bind PostgreSQL-specific properties + JdbcHelper.PostgreSQL.configure(binder); + + // Bind MyBatis properties + Names.bindProperties(binder, myBatisProperties); + + // Bing JDBC driver properties + binder.bind(Properties.class) + .annotatedWith(Names.named("JDBC.driverProperties")) + .toInstance(driverProperties); + + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/PostgreSQLGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/PostgreSQLGuacamoleProperties.java new file mode 100644 index 000000000..abb5a1249 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/PostgreSQLGuacamoleProperties.java @@ -0,0 +1,127 @@ +/* + * 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.auth.postgresql; + +import org.glyptodon.guacamole.properties.BooleanGuacamoleProperty; +import org.glyptodon.guacamole.properties.IntegerGuacamoleProperty; +import org.glyptodon.guacamole.properties.StringGuacamoleProperty; + +/** + * Properties used by the PostgreSQL Authentication plugin. + * + * @author James Muehlner + * @author Michael Jumper + */ +public class PostgreSQLGuacamoleProperties { + + /** + * This class should not be instantiated. + */ + private PostgreSQLGuacamoleProperties() {} + + /** + * The URL of the PostgreSQL server hosting the Guacamole authentication tables. + */ + public static final StringGuacamoleProperty POSTGRESQL_HOSTNAME = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "postgresql-hostname"; } + + }; + + /** + * The port of the PostgreSQL server hosting the Guacamole authentication + * tables. + */ + public static final IntegerGuacamoleProperty POSTGRESQL_PORT = + new IntegerGuacamoleProperty() { + + @Override + public String getName() { return "postgresql-port"; } + + }; + + /** + * The name of the PostgreSQL database containing the Guacamole + * authentication tables. + */ + public static final StringGuacamoleProperty POSTGRESQL_DATABASE = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "postgresql-database"; } + + }; + + /** + * The username used to authenticate to the PostgreSQL database containing + * the Guacamole authentication tables. + */ + public static final StringGuacamoleProperty POSTGRESQL_USERNAME = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "postgresql-username"; } + + }; + + /** + * The password used to authenticate to the PostgreSQL database containing + * the Guacamole authentication tables. + */ + public static final StringGuacamoleProperty POSTGRESQL_PASSWORD = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "postgresql-password"; } + + }; + + /** + * Whether or not multiple users accessing the same connection at the same + * time should be disallowed. + */ + public static final BooleanGuacamoleProperty + POSTGRESQL_DISALLOW_SIMULTANEOUS_CONNECTIONS = + new BooleanGuacamoleProperty() { + + @Override + public String getName() { return "postgresql-disallow-simultaneous-connections"; } + + }; + + /** + * Whether or not the same user accessing the same connection or connection + * group at the same time should be disallowed. + */ + public static final BooleanGuacamoleProperty + POSTGRESQL_DISALLOW_DUPLICATE_CONNECTIONS = + new BooleanGuacamoleProperty() { + + @Override + public String getName() { return "postgresql-disallow-duplicate-connections"; } + + }; + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/package-info.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/package-info.java new file mode 100644 index 000000000..1a939e1a6 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/package-info.java @@ -0,0 +1,26 @@ +/* + * 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. + */ + +/** + * The PostgreSQL authentication provider. + */ +package org.glyptodon.guacamole.auth.postgresql; diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionMapper.xml new file mode 100644 index 000000000..11d18a9b4 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionMapper.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DELETE FROM guacamole_connection + WHERE connection_id = #{identifier,jdbcType=INTEGER}::integer + + + + + + INSERT INTO guacamole_connection ( + connection_name, + parent_id, + protocol + ) + VALUES ( + #{object.name,jdbcType=VARCHAR}, + #{object.parentIdentifier,jdbcType=INTEGER}::integer, + #{object.protocol,jdbcType=VARCHAR} + ) + + + + + + UPDATE guacamole_connection + SET connection_name = #{object.name,jdbcType=VARCHAR}, + parent_id = #{object.parentIdentifier,jdbcType=INTEGER}::integer, + protocol = #{object.protocol,jdbcType=VARCHAR} + WHERE connection_id = #{object.objectID,jdbcType=INTEGER}::integer + + + \ No newline at end of file diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml new file mode 100644 index 000000000..de5f53076 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO guacamole_connection_history ( + connection_id, + user_id, + start_date, + end_date + ) + VALUES ( + #{record.connectionIdentifier,jdbcType=INTEGER}::integer, + #{record.userID,jdbcType=INTEGER}, + #{record.startDate,jdbcType=TIMESTAMP}, + #{record.endDate,jdbcType=TIMESTAMP} + ) + + + + \ No newline at end of file diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ParameterMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ParameterMapper.xml new file mode 100644 index 000000000..e3fe5d8e9 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ParameterMapper.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + DELETE FROM guacamole_connection_parameter + WHERE connection_id = #{identifier,jdbcType=INTEGER}::integer + + + + + + INSERT INTO guacamole_connection_parameter ( + connection_id, + parameter_name, + parameter_value + ) + VALUES + + (#{parameter.connectionIdentifier,jdbcType=INTEGER}::integer, + #{parameter.name,jdbcType=VARCHAR}, + #{parameter.value,jdbcType=VARCHAR}) + + + + + + \ No newline at end of file diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml new file mode 100644 index 000000000..3acf3731f --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DELETE FROM guacamole_connection_group + WHERE connection_group_id = #{identifier,jdbcType=INTEGER}::integer + + + + + + INSERT INTO guacamole_connection_group ( + connection_group_name, + parent_id, + type + ) + VALUES ( + #{object.name,jdbcType=VARCHAR}, + #{object.parentIdentifier,jdbcType=INTEGER}::integer, + #{object.type,jdbcType=VARCHAR}::guacamole_connection_group_type + ) + + + + + + UPDATE guacamole_connection_group + SET connection_group_name = #{object.name,jdbcType=VARCHAR}, + parent_id = #{object.parentIdentifier,jdbcType=INTEGER}::integer, + type = #{object.type,jdbcType=VARCHAR}::guacamole_connection_group_type + WHERE connection_group_id = #{object.objectID,jdbcType=INTEGER}::integer + + + \ No newline at end of file diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml new file mode 100644 index 000000000..33b3562ed --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + DELETE FROM guacamole_connection_group_permission + WHERE (user_id, permission, connection_group_id) IN + + (#{permission.userID,jdbcType=INTEGER}, + #{permission.type,jdbcType=VARCHAR}::guacamole_object_permission_type, + #{permission.objectIdentifier,jdbcType=INTEGER}::integer) + + + + + + + + INSERT INTO guacamole_connection_group_permission ( + user_id, + permission, + connection_group_id + ) + VALUES + + (#{permission.userID,jdbcType=INTEGER}, + #{permission.type,jdbcType=VARCHAR}::guacamole_object_permission_type, + #{permission.objectIdentifier,jdbcType=INTEGER}::integer) + + + + + \ No newline at end of file diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml new file mode 100644 index 000000000..4ed1ad7e9 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + DELETE FROM guacamole_connection_permission + WHERE (user_id, permission, connection_id) IN + + (#{permission.userID,jdbcType=INTEGER}, + #{permission.type,jdbcType=VARCHAR}::guacamole_object_permission_type, + #{permission.objectIdentifier,jdbcType=INTEGER}::integer) + + + + + + + + INSERT INTO guacamole_connection_permission ( + user_id, + permission, + connection_id + ) + VALUES + + (#{permission.userID,jdbcType=INTEGER}, + #{permission.type,jdbcType=VARCHAR}::guacamole_object_permission_type, + #{permission.objectIdentifier,jdbcType=INTEGER}::integer) + + + + + \ No newline at end of file diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/SystemPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/SystemPermissionMapper.xml new file mode 100644 index 000000000..f1222012c --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/SystemPermissionMapper.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + DELETE FROM guacamole_system_permission + WHERE (user_id, permission) IN + + (#{permission.userID,jdbcType=INTEGER}, + #{permission.type,jdbcType=VARCHAR}::guacamole_system_permission_type) + + + + + + + + INSERT INTO guacamole_system_permission ( + user_id, + permission + ) + VALUES + + (#{permission.userID,jdbcType=INTEGER}, + #{permission.type,jdbcType=VARCHAR}::guacamole_system_permission_type) + + + + + \ No newline at end of file diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/UserPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/UserPermissionMapper.xml new file mode 100644 index 000000000..e16f02291 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/UserPermissionMapper.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + DELETE FROM guacamole_user_permission + USING guacamole_user_permission + JOIN guacamole_user affected ON guacamole_user_permission.affected_user_id = affected.user_id + WHERE + (guacamole_user_permission.user_id, permission, affected.username) IN + + (#{permission.userID,jdbcType=INTEGER}, + #{permission.type,jdbcType=VARCHAR}::guacamole_object_permission_type, + #{permission.objectIdentifier,jdbcType=INTEGER}) + + + + + + + + INSERT INTO guacamole_user_permission ( + user_id, + permission, + affected_user_id + ) + SELECT permissions.user_id, permissions.permission, guacamole_user.user_id FROM + + SELECT #{permission.userID,jdbcType=INTEGER} AS user_id, + #{permission.type,jdbcType=VARCHAR}::guacamole_object_permission_type AS permission, + #{permission.objectIdentifier,jdbcType=INTEGER} AS username + + AS permissions + JOIN guacamole_user ON guacamole_user.username = permissions.username; + + + + \ No newline at end of file diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/user/UserMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/user/UserMapper.xml new file mode 100644 index 000000000..41416f7db --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/user/UserMapper.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DELETE FROM guacamole_user + WHERE username = #{identifier,jdbcType=VARCHAR} + + + + + + INSERT INTO guacamole_user ( + username, + password_hash, + password_salt + ) + VALUES ( + #{object.identifier,jdbcType=VARCHAR}, + #{object.passwordHash,jdbcType=BINARY}, + #{object.passwordSalt,jdbcType=BINARY} + ) + + + + + + UPDATE guacamole_user + SET password_hash = #{object.passwordHash,jdbcType=BINARY}, + password_salt = #{object.passwordSalt,jdbcType=BINARY} + WHERE user_id = #{object.objectID,jdbcType=VARCHAR} + + + diff --git a/extensions/guacamole-auth-jdbc/pom.xml b/extensions/guacamole-auth-jdbc/pom.xml index c6c622994..49fb5c124 100644 --- a/extensions/guacamole-auth-jdbc/pom.xml +++ b/extensions/guacamole-auth-jdbc/pom.xml @@ -20,8 +20,9 @@ modules/guacamole-auth-jdbc-base - + modules/guacamole-auth-jdbc-mysql + modules/guacamole-auth-jdbc-postgresql diff --git a/extensions/guacamole-auth-jdbc/src/main/assembly/dist.xml b/extensions/guacamole-auth-jdbc/src/main/assembly/dist.xml index 2d20b63a0..a416ea191 100644 --- a/extensions/guacamole-auth-jdbc/src/main/assembly/dist.xml +++ b/extensions/guacamole-auth-jdbc/src/main/assembly/dist.xml @@ -27,6 +27,19 @@ + + + /postgresql/schema + modules/guacamole-auth-jdbc-postgresql/schema + + + modules/guacamole-auth-jdbc-postgresql/target/extension + /postgresql + + *.jar + + +