mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
GUACAMOLE-5: Implement joining of shared connections via temporary credentials.
This commit is contained in:
@@ -75,6 +75,13 @@ public class TrackedActiveConnection extends RestrictedObject implements ActiveC
|
|||||||
*/
|
*/
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The connection ID of the connection as determined by guacd, not to be
|
||||||
|
* confused with the connection identifier determined by the database. This
|
||||||
|
* is the ID that must be supplied to guacd if joining this connection.
|
||||||
|
*/
|
||||||
|
private String connectionID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The underlying GuacamoleTunnel.
|
* The underlying GuacamoleTunnel.
|
||||||
*/
|
*/
|
||||||
@@ -107,6 +114,7 @@ public class TrackedActiveConnection extends RestrictedObject implements ActiveC
|
|||||||
|
|
||||||
// Copy all non-sensitive data from given record
|
// Copy all non-sensitive data from given record
|
||||||
this.connection = activeConnectionRecord.getConnection();
|
this.connection = activeConnectionRecord.getConnection();
|
||||||
|
this.connectionID = activeConnectionRecord.getConnectionID();
|
||||||
this.sharingProfileIdentifier = activeConnectionRecord.getSharingProfileIdentifier();
|
this.sharingProfileIdentifier = activeConnectionRecord.getSharingProfileIdentifier();
|
||||||
this.identifier = activeConnectionRecord.getUUID().toString();
|
this.identifier = activeConnectionRecord.getUUID().toString();
|
||||||
this.startDate = activeConnectionRecord.getStartDate();
|
this.startDate = activeConnectionRecord.getStartDate();
|
||||||
@@ -142,6 +150,19 @@ public class TrackedActiveConnection extends RestrictedObject implements ActiveC
|
|||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the connection ID of the in-progress connection as determined by
|
||||||
|
* guacd, not to be confused with the connection identifier determined by
|
||||||
|
* the database. This is the ID that must be supplied to guacd if joining
|
||||||
|
* this connection.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The ID of the in-progress connection, as determined by guacd.
|
||||||
|
*/
|
||||||
|
public String getConnectionID() {
|
||||||
|
return connectionID;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getConnectionIdentifier() {
|
public String getConnectionIdentifier() {
|
||||||
return connection.getIdentifier();
|
return connection.getIdentifier();
|
||||||
|
@@ -19,16 +19,17 @@
|
|||||||
|
|
||||||
package org.apache.guacamole.auth.jdbc.sharing;
|
package org.apache.guacamole.auth.jdbc.sharing;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
import org.apache.guacamole.GuacamoleUnsupportedException;
|
|
||||||
import org.apache.guacamole.auth.jdbc.activeconnection.TrackedActiveConnection;
|
import org.apache.guacamole.auth.jdbc.activeconnection.TrackedActiveConnection;
|
||||||
import org.apache.guacamole.auth.jdbc.connectiongroup.RootConnectionGroup;
|
import org.apache.guacamole.auth.jdbc.connectiongroup.RootConnectionGroup;
|
||||||
import org.apache.guacamole.auth.jdbc.sharingprofile.ModeledSharingProfile;
|
import org.apache.guacamole.auth.jdbc.sharingprofile.ModeledSharingProfile;
|
||||||
|
import org.apache.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService;
|
||||||
import org.apache.guacamole.net.GuacamoleTunnel;
|
import org.apache.guacamole.net.GuacamoleTunnel;
|
||||||
import org.apache.guacamole.net.auth.Connection;
|
import org.apache.guacamole.net.auth.Connection;
|
||||||
import org.apache.guacamole.net.auth.ConnectionRecord;
|
import org.apache.guacamole.net.auth.ConnectionRecord;
|
||||||
@@ -43,6 +44,12 @@ import org.apache.guacamole.protocol.GuacamoleConfiguration;
|
|||||||
*/
|
*/
|
||||||
public class SharedConnection implements Connection {
|
public class SharedConnection implements Connection {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for establishing tunnels to Guacamole connections.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private GuacamoleTunnelService tunnelService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Randomly-generated unique identifier, guaranteeing this shared connection
|
* Randomly-generated unique identifier, guaranteeing this shared connection
|
||||||
* does not duplicate the identifying information of the underlying
|
* does not duplicate the identifying information of the underlying
|
||||||
@@ -130,8 +137,8 @@ public class SharedConnection implements Connection {
|
|||||||
@Override
|
@Override
|
||||||
public GuacamoleTunnel connect(GuacamoleClientInformation info)
|
public GuacamoleTunnel connect(GuacamoleClientInformation info)
|
||||||
throws GuacamoleException {
|
throws GuacamoleException {
|
||||||
// STUB
|
return tunnelService.getGuacamoleTunnel(user, activeConnection,
|
||||||
throw new GuacamoleUnsupportedException("Connecting to shared connections is not yet implemented.");
|
sharingProfile, info);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -38,10 +38,11 @@ import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordMapper;
|
|||||||
import org.apache.guacamole.auth.jdbc.connection.ConnectionModel;
|
import org.apache.guacamole.auth.jdbc.connection.ConnectionModel;
|
||||||
import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel;
|
import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel;
|
||||||
import org.apache.guacamole.auth.jdbc.connection.ConnectionParameterModel;
|
import org.apache.guacamole.auth.jdbc.connection.ConnectionParameterModel;
|
||||||
import org.apache.guacamole.auth.jdbc.user.UserModel;
|
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.GuacamoleResourceNotFoundException;
|
||||||
import org.apache.guacamole.GuacamoleSecurityException;
|
import org.apache.guacamole.GuacamoleSecurityException;
|
||||||
import org.apache.guacamole.auth.jdbc.JDBCEnvironment;
|
import org.apache.guacamole.auth.jdbc.JDBCEnvironment;
|
||||||
|
import org.apache.guacamole.auth.jdbc.activeconnection.TrackedActiveConnection;
|
||||||
import org.apache.guacamole.auth.jdbc.connection.ConnectionMapper;
|
import org.apache.guacamole.auth.jdbc.connection.ConnectionMapper;
|
||||||
import org.apache.guacamole.environment.Environment;
|
import org.apache.guacamole.environment.Environment;
|
||||||
import org.apache.guacamole.net.GuacamoleSocket;
|
import org.apache.guacamole.net.GuacamoleSocket;
|
||||||
@@ -55,6 +56,11 @@ import org.apache.guacamole.token.StandardTokens;
|
|||||||
import org.apache.guacamole.token.TokenFilter;
|
import org.apache.guacamole.token.TokenFilter;
|
||||||
import org.mybatis.guice.transactional.Transactional;
|
import org.mybatis.guice.transactional.Transactional;
|
||||||
import org.apache.guacamole.auth.jdbc.connection.ConnectionParameterMapper;
|
import org.apache.guacamole.auth.jdbc.connection.ConnectionParameterMapper;
|
||||||
|
import org.apache.guacamole.auth.jdbc.sharing.SharedConnectionUser;
|
||||||
|
import org.apache.guacamole.auth.jdbc.sharingprofile.ModeledSharingProfile;
|
||||||
|
import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileParameterMapper;
|
||||||
|
import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileParameterModel;
|
||||||
|
import org.apache.guacamole.auth.jdbc.user.RemoteAuthenticatedUser;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,7 +94,13 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
|
|||||||
* Mapper for accessing connection parameters.
|
* Mapper for accessing connection parameters.
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
private ConnectionParameterMapper parameterMapper;
|
private ConnectionParameterMapper connectionParameterMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapper for accessing sharing profile parameters.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private SharingProfileParameterMapper sharingProfileParameterMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapper for accessing connection history.
|
* Mapper for accessing connection history.
|
||||||
@@ -141,7 +153,7 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
|
|||||||
* @throws GuacamoleException
|
* @throws GuacamoleException
|
||||||
* If access is denied to the given user for any reason.
|
* If access is denied to the given user for any reason.
|
||||||
*/
|
*/
|
||||||
protected abstract ModeledConnection acquire(AuthenticatedUser user,
|
protected abstract ModeledConnection acquire(RemoteAuthenticatedUser user,
|
||||||
List<ModeledConnection> connections) throws GuacamoleException;
|
List<ModeledConnection> connections) throws GuacamoleException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -155,7 +167,7 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
|
|||||||
* @param connection
|
* @param connection
|
||||||
* The connection being released.
|
* The connection being released.
|
||||||
*/
|
*/
|
||||||
protected abstract void release(AuthenticatedUser user,
|
protected abstract void release(RemoteAuthenticatedUser user,
|
||||||
ModeledConnection connection);
|
ModeledConnection connection);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,7 +184,7 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
|
|||||||
* @throws GuacamoleException
|
* @throws GuacamoleException
|
||||||
* If access is denied to the given user for any reason.
|
* If access is denied to the given user for any reason.
|
||||||
*/
|
*/
|
||||||
protected abstract void acquire(AuthenticatedUser user,
|
protected abstract void acquire(RemoteAuthenticatedUser user,
|
||||||
ModeledConnectionGroup connectionGroup) throws GuacamoleException;
|
ModeledConnectionGroup connectionGroup) throws GuacamoleException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,7 +198,7 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
|
|||||||
* @param connectionGroup
|
* @param connectionGroup
|
||||||
* The connection group being released.
|
* The connection group being released.
|
||||||
*/
|
*/
|
||||||
protected abstract void release(AuthenticatedUser user,
|
protected abstract void release(RemoteAuthenticatedUser user,
|
||||||
ModeledConnectionGroup connectionGroup);
|
ModeledConnectionGroup connectionGroup);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,7 +218,7 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
|
|||||||
* A GuacamoleConfiguration containing the protocol and parameters from
|
* A GuacamoleConfiguration containing the protocol and parameters from
|
||||||
* the given connection.
|
* the given connection.
|
||||||
*/
|
*/
|
||||||
private GuacamoleConfiguration getGuacamoleConfiguration(AuthenticatedUser user,
|
private GuacamoleConfiguration getGuacamoleConfiguration(RemoteAuthenticatedUser user,
|
||||||
ModeledConnection connection) {
|
ModeledConnection connection) {
|
||||||
|
|
||||||
// Generate configuration from available data
|
// Generate configuration from available data
|
||||||
@@ -217,7 +229,7 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
|
|||||||
config.setProtocol(model.getProtocol());
|
config.setProtocol(model.getProtocol());
|
||||||
|
|
||||||
// Set parameters from associated data
|
// Set parameters from associated data
|
||||||
Collection<ConnectionParameterModel> parameters = parameterMapper.select(connection.getIdentifier());
|
Collection<ConnectionParameterModel> parameters = connectionParameterMapper.select(connection.getIdentifier());
|
||||||
for (ConnectionParameterModel parameter : parameters)
|
for (ConnectionParameterModel parameter : parameters)
|
||||||
config.setParameter(parameter.getName(), parameter.getValue());
|
config.setParameter(parameter.getName(), parameter.getValue());
|
||||||
|
|
||||||
@@ -232,6 +244,52 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a guacamole configuration which joins the active connection
|
||||||
|
* having the given ID, using the provided sharing profile to restrict the
|
||||||
|
* access provided to the user accessing the shared connection. If tokens
|
||||||
|
* are used in the connection parameter values of the sharing profile,
|
||||||
|
* credentials from the given user will be substituted appropriately.
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
* The user whose credentials should be used if necessary.
|
||||||
|
*
|
||||||
|
* @param sharingProfile
|
||||||
|
* The sharing profile whose associated parameters dictate the level
|
||||||
|
* of access granted to the user joining the connection.
|
||||||
|
*
|
||||||
|
* @param connectionID
|
||||||
|
* The ID of the connection being joined, as provided by guacd when the
|
||||||
|
* original connection was established, or null if a new connection
|
||||||
|
* should be created instead.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A GuacamoleConfiguration containing the protocol and parameters from
|
||||||
|
* the given connection.
|
||||||
|
*/
|
||||||
|
private GuacamoleConfiguration getGuacamoleConfiguration(RemoteAuthenticatedUser user,
|
||||||
|
ModeledSharingProfile sharingProfile, String connectionID) {
|
||||||
|
|
||||||
|
// Generate configuration from available data
|
||||||
|
GuacamoleConfiguration config = new GuacamoleConfiguration();
|
||||||
|
config.setConnectionID(connectionID);
|
||||||
|
|
||||||
|
// Set parameters from associated data
|
||||||
|
Collection<SharingProfileParameterModel> parameters = sharingProfileParameterMapper.select(sharingProfile.getIdentifier());
|
||||||
|
for (SharingProfileParameterModel parameter : parameters)
|
||||||
|
config.setParameter(parameter.getName(), parameter.getValue());
|
||||||
|
|
||||||
|
// Build token filter containing credential tokens
|
||||||
|
TokenFilter tokenFilter = new TokenFilter();
|
||||||
|
StandardTokens.addStandardTokens(tokenFilter, user.getCredentials());
|
||||||
|
|
||||||
|
// Filter the configuration
|
||||||
|
tokenFilter.filterValues(config.getParameters());
|
||||||
|
|
||||||
|
return config;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the given ActiveConnectionRecord to the database. The end date of
|
* Saves the given ActiveConnectionRecord to the database. The end date of
|
||||||
* the saved record will be populated with the current time.
|
* the saved record will be populated with the current time.
|
||||||
@@ -241,17 +299,15 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
|
|||||||
*/
|
*/
|
||||||
private void saveConnectionRecord(ActiveConnectionRecord record) {
|
private void saveConnectionRecord(ActiveConnectionRecord record) {
|
||||||
|
|
||||||
// Get associated connection
|
|
||||||
ModeledConnection connection = record.getConnection();
|
|
||||||
|
|
||||||
// Get associated models
|
// Get associated models
|
||||||
AuthenticatedUser user = record.getUser();
|
|
||||||
ConnectionRecordModel recordModel = new ConnectionRecordModel();
|
ConnectionRecordModel recordModel = new ConnectionRecordModel();
|
||||||
|
|
||||||
// Copy user information and timestamps into new record
|
// Copy user information and timestamps into new record
|
||||||
recordModel.setUsername(user.getIdentifier());
|
recordModel.setUsername(record.getUsername());
|
||||||
recordModel.setConnectionIdentifier(connection.getIdentifier());
|
recordModel.setConnectionIdentifier(record.getConnectionIdentifier());
|
||||||
recordModel.setConnectionName(connection.getName());
|
recordModel.setConnectionName(record.getConnectionName());
|
||||||
|
recordModel.setSharingProfileIdentifier(record.getSharingProfileIdentifier());
|
||||||
|
recordModel.setSharingProfileName(record.getSharingProfileName());
|
||||||
recordModel.setStartDate(record.getStartDate());
|
recordModel.setStartDate(record.getStartDate());
|
||||||
recordModel.setEndDate(new Date());
|
recordModel.setEndDate(new Date());
|
||||||
|
|
||||||
@@ -329,19 +385,26 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
|
|||||||
if (!hasRun.compareAndSet(false, true))
|
if (!hasRun.compareAndSet(false, true))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Get original user and connection
|
// Remove underlying tunnel from list of active tunnels
|
||||||
AuthenticatedUser user = activeConnection.getUser();
|
|
||||||
ModeledConnection connection = activeConnection.getConnection();
|
|
||||||
|
|
||||||
// Get associated identifiers
|
|
||||||
String identifier = connection.getIdentifier();
|
|
||||||
String parentIdentifier = connection.getParentIdentifier();
|
|
||||||
|
|
||||||
// Release connection
|
|
||||||
activeTunnels.remove(activeConnection.getUUID().toString());
|
activeTunnels.remove(activeConnection.getUUID().toString());
|
||||||
activeConnections.remove(identifier, activeConnection);
|
|
||||||
activeConnectionGroups.remove(parentIdentifier, activeConnection);
|
// Get original user
|
||||||
release(user, connection);
|
RemoteAuthenticatedUser user = activeConnection.getUser();
|
||||||
|
|
||||||
|
// Release the associated connection if this is the primary connection
|
||||||
|
if (activeConnection.isPrimaryConnection()) {
|
||||||
|
|
||||||
|
// Get connection and associated identifiers
|
||||||
|
ModeledConnection connection = activeConnection.getConnection();
|
||||||
|
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
|
// Release any associated group
|
||||||
if (activeConnection.hasBalancingGroup())
|
if (activeConnection.hasBalancingGroup())
|
||||||
@@ -379,25 +442,44 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
|
|||||||
* while connection configuration information is being retrieved.
|
* while connection configuration information is being retrieved.
|
||||||
*/
|
*/
|
||||||
private GuacamoleTunnel assignGuacamoleTunnel(ActiveConnectionRecord activeConnection,
|
private GuacamoleTunnel assignGuacamoleTunnel(ActiveConnectionRecord activeConnection,
|
||||||
GuacamoleClientInformation info)
|
GuacamoleClientInformation info) throws GuacamoleException {
|
||||||
throws GuacamoleException {
|
|
||||||
|
|
||||||
ModeledConnection connection = activeConnection.getConnection();
|
|
||||||
|
|
||||||
// Record new active connection
|
// Record new active connection
|
||||||
Runnable cleanupTask = new ConnectionCleanupTask(activeConnection);
|
Runnable cleanupTask = new ConnectionCleanupTask(activeConnection);
|
||||||
activeTunnels.put(activeConnection.getUUID().toString(), activeConnection);
|
activeTunnels.put(activeConnection.getUUID().toString(), activeConnection);
|
||||||
activeConnections.put(connection.getIdentifier(), activeConnection);
|
|
||||||
activeConnectionGroups.put(connection.getParentIdentifier(), activeConnection);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
GuacamoleConfiguration config;
|
||||||
|
|
||||||
|
// Pull configuration directly from the connection if we are not
|
||||||
|
// joining an active connection
|
||||||
|
if (activeConnection.isPrimaryConnection()) {
|
||||||
|
ModeledConnection connection = activeConnection.getConnection();
|
||||||
|
activeConnections.put(connection.getIdentifier(), activeConnection);
|
||||||
|
activeConnectionGroups.put(connection.getParentIdentifier(), activeConnection);
|
||||||
|
config = getGuacamoleConfiguration(activeConnection.getUser(), connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we ARE joining an active connection, generate a configuration
|
||||||
|
// which does so
|
||||||
|
else {
|
||||||
|
|
||||||
|
// Verify that the connection ID is known
|
||||||
|
String connectionID = activeConnection.getConnectionID();
|
||||||
|
if (connectionID == null)
|
||||||
|
throw new GuacamoleResourceNotFoundException("No existing connection to be joined.");
|
||||||
|
|
||||||
|
// Build configuration from the sharing profile and the ID of
|
||||||
|
// the connection being joined
|
||||||
|
config = getGuacamoleConfiguration(activeConnection.getUser(),
|
||||||
|
activeConnection.getSharingProfile(), connectionID);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Obtain socket which will automatically run the cleanup task
|
// Obtain socket which will automatically run the cleanup task
|
||||||
GuacamoleSocket socket = new ConfiguredGuacamoleSocket(
|
ConfiguredGuacamoleSocket socket = new ConfiguredGuacamoleSocket(
|
||||||
getUnconfiguredGuacamoleSocket(cleanupTask),
|
getUnconfiguredGuacamoleSocket(cleanupTask), config, info);
|
||||||
getGuacamoleConfiguration(activeConnection.getUser(), connection),
|
|
||||||
info
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assign and return new tunnel
|
// Assign and return new tunnel
|
||||||
return activeConnection.assignGuacamoleTunnel(socket);
|
return activeConnection.assignGuacamoleTunnel(socket);
|
||||||
@@ -596,4 +678,17 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public GuacamoleTunnel getGuacamoleTunnel(SharedConnectionUser user,
|
||||||
|
TrackedActiveConnection activeConnection,
|
||||||
|
ModeledSharingProfile sharingProfile,
|
||||||
|
GuacamoleClientInformation info)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
// Connect to shared connection
|
||||||
|
return assignGuacamoleTunnel(new ActiveConnectionRecord(user, activeConnection, sharingProfile), info);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -21,13 +21,16 @@ package org.apache.guacamole.auth.jdbc.tunnel;
|
|||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import org.apache.guacamole.auth.jdbc.activeconnection.TrackedActiveConnection;
|
||||||
import org.apache.guacamole.auth.jdbc.connection.ModeledConnection;
|
import org.apache.guacamole.auth.jdbc.connection.ModeledConnection;
|
||||||
import org.apache.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup;
|
import org.apache.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup;
|
||||||
import org.apache.guacamole.auth.jdbc.user.AuthenticatedUser;
|
import org.apache.guacamole.auth.jdbc.sharingprofile.ModeledSharingProfile;
|
||||||
|
import org.apache.guacamole.auth.jdbc.user.RemoteAuthenticatedUser;
|
||||||
import org.apache.guacamole.net.AbstractGuacamoleTunnel;
|
import org.apache.guacamole.net.AbstractGuacamoleTunnel;
|
||||||
import org.apache.guacamole.net.GuacamoleSocket;
|
import org.apache.guacamole.net.GuacamoleSocket;
|
||||||
import org.apache.guacamole.net.GuacamoleTunnel;
|
import org.apache.guacamole.net.GuacamoleTunnel;
|
||||||
import org.apache.guacamole.net.auth.ConnectionRecord;
|
import org.apache.guacamole.net.auth.ConnectionRecord;
|
||||||
|
import org.apache.guacamole.protocol.ConfiguredGuacamoleSocket;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +47,7 @@ public class ActiveConnectionRecord implements ConnectionRecord {
|
|||||||
* The user that connected to the connection associated with this connection
|
* The user that connected to the connection associated with this connection
|
||||||
* record.
|
* record.
|
||||||
*/
|
*/
|
||||||
private final AuthenticatedUser user;
|
private final RemoteAuthenticatedUser user;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The balancing group from which the associated connection was chosen, if
|
* The balancing group from which the associated connection was chosen, if
|
||||||
@@ -57,6 +60,13 @@ public class ActiveConnectionRecord implements ConnectionRecord {
|
|||||||
*/
|
*/
|
||||||
private final ModeledConnection connection;
|
private final ModeledConnection connection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sharing profile that was used to access the connection associated
|
||||||
|
* with this connection record. If the connection was accessed directly
|
||||||
|
* (without involving a sharing profile), this will be null.
|
||||||
|
*/
|
||||||
|
private final ModeledSharingProfile sharingProfile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The time this connection record was created.
|
* The time this connection record was created.
|
||||||
*/
|
*/
|
||||||
@@ -66,13 +76,54 @@ public class ActiveConnectionRecord implements ConnectionRecord {
|
|||||||
* The UUID that will be assigned to the underlying tunnel.
|
* The UUID that will be assigned to the underlying tunnel.
|
||||||
*/
|
*/
|
||||||
private final UUID uuid = UUID.randomUUID();
|
private final UUID uuid = UUID.randomUUID();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The connection ID of the connection as determined by guacd, not to be
|
||||||
|
* confused with the connection identifier determined by the database. This
|
||||||
|
* is the ID that must be supplied to guacd if joining this connection.
|
||||||
|
*/
|
||||||
|
private String connectionID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The GuacamoleTunnel used by the connection associated with this
|
* The GuacamoleTunnel used by the connection associated with this
|
||||||
* connection record.
|
* connection record.
|
||||||
*/
|
*/
|
||||||
private GuacamoleTunnel tunnel;
|
private GuacamoleTunnel tunnel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new connection record associated with the given user,
|
||||||
|
* connection, balancing connection group, and sharing profile. The given
|
||||||
|
* balancing connection group MUST be the connection group from which the
|
||||||
|
* given connection was chosen, and the given sharing profile MUST be the
|
||||||
|
* sharing profile that was used to share access to the given 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 balancingGroup
|
||||||
|
* The balancing group from which the given connection was chosen, or
|
||||||
|
* null if no balancing group is being used.
|
||||||
|
*
|
||||||
|
* @param connection
|
||||||
|
* The connection to associate with this connection record.
|
||||||
|
*
|
||||||
|
* @param sharingProfile
|
||||||
|
* The sharing profile that was used to share access to the given
|
||||||
|
* connection, or null if no sharing profile was used.
|
||||||
|
*/
|
||||||
|
private ActiveConnectionRecord(RemoteAuthenticatedUser user,
|
||||||
|
ModeledConnectionGroup balancingGroup,
|
||||||
|
ModeledConnection connection,
|
||||||
|
ModeledSharingProfile sharingProfile) {
|
||||||
|
this.user = user;
|
||||||
|
this.balancingGroup = balancingGroup;
|
||||||
|
this.connection = connection;
|
||||||
|
this.sharingProfile = sharingProfile;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new connection record associated with the given user,
|
* Creates a new connection record associated with the given user,
|
||||||
* connection, and balancing connection group. The given balancing
|
* connection, and balancing connection group. The given balancing
|
||||||
@@ -90,12 +141,10 @@ public class ActiveConnectionRecord implements ConnectionRecord {
|
|||||||
* @param connection
|
* @param connection
|
||||||
* The connection to associate with this connection record.
|
* The connection to associate with this connection record.
|
||||||
*/
|
*/
|
||||||
public ActiveConnectionRecord(AuthenticatedUser user,
|
public ActiveConnectionRecord(RemoteAuthenticatedUser user,
|
||||||
ModeledConnectionGroup balancingGroup,
|
ModeledConnectionGroup balancingGroup,
|
||||||
ModeledConnection connection) {
|
ModeledConnection connection) {
|
||||||
this.user = user;
|
this(user, balancingGroup, connection, null);
|
||||||
this.balancingGroup = balancingGroup;
|
|
||||||
this.connection = connection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,11 +159,38 @@ public class ActiveConnectionRecord implements ConnectionRecord {
|
|||||||
* @param connection
|
* @param connection
|
||||||
* The connection to associate with this connection record.
|
* The connection to associate with this connection record.
|
||||||
*/
|
*/
|
||||||
public ActiveConnectionRecord(AuthenticatedUser user,
|
public ActiveConnectionRecord(RemoteAuthenticatedUser user,
|
||||||
ModeledConnection connection) {
|
ModeledConnection connection) {
|
||||||
this(user, null, connection);
|
this(user, null, connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new connection record associated with the given user, active
|
||||||
|
* connection, and sharing profile. The given sharing profile MUST be the
|
||||||
|
* sharing profile that was used to share access to the given 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 activeConnection
|
||||||
|
* The active connection which is being shared to the given user via
|
||||||
|
* the given sharing profile.
|
||||||
|
*
|
||||||
|
* @param sharingProfile
|
||||||
|
* The sharing profile that was used to share access to the given
|
||||||
|
* connection. As a record created in this way always refers to a
|
||||||
|
* shared connection, this value may NOT be null.
|
||||||
|
*/
|
||||||
|
public ActiveConnectionRecord(RemoteAuthenticatedUser user,
|
||||||
|
TrackedActiveConnection activeConnection,
|
||||||
|
ModeledSharingProfile sharingProfile) {
|
||||||
|
this(user, null, activeConnection.getConnection(), sharingProfile);
|
||||||
|
this.connectionID = activeConnection.getConnectionID();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the user that connected to the connection associated with this
|
* Returns the user that connected to the connection associated with this
|
||||||
* connection record.
|
* connection record.
|
||||||
@@ -123,7 +199,7 @@ public class ActiveConnectionRecord implements ConnectionRecord {
|
|||||||
* The user that connected to the connection associated with this
|
* The user that connected to the connection associated with this
|
||||||
* connection record.
|
* connection record.
|
||||||
*/
|
*/
|
||||||
public AuthenticatedUser getUser() {
|
public RemoteAuthenticatedUser getUser() {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +225,20 @@ public class ActiveConnectionRecord implements ConnectionRecord {
|
|||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the sharing profile that was used to access the connection
|
||||||
|
* associated with this connection record. If the connection was accessed
|
||||||
|
* directly (without involving a sharing profile), this will be null.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The sharing profile that was used to access the connection
|
||||||
|
* associated with this connection record, or null if the connection
|
||||||
|
* was accessed directly.
|
||||||
|
*/
|
||||||
|
public ModeledSharingProfile getSharingProfile() {
|
||||||
|
return sharingProfile;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the connection associated with this connection record
|
* Returns whether the connection associated with this connection record
|
||||||
* was chosen from a balancing group.
|
* was chosen from a balancing group.
|
||||||
@@ -161,6 +251,21 @@ public class ActiveConnectionRecord implements ConnectionRecord {
|
|||||||
return balancingGroup != null;
|
return balancingGroup != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether this connection record is associated with a connection
|
||||||
|
* being used directly, in the absence of a sharing profile. If a connection
|
||||||
|
* is shared, this will continue to return false for the connection being
|
||||||
|
* shared, but will return true for the connections which join that
|
||||||
|
* connection.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* true if the connection associated with this connection record is
|
||||||
|
* being used directly, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isPrimaryConnection() {
|
||||||
|
return sharingProfile == null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getConnectionIdentifier() {
|
public String getConnectionIdentifier() {
|
||||||
return connection.getIdentifier();
|
return connection.getIdentifier();
|
||||||
@@ -173,12 +278,26 @@ public class ActiveConnectionRecord implements ConnectionRecord {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getSharingProfileIdentifier() {
|
public String getSharingProfileIdentifier() {
|
||||||
|
|
||||||
|
// Return sharing profile identifier if known
|
||||||
|
if (sharingProfile != null)
|
||||||
|
return sharingProfile.getIdentifier();
|
||||||
|
|
||||||
|
// No associated sharing profile
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getSharingProfileName() {
|
public String getSharingProfileName() {
|
||||||
|
|
||||||
|
// Return sharing profile name if known
|
||||||
|
if (sharingProfile != null)
|
||||||
|
return sharingProfile.getName();
|
||||||
|
|
||||||
|
// No associated sharing profile
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -201,7 +320,7 @@ public class ActiveConnectionRecord implements ConnectionRecord {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUsername() {
|
public String getUsername() {
|
||||||
return user.getUser().getIdentifier();
|
return user.getIdentifier();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -229,13 +348,13 @@ public class ActiveConnectionRecord implements ConnectionRecord {
|
|||||||
* given socket.
|
* given socket.
|
||||||
*
|
*
|
||||||
* @param socket
|
* @param socket
|
||||||
* The GuacamoleSocket to use to create the tunnel associated with this
|
* The ConfiguredGuacamoleSocket to use to create the tunnel associated
|
||||||
* connection record.
|
* with this connection record.
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
* The newly-created tunnel associated with this connection record.
|
* The newly-created tunnel associated with this connection record.
|
||||||
*/
|
*/
|
||||||
public GuacamoleTunnel assignGuacamoleTunnel(final GuacamoleSocket socket) {
|
public GuacamoleTunnel assignGuacamoleTunnel(final ConfiguredGuacamoleSocket socket) {
|
||||||
|
|
||||||
// Create tunnel with given socket
|
// Create tunnel with given socket
|
||||||
this.tunnel = new AbstractGuacamoleTunnel() {
|
this.tunnel = new AbstractGuacamoleTunnel() {
|
||||||
@@ -252,6 +371,10 @@ public class ActiveConnectionRecord implements ConnectionRecord {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Store connection ID of the primary connection only
|
||||||
|
if (isPrimaryConnection())
|
||||||
|
this.connectionID = socket.getConnectionID();
|
||||||
|
|
||||||
// Return newly-created tunnel
|
// Return newly-created tunnel
|
||||||
return this.tunnel;
|
return this.tunnel;
|
||||||
|
|
||||||
@@ -268,5 +391,20 @@ public class ActiveConnectionRecord implements ConnectionRecord {
|
|||||||
public UUID getUUID() {
|
public UUID getUUID() {
|
||||||
return uuid;
|
return uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the connection ID of the in-progress connection as determined by
|
||||||
|
* guacd, not to be confused with the connection identifier determined by
|
||||||
|
* the database. This is the ID that must be supplied to guacd if joining
|
||||||
|
* this connection. If the in-progress connection is joining another
|
||||||
|
* connection, this will be the ID of the connection being joined, NOT the
|
||||||
|
* ID of the connection directly represented by this record.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The ID of the in-progress connection, as determined by guacd.
|
||||||
|
*/
|
||||||
|
public String getConnectionID() {
|
||||||
|
return connectionID;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -24,6 +24,9 @@ import org.apache.guacamole.auth.jdbc.user.AuthenticatedUser;
|
|||||||
import org.apache.guacamole.auth.jdbc.connection.ModeledConnection;
|
import org.apache.guacamole.auth.jdbc.connection.ModeledConnection;
|
||||||
import org.apache.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup;
|
import org.apache.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.auth.jdbc.activeconnection.TrackedActiveConnection;
|
||||||
|
import org.apache.guacamole.auth.jdbc.sharing.SharedConnectionUser;
|
||||||
|
import org.apache.guacamole.auth.jdbc.sharingprofile.ModeledSharingProfile;
|
||||||
import org.apache.guacamole.net.GuacamoleTunnel;
|
import org.apache.guacamole.net.GuacamoleTunnel;
|
||||||
import org.apache.guacamole.net.auth.Connection;
|
import org.apache.guacamole.net.auth.Connection;
|
||||||
import org.apache.guacamole.net.auth.ConnectionGroup;
|
import org.apache.guacamole.net.auth.ConnectionGroup;
|
||||||
@@ -145,4 +148,39 @@ public interface GuacamoleTunnelService {
|
|||||||
*/
|
*/
|
||||||
public Collection<ActiveConnectionRecord> getActiveConnections(ConnectionGroup connectionGroup);
|
public Collection<ActiveConnectionRecord> getActiveConnections(ConnectionGroup connectionGroup);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a socket for the given user which joins the given active
|
||||||
|
* 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 activeConnection
|
||||||
|
* The active connection the user is joining.
|
||||||
|
*
|
||||||
|
* @param sharingProfile
|
||||||
|
* The sharing profile whose associated parameters dictate the level
|
||||||
|
* of access granted to the user joining the connection.
|
||||||
|
*
|
||||||
|
* @param info
|
||||||
|
* Information describing the Guacamole client connecting to the given
|
||||||
|
* connection.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A new GuacamoleTunnel which is configured and connected to the given
|
||||||
|
* active connection.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If the connection cannot be established due to concurrent usage
|
||||||
|
* rules.
|
||||||
|
*/
|
||||||
|
GuacamoleTunnel getGuacamoleTunnel(SharedConnectionUser user,
|
||||||
|
TrackedActiveConnection activeConnection,
|
||||||
|
ModeledSharingProfile sharingProfile,
|
||||||
|
GuacamoleClientInformation info)
|
||||||
|
throws GuacamoleException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -27,12 +27,12 @@ import java.util.Comparator;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import org.apache.guacamole.GuacamoleClientTooManyException;
|
import org.apache.guacamole.GuacamoleClientTooManyException;
|
||||||
import org.apache.guacamole.auth.jdbc.user.AuthenticatedUser;
|
|
||||||
import org.apache.guacamole.auth.jdbc.connection.ModeledConnection;
|
import org.apache.guacamole.auth.jdbc.connection.ModeledConnection;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
import org.apache.guacamole.GuacamoleResourceConflictException;
|
import org.apache.guacamole.GuacamoleResourceConflictException;
|
||||||
import org.apache.guacamole.auth.jdbc.JDBCEnvironment;
|
import org.apache.guacamole.auth.jdbc.JDBCEnvironment;
|
||||||
import org.apache.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup;
|
import org.apache.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup;
|
||||||
|
import org.apache.guacamole.auth.jdbc.user.RemoteAuthenticatedUser;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,7 +166,7 @@ public class RestrictedGuacamoleTunnelService
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ModeledConnection acquire(AuthenticatedUser user,
|
protected ModeledConnection acquire(RemoteAuthenticatedUser user,
|
||||||
List<ModeledConnection> connections) throws GuacamoleException {
|
List<ModeledConnection> connections) throws GuacamoleException {
|
||||||
|
|
||||||
// Do not acquire connection unless within overall limits
|
// Do not acquire connection unless within overall limits
|
||||||
@@ -174,7 +174,7 @@ public class RestrictedGuacamoleTunnelService
|
|||||||
throw new GuacamoleResourceConflictException("Cannot connect. Overall maximum connections reached.");
|
throw new GuacamoleResourceConflictException("Cannot connect. Overall maximum connections reached.");
|
||||||
|
|
||||||
// Get username
|
// Get username
|
||||||
String username = user.getUser().getIdentifier();
|
String username = user.getIdentifier();
|
||||||
|
|
||||||
// Sort connections in ascending order of usage
|
// Sort connections in ascending order of usage
|
||||||
ModeledConnection[] sortedConnections = connections.toArray(new ModeledConnection[connections.size()]);
|
ModeledConnection[] sortedConnections = connections.toArray(new ModeledConnection[connections.size()]);
|
||||||
@@ -230,18 +230,18 @@ public class RestrictedGuacamoleTunnelService
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void release(AuthenticatedUser user, ModeledConnection connection) {
|
protected void release(RemoteAuthenticatedUser user, ModeledConnection connection) {
|
||||||
activeSeats.remove(new Seat(user.getUser().getIdentifier(), connection.getIdentifier()));
|
activeSeats.remove(new Seat(user.getIdentifier(), connection.getIdentifier()));
|
||||||
activeConnections.remove(connection.getIdentifier());
|
activeConnections.remove(connection.getIdentifier());
|
||||||
totalActiveConnections.decrementAndGet();
|
totalActiveConnections.decrementAndGet();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void acquire(AuthenticatedUser user,
|
protected void acquire(RemoteAuthenticatedUser user,
|
||||||
ModeledConnectionGroup connectionGroup) throws GuacamoleException {
|
ModeledConnectionGroup connectionGroup) throws GuacamoleException {
|
||||||
|
|
||||||
// Get username
|
// Get username
|
||||||
String username = user.getUser().getIdentifier();
|
String username = user.getIdentifier();
|
||||||
|
|
||||||
// Attempt to aquire connection group according to per-user limits
|
// Attempt to aquire connection group according to per-user limits
|
||||||
Seat seat = new Seat(username, connectionGroup.getIdentifier());
|
Seat seat = new Seat(username, connectionGroup.getIdentifier());
|
||||||
@@ -267,9 +267,9 @@ public class RestrictedGuacamoleTunnelService
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void release(AuthenticatedUser user,
|
protected void release(RemoteAuthenticatedUser user,
|
||||||
ModeledConnectionGroup connectionGroup) {
|
ModeledConnectionGroup connectionGroup) {
|
||||||
activeGroupSeats.remove(new Seat(user.getUser().getIdentifier(), connectionGroup.getIdentifier()));
|
activeGroupSeats.remove(new Seat(user.getIdentifier(), connectionGroup.getIdentifier()));
|
||||||
activeGroups.remove(connectionGroup.getIdentifier());
|
activeGroups.remove(connectionGroup.getIdentifier());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user