/*
* 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 net.sourceforge.guacamole.net.auth.mysql;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.glyptodon.guacamole.GuacamoleClientException;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleSecurityException;
import org.glyptodon.guacamole.net.auth.Directory;
import org.glyptodon.guacamole.net.auth.User;
import net.sourceforge.guacamole.net.auth.mysql.dao.ConnectionGroupPermissionMapper;
import net.sourceforge.guacamole.net.auth.mysql.dao.ConnectionPermissionMapper;
import net.sourceforge.guacamole.net.auth.mysql.dao.SystemPermissionMapper;
import net.sourceforge.guacamole.net.auth.mysql.dao.UserPermissionMapper;
import net.sourceforge.guacamole.net.auth.mysql.model.ConnectionGroupPermissionExample;
import net.sourceforge.guacamole.net.auth.mysql.model.ConnectionGroupPermissionKey;
import net.sourceforge.guacamole.net.auth.mysql.model.ConnectionPermissionExample;
import net.sourceforge.guacamole.net.auth.mysql.model.ConnectionPermissionKey;
import net.sourceforge.guacamole.net.auth.mysql.model.SystemPermissionExample;
import net.sourceforge.guacamole.net.auth.mysql.model.SystemPermissionKey;
import net.sourceforge.guacamole.net.auth.mysql.model.UserPermissionExample;
import net.sourceforge.guacamole.net.auth.mysql.model.UserPermissionKey;
import net.sourceforge.guacamole.net.auth.mysql.service.ConnectionGroupService;
import net.sourceforge.guacamole.net.auth.mysql.service.ConnectionService;
import net.sourceforge.guacamole.net.auth.mysql.service.PermissionCheckService;
import net.sourceforge.guacamole.net.auth.mysql.service.UserService;
import org.glyptodon.guacamole.GuacamoleUnsupportedException;
import org.glyptodon.guacamole.net.auth.permission.ConnectionGroupPermission;
import org.glyptodon.guacamole.net.auth.permission.ConnectionPermission;
import org.glyptodon.guacamole.net.auth.permission.Permission;
import org.glyptodon.guacamole.net.auth.permission.SystemPermission;
import org.glyptodon.guacamole.net.auth.permission.UserPermission;
import org.mybatis.guice.transactional.Transactional;
/**
* A MySQL based implementation of the User Directory.
* @author James Muehlner
*/
public class UserDirectory implements Directory<String, User> {
/**
* The ID of the user who this user directory belongs to.
* Access is based on his/her permission settings.
*/
private int user_id;
/**
* Service for accessing users.
*/
@Inject
private UserService userService;
/**
* Service for accessing connections.
*/
@Inject
private ConnectionService connectionService;
/**
* Service for accessing connection groups.
*/
@Inject
private ConnectionGroupService connectionGroupService;
/**
* DAO for accessing user permissions, which will be injected.
*/
@Inject
private UserPermissionMapper userPermissionDAO;
/**
* DAO for accessing connection permissions, which will be injected.
*/
@Inject
private ConnectionPermissionMapper connectionPermissionDAO;
/**
* DAO for accessing connection group permissions, which will be injected.
*/
@Inject
private ConnectionGroupPermissionMapper connectionGroupPermissionDAO;
/**
* DAO for accessing system permissions, which will be injected.
*/
@Inject
private SystemPermissionMapper systemPermissionDAO;
/**
* Service for checking various permissions, which will be injected.
*/
@Inject
private PermissionCheckService permissionCheckService;
/**
* Set the user for this directory.
*
* @param user_id The ID of the user whose permissions define the visibility
* of other users in this directory.
*/
public void init(int user_id) {
this.user_id = user_id;
}
@Transactional
@Override
public org.glyptodon.guacamole.net.auth.User get(String identifier)
throws GuacamoleException {
// Get user
MySQLUser user = userService.retrieveUser(identifier);
if(user == null)
return null;
// Verify access is granted
permissionCheckService.verifyUserAccess(this.user_id,
user.getUserID(),
MySQLConstants.USER_READ);
// Return user
return user;
}
@Transactional
@Override
public Set<String> getIdentifiers() throws GuacamoleException {
return permissionCheckService.retrieveUsernames(user_id,
MySQLConstants.USER_READ);
}
@Override
@Transactional
public void add(org.glyptodon.guacamole.net.auth.User object)
throws GuacamoleException {
String username = object.getUsername().trim();
if(username.isEmpty())
throw new GuacamoleClientException("The username cannot be blank.");
// Verify current user has permission to create users
permissionCheckService.verifySystemAccess(this.user_id,
MySQLConstants.SYSTEM_USER_CREATE);
Preconditions.checkNotNull(object);
// Verify that no user already exists with this username.
MySQLUser previousUser = userService.retrieveUser(username);
if(previousUser != null)
throw new GuacamoleClientException("That username is already in use.");
// Create new user
MySQLUser user = userService.createUser(username, object.getPassword());
// Create permissions of new user in database
createPermissions(user.getUserID(), object.getPermissions());
// Give the current user full access to the newly created user.
UserPermissionKey newUserPermission = new UserPermissionKey();
newUserPermission.setUser_id(this.user_id);
newUserPermission.setAffected_user_id(user.getUserID());
// READ permission on new user
newUserPermission.setPermission(MySQLConstants.USER_READ);
userPermissionDAO.insert(newUserPermission);
// UPDATE permission on new user
newUserPermission.setPermission(MySQLConstants.USER_UPDATE);
userPermissionDAO.insert(newUserPermission);
// DELETE permission on new user
newUserPermission.setPermission(MySQLConstants.USER_DELETE);
userPermissionDAO.insert(newUserPermission);
// ADMINISTER permission on new user
newUserPermission.setPermission(MySQLConstants.USER_ADMINISTER);
userPermissionDAO.insert(newUserPermission);
}
/**
* Add the given permissions to the given user.
*
* @param user_id The ID of the user whose permissions should be updated.
* @param permissions The permissions to add.
* @throws GuacamoleException If an error occurs while updating the
* permissions of the given user.
*/
private void createPermissions(int user_id, Set<Permission> permissions) throws GuacamoleException {
// Partition given permissions by permission type
List<UserPermission> newUserPermissions = new ArrayList<UserPermission>();
List<ConnectionPermission> newConnectionPermissions = new ArrayList<ConnectionPermission>();
List<ConnectionGroupPermission> newConnectionGroupPermissions = new ArrayList<ConnectionGroupPermission>();
List<SystemPermission> newSystemPermissions = new ArrayList<SystemPermission>();
for (Permission permission : permissions) {
if (permission instanceof UserPermission)
newUserPermissions.add((UserPermission) permission);
else if (permission instanceof ConnectionPermission)
newConnectionPermissions.add((ConnectionPermission) permission);
else if (permission instanceof ConnectionGroupPermission)
newConnectionGroupPermissions.add((ConnectionGroupPermission) permission);
else if (permission instanceof SystemPermission)
newSystemPermissions.add((SystemPermission) permission);
}
// Create the new permissions
createUserPermissions(user_id, newUserPermissions);
createConnectionPermissions(user_id, newConnectionPermissions);
createConnectionGroupPermissions(user_id, newConnectionGroupPermissions);
createSystemPermissions(user_id, newSystemPermissions);
}
/**
* Remove the given permissions from the given user.
*
* @param user_id The ID of the user whose permissions should be updated.
* @param permissions The permissions to remove.
* @throws GuacamoleException If an error occurs while updating the
* permissions of the given user.
*/
private void removePermissions(int user_id, Set<Permission> permissions)
throws GuacamoleException {
// Partition given permissions by permission type
List<UserPermission> removedUserPermissions = new ArrayList<UserPermission>();
List<ConnectionPermission> removedConnectionPermissions = new ArrayList<ConnectionPermission>();
List<ConnectionGroupPermission> removedConnectionGroupPermissions = new ArrayList<ConnectionGroupPermission>();
List<SystemPermission> removedSystemPermissions = new ArrayList<SystemPermission>();
for (Permission permission : permissions) {
if (permission instanceof UserPermission)
removedUserPermissions.add((UserPermission) permission);
else if (permission instanceof ConnectionPermission)
removedConnectionPermissions.add((ConnectionPermission) permission);
else if (permission instanceof ConnectionGroupPermission)
removedConnectionGroupPermissions.add((ConnectionGroupPermission) permission);
else if (permission instanceof SystemPermission)
removedSystemPermissions.add((SystemPermission) permission);
}
// Delete the removed permissions.
deleteUserPermissions(user_id, removedUserPermissions);
deleteConnectionPermissions(user_id, removedConnectionPermissions);
deleteConnectionGroupPermissions(user_id, removedConnectionGroupPermissions);
deleteSystemPermissions(user_id, removedSystemPermissions);
}
/**
* Create the given user permissions for the given user.
*
* @param user_id The ID of the user to change the permissions of.
* @param permissions The new permissions the given user should have when
* this operation completes.
* @throws GuacamoleException If permission to alter the access permissions
* of affected objects is denied.
*/
private void createUserPermissions(int user_id,
Collection<UserPermission> permissions)
throws GuacamoleException {
// If no permissions given, stop now
if(permissions.isEmpty())
return;
// Get list of administerable user IDs
List<Integer> administerableUserIDs =
permissionCheckService.retrieveUserIDs(this.user_id,
MySQLConstants.USER_ADMINISTER);
// Get set of usernames corresponding to administerable users
Map<String, Integer> administerableUsers =
userService.translateUsernames(administerableUserIDs);
// Insert all given permissions
for (UserPermission permission : permissions) {
// Get original ID
Integer affected_id =
administerableUsers.get(permission.getObjectIdentifier());
// Verify that the user actually has permission to administrate
// every one of these users
if (affected_id == null)
throw new GuacamoleSecurityException(
"User #" + this.user_id
+ " does not have permission to administrate user "
+ permission.getObjectIdentifier());
// Create new permission
UserPermissionKey newPermission = new UserPermissionKey();
newPermission.setUser_id(user_id);
newPermission.setPermission(MySQLConstants.getUserConstant(permission.getType()));
newPermission.setAffected_user_id(affected_id);
userPermissionDAO.insert(newPermission);
}
}
/**
* Delete permissions having to do with users for a given user.
*
* @param user_id The ID of the user to change the permissions of.
* @param permissions The permissions the given user should no longer have
* when this operation completes.
* @throws GuacamoleException If permission to alter the access permissions
* of affected objects is denied.
*/
private void deleteUserPermissions(int user_id,
Collection<UserPermission> permissions)
throws GuacamoleException {
// If no permissions given, stop now
if(permissions.isEmpty())
return;
// Get list of administerable user IDs
List<Integer> administerableUserIDs =
permissionCheckService.retrieveUserIDs(this.user_id,
MySQLConstants.USER_ADMINISTER);
// Get set of usernames corresponding to administerable users
Map<String, Integer> administerableUsers =
userService.translateUsernames(administerableUserIDs);
// Delete requested permissions
for (UserPermission permission : permissions) {
// Get original ID
Integer affected_id =
administerableUsers.get(permission.getObjectIdentifier());
// Verify that the user actually has permission to administrate
// every one of these users
if (affected_id == null)
throw new GuacamoleSecurityException(
"User #" + this.user_id
+ " does not have permission to administrate user "
+ permission.getObjectIdentifier());
// Delete requested permission
UserPermissionExample userPermissionExample = new UserPermissionExample();
userPermissionExample.createCriteria()
.andUser_idEqualTo(user_id)
.andPermissionEqualTo(MySQLConstants.getUserConstant(permission.getType()))
.andAffected_user_idEqualTo(affected_id);
userPermissionDAO.deleteByExample(userPermissionExample);
}
}
/**
* Create any new permissions having to do with connections for a given
* user.
*
* @param user_id The ID of the user to assign or remove permissions from.
* @param permissions The new permissions the user should have after this
* operation completes.
* @throws GuacamoleException If permission to alter the access permissions
* of affected objects is deniedD
*/
private void createConnectionPermissions(int user_id,
Collection<ConnectionPermission> permissions)
throws GuacamoleException {
// If no permissions given, stop now
if(permissions.isEmpty())
return;
// Get list of administerable connection IDs
Set<Integer> administerableConnectionIDs = Sets.<Integer>newHashSet(
permissionCheckService.retrieveConnectionIDs(this.user_id,
MySQLConstants.CONNECTION_ADMINISTER));
// Insert all given permissions
for (ConnectionPermission permission : permissions) {
// Get original ID
Integer connection_id = Integer.valueOf(permission.getObjectIdentifier());
// Throw exception if permission to administer this connection
// is not granted
if (!administerableConnectionIDs.contains(connection_id))
throw new GuacamoleSecurityException(
"User #" + this.user_id
+ " does not have permission to administrate connection "
+ permission.getObjectIdentifier());
// Create new permission
ConnectionPermissionKey newPermission = new ConnectionPermissionKey();
newPermission.setUser_id(user_id);
newPermission.setPermission(MySQLConstants.getConnectionConstant(permission.getType()));
newPermission.setConnection_id(connection_id);
connectionPermissionDAO.insert(newPermission);
}
}
/**
* Create any new permissions having to do with connection groups
* for a given user.
*
* @param user_id The ID of the user to assign or remove permissions from.
* @param permissions The new permissions the user should have after this
* operation completes.
* @throws GuacamoleException If permission to alter the access permissions
* of affected objects is deniedD
*/
private void createConnectionGroupPermissions(int user_id,
Collection<ConnectionGroupPermission> permissions)
throws GuacamoleException {
// If no permissions given, stop now
if(permissions.isEmpty())
return;
// Get list of administerable connection group IDs
Set<Integer> administerableConnectionGroupIDs = Sets.<Integer>newHashSet(
permissionCheckService.retrieveConnectionGroupIDs(this.user_id,
MySQLConstants.CONNECTION_GROUP_ADMINISTER));
// Insert all given permissions
for (ConnectionGroupPermission permission : permissions) {
// Get original ID
Integer connection_group_id = Integer.valueOf(permission.getObjectIdentifier());
// Throw exception if permission to administer this connection group
// is not granted
if (!administerableConnectionGroupIDs.contains(connection_group_id))
throw new GuacamoleSecurityException(
"User #" + this.user_id
+ " does not have permission to administrate connection group"
+ permission.getObjectIdentifier());
// Create new permission
ConnectionGroupPermissionKey newPermission = new ConnectionGroupPermissionKey();
newPermission.setUser_id(user_id);
newPermission.setPermission(MySQLConstants.getConnectionGroupConstant(permission.getType()));
newPermission.setConnection_group_id(connection_group_id);
connectionGroupPermissionDAO.insert(newPermission);
}
}
/**
* Delete permissions having to do with connections for a given user.
*
* @param user_id The ID of the user to change the permissions of.
* @param permissions The permissions the given user should no longer have
* when this operation completes.
* @throws GuacamoleException If permission to alter the access permissions
* of affected objects is denied.
*/
private void deleteConnectionPermissions(int user_id,
Collection<ConnectionPermission> permissions)
throws GuacamoleException {
// If no permissions given, stop now
if(permissions.isEmpty())
return;
// Get list of administerable connection IDs
Set<Integer> administerableConnectionIDs = Sets.<Integer>newHashSet(
permissionCheckService.retrieveConnectionIDs(this.user_id,
MySQLConstants.CONNECTION_ADMINISTER));
// Delete requested permissions
for (ConnectionPermission permission : permissions) {
// Get original ID
Integer connection_id = Integer.valueOf(permission.getObjectIdentifier());
// Verify that the user actually has permission to administrate
// every one of these connections
if (!administerableConnectionIDs.contains(connection_id))
throw new GuacamoleSecurityException(
"User #" + this.user_id
+ " does not have permission to administrate connection "
+ permission.getObjectIdentifier());
ConnectionPermissionExample connectionPermissionExample = new ConnectionPermissionExample();
connectionPermissionExample.createCriteria()
.andUser_idEqualTo(user_id)
.andPermissionEqualTo(MySQLConstants.getConnectionConstant(permission.getType()))
.andConnection_idEqualTo(connection_id);
connectionPermissionDAO.deleteByExample(connectionPermissionExample);
}
}
/**
* Delete permissions having to do with connection groups for a given user.
*
* @param user_id The ID of the user to change the permissions of.
* @param permissions The permissions the given user should no longer have
* when this operation completes.
* @throws GuacamoleException If permission to alter the access permissions
* of affected objects is denied.
*/
private void deleteConnectionGroupPermissions(int user_id,
Collection<ConnectionGroupPermission> permissions)
throws GuacamoleException {
// If no permissions given, stop now
if(permissions.isEmpty())
return;
// Get list of administerable connection group IDs
Set<Integer> administerableConnectionGroupIDs = Sets.<Integer>newHashSet(
permissionCheckService.retrieveConnectionGroupIDs(this.user_id,
MySQLConstants.CONNECTION_GROUP_ADMINISTER));
// Delete requested permissions
for (ConnectionGroupPermission permission : permissions) {
// Get original ID
Integer connection_group_id = Integer.valueOf(permission.getObjectIdentifier());
// Verify that the user actually has permission to administrate
// every one of these connection groups
if (!administerableConnectionGroupIDs.contains(connection_group_id))
throw new GuacamoleSecurityException(
"User #" + this.user_id
+ " does not have permission to administrate connection group"
+ permission.getObjectIdentifier());
ConnectionGroupPermissionExample connectionGroupPermissionExample = new ConnectionGroupPermissionExample();
connectionGroupPermissionExample.createCriteria()
.andUser_idEqualTo(user_id)
.andPermissionEqualTo(MySQLConstants.getConnectionGroupConstant(permission.getType()))
.andConnection_group_idEqualTo(connection_group_id);
connectionGroupPermissionDAO.deleteByExample(connectionGroupPermissionExample);
}
}
/**
* Create any new system permissions for a given user. All permissions in
* the given list will be inserted.
*
* @param user_id The ID of the user whose permissions should be updated.
* @param permissions The new system permissions that the given user should
* have when this operation completes.
* @throws GuacamoleException If permission to administer system permissions
* is denied.
*/
private void createSystemPermissions(int user_id,
Collection<SystemPermission> permissions) throws GuacamoleException {
// If no permissions given, stop now
if(permissions.isEmpty())
return;
// Only a system administrator can add system permissions.
permissionCheckService.verifySystemAccess(
this.user_id, SystemPermission.Type.ADMINISTER.name());
// Insert all requested permissions
for (SystemPermission permission : permissions) {
// Insert permission
SystemPermissionKey newSystemPermission = new SystemPermissionKey();
newSystemPermission.setUser_id(user_id);
newSystemPermission.setPermission(MySQLConstants.getSystemConstant(permission.getType()));
systemPermissionDAO.insert(newSystemPermission);
}
}
/**
* Delete system permissions for a given user. All permissions in
* the given list will be removed from the user.
*
* @param user_id The ID of the user whose permissions should be updated.
* @param permissions The permissions the given user should no longer have
* when this operation completes.
* @throws GuacamoleException If the permissions specified could not be
* removed due to system restrictions.
*/
private void deleteSystemPermissions(int user_id,
Collection<SystemPermission> permissions)
throws GuacamoleException {
// If no permissions given, stop now
if (permissions.isEmpty())
return;
// Prevent self-de-adminifying
if (user_id == this.user_id)
throw new GuacamoleUnsupportedException("Removing your own administrative permissions is not allowed.");
// Build list of requested system permissions
List<String> systemPermissionTypes = new ArrayList<String>();
for (SystemPermission permission : permissions)
systemPermissionTypes.add(MySQLConstants.getSystemConstant(permission.getType()));
// Delete the requested system permissions for this user
SystemPermissionExample systemPermissionExample = new SystemPermissionExample();
systemPermissionExample.createCriteria().andUser_idEqualTo(user_id)
.andPermissionIn(systemPermissionTypes);
systemPermissionDAO.deleteByExample(systemPermissionExample);
}
@Override
@Transactional
public void update(org.glyptodon.guacamole.net.auth.User object)
throws GuacamoleException {
// If user not actually from this auth provider, we can't handle updated
// permissions.
if (!(object instanceof MySQLUser))
throw new GuacamoleUnsupportedException("User not from database.");
MySQLUser mySQLUser = (MySQLUser) object;
// Validate permission to update this user is granted
permissionCheckService.verifyUserAccess(this.user_id,
mySQLUser.getUserID(),
MySQLConstants.USER_UPDATE);
// Update the user in the database
userService.updateUser(mySQLUser);
// Update permissions in database
createPermissions(mySQLUser.getUserID(), mySQLUser.getNewPermissions());
removePermissions(mySQLUser.getUserID(), mySQLUser.getRemovedPermissions());
// The appropriate permissions have been inserted and deleted, so
// reset the new and removed permission sets.
mySQLUser.resetPermissions();
}
@Override
@Transactional
public void remove(String identifier) throws GuacamoleException {
// Get user pending deletion
MySQLUser user = userService.retrieveUser(identifier);
// Prevent self-deletion
if (user.getUserID() == this.user_id)
throw new GuacamoleUnsupportedException("Deleting your own user is not allowed.");
// Validate current user has permission to remove the specified user
permissionCheckService.verifyUserAccess(this.user_id,
user.getUserID(),
MySQLConstants.USER_DELETE);
// Delete specified user
userService.deleteUser(user.getUserID());
}
@Override
public void move(String identifier, Directory<String, User> groupIdentifier)
throws GuacamoleException {
throw new GuacamoleSecurityException("Permission denied.");
}
}