mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
GUACAMOLE-96: Restrict submitted attributes to those explicitly declared by the UserContext.
This commit is contained in:
@@ -22,6 +22,7 @@ package org.apache.guacamole.rest.activeconnection;
|
|||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
import org.apache.guacamole.GuacamoleUnsupportedException;
|
import org.apache.guacamole.GuacamoleUnsupportedException;
|
||||||
import org.apache.guacamole.net.auth.ActiveConnection;
|
import org.apache.guacamole.net.auth.ActiveConnection;
|
||||||
|
import org.apache.guacamole.net.auth.UserContext;
|
||||||
import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
|
import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,7 +31,7 @@ import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
|
|||||||
* toExternalObject() is implemented here.
|
* toExternalObject() is implemented here.
|
||||||
*/
|
*/
|
||||||
public class ActiveConnectionObjectTranslator
|
public class ActiveConnectionObjectTranslator
|
||||||
implements DirectoryObjectTranslator<ActiveConnection, APIActiveConnection> {
|
extends DirectoryObjectTranslator<ActiveConnection, APIActiveConnection> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public APIActiveConnection toExternalObject(ActiveConnection object)
|
public APIActiveConnection toExternalObject(ActiveConnection object)
|
||||||
@@ -56,4 +57,10 @@ public class ActiveConnectionObjectTranslator
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filterExternalObject(UserContext context,
|
||||||
|
APIActiveConnection object) throws GuacamoleException {
|
||||||
|
// Nothing to filter on ActiveConnections (no attributes)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -89,7 +89,7 @@ public class ActiveConnectionResource
|
|||||||
@Assisted Directory<ActiveConnection> directory,
|
@Assisted Directory<ActiveConnection> directory,
|
||||||
@Assisted ActiveConnection activeConnection,
|
@Assisted ActiveConnection activeConnection,
|
||||||
DirectoryObjectTranslator<ActiveConnection, APIActiveConnection> translator) {
|
DirectoryObjectTranslator<ActiveConnection, APIActiveConnection> translator) {
|
||||||
super(directory, activeConnection, translator);
|
super(userContext, directory, activeConnection, translator);
|
||||||
this.userContext = userContext;
|
this.userContext = userContext;
|
||||||
this.activeConnection = activeConnection;
|
this.activeConnection = activeConnection;
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@ package org.apache.guacamole.rest.connection;
|
|||||||
|
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
import org.apache.guacamole.net.auth.Connection;
|
import org.apache.guacamole.net.auth.Connection;
|
||||||
|
import org.apache.guacamole.net.auth.UserContext;
|
||||||
import org.apache.guacamole.protocol.GuacamoleConfiguration;
|
import org.apache.guacamole.protocol.GuacamoleConfiguration;
|
||||||
import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
|
import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
|
|||||||
* objects.
|
* objects.
|
||||||
*/
|
*/
|
||||||
public class ConnectionObjectTranslator
|
public class ConnectionObjectTranslator
|
||||||
implements DirectoryObjectTranslator<Connection, APIConnection> {
|
extends DirectoryObjectTranslator<Connection, APIConnection> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public APIConnection toExternalObject(Connection object)
|
public APIConnection toExternalObject(Connection object)
|
||||||
@@ -59,4 +60,14 @@ public class ConnectionObjectTranslator
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filterExternalObject(UserContext userContext,
|
||||||
|
APIConnection object) throws GuacamoleException {
|
||||||
|
|
||||||
|
// Filter object attributes by defined schema
|
||||||
|
object.setAttributes(filterAttributes(
|
||||||
|
userContext.getConnectionAttributes(), object.getAttributes()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -100,7 +100,7 @@ public class ConnectionResource extends DirectoryObjectResource<Connection, APIC
|
|||||||
@Assisted Directory<Connection> directory,
|
@Assisted Directory<Connection> directory,
|
||||||
@Assisted Connection connection,
|
@Assisted Connection connection,
|
||||||
DirectoryObjectTranslator<Connection, APIConnection> translator) {
|
DirectoryObjectTranslator<Connection, APIConnection> translator) {
|
||||||
super(directory, connection, translator);
|
super(userContext, directory, connection, translator);
|
||||||
this.userContext = userContext;
|
this.userContext = userContext;
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@ package org.apache.guacamole.rest.connectiongroup;
|
|||||||
|
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
import org.apache.guacamole.net.auth.ConnectionGroup;
|
import org.apache.guacamole.net.auth.ConnectionGroup;
|
||||||
|
import org.apache.guacamole.net.auth.UserContext;
|
||||||
import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
|
import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +29,7 @@ import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
|
|||||||
* APIConnectionGroup objects.
|
* APIConnectionGroup objects.
|
||||||
*/
|
*/
|
||||||
public class ConnectionGroupObjectTranslator
|
public class ConnectionGroupObjectTranslator
|
||||||
implements DirectoryObjectTranslator<ConnectionGroup, APIConnectionGroup> {
|
extends DirectoryObjectTranslator<ConnectionGroup, APIConnectionGroup> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public APIConnectionGroup toExternalObject(ConnectionGroup object)
|
public APIConnectionGroup toExternalObject(ConnectionGroup object)
|
||||||
@@ -53,4 +54,15 @@ public class ConnectionGroupObjectTranslator
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filterExternalObject(UserContext userContext,
|
||||||
|
APIConnectionGroup object) throws GuacamoleException {
|
||||||
|
|
||||||
|
// Filter object attributes by defined schema
|
||||||
|
object.setAttributes(filterAttributes(
|
||||||
|
userContext.getConnectionGroupAttributes(),
|
||||||
|
object.getAttributes()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -79,7 +79,7 @@ public class ConnectionGroupResource
|
|||||||
@Assisted Directory<ConnectionGroup> directory,
|
@Assisted Directory<ConnectionGroup> directory,
|
||||||
@Assisted ConnectionGroup connectionGroup,
|
@Assisted ConnectionGroup connectionGroup,
|
||||||
DirectoryObjectTranslator<ConnectionGroup, APIConnectionGroup> translator) {
|
DirectoryObjectTranslator<ConnectionGroup, APIConnectionGroup> translator) {
|
||||||
super(directory, connectionGroup, translator);
|
super(userContext, directory, connectionGroup, translator);
|
||||||
this.userContext = userContext;
|
this.userContext = userContext;
|
||||||
this.connectionGroup = connectionGroup;
|
this.connectionGroup = connectionGroup;
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,7 @@ import org.apache.guacamole.GuacamoleClientException;
|
|||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
import org.apache.guacamole.net.auth.Directory;
|
import org.apache.guacamole.net.auth.Directory;
|
||||||
import org.apache.guacamole.net.auth.Identifiable;
|
import org.apache.guacamole.net.auth.Identifiable;
|
||||||
|
import org.apache.guacamole.net.auth.UserContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A REST resource which abstracts the operations available on an existing
|
* A REST resource which abstracts the operations available on an existing
|
||||||
@@ -50,6 +51,12 @@ import org.apache.guacamole.net.auth.Identifiable;
|
|||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public abstract class DirectoryObjectResource<InternalType extends Identifiable, ExternalType> {
|
public abstract class DirectoryObjectResource<InternalType extends Identifiable, ExternalType> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The UserContext associated with the Directory containing the object
|
||||||
|
* represented by this DirectoryObjectResource.
|
||||||
|
*/
|
||||||
|
private final UserContext userContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Directory which contains the object represented by this
|
* The Directory which contains the object represented by this
|
||||||
* DirectoryObjectResource.
|
* DirectoryObjectResource.
|
||||||
@@ -71,6 +78,9 @@ public abstract class DirectoryObjectResource<InternalType extends Identifiable,
|
|||||||
* Creates a new DirectoryObjectResource which exposes the operations
|
* Creates a new DirectoryObjectResource which exposes the operations
|
||||||
* available for the given object.
|
* available for the given object.
|
||||||
*
|
*
|
||||||
|
* @param userContext
|
||||||
|
* The UserContext associated with the given Directory.
|
||||||
|
*
|
||||||
* @param directory
|
* @param directory
|
||||||
* The Directory which contains the given object.
|
* The Directory which contains the given object.
|
||||||
*
|
*
|
||||||
@@ -81,8 +91,10 @@ public abstract class DirectoryObjectResource<InternalType extends Identifiable,
|
|||||||
* A DirectoryObjectTranslator implementation which handles the type of
|
* A DirectoryObjectTranslator implementation which handles the type of
|
||||||
* object given.
|
* object given.
|
||||||
*/
|
*/
|
||||||
public DirectoryObjectResource(Directory<InternalType> directory, InternalType object,
|
public DirectoryObjectResource(UserContext userContext,
|
||||||
|
Directory<InternalType> directory, InternalType object,
|
||||||
DirectoryObjectTranslator<InternalType, ExternalType> translator) {
|
DirectoryObjectTranslator<InternalType, ExternalType> translator) {
|
||||||
|
this.userContext = userContext;
|
||||||
this.directory = directory;
|
this.directory = directory;
|
||||||
this.object = object;
|
this.object = object;
|
||||||
this.translator = translator;
|
this.translator = translator;
|
||||||
@@ -121,6 +133,9 @@ public abstract class DirectoryObjectResource<InternalType extends Identifiable,
|
|||||||
if (modifiedObject == null)
|
if (modifiedObject == null)
|
||||||
throw new GuacamoleClientException("Data must be submitted when updating objects.");
|
throw new GuacamoleClientException("Data must be submitted when updating objects.");
|
||||||
|
|
||||||
|
// Filter/sanitize object contents
|
||||||
|
translator.filterExternalObject(userContext, modifiedObject);
|
||||||
|
|
||||||
// Perform update
|
// Perform update
|
||||||
translator.applyExternalChanges(object, modifiedObject);
|
translator.applyExternalChanges(object, modifiedObject);
|
||||||
directory.update(object);
|
directory.update(object);
|
||||||
|
@@ -19,8 +19,14 @@
|
|||||||
|
|
||||||
package org.apache.guacamole.rest.directory;
|
package org.apache.guacamole.rest.directory;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.form.Field;
|
||||||
|
import org.apache.guacamole.form.Form;
|
||||||
import org.apache.guacamole.net.auth.Identifiable;
|
import org.apache.guacamole.net.auth.Identifiable;
|
||||||
|
import org.apache.guacamole.net.auth.UserContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides bidirectional conversion between REST-specific objects and the
|
* Provides bidirectional conversion between REST-specific objects and the
|
||||||
@@ -35,7 +41,7 @@ import org.apache.guacamole.net.auth.Identifiable;
|
|||||||
* deserialized as JSON) between REST clients and resource implementations
|
* deserialized as JSON) between REST clients and resource implementations
|
||||||
* when representing the InternalType.
|
* when representing the InternalType.
|
||||||
*/
|
*/
|
||||||
public interface DirectoryObjectTranslator<InternalType extends Identifiable, ExternalType> {
|
public abstract class DirectoryObjectTranslator<InternalType extends Identifiable, ExternalType> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the given object to an object which is intended to be used in
|
* Converts the given object to an object which is intended to be used in
|
||||||
@@ -51,7 +57,7 @@ public interface DirectoryObjectTranslator<InternalType extends Identifiable, Ex
|
|||||||
* @throws GuacamoleException
|
* @throws GuacamoleException
|
||||||
* If the provided object cannot be converted for any reason.
|
* If the provided object cannot be converted for any reason.
|
||||||
*/
|
*/
|
||||||
ExternalType toExternalObject(InternalType object)
|
public abstract ExternalType toExternalObject(InternalType object)
|
||||||
throws GuacamoleException;
|
throws GuacamoleException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,7 +75,7 @@ public interface DirectoryObjectTranslator<InternalType extends Identifiable, Ex
|
|||||||
* @throws GuacamoleException
|
* @throws GuacamoleException
|
||||||
* If the provided object cannot be converted for any reason.
|
* If the provided object cannot be converted for any reason.
|
||||||
*/
|
*/
|
||||||
InternalType toInternalObject(ExternalType object)
|
public abstract InternalType toInternalObject(ExternalType object)
|
||||||
throws GuacamoleException;
|
throws GuacamoleException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,7 +93,67 @@ public interface DirectoryObjectTranslator<InternalType extends Identifiable, Ex
|
|||||||
* @throws GuacamoleException
|
* @throws GuacamoleException
|
||||||
* If the provided modifications cannot be applied for any reason.
|
* If the provided modifications cannot be applied for any reason.
|
||||||
*/
|
*/
|
||||||
void applyExternalChanges(InternalType existingObject, ExternalType object)
|
public abstract void applyExternalChanges(InternalType existingObject,
|
||||||
throws GuacamoleException;
|
ExternalType object) throws GuacamoleException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies filtering to the contents of the given external object which
|
||||||
|
* came from an untrusted source. Implementations MUST sanitize the
|
||||||
|
* contents of the external object as necessary to guarantee that the
|
||||||
|
* object conforms to declared schema, such as the attributes declared for
|
||||||
|
* each object type at the UserContext level.
|
||||||
|
*
|
||||||
|
* @param userContext
|
||||||
|
* The UserContext associated with the object being filtered.
|
||||||
|
*
|
||||||
|
* @param object
|
||||||
|
* The object to modify such that it strictly conforms to the declared
|
||||||
|
* schema.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the object cannot be filtered due to an error.
|
||||||
|
*/
|
||||||
|
public abstract void filterExternalObject(UserContext userContext,
|
||||||
|
ExternalType object) throws GuacamoleException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the given map of attribute name/value pairs, producing a new
|
||||||
|
* map containing only attributes defined as fields within the given schema.
|
||||||
|
*
|
||||||
|
* @param schema
|
||||||
|
* The schema whose fields should be used to filter the given map of
|
||||||
|
* attributes.
|
||||||
|
*
|
||||||
|
* @param attributes
|
||||||
|
* The map of attribute name/value pairs to filter.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A new map containing only the attributes defined as fields within
|
||||||
|
* the given schema.
|
||||||
|
*/
|
||||||
|
public Map<String, String> filterAttributes(Collection<Form> schema,
|
||||||
|
Map<String, String> attributes) {
|
||||||
|
|
||||||
|
Map<String, String> filtered = new HashMap<String, String>();
|
||||||
|
|
||||||
|
// Grab all attribute value strictly for defined fields
|
||||||
|
for (Form form : schema) {
|
||||||
|
for (Field field : form.getFields()) {
|
||||||
|
|
||||||
|
// Pull the associated attribute value from given map
|
||||||
|
String attributeName = field.getName();
|
||||||
|
String attributeValue = attributes.get(attributeName);
|
||||||
|
|
||||||
|
// Include attribute value within filtered map only if
|
||||||
|
// (1) defined and (2) present within provided map
|
||||||
|
if (attributeValue != null)
|
||||||
|
filtered.put(attributeName, attributeValue);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -222,6 +222,9 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
|
|||||||
if (object == null)
|
if (object == null)
|
||||||
throw new GuacamoleClientException("Data must be submitted when creating objects.");
|
throw new GuacamoleClientException("Data must be submitted when creating objects.");
|
||||||
|
|
||||||
|
// Filter/sanitize object contents
|
||||||
|
translator.filterExternalObject(userContext, object);
|
||||||
|
|
||||||
// Create the new object within the directory
|
// Create the new object within the directory
|
||||||
directory.add(translator.toInternalObject(object));
|
directory.add(translator.toInternalObject(object));
|
||||||
|
|
||||||
|
@@ -21,6 +21,7 @@ package org.apache.guacamole.rest.sharingprofile;
|
|||||||
|
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
import org.apache.guacamole.net.auth.SharingProfile;
|
import org.apache.guacamole.net.auth.SharingProfile;
|
||||||
|
import org.apache.guacamole.net.auth.UserContext;
|
||||||
import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
|
import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +29,7 @@ import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
|
|||||||
* APISharingProfile objects.
|
* APISharingProfile objects.
|
||||||
*/
|
*/
|
||||||
public class SharingProfileObjectTranslator
|
public class SharingProfileObjectTranslator
|
||||||
implements DirectoryObjectTranslator<SharingProfile, APISharingProfile> {
|
extends DirectoryObjectTranslator<SharingProfile, APISharingProfile> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public APISharingProfile toExternalObject(SharingProfile object)
|
public APISharingProfile toExternalObject(SharingProfile object)
|
||||||
@@ -53,4 +54,15 @@ public class SharingProfileObjectTranslator
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filterExternalObject(UserContext userContext,
|
||||||
|
APISharingProfile object) throws GuacamoleException {
|
||||||
|
|
||||||
|
// Filter object attributes by defined schema
|
||||||
|
object.setAttributes(filterAttributes(
|
||||||
|
userContext.getSharingProfileAttributes(),
|
||||||
|
object.getAttributes()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -82,7 +82,7 @@ public class SharingProfileResource
|
|||||||
@Assisted Directory<SharingProfile> directory,
|
@Assisted Directory<SharingProfile> directory,
|
||||||
@Assisted SharingProfile sharingProfile,
|
@Assisted SharingProfile sharingProfile,
|
||||||
DirectoryObjectTranslator<SharingProfile, APISharingProfile> translator) {
|
DirectoryObjectTranslator<SharingProfile, APISharingProfile> translator) {
|
||||||
super(directory, sharingProfile, translator);
|
super(userContext, directory, sharingProfile, translator);
|
||||||
this.userContext = userContext;
|
this.userContext = userContext;
|
||||||
this.sharingProfile = sharingProfile;
|
this.sharingProfile = sharingProfile;
|
||||||
}
|
}
|
||||||
|
@@ -21,13 +21,14 @@ package org.apache.guacamole.rest.user;
|
|||||||
|
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
import org.apache.guacamole.net.auth.User;
|
import org.apache.guacamole.net.auth.User;
|
||||||
|
import org.apache.guacamole.net.auth.UserContext;
|
||||||
import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
|
import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translator which converts between User objects and APIUser objects.
|
* Translator which converts between User objects and APIUser objects.
|
||||||
*/
|
*/
|
||||||
public class UserObjectTranslator
|
public class UserObjectTranslator
|
||||||
implements DirectoryObjectTranslator<User, APIUser> {
|
extends DirectoryObjectTranslator<User, APIUser> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public APIUser toExternalObject(User object)
|
public APIUser toExternalObject(User object)
|
||||||
@@ -54,4 +55,14 @@ public class UserObjectTranslator
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filterExternalObject(UserContext userContext, APIUser object)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
// Filter object attributes by defined schema
|
||||||
|
object.setAttributes(filterAttributes(userContext.getUserAttributes(),
|
||||||
|
object.getAttributes()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -92,7 +92,7 @@ public class UserResource
|
|||||||
@Assisted Directory<User> directory,
|
@Assisted Directory<User> directory,
|
||||||
@Assisted User user,
|
@Assisted User user,
|
||||||
DirectoryObjectTranslator<User, APIUser> translator) {
|
DirectoryObjectTranslator<User, APIUser> translator) {
|
||||||
super(directory, user, translator);
|
super(userContext, directory, user, translator);
|
||||||
this.userContext = userContext;
|
this.userContext = userContext;
|
||||||
this.directory = directory;
|
this.directory = directory;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
|
Reference in New Issue
Block a user