GUAC-1104: Add parent group validation. Switch to beforeCreate/beforeUpdate/beforeDelete validation functions.

This commit is contained in:
Michael Jumper
2015-03-07 15:13:51 -08:00
parent eb676c8b3f
commit c6132d2f09
5 changed files with 296 additions and 84 deletions

View File

@@ -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 <InternalType>
@@ -215,52 +215,77 @@ public abstract class DirectoryObjectService<InternalType extends DirectoryObjec
}
/**
* Returns whether the contents of the given model are valid and can be
* used to create a new object as-is. The object does not yet exist in the
* database, but the user desires to create a new object with the given
* model. This function will be called prior to any creation operation, and
* provides a means for the implementation to abort prior to completion. The
* default implementation does nothing.
* Called before any object is created through this directory object
* service. This function serves as a final point of validation before
* the create operation occurs. In its default implementation,
* beforeCreate() performs basic permissions checks.
*
* @param user
* The user creating the object.
*
* @param model
* The model to validate.
* The model of the object being created.
*
* @throws GuacamoleException
* If the object is invalid, or an error prevents validating the given
* object.
*/
protected void validateNewModel(AuthenticatedUser user,
ModelType model) throws GuacamoleException {
protected void beforeCreate(AuthenticatedUser user,
ModelType model ) throws GuacamoleException {
// By default, do nothing.
// Verify permission to create objects
if (!user.getUser().isAdministrator() && !hasCreatePermission(user))
throw new GuacamoleSecurityException("Permission denied.");
}
/**
* Returns whether the given model is valid and can be used to update an
* existing object as-is. The object already exists in the database, but the
* user desires to update the object to the given model. This function will
* be called prior to update operation, and provides a means for the
* implementation to abort prior to completion. The default implementation
* does nothing.
* Called before any object is updated through this directory object
* service. This function serves as a final point of validation before
* the update operation occurs. In its default implementation,
* beforeUpdate() performs basic permissions checks.
*
* @param user
* The user updating the existing object.
*
* @param model
* The model to validate.
* The model of the object being updated.
*
* @throws GuacamoleException
* If the object is invalid, or an error prevents validating the given
* object.
*/
protected void validateExistingModel(AuthenticatedUser user,
protected void beforeUpdate(AuthenticatedUser user,
ModelType model) throws GuacamoleException {
// By default, do nothing.
if (!hasObjectPermission(user, model.getIdentifier(), ObjectPermission.Type.UPDATE))
throw new GuacamoleSecurityException("Permission denied.");
}
/**
* Called before any object is deleted through this directory object
* service. This function serves as a final point of validation before
* the delete operation occurs. In its default implementation,
* beforeDelete() performs basic permissions checks.
*
* @param user
* The user deleting the existing object.
*
* @param identifier
* The identifier of the object being deleted.
*
* @throws GuacamoleException
* If the object is invalid, or an error prevents validating the given
* object.
*/
protected void beforeDelete(AuthenticatedUser user,
String identifier) throws GuacamoleException {
// Verify permission to delete objects
if (!hasObjectPermission(user, identifier, ObjectPermission.Type.DELETE))
throw new GuacamoleSecurityException("Permission denied.");
}
@@ -359,43 +384,35 @@ public abstract class DirectoryObjectService<InternalType extends DirectoryObjec
public InternalType createObject(AuthenticatedUser user, ExternalType object)
throws GuacamoleException {
// Only create object if user has permission to do so
if (user.getUser().isAdministrator() || hasCreatePermission(user)) {
ModelType model = getModelInstance(user, object);
beforeCreate(user, model);
// Create object
getObjectMapper().insert(model);
// Validate object prior to creation
ModelType model = getModelInstance(user, object);
validateNewModel(user, model);
// Build list of implicit permissions
Collection<ObjectPermissionModel> implicitPermissions =
new ArrayList<ObjectPermissionModel>(IMPLICIT_OBJECT_PERMISSIONS.length);
// Create object
getObjectMapper().insert(model);
UserModel userModel = user.getUser().getModel();
for (ObjectPermission.Type permission : IMPLICIT_OBJECT_PERMISSIONS) {
// Build list of implicit permissions
Collection<ObjectPermissionModel> implicitPermissions =
new ArrayList<ObjectPermissionModel>(IMPLICIT_OBJECT_PERMISSIONS.length);
// Create model which grants this permission to the current user
ObjectPermissionModel permissionModel = new ObjectPermissionModel();
permissionModel.setUserID(userModel.getObjectID());
permissionModel.setUsername(userModel.getIdentifier());
permissionModel.setType(permission);
permissionModel.setObjectIdentifier(model.getIdentifier());
UserModel userModel = user.getUser().getModel();
for (ObjectPermission.Type permission : IMPLICIT_OBJECT_PERMISSIONS) {
// Create model which grants this permission to the current user
ObjectPermissionModel permissionModel = new ObjectPermissionModel();
permissionModel.setUserID(userModel.getObjectID());
permissionModel.setUsername(userModel.getIdentifier());
permissionModel.setType(permission);
permissionModel.setObjectIdentifier(model.getIdentifier());
// Add permission
implicitPermissions.add(permissionModel);
}
// Add implicit permissions
getPermissionMapper().insert(implicitPermissions);
return getObjectInstance(user, model);
// Add permission
implicitPermissions.add(permissionModel);
}
// User lacks permission to create
throw new GuacamoleSecurityException("Permission denied.");
// Add implicit permissions
getPermissionMapper().insert(implicitPermissions);
return getObjectInstance(user, model);
}
@@ -416,14 +433,10 @@ public abstract class DirectoryObjectService<InternalType extends DirectoryObjec
public void deleteObject(AuthenticatedUser user, String identifier)
throws GuacamoleException {
// Only delete object if user has permission to do so
if (hasObjectPermission(user, identifier, ObjectPermission.Type.DELETE)) {
getObjectMapper().delete(identifier);
return;
}
// User lacks permission to delete
throw new GuacamoleSecurityException("Permission denied.");
beforeDelete(user, identifier);
// Delete object
getObjectMapper().delete(identifier);
}
@@ -444,20 +457,11 @@ public abstract class DirectoryObjectService<InternalType extends DirectoryObjec
public void updateObject(AuthenticatedUser user, InternalType object)
throws GuacamoleException {
// Only update object if user has permission to do so
if (hasObjectPermission(user, object.getIdentifier(), ObjectPermission.Type.UPDATE)) {
// Validate object prior to creation
ModelType model = object.getModel();
validateExistingModel(user, model);
// Update object
getObjectMapper().update(model);
return;
}
// User lacks permission to update
throw new GuacamoleSecurityException("Permission denied.");
ModelType model = object.getModel();
beforeUpdate(user, model);
// Update object
getObjectMapper().update(model);
}

View File

@@ -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 <InternalType>
* The specific internal implementation of the type of object this service
* provides access to.
*
* @param <ExternalType>
* The external interface or implementation of the type of object this
* service provides access to, as defined by the guacamole-ext API.
*
* @param <ModelType>
* The underlying model object used to represent InternalType in the
* database.
*/
public abstract class GroupedDirectoryObjectService<InternalType extends GroupedDirectoryObject<ModelType>,
ExternalType, ModelType extends GroupedObjectModel>
extends DirectoryObjectService<InternalType, ExternalType, ModelType> {
/**
* 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<String> 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<String> groups = new ArrayList<String>(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<String> modifiedGroups = getModifiedGroups(user, identifier, model);
if (!modifiedGroups.isEmpty()) {
ObjectPermissionSet permissionSet = user.getUser().getConnectionGroupPermissions();
Collection<String> 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.");
}
}

View File

@@ -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<ModeledConnection, Connection, ConnectionModel> {
public class ConnectionService extends GroupedDirectoryObjectService<ModeledConnection, Connection, ConnectionModel> {
/**
* Mapper for accessing connections.
@@ -148,9 +148,11 @@ public class ConnectionService extends DirectoryObjectService<ModeledConnection,
}
@Override
protected void validateNewModel(AuthenticatedUser user,
protected void beforeCreate(AuthenticatedUser user,
ConnectionModel model) throws GuacamoleException {
super.beforeCreate(user, model);
// Name must not be blank
if (model.getName().trim().isEmpty())
throw new GuacamoleClientException("Connection names must not be blank.");
@@ -163,9 +165,11 @@ public class ConnectionService extends DirectoryObjectService<ModeledConnection,
}
@Override
protected void validateExistingModel(AuthenticatedUser user,
protected void beforeUpdate(AuthenticatedUser user,
ConnectionModel model) throws GuacamoleException {
super.beforeUpdate(user, model);
// Name must not be blank
if (model.getName().trim().isEmpty())
throw new GuacamoleClientException("Connection names must not be blank.");
@@ -179,7 +183,7 @@ public class ConnectionService extends DirectoryObjectService<ModeledConnection,
throw new GuacamoleClientException("The connection \"" + model.getName() + "\" already exists.");
}
}
/**

View File

@@ -27,11 +27,11 @@ 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.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 +48,7 @@ import org.glyptodon.guacamole.protocol.GuacamoleClientInformation;
*
* @author Michael Jumper, James Muehlner
*/
public class ConnectionGroupService extends DirectoryObjectService<ModeledConnectionGroup,
public class ConnectionGroupService extends GroupedDirectoryObjectService<ModeledConnectionGroup,
ConnectionGroup, ConnectionGroupModel> {
/**
@@ -130,9 +130,11 @@ public class ConnectionGroupService extends DirectoryObjectService<ModeledConnec
}
@Override
protected void validateNewModel(AuthenticatedUser user,
protected void beforeCreate(AuthenticatedUser user,
ConnectionGroupModel model) throws GuacamoleException {
super.beforeCreate(user, model);
// Name must not be blank
if (model.getName().trim().isEmpty())
throw new GuacamoleClientException("Connection group names must not be blank.");
@@ -145,9 +147,11 @@ public class ConnectionGroupService extends DirectoryObjectService<ModeledConnec
}
@Override
protected void validateExistingModel(AuthenticatedUser user,
protected void beforeUpdate(AuthenticatedUser user,
ConnectionGroupModel model) throws GuacamoleException {
super.beforeUpdate(user, model);
// Name must not be blank
if (model.getName().trim().isEmpty())
throw new GuacamoleClientException("Connection group names must not be blank.");

View File

@@ -126,9 +126,11 @@ public class UserService extends DirectoryObjectService<ModeledUser, User, UserM
}
@Override
protected void validateNewModel(AuthenticatedUser user, UserModel model)
protected void beforeCreate(AuthenticatedUser user, UserModel model)
throws GuacamoleException {
super.beforeCreate(user, model);
// Username must not be blank
if (model.getIdentifier().trim().isEmpty())
throw new GuacamoleClientException("The username must not be blank.");
@@ -141,9 +143,11 @@ public class UserService extends DirectoryObjectService<ModeledUser, User, UserM
}
@Override
protected void validateExistingModel(AuthenticatedUser user,
protected void beforeUpdate(AuthenticatedUser user,
UserModel model) throws GuacamoleException {
super.beforeUpdate(user, model);
// Username must not be blank
if (model.getIdentifier().trim().isEmpty())
throw new GuacamoleClientException("The username must not be blank.");