mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
GUAC-1101: Include active connections in history. Insert history records into database when connections close.
This commit is contained in:
@@ -180,7 +180,7 @@ public class MySQLConnection implements Connection, DirectoryObject<ConnectionMo
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<? extends ConnectionRecord> getHistory() throws GuacamoleException {
|
public List<? extends ConnectionRecord> getHistory() throws GuacamoleException {
|
||||||
return connectionService.retrieveHistory(currentUser, this.getIdentifier());
|
return connectionService.retrieveHistory(currentUser, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -190,7 +190,7 @@ public class MySQLConnection implements Connection, DirectoryObject<ConnectionMo
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getActiveConnections() {
|
public int getActiveConnections() {
|
||||||
return socketService.getActiveConnections(this);
|
return socketService.getActiveConnections(this).size();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -24,18 +24,26 @@ package net.sourceforge.guacamole.net.auth.mysql.service;
|
|||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.Collections;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import net.sourceforge.guacamole.net.auth.mysql.AuthenticatedUser;
|
import net.sourceforge.guacamole.net.auth.mysql.AuthenticatedUser;
|
||||||
import net.sourceforge.guacamole.net.auth.mysql.MySQLConnection;
|
import net.sourceforge.guacamole.net.auth.mysql.MySQLConnection;
|
||||||
|
import net.sourceforge.guacamole.net.auth.mysql.dao.ConnectionRecordMapper;
|
||||||
import net.sourceforge.guacamole.net.auth.mysql.dao.ParameterMapper;
|
import net.sourceforge.guacamole.net.auth.mysql.dao.ParameterMapper;
|
||||||
import net.sourceforge.guacamole.net.auth.mysql.model.ConnectionModel;
|
import net.sourceforge.guacamole.net.auth.mysql.model.ConnectionModel;
|
||||||
|
import net.sourceforge.guacamole.net.auth.mysql.model.ConnectionRecordModel;
|
||||||
import net.sourceforge.guacamole.net.auth.mysql.model.ParameterModel;
|
import net.sourceforge.guacamole.net.auth.mysql.model.ParameterModel;
|
||||||
|
import net.sourceforge.guacamole.net.auth.mysql.model.UserModel;
|
||||||
import org.glyptodon.guacamole.GuacamoleException;
|
import org.glyptodon.guacamole.GuacamoleException;
|
||||||
import org.glyptodon.guacamole.environment.Environment;
|
import org.glyptodon.guacamole.environment.Environment;
|
||||||
import org.glyptodon.guacamole.net.GuacamoleSocket;
|
import org.glyptodon.guacamole.net.GuacamoleSocket;
|
||||||
import org.glyptodon.guacamole.net.InetGuacamoleSocket;
|
import org.glyptodon.guacamole.net.InetGuacamoleSocket;
|
||||||
import org.glyptodon.guacamole.net.auth.Connection;
|
import org.glyptodon.guacamole.net.auth.Connection;
|
||||||
|
import org.glyptodon.guacamole.net.auth.ConnectionRecord;
|
||||||
import org.glyptodon.guacamole.protocol.ConfiguredGuacamoleSocket;
|
import org.glyptodon.guacamole.protocol.ConfiguredGuacamoleSocket;
|
||||||
import org.glyptodon.guacamole.protocol.GuacamoleClientInformation;
|
import org.glyptodon.guacamole.protocol.GuacamoleClientInformation;
|
||||||
import org.glyptodon.guacamole.protocol.GuacamoleConfiguration;
|
import org.glyptodon.guacamole.protocol.GuacamoleConfiguration;
|
||||||
@@ -62,12 +70,18 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS
|
|||||||
@Inject
|
@Inject
|
||||||
private ParameterMapper parameterMapper;
|
private ParameterMapper parameterMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapper for accessing connection history.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private ConnectionRecordMapper connectionRecordMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current number of concurrent uses of the connection having a given
|
* The current number of concurrent uses of the connection having a given
|
||||||
* identifier.
|
* identifier.
|
||||||
*/
|
*/
|
||||||
private final ConcurrentHashMap<String, AtomicInteger> activeConnectionCount =
|
private final Map<String, LinkedList<ConnectionRecord>> activeConnections =
|
||||||
new ConcurrentHashMap<String, AtomicInteger>();
|
new HashMap<String, LinkedList<ConnectionRecord>>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Atomically increments the current usage count for the given connection.
|
* Atomically increments the current usage count for the given connection.
|
||||||
@@ -75,13 +89,22 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS
|
|||||||
* @param connection
|
* @param connection
|
||||||
* The connection which is being used.
|
* The connection which is being used.
|
||||||
*/
|
*/
|
||||||
private void incrementUsage(MySQLConnection connection) {
|
private void addActiveConnection(Connection connection, ConnectionRecord record) {
|
||||||
|
synchronized (activeConnections) {
|
||||||
|
|
||||||
// Increment or initialize usage count atomically
|
String identifier = connection.getIdentifier();
|
||||||
AtomicInteger count = activeConnectionCount.putIfAbsent(connection.getIdentifier(), new AtomicInteger(1));
|
|
||||||
if (count != null)
|
|
||||||
count.incrementAndGet();
|
|
||||||
|
|
||||||
|
// Get set of active connection records, creating if necessary
|
||||||
|
LinkedList<ConnectionRecord> connections = activeConnections.get(identifier);
|
||||||
|
if (connections == null) {
|
||||||
|
connections = new LinkedList<ConnectionRecord>();
|
||||||
|
activeConnections.put(identifier, connections);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add active connection
|
||||||
|
connections.addFirst(record);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,15 +116,23 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS
|
|||||||
* @param connection
|
* @param connection
|
||||||
* The connection which is no longer being used.
|
* The connection which is no longer being used.
|
||||||
*/
|
*/
|
||||||
private void decrementUsage(MySQLConnection connection) {
|
private void removeActiveConnection(Connection connection, ConnectionRecord record) {
|
||||||
|
synchronized (activeConnections) {
|
||||||
|
|
||||||
|
String identifier = connection.getIdentifier();
|
||||||
|
|
||||||
|
// Get set of active connection records
|
||||||
|
LinkedList<ConnectionRecord> connections = activeConnections.get(identifier);
|
||||||
|
assert(connections != null);
|
||||||
|
|
||||||
|
// Remove old record
|
||||||
|
connections.remove(record);
|
||||||
|
|
||||||
|
// If now empty, clean the tracking entry
|
||||||
|
if (connections.isEmpty())
|
||||||
|
activeConnections.remove(identifier);
|
||||||
|
|
||||||
// Decrement usage count, remove entry if it becomes zero
|
|
||||||
AtomicInteger count = activeConnectionCount.get(connection.getIdentifier());
|
|
||||||
if (count != null) {
|
|
||||||
count.decrementAndGet();
|
|
||||||
activeConnectionCount.remove(connection.getIdentifier(), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,36 +166,14 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS
|
|||||||
protected abstract void release(AuthenticatedUser user,
|
protected abstract void release(AuthenticatedUser user,
|
||||||
MySQLConnection connection);
|
MySQLConnection connection);
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a socket for the given user which connects to the given
|
|
||||||
* connection. The given client information will be passed to guacd when
|
|
||||||
* the connection is established. This function will apply any concurrent
|
|
||||||
* usage rules in effect, but will NOT test object- or system-level
|
|
||||||
* permissions.
|
|
||||||
*
|
|
||||||
* @param user
|
|
||||||
* The user for whom the connection is being established.
|
|
||||||
*
|
|
||||||
* @param connection
|
|
||||||
* The connection the user is connecting to.
|
|
||||||
*
|
|
||||||
* @param info
|
|
||||||
* Information describing the Guacamole client connecting to the given
|
|
||||||
* connection.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
* A new GuacamoleSocket which is configured and connected to the given
|
|
||||||
* connection.
|
|
||||||
*
|
|
||||||
* @throws GuacamoleException
|
|
||||||
* If the connection cannot be established due to concurrent usage
|
|
||||||
* rules.
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public GuacamoleSocket getGuacamoleSocket(final AuthenticatedUser user,
|
public GuacamoleSocket getGuacamoleSocket(final AuthenticatedUser user,
|
||||||
final MySQLConnection connection, GuacamoleClientInformation info)
|
final MySQLConnection connection, GuacamoleClientInformation info)
|
||||||
throws GuacamoleException {
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
// Create record for active connection
|
||||||
|
final ActiveConnectionRecord activeConnection = new ActiveConnectionRecord(user);
|
||||||
|
|
||||||
// Generate configuration from available data
|
// Generate configuration from available data
|
||||||
GuacamoleConfiguration config = new GuacamoleConfiguration();
|
GuacamoleConfiguration config = new GuacamoleConfiguration();
|
||||||
|
|
||||||
@@ -182,7 +191,7 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS
|
|||||||
|
|
||||||
// Atomically gain access to connection
|
// Atomically gain access to connection
|
||||||
acquire(user, connection);
|
acquire(user, connection);
|
||||||
incrementUsage(connection);
|
addActiveConnection(connection, activeConnection);
|
||||||
|
|
||||||
// Return newly-reserved connection
|
// Return newly-reserved connection
|
||||||
return new ConfiguredGuacamoleSocket(
|
return new ConfiguredGuacamoleSocket(
|
||||||
@@ -200,9 +209,22 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS
|
|||||||
super.close();
|
super.close();
|
||||||
|
|
||||||
// Release connection upon close
|
// Release connection upon close
|
||||||
decrementUsage(connection);
|
removeActiveConnection(connection, activeConnection);
|
||||||
release(user, connection);
|
release(user, connection);
|
||||||
|
|
||||||
|
UserModel userModel = user.getUser().getModel();
|
||||||
|
ConnectionRecordModel recordModel = new ConnectionRecordModel();
|
||||||
|
|
||||||
|
// Copy user information and timestamps into new record
|
||||||
|
recordModel.setUserID(userModel.getUserID());
|
||||||
|
recordModel.setUsername(userModel.getUsername());
|
||||||
|
recordModel.setConnectionIdentifier(connection.getIdentifier());
|
||||||
|
recordModel.setStartDate(activeConnection.getStartDate());
|
||||||
|
recordModel.setEndDate(new Date());
|
||||||
|
|
||||||
|
// Insert connection record
|
||||||
|
connectionRecordMapper.insert(recordModel);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -213,7 +235,7 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS
|
|||||||
catch (GuacamoleException e) {
|
catch (GuacamoleException e) {
|
||||||
|
|
||||||
// Atomically release access to connection
|
// Atomically release access to connection
|
||||||
decrementUsage(connection);
|
removeActiveConnection(connection, activeConnection);
|
||||||
release(user, connection);
|
release(user, connection);
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
@@ -223,16 +245,19 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getActiveConnections(Connection connection) {
|
public List<ConnectionRecord> getActiveConnections(Connection connection) {
|
||||||
|
synchronized (activeConnections) {
|
||||||
|
|
||||||
// If no such active connection, zero active users
|
String identifier = connection.getIdentifier();
|
||||||
AtomicInteger count = activeConnectionCount.get(connection.getIdentifier());
|
|
||||||
if (count == null)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
// Otherwise, return stored value
|
// Get set of active connection records
|
||||||
return count.intValue();
|
LinkedList<ConnectionRecord> connections = activeConnections.get(identifier);
|
||||||
|
if (connections != null)
|
||||||
|
return Collections.unmodifiableList(connections);
|
||||||
|
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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 net.sourceforge.guacamole.net.auth.mysql.service;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import net.sourceforge.guacamole.net.auth.mysql.AuthenticatedUser;
|
||||||
|
import org.glyptodon.guacamole.net.auth.ConnectionRecord;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A connection record implementation that describes an active connection. As
|
||||||
|
* the associated connection has not yet ended, getEndDate() will always return
|
||||||
|
* null, and isActive() will always return true. The associated start date will
|
||||||
|
* be the time of this objects creation.
|
||||||
|
*
|
||||||
|
* @author Michael Jumper
|
||||||
|
*/
|
||||||
|
public class ActiveConnectionRecord implements ConnectionRecord {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user that connected to the connection associated with this connection
|
||||||
|
* record.
|
||||||
|
*/
|
||||||
|
private final AuthenticatedUser user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
* The user that connected to the connection associated with this
|
||||||
|
* connection record.
|
||||||
|
*/
|
||||||
|
public ActiveConnectionRecord(AuthenticatedUser user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date getStartDate() {
|
||||||
|
return startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date getEndDate() {
|
||||||
|
|
||||||
|
// Active connections have not yet ended
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
return user.getUser().getIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isActive() {
|
||||||
|
|
||||||
|
// Active connections are active by definition
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -45,6 +45,7 @@ import org.glyptodon.guacamole.GuacamoleException;
|
|||||||
import org.glyptodon.guacamole.GuacamoleSecurityException;
|
import org.glyptodon.guacamole.GuacamoleSecurityException;
|
||||||
import org.glyptodon.guacamole.net.GuacamoleSocket;
|
import org.glyptodon.guacamole.net.GuacamoleSocket;
|
||||||
import org.glyptodon.guacamole.net.auth.Connection;
|
import org.glyptodon.guacamole.net.auth.Connection;
|
||||||
|
import org.glyptodon.guacamole.net.auth.ConnectionRecord;
|
||||||
import org.glyptodon.guacamole.net.auth.permission.ObjectPermission;
|
import org.glyptodon.guacamole.net.auth.permission.ObjectPermission;
|
||||||
import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet;
|
import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet;
|
||||||
import org.glyptodon.guacamole.net.auth.permission.SystemPermission;
|
import org.glyptodon.guacamole.net.auth.permission.SystemPermission;
|
||||||
@@ -274,29 +275,37 @@ public class ConnectionService extends DirectoryObjectService<MySQLConnection, C
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the connection history of the connection with the given
|
* Retrieves the connection history of the given connection, including any
|
||||||
* identifier.
|
* active connections.
|
||||||
*
|
*
|
||||||
* @param user
|
* @param user
|
||||||
* The user retrieving the connection history.
|
* The user retrieving the connection history.
|
||||||
*
|
*
|
||||||
* @param identifier
|
* @param connection
|
||||||
* The identifier of the connection whose history is being retrieved.
|
* The connection whose history is being retrieved.
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
* @throws GuacamoleException
|
* The connection history of the given connection, including any
|
||||||
|
* active connections.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If permission to read the connection history is denied.
|
||||||
*/
|
*/
|
||||||
public List<MySQLConnectionRecord> retrieveHistory(AuthenticatedUser user,
|
public List<ConnectionRecord> retrieveHistory(AuthenticatedUser user,
|
||||||
String identifier) throws GuacamoleException {
|
MySQLConnection connection) throws GuacamoleException {
|
||||||
|
|
||||||
|
String identifier = connection.getIdentifier();
|
||||||
|
|
||||||
// Retrieve history only if READ permission is granted
|
// Retrieve history only if READ permission is granted
|
||||||
if (hasObjectPermission(user, identifier, ObjectPermission.Type.READ)) {
|
if (hasObjectPermission(user, identifier, ObjectPermission.Type.READ)) {
|
||||||
|
|
||||||
// Retrieve history
|
// Retrieve history
|
||||||
List<ConnectionRecordModel> models = connectionRecordMapper.select(identifier);
|
List<ConnectionRecordModel> models = connectionRecordMapper.select(identifier);
|
||||||
|
|
||||||
// Convert model objects into standard records
|
// Get currently-active connections
|
||||||
List<MySQLConnectionRecord> records = new ArrayList<MySQLConnectionRecord>(models.size());
|
List<ConnectionRecord> records = new ArrayList<ConnectionRecord>(socketService.getActiveConnections(connection));
|
||||||
|
|
||||||
|
// Add past connections from model objects
|
||||||
for (ConnectionRecordModel model : models)
|
for (ConnectionRecordModel model : models)
|
||||||
records.add(new MySQLConnectionRecord(model));
|
records.add(new MySQLConnectionRecord(model));
|
||||||
|
|
||||||
|
@@ -22,11 +22,13 @@
|
|||||||
|
|
||||||
package net.sourceforge.guacamole.net.auth.mysql.service;
|
package net.sourceforge.guacamole.net.auth.mysql.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import net.sourceforge.guacamole.net.auth.mysql.AuthenticatedUser;
|
import net.sourceforge.guacamole.net.auth.mysql.AuthenticatedUser;
|
||||||
import net.sourceforge.guacamole.net.auth.mysql.MySQLConnection;
|
import net.sourceforge.guacamole.net.auth.mysql.MySQLConnection;
|
||||||
import org.glyptodon.guacamole.GuacamoleException;
|
import org.glyptodon.guacamole.GuacamoleException;
|
||||||
import org.glyptodon.guacamole.net.GuacamoleSocket;
|
import org.glyptodon.guacamole.net.GuacamoleSocket;
|
||||||
import org.glyptodon.guacamole.net.auth.Connection;
|
import org.glyptodon.guacamole.net.auth.Connection;
|
||||||
|
import org.glyptodon.guacamole.net.auth.ConnectionRecord;
|
||||||
import org.glyptodon.guacamole.protocol.GuacamoleClientInformation;
|
import org.glyptodon.guacamole.protocol.GuacamoleClientInformation;
|
||||||
|
|
||||||
|
|
||||||
@@ -68,14 +70,17 @@ public interface GuacamoleSocketService {
|
|||||||
throws GuacamoleException;
|
throws GuacamoleException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of active connections using the given connection.
|
* Returns a list containing connection records representing all currently-
|
||||||
|
* active connections using the given connection. These records will have
|
||||||
|
* usernames and start dates, but no end date.
|
||||||
*
|
*
|
||||||
* @param connection
|
* @param connection
|
||||||
* The connection to check.
|
* The connection to check.
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
* The number of active connections using the given connection.
|
* A list containing connection records representing all currently-
|
||||||
|
* active connections.
|
||||||
*/
|
*/
|
||||||
public int getActiveConnections(Connection connection);
|
public List<ConnectionRecord> getActiveConnections(Connection connection);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user