GUACAMOLE-462: Merge add support for associating connection history with logs/recordings.

This commit is contained in:
Virtually Nick
2022-03-02 16:27:00 -05:00
committed by GitHub
50 changed files with 2747 additions and 739 deletions

View File

@@ -0,0 +1,147 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.jdbc.base;
import java.util.Collection;
import java.util.List;
import org.apache.guacamole.auth.jdbc.user.UserModel;
import org.apache.ibatis.annotations.Param;
/**
* Common interface for mapping activity records.
*
* @param <ModelType>
* The type of model object representing the activity records mapped by
* this mapper.
*/
public interface ActivityRecordMapper<ModelType> {
/**
* Inserts the given activity record.
*
* @param record
* The activity record to insert.
*
* @return
* The number of rows inserted.
*/
int insert(@Param("record") ModelType record);
/**
* Updates the given activity record in the database, assigning an end
* date. No column of the existing activity record is updated except for
* the end date. If the record does not actually exist, this operation has
* no effect.
*
* @param record
* The activity record to update.
*
* @return
* The number of rows updated.
*/
int updateEndDate(@Param("record") ModelType record);
/**
* Searches for up to <code>limit</code> activity records that contain
* the given terms, sorted by the given predicates, regardless of whether
* the data they are associated with is readable by any particular user.
* This should only be called on behalf of a system administrator. If
* records are needed by a non-administrative user who must have explicit
* read rights, use {@link searchReadable()} instead.
*
* @param identifier
* The optional identifier of the object whose history is being
* retrieved, or null if records related to any such object should be
* retrieved.
*
* @param recordIdentifier
* The identifier of the specific history record to retrieve, if not
* all matching records. Search terms, etc. will still be applied to
* the single record.
*
* @param terms
* The search terms that must match the returned records.
*
* @param sortPredicates
* A list of predicates to sort the returned records by, in order of
* priority.
*
* @param limit
* The maximum number of records that should be returned.
*
* @return
* The results of the search performed with the given parameters.
*/
List<ModelType> search(@Param("identifier") String identifier,
@Param("recordIdentifier") String recordIdentifier,
@Param("terms") Collection<ActivityRecordSearchTerm> terms,
@Param("sortPredicates") List<ActivityRecordSortPredicate> sortPredicates,
@Param("limit") int limit);
/**
* Searches for up to <code>limit</code> activity records that contain
* the given terms, sorted by the given predicates. Only records that are
* associated with data explicitly readable by the given user will be
* returned. If records are needed by a system administrator (who, by
* definition, does not need explicit read rights), use {@link search()}
* instead.
*
* @param identifier
* The optional identifier of the object whose history is being
* retrieved, or null if records related to any such object should be
* retrieved.
*
* @param user
* The user whose permissions should determine whether a record is
* returned.
*
* @param recordIdentifier
* The identifier of the specific history record to retrieve, if not
* all matching records. Search terms, etc. will still be applied to
* the single record.
*
* @param terms
* The search terms that must match the returned records.
*
* @param sortPredicates
* A list of predicates to sort the returned records by, in order of
* priority.
*
* @param limit
* The maximum number of records that should be returned.
*
* @param effectiveGroups
* The identifiers of all groups that should be taken into account
* when determining the permissions effectively granted to the user. If
* no groups are given, only permissions directly granted to the user
* will be used.
*
* @return
* The results of the search performed with the given parameters.
*/
List<ModelType> searchReadable(@Param("identifier") String identifier,
@Param("user") UserModel user,
@Param("recordIdentifier") String recordIdentifier,
@Param("terms") Collection<ActivityRecordSearchTerm> terms,
@Param("sortPredicates") List<ActivityRecordSortPredicate> sortPredicates,
@Param("limit") int limit,
@Param("effectiveGroups") Collection<String> effectiveGroups);
}

View File

@@ -19,8 +19,9 @@
package org.apache.guacamole.auth.jdbc.base;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.UUID;
import org.apache.guacamole.net.auth.ActivityRecord;
/**
@@ -33,16 +34,45 @@ public class ModeledActivityRecord implements ActivityRecord {
*/
private final ActivityRecordModel model;
/**
* The UUID namespace of the type 3 name UUID to generate for the record.
* This namespace should correspond to the source of IDs for the model such
* that the combination of this namespace with the numeric record ID will
* always be unique and deterministic across all activity records,
* regardless of record type.
*/
private final UUID namespace;
/**
* Creates a new ModeledActivityRecord backed by the given model object.
* Changes to this record will affect the backing model object, and changes
* to the backing model object will affect this record.
*
*
* @param namespace
* The UUID namespace of the type 3 name UUID to generate for the
* record. This namespace should correspond to the source of IDs for
* the model such that the combination of this namespace with the
* numeric record ID will always be unique and deterministic across all
* activity records, regardless of record type.
*
* @param model
* The model object to use to back this activity record.
*/
public ModeledActivityRecord(ActivityRecordModel model) {
public ModeledActivityRecord(UUID namespace, ActivityRecordModel model) {
this.model = model;
this.namespace = namespace;
}
/**
* Returns the backing model object. Changes to this record will affect the
* backing model object, and changes to the backing model object will
* affect this record.
*
* @return
* The backing model object.
*/
public ActivityRecordModel getModel() {
return model;
}
@Override
@@ -70,4 +100,31 @@ public class ModeledActivityRecord implements ActivityRecord {
return false;
}
@Override
public String getIdentifier() {
Integer id = model.getRecordID();
if (id == null)
return null;
return id.toString();
}
@Override
public UUID getUUID() {
Integer id = model.getRecordID();
if (id == null)
return null;
// Convert record ID to a name UUID in the given namespace
return UUID.nameUUIDFromBytes(ByteBuffer.allocate(24)
.putLong(namespace.getMostSignificantBits())
.putLong(namespace.getLeastSignificantBits())
.putLong(id)
.array());
}
}

View File

@@ -29,6 +29,8 @@ import org.apache.guacamole.net.auth.ActivityRecord;
import org.apache.guacamole.net.auth.ActivityRecordSet;
import org.apache.guacamole.net.auth.ActivityRecordSet.SortableProperty;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A JDBC implementation of ActivityRecordSet. Calls to asCollection() will
@@ -41,6 +43,11 @@ import org.apache.guacamole.net.auth.AuthenticatedUser;
public abstract class ModeledActivityRecordSet<RecordType extends ActivityRecord>
extends RestrictedObject implements ActivityRecordSet<RecordType> {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(ModeledActivityRecordSet.class);
/**
* The set of strings that each must occur somewhere within the returned
* records, whether within the associated username, an associated date, or
@@ -73,6 +80,11 @@ public abstract class ModeledActivityRecordSet<RecordType extends ActivityRecord
* @param user
* The user retrieving the history.
*
* @param recordIdentifier
* The identifier of the specific history record to retrieve, if not
* all matching records. Search terms, etc. will still be applied to
* the single record.
*
* @param requiredContents
* The search terms that must be contained somewhere within each of the
* returned records.
@@ -90,16 +102,35 @@ public abstract class ModeledActivityRecordSet<RecordType extends ActivityRecord
* @throws GuacamoleException
* If permission to read the history records is denied.
*/
protected abstract Collection<RecordType> retrieveHistory(
AuthenticatedUser user,
protected abstract List<RecordType> retrieveHistory(
AuthenticatedUser user, String recordIdentifier,
Set<ActivityRecordSearchTerm> requiredContents,
List<ActivityRecordSortPredicate> sortPredicates,
int limit) throws GuacamoleException;
@Override
public RecordType get(String identifier) throws GuacamoleException {
List<RecordType> records = retrieveHistory(getCurrentUser(),
identifier, requiredContents, sortPredicates, limit);
if (records.isEmpty())
return null;
if (records.size() == 1)
return records.get(0);
logger.warn("Multiple history records match ID \"{}\"! This should "
+ "not be possible and may indicate a bug or database "
+ "corruption.", identifier);
return null;
}
@Override
public Collection<RecordType> asCollection()
throws GuacamoleException {
return retrieveHistory(getCurrentUser(), requiredContents,
return retrieveHistory(getCurrentUser(), null, requiredContents,
sortPredicates, limit);
}

View File

@@ -19,113 +19,9 @@
package org.apache.guacamole.auth.jdbc.connection;
import java.util.Collection;
import java.util.List;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordSearchTerm;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordSortPredicate;
import org.apache.ibatis.annotations.Param;
import org.apache.guacamole.auth.jdbc.user.UserModel;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordMapper;
/**
* Mapper for connection record objects.
*/
public interface ConnectionRecordMapper {
/**
* Returns a collection of all connection records associated with the
* connection having the given identifier.
*
* @param identifier
* The identifier of the connection whose records are to be retrieved.
*
* @return
* A collection of all connection records associated with the
* connection having the given identifier. This collection will be
* empty if no such connection exists.
*/
List<ConnectionRecordModel> select(@Param("identifier") String identifier);
/**
* Inserts the given connection record.
*
* @param record
* The connection record to insert.
*
* @return
* The number of rows inserted.
*/
int insert(@Param("record") ConnectionRecordModel record);
/**
* Searches for up to <code>limit</code> connection records that contain
* the given terms, sorted by the given predicates, regardless of whether
* the data they are associated with is is readable by any particular user.
* This should only be called on behalf of a system administrator. If
* records are needed by a non-administrative user who must have explicit
* read rights, use {@link searchReadable()} instead.
*
* @param identifier
* The optional connection identifier to which records should be limited,
* or null if all records should be retrieved.
*
* @param terms
* The search terms that must match the returned records.
*
* @param sortPredicates
* A list of predicates to sort the returned records by, in order of
* priority.
*
* @param limit
* The maximum number of records that should be returned.
*
* @return
* The results of the search performed with the given parameters.
*/
List<ConnectionRecordModel> search(@Param("identifier") String identifier,
@Param("terms") Collection<ActivityRecordSearchTerm> terms,
@Param("sortPredicates") List<ActivityRecordSortPredicate> sortPredicates,
@Param("limit") int limit);
/**
* Searches for up to <code>limit</code> connection records that contain
* the given terms, sorted by the given predicates. Only records that are
* associated with data explicitly readable by the given user will be
* returned. If records are needed by a system administrator (who, by
* definition, does not need explicit read rights), use {@link search()}
* instead.
*
* @param identifier
* The optional connection identifier for which records should be
* retrieved, or null if all readable records should be retrieved.
*
* @param user
* The user whose permissions should determine whether a record is
* returned.
*
* @param terms
* The search terms that must match the returned records.
*
* @param sortPredicates
* A list of predicates to sort the returned records by, in order of
* priority.
*
* @param limit
* The maximum number of records that should be returned.
*
* @param effectiveGroups
* The identifiers of all groups that should be taken into account
* when determining the permissions effectively granted to the user. If
* no groups are given, only permissions directly granted to the user
* will be used.
*
* @return
* The results of the search performed with the given parameters.
*/
List<ConnectionRecordModel> searchReadable(@Param("identifier") String identifier,
@Param("user") UserModel user,
@Param("terms") Collection<ActivityRecordSearchTerm> terms,
@Param("sortPredicates") List<ActivityRecordSortPredicate> sortPredicates,
@Param("limit") int limit,
@Param("effectiveGroups") Collection<String> effectiveGroups);
}
public interface ConnectionRecordMapper extends ActivityRecordMapper<ConnectionRecordModel> {}

View File

@@ -20,9 +20,9 @@
package org.apache.guacamole.auth.jdbc.connection;
import com.google.inject.Inject;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordSearchTerm;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordSortPredicate;
@@ -38,6 +38,15 @@ import org.apache.guacamole.net.auth.ConnectionRecord;
*/
public class ConnectionRecordSet extends ModeledActivityRecordSet<ConnectionRecord> {
/**
* The namespace for the type 3 UUIDs generated for connection history
* records. This UUID namespace is itself a type 3 UUID within the "ns:OID"
* namespace for the OID "1.3.6.1.4.1.18060.18.2.1.2", which has been
* specifically allocated for Apache Guacamole database connection
* history records.
*/
public static final UUID UUID_NAMESPACE = UUID.fromString("8b55f070-95f4-3d31-93ee-9c5845e7aa40");
/**
* Service for managing connection objects.
*/
@@ -69,14 +78,15 @@ public class ConnectionRecordSet extends ModeledActivityRecordSet<ConnectionReco
}
@Override
protected Collection<ConnectionRecord> retrieveHistory(
AuthenticatedUser user, Set<ActivityRecordSearchTerm> requiredContents,
List<ActivityRecordSortPredicate> sortPredicates, int limit)
throws GuacamoleException {
protected List<ConnectionRecord> retrieveHistory(
AuthenticatedUser user, String recordIdentifier,
Set<ActivityRecordSearchTerm> requiredContents,
List<ActivityRecordSortPredicate> sortPredicates,
int limit) throws GuacamoleException {
// Retrieve history from database
return connectionService.retrieveHistory(identifier, getCurrentUser(),
requiredContents, sortPredicates, limit);
recordIdentifier, requiredContents, sortPredicates, limit);
}

View File

@@ -390,40 +390,6 @@ public class ConnectionService extends ModeledChildDirectoryObjectService<Modele
}
/**
* Retrieves the connection history of the given connection, including any
* active connections.
*
* @param user
* The user retrieving the connection history.
*
* @param connection
* The connection whose history is being retrieved.
*
* @return
* The connection history of the given connection, including any
* active connections.
*
* @throws GuacamoleException
* If permission to read the connection history is denied.
*/
public List<ConnectionRecord> retrieveHistory(ModeledAuthenticatedUser user,
ModeledConnection connection) throws GuacamoleException {
String identifier = connection.getIdentifier();
// Get current active connections.
List<ConnectionRecord> records = new ArrayList<>(tunnelService.getActiveConnections(connection));
Collections.reverse(records);
// Add in the history records.
records.addAll(retrieveHistory(identifier, user, Collections.emptyList(),
Collections.emptyList(), Integer.MAX_VALUE));
return records;
}
/**
* Retrieves the connection history records matching the given criteria.
* Retrieves up to <code>limit</code> connection history records matching
@@ -437,6 +403,11 @@ public class ConnectionService extends ModeledChildDirectoryObjectService<Modele
* @param user
* The user retrieving the connection history.
*
* @param recordIdentifier
* The identifier of the specific history record to retrieve, if not
* all matching records. Search terms, etc. will still be applied to
* the single record.
*
* @param requiredContents
* The search terms that must be contained somewhere within each of the
* returned records.
@@ -456,7 +427,7 @@ public class ConnectionService extends ModeledChildDirectoryObjectService<Modele
* If permission to read the connection history is denied.
*/
public List<ConnectionRecord> retrieveHistory(String identifier,
ModeledAuthenticatedUser user,
ModeledAuthenticatedUser user, String recordIdentifier,
Collection<ActivityRecordSearchTerm> requiredContents,
List<ActivityRecordSortPredicate> sortPredicates, int limit)
throws GuacamoleException {
@@ -465,54 +436,19 @@ public class ConnectionService extends ModeledChildDirectoryObjectService<Modele
// Bypass permission checks if the user is privileged
if (user.isPrivileged())
searchResults = connectionRecordMapper.search(identifier, requiredContents,
sortPredicates, limit);
searchResults = connectionRecordMapper.search(identifier,
recordIdentifier, requiredContents, sortPredicates, limit);
// Otherwise only return explicitly readable history records
else
searchResults = connectionRecordMapper.searchReadable(identifier,
user.getUser().getModel(), requiredContents, sortPredicates,
limit, user.getEffectiveUserGroups());
user.getUser().getModel(), recordIdentifier,
requiredContents, sortPredicates, limit,
user.getEffectiveUserGroups());
return getObjectInstances(searchResults);
}
/**
* Retrieves the connection history records matching the given criteria.
* Retrieves up to <code>limit</code> connection history records matching
* the given terms and sorted by the given predicates. Only history records
* associated with data that the given user can read are returned.
*
* @param user
* The user retrieving the connection history.
*
* @param requiredContents
* The search terms that must be contained somewhere within each of the
* returned records.
*
* @param sortPredicates
* A list of predicates to sort the returned records by, in order of
* priority.
*
* @param limit
* The maximum number of records that should be returned.
*
* @return
* The connection history of the given connection, including any
* active connections.
*
* @throws GuacamoleException
* If permission to read the connection history is denied.
*/
public List<ConnectionRecord> retrieveHistory(ModeledAuthenticatedUser user,
Collection<ActivityRecordSearchTerm> requiredContents,
List<ActivityRecordSortPredicate> sortPredicates, int limit)
throws GuacamoleException {
return retrieveHistory(null, user, requiredContents, sortPredicates, limit);
}
/**
* Connects to the given connection as the given user, using the given

View File

@@ -43,7 +43,7 @@ public class ModeledConnectionRecord extends ModeledActivityRecord
* The model object to use to back this connection record.
*/
public ModeledConnectionRecord(ConnectionRecordModel model) {
super(model);
super(ConnectionRecordSet.UUID_NAMESPACE, model);
this.model = model;
}
@@ -67,4 +67,9 @@ public class ModeledConnectionRecord extends ModeledActivityRecord
return model.getSharingProfileName();
}
@Override
public ConnectionRecordModel getModel() {
return model;
}
}

View File

@@ -25,10 +25,12 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser;
@@ -55,6 +57,7 @@ import org.apache.guacamole.protocol.GuacamoleConfiguration;
import org.apache.guacamole.token.TokenFilter;
import org.mybatis.guice.transactional.Transactional;
import org.apache.guacamole.auth.jdbc.connection.ConnectionParameterMapper;
import org.apache.guacamole.auth.jdbc.sharing.SharedConnectionMap;
import org.apache.guacamole.auth.jdbc.sharing.connection.SharedConnectionDefinition;
import org.apache.guacamole.auth.jdbc.sharingprofile.ModeledSharingProfile;
import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileParameterMapper;
@@ -109,10 +112,10 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
private ConnectionRecordMapper connectionRecordMapper;
/**
* Provider for creating active connection records.
* Map of all currently-shared connections.
*/
@Inject
private Provider<ActiveConnectionRecord> activeConnectionRecordProvider;
private SharedConnectionMap connectionMap;
/**
* All active connections through the tunnel having a given UUID.
@@ -252,33 +255,6 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
}
/**
* Saves the given ActiveConnectionRecord to the database. The end date of
* the saved record will be populated with the current time.
*
* @param record
* The record to save.
*/
private void saveConnectionRecord(ActiveConnectionRecord record) {
// Get associated models
ConnectionRecordModel recordModel = new ConnectionRecordModel();
// Copy user information and timestamps into new record
recordModel.setUsername(record.getUsername());
recordModel.setConnectionIdentifier(record.getConnectionIdentifier());
recordModel.setConnectionName(record.getConnectionName());
recordModel.setRemoteHost(record.getRemoteHost());
recordModel.setSharingProfileIdentifier(record.getSharingProfileIdentifier());
recordModel.setSharingProfileName(record.getSharingProfileName());
recordModel.setStartDate(record.getStartDate());
recordModel.setEndDate(new Date());
// Insert connection record
connectionRecordMapper.insert(recordModel);
}
/**
* Returns an unconfigured GuacamoleSocket that is already connected to
* guacd as specified in guacamole.properties, using SSL if necessary.
@@ -369,7 +345,9 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
activeConnection.invalidate();
// Remove underlying tunnel from list of active tunnels
activeTunnels.remove(activeConnection.getUUID().toString());
UUID uuid = activeConnection.getUUID(); // May be null if record not successfully inserted
if (uuid != null)
activeTunnels.remove(uuid.toString());
// Get original user
RemoteAuthenticatedUser user = activeConnection.getUser();
@@ -392,9 +370,11 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
// Release any associated group
if (activeConnection.hasBalancingGroup())
release(user, activeConnection.getBalancingGroup());
// Save history record to database
saveConnectionRecord(activeConnection);
// Update history record with end date
ConnectionRecordModel recordModel = activeConnection.getModel();
recordModel.setEndDate(new Date());
connectionRecordMapper.updateEndDate(recordModel);
}
@@ -438,7 +418,16 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
// Record new active connection
Runnable cleanupTask = new ConnectionCleanupTask(activeConnection);
activeTunnels.put(activeConnection.getUUID().toString(), activeConnection);
try {
connectionRecordMapper.insert(activeConnection.getModel()); // This MUST happen before getUUID() is invoked, to ensure the ID driving the UUID exists
activeTunnels.put(activeConnection.getUUID().toString(), activeConnection);
}
// Execute cleanup if connection history could not be updated
catch (RuntimeException | Error e) {
cleanupTask.run();
throw e;
}
try {
@@ -471,6 +460,10 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
}
// Include history record UUID as token
tokens = new HashMap<>(tokens);
tokens.put("HISTORY_UUID", activeConnection.getUUID().toString());
// Build token filter containing credential tokens
TokenFilter tokenFilter = new TokenFilter();
tokenFilter.setTokens(tokens);
@@ -638,8 +631,7 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
acquire(user, Collections.singletonList(connection), true);
// Connect only if the connection was successfully acquired
ActiveConnectionRecord connectionRecord = activeConnectionRecordProvider.get();
connectionRecord.init(user, connection);
ActiveConnectionRecord connectionRecord = new ActiveConnectionRecord(connectionMap, user, connection);
return assignGuacamoleTunnel(connectionRecord, info, tokens, false);
}
@@ -685,8 +677,7 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
try {
// Connect to acquired child
ActiveConnectionRecord connectionRecord = activeConnectionRecordProvider.get();
connectionRecord.init(user, connectionGroup, connection);
ActiveConnectionRecord connectionRecord = new ActiveConnectionRecord(connectionMap, user, connectionGroup, connection);
GuacamoleTunnel tunnel = assignGuacamoleTunnel(connectionRecord,
info, tokens, connections.size() > 1);
@@ -741,9 +732,8 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
throws GuacamoleException {
// Create a connection record which describes the shared connection
ActiveConnectionRecord connectionRecord = activeConnectionRecordProvider.get();
connectionRecord.init(user, definition.getActiveConnection(),
definition.getSharingProfile());
ActiveConnectionRecord connectionRecord = new ActiveConnectionRecord(connectionMap,
user, definition.getActiveConnection(), definition.getSharingProfile());
// Connect to shared connection described by the created record
GuacamoleTunnel tunnel = assignGuacamoleTunnel(connectionRecord, info, tokens, false);

View File

@@ -19,10 +19,11 @@
package org.apache.guacamole.auth.jdbc.tunnel;
import com.google.inject.Inject;
import java.util.Date;
import java.util.UUID;
import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel;
import org.apache.guacamole.auth.jdbc.connection.ModeledConnection;
import org.apache.guacamole.auth.jdbc.connection.ModeledConnectionRecord;
import org.apache.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup;
import org.apache.guacamole.auth.jdbc.sharing.SharedConnectionMap;
import org.apache.guacamole.auth.jdbc.sharing.SharedObjectManager;
@@ -31,7 +32,6 @@ import org.apache.guacamole.auth.jdbc.user.RemoteAuthenticatedUser;
import org.apache.guacamole.net.AbstractGuacamoleTunnel;
import org.apache.guacamole.net.GuacamoleSocket;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.auth.ConnectionRecord;
/**
@@ -39,41 +39,31 @@ import org.apache.guacamole.net.auth.ConnectionRecord;
* the associated connection has not yet ended, getEndDate() will always return
* null. The associated start date will be the time of this objects creation.
*/
public class ActiveConnectionRecord implements ConnectionRecord {
public class ActiveConnectionRecord extends ModeledConnectionRecord {
/**
* The user that connected to the connection associated with this connection
* record.
*/
private RemoteAuthenticatedUser user;
private final RemoteAuthenticatedUser user;
/**
* The balancing group from which the associated connection was chosen, if
* any. If no balancing group was used, this will be null.
*/
private ModeledConnectionGroup balancingGroup;
private final ModeledConnectionGroup balancingGroup;
/**
* The connection associated with this connection record.
*/
private 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 ModeledSharingProfile sharingProfile;
/**
* The time this connection record was created.
*/
private final Date startDate = new Date();
/**
* The UUID that will be assigned to the underlying tunnel.
*/
private final UUID uuid = UUID.randomUUID();
private final ModeledSharingProfile sharingProfile;
/**
* The connection ID of the connection as determined by guacd, not to be
@@ -91,8 +81,7 @@ public class ActiveConnectionRecord implements ConnectionRecord {
/**
* Map of all currently-shared connections.
*/
@Inject
private SharedConnectionMap connectionMap;
private final SharedConnectionMap connectionMap;
/**
* Manager which tracks all share keys associated with this connection
@@ -111,7 +100,57 @@ public class ActiveConnectionRecord implements ConnectionRecord {
};
/**
* Initializes this connection record, associating it with the given user,
* Creates a new connection record model object, associating it with the
* given user, 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. No end date will be assigned.
*
* @param user
* The user that connected to the connection associated with this
* connection record.
*
* @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.
*
* @return
* A new connection record model object associated with the given user,
* connection, and sharing profile, and having the current date/time as
* its start date.
*/
private static ConnectionRecordModel createModel(RemoteAuthenticatedUser user,
ModeledConnection connection,
ModeledSharingProfile sharingProfile) {
// Create model object representing an active connection that started
// at the current time ...
ConnectionRecordModel recordModel = new ConnectionRecordModel();
recordModel.setStartDate(new Date());
// ... was established by the given user ...
recordModel.setUsername(user.getIdentifier());
recordModel.setRemoteHost(user.getRemoteHost());
// ... to the given connection ...
recordModel.setConnectionIdentifier(connection.getIdentifier());
recordModel.setConnectionName(connection.getName());
// ... using the given sharing profile (if any)
if (sharingProfile != null) {
recordModel.setSharingProfileIdentifier(sharingProfile.getIdentifier());
recordModel.setSharingProfileName(sharingProfile.getName());
}
return recordModel;
}
/**
* Creates a new ActiveConnectionRecord 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
@@ -119,6 +158,10 @@ public class ActiveConnectionRecord implements ConnectionRecord {
* The start date of this connection record will be the time of its
* creation.
*
* @param connectionMap
* The SharedConnectionMap instance tracking all active shared
* connections.
*
* @param user
* The user that connected to the connection associated with this
* connection record.
@@ -134,10 +177,13 @@ public class ActiveConnectionRecord implements ConnectionRecord {
* The sharing profile that was used to share access to the given
* connection, or null if no sharing profile was used.
*/
private void init(RemoteAuthenticatedUser user,
public ActiveConnectionRecord(SharedConnectionMap connectionMap,
RemoteAuthenticatedUser user,
ModeledConnectionGroup balancingGroup,
ModeledConnection connection,
ModeledSharingProfile sharingProfile) {
super(createModel(user, connection, sharingProfile));
this.connectionMap = connectionMap;
this.user = user;
this.balancingGroup = balancingGroup;
this.connection = connection;
@@ -145,12 +191,16 @@ public class ActiveConnectionRecord implements ConnectionRecord {
}
/**
* Initializes this connection record, associating it with the given user,
* Creates a new ActiveConnectionRecord associated with the given user,
* connection, and balancing connection group. The given balancing
* connection group MUST be the connection group from which the given
* connection was chosen. The start date of this connection record will be
* the time of its creation.
*
* @param connectionMap
* The SharedConnectionMap instance tracking all active shared
* connections.
*
* @param user
* The user that connected to the connection associated with this
* connection record.
@@ -161,17 +211,22 @@ public class ActiveConnectionRecord implements ConnectionRecord {
* @param connection
* The connection to associate with this connection record.
*/
public void init(RemoteAuthenticatedUser user,
public ActiveConnectionRecord(SharedConnectionMap connectionMap,
RemoteAuthenticatedUser user,
ModeledConnectionGroup balancingGroup,
ModeledConnection connection) {
init(user, balancingGroup, connection, null);
this(connectionMap, user, balancingGroup, connection, null);
}
/**
* Initializes this connection record, associating it with the given user
* Creates a new ActiveConnectionRecord associated with the given user,
* and connection. The start date of this connection record will be the time
* of its creation.
*
* @param connectionMap
* The SharedConnectionMap instance tracking all active shared
* connections.
*
* @param user
* The user that connected to the connection associated with this
* connection record.
@@ -179,18 +234,22 @@ public class ActiveConnectionRecord implements ConnectionRecord {
* @param connection
* The connection to associate with this connection record.
*/
public void init(RemoteAuthenticatedUser user,
ModeledConnection connection) {
init(user, null, connection);
public ActiveConnectionRecord(SharedConnectionMap connectionMap,
RemoteAuthenticatedUser user, ModeledConnection connection) {
this(connectionMap, user, null, connection);
}
/**
* Initializes this connection record, associating it with the given user,
* Creates a new ActiveConnectionRecord 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 connectionMap
* The SharedConnectionMap instance tracking all active shared
* connections.
*
* @param user
* The user that connected to the connection associated with this
* connection record.
@@ -204,10 +263,11 @@ public class ActiveConnectionRecord implements ConnectionRecord {
* connection, or null if no sharing profile should be used (access to
* the connection is unrestricted).
*/
public void init(RemoteAuthenticatedUser user,
public ActiveConnectionRecord(SharedConnectionMap connectionMap,
RemoteAuthenticatedUser user,
ActiveConnectionRecord activeConnection,
ModeledSharingProfile sharingProfile) {
init(user, null, activeConnection.getConnection(), sharingProfile);
this(connectionMap, user, null, activeConnection.getConnection(), sharingProfile);
this.connectionID = activeConnection.getConnectionID();
}
@@ -286,63 +346,6 @@ public class ActiveConnectionRecord implements ConnectionRecord {
return sharingProfile == null;
}
@Override
public String getConnectionIdentifier() {
return connection.getIdentifier();
}
@Override
public String getConnectionName() {
return connection.getName();
}
@Override
public String getSharingProfileIdentifier() {
// Return sharing profile identifier if known
if (sharingProfile != null)
return sharingProfile.getIdentifier();
// No associated sharing profile
return null;
}
@Override
public String getSharingProfileName() {
// Return sharing profile name if known
if (sharingProfile != null)
return sharingProfile.getName();
// No associated sharing profile
return null;
}
@Override
public Date getStartDate() {
return startDate;
}
@Override
public Date getEndDate() {
// Active connections have not yet ended
return null;
}
@Override
public String getRemoteHost() {
return user.getRemoteHost();
}
@Override
public String getUsername() {
return user.getIdentifier();
}
@Override
public boolean isActive() {
return tunnel != null && tunnel.isOpen();
@@ -387,7 +390,7 @@ public class ActiveConnectionRecord implements ConnectionRecord {
@Override
public UUID getUUID() {
return uuid;
return ActiveConnectionRecord.this.getUUID();
}
};
@@ -401,18 +404,6 @@ public class ActiveConnectionRecord implements ConnectionRecord {
}
/**
* Returns the UUID of the underlying tunnel. If there is no underlying
* tunnel, this will be the UUID assigned to the underlying tunnel when the
* tunnel is set.
*
* @return
* The current or future UUID of the underlying tunnel.
*/
public UUID getUUID() {
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

View File

@@ -291,7 +291,7 @@ public class ModeledUserContext extends RestrictedObject
// Record logout time only if login time was recorded
if (userRecord != null) {
userRecord.setEndDate(new Date());
userRecordMapper.update(userRecord);
userRecordMapper.updateEndDate(userRecord);
}
}

View File

@@ -19,124 +19,10 @@
package org.apache.guacamole.auth.jdbc.user;
import java.util.Collection;
import java.util.List;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordMapper;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordModel;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordSearchTerm;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordSortPredicate;
import org.apache.ibatis.annotations.Param;
/**
* Mapper for user login activity records.
*/
public interface UserRecordMapper {
/**
* Returns a collection of all user login records associated with the user
* having the given username.
*
* @param username
* The username of the user whose login records are to be retrieved.
*
* @return
* A collection of all user login records associated with the user
* having the given username. This collection will be empty if no such
* user exists.
*/
List<ActivityRecordModel> select(@Param("username") String username);
/**
* Inserts the given user login record.
*
* @param record
* The user login record to insert.
*
* @return
* The number of rows inserted.
*/
int insert(@Param("record") ActivityRecordModel record);
/**
* Updates the given user login record.
*
* @param record
* The user login record to update.
*
* @return
* The number of rows updated.
*/
int update(@Param("record") ActivityRecordModel record);
/**
* Searches for up to <code>limit</code> user login records that contain
* the given terms, sorted by the given predicates, regardless of whether
* the data they are associated with is is readable by any particular user.
* This should only be called on behalf of a system administrator. If
* records are needed by a non-administrative user who must have explicit
* read rights, use {@link searchReadable()} instead.
*
* @param username
* The optional username to which records should be limited, or null
* if all records should be retrieved.
*
* @param terms
* The search terms that must match the returned records.
*
* @param sortPredicates
* A list of predicates to sort the returned records by, in order of
* priority.
*
* @param limit
* The maximum number of records that should be returned.
*
* @return
* The results of the search performed with the given parameters.
*/
List<ActivityRecordModel> search(@Param("username") String username,
@Param("terms") Collection<ActivityRecordSearchTerm> terms,
@Param("sortPredicates") List<ActivityRecordSortPredicate> sortPredicates,
@Param("limit") int limit);
/**
* Searches for up to <code>limit</code> user login records that contain
* the given terms, sorted by the given predicates. Only records that are
* associated with data explicitly readable by the given user will be
* returned. If records are needed by a system administrator (who, by
* definition, does not need explicit read rights), use {@link search()}
* instead.
*
* @param username
* The optional username to which records should be limited, or null
* if all readable records should be retrieved.
*
* @param user
* The user whose permissions should determine whether a record is
* returned.
*
* @param terms
* The search terms that must match the returned records.
*
* @param sortPredicates
* A list of predicates to sort the returned records by, in order of
* priority.
*
* @param limit
* The maximum number of records that should be returned.
*
* @param effectiveGroups
* The identifiers of all groups that should be taken into account
* when determining the permissions effectively granted to the user. If
* no groups are given, only permissions directly granted to the user
* will be used.
*
* @return
* The results of the search performed with the given parameters.
*/
List<ActivityRecordModel> searchReadable(@Param("username") String username,
@Param("user") UserModel user,
@Param("terms") Collection<ActivityRecordSearchTerm> terms,
@Param("sortPredicates") List<ActivityRecordSortPredicate> sortPredicates,
@Param("limit") int limit,
@Param("effectiveGroups") Collection<String> effectiveGroups);
}
public interface UserRecordMapper extends ActivityRecordMapper<ActivityRecordModel> {}

View File

@@ -20,9 +20,9 @@
package org.apache.guacamole.auth.jdbc.user;
import com.google.inject.Inject;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordSearchTerm;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordSortPredicate;
@@ -38,6 +38,15 @@ import org.apache.guacamole.net.auth.AuthenticatedUser;
*/
public class UserRecordSet extends ModeledActivityRecordSet<ActivityRecord> {
/**
* The namespace for the type 3 UUIDs generated for user history records.
* This UUID namespace is itself a type 3 UUID within the "ns:OID"
* namespace for the OID "1.3.6.1.4.1.18060.18.2.1.1", which has been
* specifically allocated for Apache Guacamole database user history
* records.
*/
public static final UUID UUID_NAMESPACE = UUID.fromString("e104741a-c949-3947-8d79-f3f2cdce7d6f");
/**
* Service for managing user objects.
*/
@@ -70,14 +79,15 @@ public class UserRecordSet extends ModeledActivityRecordSet<ActivityRecord> {
}
@Override
protected Collection<ActivityRecord> retrieveHistory(
AuthenticatedUser user, Set<ActivityRecordSearchTerm> requiredContents,
protected List<ActivityRecord> retrieveHistory(
AuthenticatedUser user, String recordIdentifier,
Set<ActivityRecordSearchTerm> requiredContents,
List<ActivityRecordSortPredicate> sortPredicates, int limit)
throws GuacamoleException {
// Retrieve history from database
return userService.retrieveHistory(identifier, getCurrentUser(),
requiredContents, sortPredicates, limit);
recordIdentifier, requiredContents, sortPredicates, limit);
}

View File

@@ -546,7 +546,7 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
* A connection record object which is backed by the given model.
*/
protected ActivityRecord getObjectInstance(ActivityRecordModel model) {
return new ModeledActivityRecord(model);
return new ModeledActivityRecord(UserRecordSet.UUID_NAMESPACE, model);
}
/**
@@ -572,32 +572,6 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
}
/**
* Retrieves the login history of the given user, including any active
* sessions.
*
* @param authenticatedUser
* The user retrieving the login history.
*
* @param user
* The user whose history is being retrieved.
*
* @return
* The login history of the given user, including any active sessions.
*
* @throws GuacamoleException
* If permission to read the login history is denied.
*/
public List<ActivityRecord> retrieveHistory(ModeledAuthenticatedUser authenticatedUser,
ModeledUser user) throws GuacamoleException {
String username = user.getIdentifier();
return retrieveHistory(username, authenticatedUser, Collections.emptyList(),
Collections.emptyList(), Integer.MAX_VALUE);
}
/**
* Retrieves user login history records matching the given criteria.
* Retrieves up to <code>limit</code> user history records matching the
@@ -611,6 +585,11 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
* @param user
* The user retrieving the login history.
*
* @param recordIdentifier
* The identifier of the specific history record to retrieve, if not
* all matching records. Search terms, etc. will still be applied to
* the single record.
*
* @param requiredContents
* The search terms that must be contained somewhere within each of the
* returned records.
@@ -629,7 +608,7 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
* If permission to read the user login history is denied.
*/
public List<ActivityRecord> retrieveHistory(String username,
ModeledAuthenticatedUser user,
ModeledAuthenticatedUser user, String recordIdentifier,
Collection<ActivityRecordSearchTerm> requiredContents,
List<ActivityRecordSortPredicate> sortPredicates, int limit)
throws GuacamoleException {
@@ -638,52 +617,18 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
// Bypass permission checks if the user is privileged
if (user.isPrivileged())
searchResults = userRecordMapper.search(username, requiredContents,
sortPredicates, limit);
searchResults = userRecordMapper.search(username, recordIdentifier,
requiredContents, sortPredicates, limit);
// Otherwise only return explicitly readable history records
else
searchResults = userRecordMapper.searchReadable(username,
user.getUser().getModel(),
requiredContents, sortPredicates, limit, user.getEffectiveUserGroups());
user.getUser().getModel(), recordIdentifier,
requiredContents, sortPredicates, limit,
user.getEffectiveUserGroups());
return getObjectInstances(searchResults);
}
/**
* Retrieves user login history records matching the given criteria.
* Retrieves up to <code>limit</code> user history records matching the
* given terms and sorted by the given predicates. Only history records
* associated with data that the given user can read are returned.
*
* @param user
* The user retrieving the login history.
*
* @param requiredContents
* The search terms that must be contained somewhere within each of the
* returned records.
*
* @param sortPredicates
* A list of predicates to sort the returned records by, in order of
* priority.
*
* @param limit
* The maximum number of records that should be returned.
*
* @return
* The login history of the given user, including any active sessions.
*
* @throws GuacamoleException
* If permission to read the user login history is denied.
*/
public List<ActivityRecord> retrieveHistory(ModeledAuthenticatedUser user,
Collection<ActivityRecordSearchTerm> requiredContents,
List<ActivityRecordSortPredicate> sortPredicates, int limit)
throws GuacamoleException {
return retrieveHistory(null, user, requiredContents, sortPredicates, limit);
}
}

View File

@@ -25,6 +25,7 @@
<!-- Result mapper for system permissions -->
<resultMap id="ConnectionRecordResultMap" type="org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel">
<id column="history_id" property="recordID" jdbcType="INTEGER"/>
<result column="connection_id" property="connectionIdentifier" jdbcType="INTEGER"/>
<result column="connection_name" property="connectionName" jdbcType="VARCHAR"/>
<result column="remote_host" property="remoteHost" jdbcType="VARCHAR"/>
@@ -36,30 +37,9 @@
<result column="end_date" property="endDate" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- Select all connection records from a given connection -->
<select id="select" resultMap="ConnectionRecordResultMap">
SELECT
guacamole_connection_history.connection_id,
guacamole_connection_history.connection_name,
guacamole_connection_history.remote_host,
guacamole_connection_history.sharing_profile_id,
guacamole_connection_history.sharing_profile_name,
guacamole_connection_history.user_id,
guacamole_connection_history.username,
guacamole_connection_history.start_date,
guacamole_connection_history.end_date
FROM guacamole_connection_history
WHERE
guacamole_connection_history.connection_id = #{identifier,jdbcType=VARCHAR}
ORDER BY
guacamole_connection_history.start_date DESC,
guacamole_connection_history.end_date DESC
</select>
<!-- Insert the given connection record -->
<insert id="insert" parameterType="org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel">
<insert id="insert" useGeneratedKeys="true" keyProperty="record.recordID"
parameterType="org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel">
INSERT INTO guacamole_connection_history (
connection_id,
@@ -90,10 +70,18 @@
</insert>
<!-- Update the given connection record, assigning an end date -->
<update id="updateEndDate" parameterType="org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel">
UPDATE guacamole_connection_history
SET end_date = #{record.endDate,jdbcType=TIMESTAMP}
WHERE history_id = #{record.recordID,jdbcType=INTEGER}
</update>
<!-- Search for specific connection records -->
<select id="search" resultMap="ConnectionRecordResultMap">
SELECT
guacamole_connection_history.history_id,
guacamole_connection_history.connection_id,
guacamole_connection_history.connection_name,
guacamole_connection_history.remote_host,
@@ -109,9 +97,13 @@
<!-- Search terms -->
<where>
<if test="recordIdentifier != null">
guacamole_connection_history.history_id = #{recordIdentifier,jdbcType=VARCHAR}
</if>
<if test="identifier != null">
guacamole_connection_history.connection_id = #{identifier,jdbcType=VARCHAR}
AND guacamole_connection_history.connection_id = #{identifier,jdbcType=VARCHAR}
</if>
<foreach collection="terms" item="term" open=" AND " separator=" AND ">
@@ -159,6 +151,7 @@
<select id="searchReadable" resultMap="ConnectionRecordResultMap">
SELECT
guacamole_connection_history.history_id,
guacamole_connection_history.connection_id,
guacamole_connection_history.connection_name,
guacamole_connection_history.remote_host,
@@ -174,9 +167,13 @@
<!-- Search terms -->
<where>
<if test="recordIdentifier != null">
guacamole_connection_history.history_id = #{recordIdentifier,jdbcType=VARCHAR}
</if>
<!-- Restrict to readable connections -->
guacamole_connection_history.connection_id IN (
AND guacamole_connection_history.connection_id IN (
<include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
<property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
<property name="groups" value="effectiveGroups"/>

View File

@@ -33,26 +33,6 @@
<result column="end_date" property="endDate" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- Select all user records from a given user -->
<select id="select" resultMap="UserRecordResultMap">
SELECT
guacamole_user_history.remote_host,
guacamole_user_history.user_id,
guacamole_user_history.username,
guacamole_user_history.start_date,
guacamole_user_history.end_date
FROM guacamole_user_history
JOIN guacamole_user ON guacamole_user_history.user_id = guacamole_user.user_id
JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
WHERE
guacamole_entity.name = #{username,jdbcType=VARCHAR}
ORDER BY
guacamole_user_history.start_date DESC,
guacamole_user_history.end_date DESC
</select>
<!-- Insert the given user record -->
<insert id="insert" useGeneratedKeys="true" keyProperty="record.recordID"
parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
@@ -78,18 +58,10 @@
</insert>
<!-- Update the given user record -->
<update id="update" parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
<!-- Update the given user record, assigning an end date -->
<update id="updateEndDate" parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
UPDATE guacamole_user_history
SET remote_host = #{record.remoteHost,jdbcType=VARCHAR},
user_id = (SELECT user_id FROM guacamole_user
JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
WHERE
guacamole_entity.name = #{record.username,jdbcType=VARCHAR}
AND guacamole_entity.type = 'USER'),
username = #{record.username,jdbcType=VARCHAR},
start_date = #{record.startDate,jdbcType=TIMESTAMP},
end_date = #{record.endDate,jdbcType=TIMESTAMP}
SET end_date = #{record.endDate,jdbcType=TIMESTAMP}
WHERE history_id = #{record.recordID,jdbcType=INTEGER}
</update>
@@ -97,6 +69,7 @@
<select id="search" resultMap="UserRecordResultMap">
SELECT
guacamole_user_history.history_id,
guacamole_user_history.remote_host,
guacamole_user_history.user_id,
guacamole_user_history.username,
@@ -107,8 +80,8 @@
<!-- Search terms -->
<where>
<if test="username != null">
guacamole_user_history.username = #{username,jdbcType=VARCHAR}
<if test="identifier != null">
guacamole_user_history.username = #{identifier,jdbcType=VARCHAR}
</if>
<foreach collection="terms" item="term" open=" AND " separator=" AND ">
@@ -153,6 +126,7 @@
<select id="searchReadable" resultMap="UserRecordResultMap">
SELECT
guacamole_user_history.history_id,
guacamole_user_history.remote_host,
guacamole_user_history.user_id,
guacamole_user_history.username,
@@ -171,8 +145,8 @@
</include>
)
<if test="username != null">
AND guacamole_entity.name = #{username,jdbcType=VARCHAR}
<if test="identifier != null">
AND guacamole_entity.name = #{identifier,jdbcType=VARCHAR}
</if>
<foreach collection="terms" item="term" open=" AND " separator=" AND ">

View File

@@ -25,6 +25,7 @@
<!-- Result mapper for system permissions -->
<resultMap id="ConnectionRecordResultMap" type="org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel">
<id column="history_id" property="recordID" jdbcType="INTEGER"/>
<result column="connection_id" property="connectionIdentifier" jdbcType="INTEGER"/>
<result column="connection_name" property="connectionName" jdbcType="VARCHAR"/>
<result column="remote_host" property="remoteHost" jdbcType="VARCHAR"/>
@@ -36,30 +37,9 @@
<result column="end_date" property="endDate" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- Select all connection records from a given connection -->
<select id="select" resultMap="ConnectionRecordResultMap">
SELECT
guacamole_connection_history.connection_id,
guacamole_connection_history.connection_name,
guacamole_connection_history.remote_host,
guacamole_connection_history.sharing_profile_id,
guacamole_connection_history.sharing_profile_name,
guacamole_connection_history.user_id,
guacamole_connection_history.username,
guacamole_connection_history.start_date,
guacamole_connection_history.end_date
FROM guacamole_connection_history
WHERE
guacamole_connection_history.connection_id = #{identifier,jdbcType=INTEGER}::integer
ORDER BY
guacamole_connection_history.start_date DESC,
guacamole_connection_history.end_date DESC
</select>
<!-- Insert the given connection record -->
<insert id="insert" parameterType="org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel">
<insert id="insert" useGeneratedKeys="true" keyProperty="record.recordID"
parameterType="org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel">
INSERT INTO guacamole_connection_history (
connection_id,
@@ -90,10 +70,18 @@
</insert>
<!-- Update the given connection record, assigning an end date -->
<update id="updateEndDate" parameterType="org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel">
UPDATE guacamole_connection_history
SET end_date = #{record.endDate,jdbcType=TIMESTAMP}
WHERE history_id = #{record.recordID,jdbcType=INTEGER}::integer
</update>
<!-- Search for specific connection records -->
<select id="search" resultMap="ConnectionRecordResultMap">
SELECT
guacamole_connection_history.history_id,
guacamole_connection_history.connection_id,
guacamole_connection_history.connection_name,
guacamole_connection_history.remote_host,
@@ -107,11 +95,15 @@
<!-- Search terms -->
<where>
<if test="identifier != null">
guacamole_connection_history.connection_id = #{identifier,jdbcType=INTEGER}::integer
<if test="recordIdentifier != null">
guacamole_connection_history.history_id = #{recordIdentifier,jdbcType=INTEGER}::integer
</if>
<if test="identifier != null">
AND guacamole_connection_history.connection_id = #{identifier,jdbcType=INTEGER}::integer
</if>
<foreach collection="terms" item="term" open=" AND " separator=" AND ">
(
@@ -157,6 +149,7 @@
<select id="searchReadable" resultMap="ConnectionRecordResultMap">
SELECT
guacamole_connection_history.history_id,
guacamole_connection_history.connection_id,
guacamole_connection_history.connection_name,
guacamole_connection_history.remote_host,
@@ -172,9 +165,13 @@
<!-- Search terms -->
<where>
<if test="recordIdentifier != null">
guacamole_connection_history.history_id = #{recordIdentifier,jdbcType=INTEGER}::integer
</if>
<!-- Restrict to readable connections -->
guacamole_connection_history.connection_id IN (
AND guacamole_connection_history.connection_id IN (
<include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
<property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
<property name="groups" value="effectiveGroups"/>

View File

@@ -33,26 +33,6 @@
<result column="end_date" property="endDate" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- Select all user records from a given user -->
<select id="select" resultMap="UserRecordResultMap">
SELECT
guacamole_user_history.remote_host,
guacamole_user_history.user_id,
guacamole_user_history.username,
guacamole_user_history.start_date,
guacamole_user_history.end_date
FROM guacamole_user_history
JOIN guacamole_user ON guacamole_user_history.user_id = guacamole_user.user_id
JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
WHERE
guacamole_entity.name = #{username,jdbcType=VARCHAR}
ORDER BY
guacamole_user_history.start_date DESC,
guacamole_user_history.end_date DESC
</select>
<!-- Insert the given user record -->
<insert id="insert" useGeneratedKeys="true" keyProperty="record.recordID"
parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
@@ -78,18 +58,10 @@
</insert>
<!-- Update the given user record -->
<update id="update" parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
<!-- Update the given user record, assigning an end date -->
<update id="updateEndDate" parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
UPDATE guacamole_user_history
SET remote_host = #{record.remoteHost,jdbcType=VARCHAR},
user_id = (SELECT user_id FROM guacamole_user
JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
WHERE
guacamole_entity.name = #{record.username,jdbcType=VARCHAR}
AND guacamole_entity.type = 'USER'::guacamole_entity_type),
username = #{record.username,jdbcType=VARCHAR},
start_date = #{record.startDate,jdbcType=TIMESTAMP},
end_date = #{record.endDate,jdbcType=TIMESTAMP}
SET end_date = #{record.endDate,jdbcType=TIMESTAMP}
WHERE history_id = #{record.recordID,jdbcType=INTEGER}::integer
</update>
@@ -97,6 +69,7 @@
<select id="search" resultMap="UserRecordResultMap">
SELECT
guacamole_user_history.history_id,
guacamole_user_history.remote_host,
guacamole_user_history.user_id,
guacamole_user_history.username,
@@ -107,8 +80,8 @@
<!-- Search terms -->
<where>
<if test="username != null">
guacamole_user_history.username = #{username,jdbcType=VARCHAR}
<if test="identifier != null">
guacamole_user_history.username = #{identifier,jdbcType=VARCHAR}
</if>
<foreach collection="terms" item="term" open=" AND " separator=" AND ">
@@ -153,6 +126,7 @@
<select id="searchReadable" resultMap="UserRecordResultMap">
SELECT
guacamole_user_history.history_id,
guacamole_user_history.remote_host,
guacamole_user_history.user_id,
guacamole_user_history.username,
@@ -171,8 +145,8 @@
</include>
)
<if test="username != null">
AND guacamole_entity.name = #{username,jdbcType=VARCHAR}
<if test="identifier != null">
AND guacamole_entity.name = #{identifier,jdbcType=VARCHAR}
</if>
<foreach collection="terms" item="term" open=" AND " separator=" AND ">

View File

@@ -25,6 +25,7 @@
<!-- Result mapper for system permissions -->
<resultMap id="ConnectionRecordResultMap" type="org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel">
<id column="history_id" property="recordID" jdbcType="INTEGER"/>
<result column="connection_id" property="connectionIdentifier" jdbcType="INTEGER"/>
<result column="connection_name" property="connectionName" jdbcType="VARCHAR"/>
<result column="remote_host" property="remoteHost" jdbcType="VARCHAR"/>
@@ -36,30 +37,16 @@
<result column="end_date" property="endDate" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- Select all connection records from a given connection -->
<select id="select" resultMap="ConnectionRecordResultMap">
SELECT
[guacamole_connection_history].connection_id,
[guacamole_connection_history].connection_name,
[guacamole_connection_history].remote_host,
[guacamole_connection_history].sharing_profile_id,
[guacamole_connection_history].sharing_profile_name,
[guacamole_connection_history].user_id,
[guacamole_connection_history].username,
[guacamole_connection_history].start_date,
[guacamole_connection_history].end_date
FROM [guacamole_connection_history]
WHERE
[guacamole_connection_history].connection_id = #{identifier,jdbcType=INTEGER}
ORDER BY
[guacamole_connection_history].start_date DESC,
[guacamole_connection_history].end_date DESC
</select>
<!-- Update the given connection record, assigning an end date -->
<update id="updateEndDate" parameterType="org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel">
UPDATE [guacamole_connection_history]
SET end_date = #{record.endDate,jdbcType=TIMESTAMP}
WHERE history_id = #{record.recordID,jdbcType=INTEGER}
</update>
<!-- Insert the given connection record -->
<insert id="insert" parameterType="org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel">
<insert id="insert" useGeneratedKeys="true" keyProperty="record.recordID"
parameterType="org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel">
INSERT INTO [guacamole_connection_history] (
connection_id,
@@ -94,6 +81,7 @@
<select id="search" resultMap="ConnectionRecordResultMap">
SELECT TOP (#{limit,jdbcType=INTEGER})
[guacamole_connection_history].history_id,
[guacamole_connection_history].connection_id,
[guacamole_connection_history].connection_name,
[guacamole_connection_history].remote_host,
@@ -107,9 +95,13 @@
<!-- Search terms -->
<where>
<if test="recordIdentifier != null">
[guacamole_connection_history].history_id = #{recordIdentifier,jdbcType=INTEGER}
</if>
<if test="identifier != null">
[guacamole_connection_history].connection_id = #{identifier,jdbcType=INTEGER}
AND [guacamole_connection_history].connection_id = #{identifier,jdbcType=INTEGER}
</if>
<foreach collection="terms" item="term" open=" AND " separator=" AND ">
@@ -155,6 +147,7 @@
<select id="searchReadable" resultMap="ConnectionRecordResultMap">
SELECT TOP (#{limit,jdbcType=INTEGER})
[guacamole_connection_history].history_id,
[guacamole_connection_history].connection_id,
[guacamole_connection_history].connection_name,
[guacamole_connection_history].remote_host,
@@ -170,9 +163,13 @@
<!-- Search terms -->
<where>
<if test="recordIdentifier != null">
[guacamole_connection_history].history_id = #{recordIdentifier,jdbcType=INTEGER}
</if>
<!-- Restrict to readable connections -->
[guacamole_connection_history].connection_id IN (
AND [guacamole_connection_history].connection_id IN (
<include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
<property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
<property name="groups" value="effectiveGroups"/>

View File

@@ -33,26 +33,6 @@
<result column="end_date" property="endDate" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- Select all user records from a given user -->
<select id="select" resultMap="UserRecordResultMap">
SELECT
[guacamole_user_history].remote_host,
[guacamole_user_history].user_id,
[guacamole_user_history].username,
[guacamole_user_history].start_date,
[guacamole_user_history].end_date
FROM [guacamole_user_history]
JOIN [guacamole_user] ON [guacamole_user_history].user_id = [guacamole_user].user_id
JOIN [guacamole_entity] ON [guacamole_user].entity_id = [guacamole_entity].entity_id
WHERE
[guacamole_entity].name = #{username,jdbcType=VARCHAR}
ORDER BY
[guacamole_user_history].start_date DESC,
[guacamole_user_history].end_date DESC
</select>
<!-- Insert the given user record -->
<insert id="insert" useGeneratedKeys="true" keyProperty="record.recordID"
parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
@@ -78,18 +58,10 @@
</insert>
<!-- Update the given user record -->
<update id="update" parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
<!-- Update the given user record, assigning an end date -->
<update id="updateEndDate" parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
UPDATE [guacamole_user_history]
SET remote_host = #{record.remoteHost,jdbcType=VARCHAR},
user_id = (SELECT user_id FROM [guacamole_user]
JOIN [guacamole_entity] ON [guacamole_user].entity_id = [guacamole_entity].entity_id
WHERE
[guacamole_entity].name = #{record.username,jdbcType=VARCHAR}
AND [guacamole_entity].type = 'USER'),
username = #{record.username,jdbcType=VARCHAR},
start_date = #{record.startDate,jdbcType=TIMESTAMP},
end_date = #{record.endDate,jdbcType=TIMESTAMP}
SET end_date = #{record.endDate,jdbcType=TIMESTAMP}
WHERE history_id = #{record.recordID,jdbcType=INTEGER}
</update>
@@ -97,6 +69,7 @@
<select id="search" resultMap="UserRecordResultMap">
SELECT TOP (#{limit,jdbcType=INTEGER})
[guacamole_user_history].history_id,
[guacamole_user_history].remote_host,
[guacamole_user_history].user_id,
[guacamole_user_history].username,
@@ -107,8 +80,8 @@
<!-- Search terms -->
<where>
<if test="username != null">
[guacamole_user_history].username = #{username,jdbcType=VARCHAR}
<if test="identifier != null">
[guacamole_user_history].username = #{identifier,jdbcType=VARCHAR}
</if>
<foreach collection="terms" item="term" open=" AND " separator=" AND ">
@@ -151,6 +124,7 @@
<select id="searchReadable" resultMap="UserRecordResultMap">
SELECT TOP (#{limit,jdbcType=INTEGER})
[guacamole_user_history].history_id,
[guacamole_user_history].remote_host,
[guacamole_user_history].user_id,
[guacamole_user_history].username,
@@ -169,8 +143,8 @@
</include>
)
<if test="username != null">
AND [guacamole_entity].name = #{username,jdbcType=VARCHAR}
<if test="identifier != null">
AND [guacamole_entity].name = #{identifier,jdbcType=VARCHAR}
</if>
<foreach collection="terms" item="term" open=" AND " separator=" AND ">

View File

@@ -0,0 +1,3 @@
src/main/resources/generated/
target/
*~

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-history-recording-storage</artifactId>
<packaging>jar</packaging>
<version>1.4.0</version>
<name>guacamole-history-recording-storage</name>
<url>http://guacamole.apache.org/</url>
<parent>
<groupId>org.apache.guacamole</groupId>
<artifactId>extensions</artifactId>
<version>1.4.0</version>
<relativePath>../</relativePath>
</parent>
<dependencies>
<!-- Guacamole Extension API -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
<version>1.4.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
<id>dist</id>
<baseDirectory>${project.artifactId}-${project.version}</baseDirectory>
<!-- Output tar.gz -->
<formats>
<format>tar.gz</format>
</formats>
<!-- Include licenses and extension .jar -->
<fileSets>
<!-- Include licenses -->
<fileSet>
<outputDirectory></outputDirectory>
<directory>target/licenses</directory>
</fileSet>
<!-- Include extension .jar -->
<fileSet>
<directory>target</directory>
<outputDirectory></outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,89 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.history;
import java.io.File;
import org.apache.guacamole.history.user.HistoryUserContext;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.properties.FileGuacamoleProperty;
/**
* AuthenticationProvider implementation which automatically associates history
* entries with session recordings, typescripts, etc. History association is
* determined by matching the history entry UUID with the filenames of files
* located within a standardized/configurable directory.
*/
public class HistoryAuthenticationProvider extends AbstractAuthenticationProvider {
/**
* The default directory to search for associated session recordings, if
* not overridden with the "recording-search-path" property.
*/
private static final File DEFAULT_RECORDING_SEARCH_PATH = new File("/var/lib/guacamole/recordings");
/**
* The directory to search for associated session recordings. By default,
* "/var/lib/guacamole/recordings" will be used.
*/
private static final FileGuacamoleProperty RECORDING_SEARCH_PATH = new FileGuacamoleProperty() {
@Override
public String getName() {
return "recording-search-path";
}
};
/**
* Returns the directory that should be searched for session recordings
* associated with history entries.
*
* @return
* The directory that should be searched for session recordings
* associated with history entries.
*
* @throws GuacamoleException
* If the "recording-search-path" property cannot be parsed.
*/
public static File getRecordingSearchPath() throws GuacamoleException {
Environment environment = LocalEnvironment.getInstance();
return environment.getProperty(RECORDING_SEARCH_PATH,
DEFAULT_RECORDING_SEARCH_PATH);
}
@Override
public String getIdentifier() {
return "recording-storage";
}
@Override
public UserContext decorate(UserContext context,
AuthenticatedUser authenticatedUser, Credentials credentials)
throws GuacamoleException {
return new HistoryUserContext(context.self(), context);
}
}

View File

@@ -0,0 +1,71 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.history.connection;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.ActivityRecordSet;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.ConnectionRecord;
import org.apache.guacamole.net.auth.DelegatingConnection;
import org.apache.guacamole.net.auth.User;
/**
* Connection implementation that automatically defines ActivityLogs for
* files that relate to history entries associated with the wrapped connection.
*/
public class HistoryConnection extends DelegatingConnection {
/**
* The current Guacamole user.
*/
private final User currentUser;
/**
* Creates a new HistoryConnection that wraps the given connection,
* automatically associating history entries with ActivityLogs based on
* related files (session recordings, typescripts, etc.).
*
* @param currentUser
* The current Guacamole user.
*
* @param connection
* The connection to wrap.
*/
public HistoryConnection(User currentUser, Connection connection) {
super(connection);
this.currentUser = currentUser;
}
/**
* Returns the connection wrapped by this HistoryConnection.
*
* @return
* The connection wrapped by this HistoryConnection.
*/
public Connection getWrappedConnection() {
return getDelegateConnection();
}
@Override
public ActivityRecordSet<ConnectionRecord> getConnectionHistory() throws GuacamoleException {
return new RecordedConnectionActivityRecordSet(currentUser, super.getConnectionHistory());
}
}

View File

@@ -0,0 +1,284 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.history.connection;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.MalformedURLException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.history.HistoryAuthenticationProvider;
import org.apache.guacamole.io.GuacamoleReader;
import org.apache.guacamole.io.ReaderGuacamoleReader;
import org.apache.guacamole.language.TranslatableMessage;
import org.apache.guacamole.net.auth.ActivityLog;
import org.apache.guacamole.net.auth.ConnectionRecord;
import org.apache.guacamole.net.auth.DelegatingConnectionRecord;
import org.apache.guacamole.net.auth.FileActivityLog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ConnectionRecord implementation that automatically defines ActivityLogs for
* files that relate to the wrapped record.
*/
public class HistoryConnectionRecord extends DelegatingConnectionRecord {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(HistoryConnectionRecord.class);
/**
* The namespace for URL UUIDs as defined by RFC 4122.
*/
private static final UUID UUID_NAMESPACE_URL = UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8");
/**
* The filename suffix of typescript timing files.
*/
private static final String TIMING_FILE_SUFFIX = ".timing";
/**
* The recording file associated with the wrapped connection record. This
* may be a single file or a directory that may contain any number of
* relevant recordings.
*/
private final File recording;
/**
* Creates a new HistoryConnectionRecord that wraps the given
* ConnectionRecord, automatically associating ActivityLogs based on
* related files (session recordings, typescripts, etc.).
*
* @param record
* The ConnectionRecord to wrap.
*
* @throws GuacamoleException
* If the configured path for stored recordings cannot be read.
*/
public HistoryConnectionRecord(ConnectionRecord record) throws GuacamoleException {
super(record);
String uuid = record.getUUID().toString();
File recordingFile = new File(HistoryAuthenticationProvider.getRecordingSearchPath(), uuid);
this.recording = recordingFile.canRead() ? recordingFile : null;
}
/**
* Returns whether the given file appears to be a Guacamole session
* recording. As there is no standard extension for session recordings,
* this is determined by attempting to read a single Guacamole instruction
* from the file.
*
* @param file
* The file to test.
*
* @return
* true if the file appears to be a Guacamole session recording, false
* otherwise.
*/
private boolean isSessionRecording(File file) {
try (Reader reader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
GuacamoleReader guacReader = new ReaderGuacamoleReader(reader);
if (guacReader.readInstruction() != null)
return true;
}
catch (GuacamoleException e) {
logger.debug("File \"{}\" does not appear to be a session "
+ "recording, as it could not be parsed as Guacamole "
+ "protocol data.", file, e);
}
catch (IOException e) {
logger.warn("Possible session recording \"{}\" could not be "
+ "identified as it cannot be read: {}", file, e.getMessage());
logger.debug("Possible session recording \"{}\" could not be read.", file, e);
}
return false;
}
/**
* Returns whether the given file appears to be a typescript (text
* recording of a terminal session). As there is no standard extension for
* session recordings, this is determined by testing whether there is an
* associated timing file. Guacamole will always include a timing file for
* its typescripts.
*
* @param file
* The file to test.
*
* @return
* true if the file appears to be a typescript, false otherwise.
*/
private boolean isTypescript(File file) {
return new File(file.getAbsolutePath() + TIMING_FILE_SUFFIX).exists();
}
/**
* Returns whether the given file appears to be a typescript timing file.
* Typescript timing files have the standard extension ".timing".
*
* @param file
* The file to test.
*
* @return
* true if the file appears to be a typescript timing file, false
* otherwise.
*/
private boolean isTypescriptTiming(File file) {
return file.getName().endsWith(TIMING_FILE_SUFFIX);
}
/**
* Returns the type of session recording or log contained within the given
* file by inspecting its name and contents.
*
* @param file
* The file to test.
*
* @return
* The type of session recording or log contained within the given
* file, or null if this cannot be determined.
*/
private ActivityLog.Type getType(File file) {
if (isSessionRecording(file))
return ActivityLog.Type.GUACAMOLE_SESSION_RECORDING;
if (isTypescript(file))
return ActivityLog.Type.TYPESCRIPT;
if (isTypescriptTiming(file))
return ActivityLog.Type.TYPESCRIPT_TIMING;
return ActivityLog.Type.SERVER_LOG;
}
/**
* Returns a new ActivityLog instance representing the session recording or
* log contained within the given file. If the type of recording/log cannot
* be determined, or if the file is unreadable, null is returned.
*
* @param file
* The file to produce an ActivityLog instance for.
*
* @return
* A new ActivityLog instance representing the recording/log contained
* within the given file, or null if the file is unreadable or cannot
* be identified.
*/
private ActivityLog getActivityLog(File file) {
// Verify file can actually be read
if (!file.canRead()) {
logger.warn("Ignoring file \"{}\" relevant to connection history "
+ "record as it cannot be read.", file);
return null;
}
// Determine type of recording/log by inspecting file
ActivityLog.Type logType = getType(file);
if (logType == null) {
logger.warn("Recording/log type of \"{}\" cannot be determined.", file);
return null;
}
return new FileActivityLog(
logType,
new TranslatableMessage("RECORDING_STORAGE.INFO_" + logType.name()),
file
);
}
/**
* Adds an ActivityLog instance representing the session recording or log
* contained within the given file to the given map of logs. If no
* ActivityLog can be produced for the given file (it is unreadable or
* cannot be identified), this function has no effect.
*
* @param logs
* The map of logs to add the ActivityLog to.
*
* @param file
* The file to produce an ActivityLog instance for.
*/
private void addActivityLog(Map<String, ActivityLog> logs, File file) {
ActivityLog log = getActivityLog(file);
if (log == null)
return;
// Convert file into deterministic name UUID within URL namespace
UUID fileUUID;
try {
byte[] urlBytes = file.toURI().toURL().toString().getBytes(StandardCharsets.UTF_8);
fileUUID = UUID.nameUUIDFromBytes(ByteBuffer.allocate(16 + urlBytes.length)
.putLong(UUID_NAMESPACE_URL.getMostSignificantBits())
.putLong(UUID_NAMESPACE_URL.getLeastSignificantBits())
.put(urlBytes)
.array());
}
catch (MalformedURLException e) {
logger.warn("Ignoring file \"{}\" as a unique URL and UUID for that file could not be generated: {}", e.getMessage());
logger.debug("URL for file \"{}\" could not be determined.", file, e);
return;
}
logs.put(fileUUID.toString(), log);
}
@Override
public Map<String, ActivityLog> getLogs() {
// Do nothing if there are no associated logs
if (recording == null)
return super.getLogs();
// Add associated log (or logs, if this is a directory)
Map<String, ActivityLog> logs = new HashMap<>(super.getLogs());
if (recording.isDirectory()) {
Arrays.asList(recording.listFiles()).stream()
.forEach((file) -> addActivityLog(logs, file));
}
else
addActivityLog(logs, recording);
return logs;
}
}

View File

@@ -0,0 +1,125 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.history.connection;
import java.util.Collections;
import java.util.Set;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.ActivityRecordSet;
import org.apache.guacamole.net.auth.ConnectionRecord;
import org.apache.guacamole.net.auth.DecoratingActivityRecordSet;
import org.apache.guacamole.net.auth.Permissions;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.permission.ObjectPermission;
import org.apache.guacamole.net.auth.permission.SystemPermission;
/**
* ActivityRecordSet implementation that automatically defines ActivityLogs for
* files that relate to history entries within the wrapped set.
*/
public class RecordedConnectionActivityRecordSet extends DecoratingActivityRecordSet<ConnectionRecord> {
/**
* Whether the current user is an administrator.
*/
private final boolean isAdmin;
/**
* The overall set of connection permissions defined for the current user.
*/
private final Set<ObjectPermission> connectionPermissions;
/**
* Creates a new RecordedConnectionActivityRecordSet that wraps the given
* ActivityRecordSet, automatically associating history entries with
* ActivityLogs based on related files (session recordings, typescripts,
* etc.).
*
* @param currentUser
* The current Guacamole user.
*
* @param activityRecordSet
* The ActivityRecordSet to wrap.
*
* @throws GuacamoleException
* If the permissions for the current user cannot be retrieved.
*/
public RecordedConnectionActivityRecordSet(User currentUser,
ActivityRecordSet<ConnectionRecord> activityRecordSet)
throws GuacamoleException {
super(activityRecordSet);
// Determine whether current user is an administrator
Permissions perms = currentUser.getEffectivePermissions();
isAdmin = perms.getSystemPermissions().hasPermission(SystemPermission.Type.ADMINISTER);
// If not an admin, additionally pull specific connection permissions
if (isAdmin)
connectionPermissions = Collections.emptySet();
else
connectionPermissions = perms.getConnectionPermissions().getPermissions();
}
/**
* Returns whether the current user has permission to view the logs
* associated with the given history record. It is already given that the
* user has permission to view the history record itself. This extension
* considers a user to have permission to view history logs if they are
* an administrator or if they have permission to edit the associated
* connection.
*
* @param record
* The record to check.
*
* @return
* true if the current user has permission to view the logs associated
* with the given record, false otherwise.
*/
private boolean canViewLogs(ConnectionRecord record) {
// Administrator can always view
if (isAdmin)
return true;
// Non-administrator CANNOT view if permissions cannot be verified
String identifier = record.getConnectionIdentifier();
if (identifier == null)
return false;
// Non-administer can only view if they implicitly have permission to
// configure recordings (they have permission to edit)
ObjectPermission canUpdate = new ObjectPermission(ObjectPermission.Type.UPDATE, identifier);
return connectionPermissions.contains(canUpdate);
}
@Override
protected ConnectionRecord decorate(ConnectionRecord record) throws GuacamoleException {
// Provide access to logs only if permission is granted
if (canViewLogs(record))
return new HistoryConnectionRecord(record);
return record;
}
}

View File

@@ -0,0 +1,122 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.history.user;
import java.util.Collections;
import java.util.Map;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.history.HistoryAuthenticationProvider;
import org.apache.guacamole.history.connection.HistoryConnection;
import org.apache.guacamole.history.connection.RecordedConnectionActivityRecordSet;
import org.apache.guacamole.net.auth.ActivityRecordSet;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.ConnectionGroup;
import org.apache.guacamole.net.auth.ConnectionRecord;
import org.apache.guacamole.net.auth.DecoratingDirectory;
import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.TokenInjectingUserContext;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.UserContext;
/**
* UserContext implementation that automatically defines ActivityLogs for
* files that relate to history entries.
*/
public class HistoryUserContext extends TokenInjectingUserContext {
/**
* The name of the parameter token that contains the automatically-searched
* history recording/log path.
*/
private static final String HISTORY_PATH_TOKEN_NAME = "HISTORY_PATH";
/**
* The current Guacamole user.
*/
private final User currentUser;
/**
* Creates a new HistoryUserContext that wraps the given UserContext,
* automatically associating history entries with ActivityLogs based on
* related files (session recordings, typescripts, etc.).
*
* @param currentUser
* The current Guacamole user.
*
* @param context
* The UserContext to wrap.
*/
public HistoryUserContext(User currentUser, UserContext context) {
super(context);
this.currentUser = currentUser;
}
/**
* Returns the tokens which should be added to an in-progress call to
* connect() for any Connectable object.
*
* @return
* The tokens which should be added to the in-progress call to
* connect().
*
* @throws GuacamoleException
* If the relevant tokens cannot be generated.
*/
private Map<String, String> getTokens() throws GuacamoleException {
return Collections.singletonMap(HISTORY_PATH_TOKEN_NAME,
HistoryAuthenticationProvider.getRecordingSearchPath().getAbsolutePath());
}
@Override
protected Map<String, String> getTokens(ConnectionGroup connectionGroup)
throws GuacamoleException {
return getTokens();
}
@Override
protected Map<String, String> getTokens(Connection connection)
throws GuacamoleException {
return getTokens();
}
@Override
public Directory<Connection> getConnectionDirectory() throws GuacamoleException {
return new DecoratingDirectory<Connection>(super.getConnectionDirectory()) {
@Override
protected Connection decorate(Connection object) {
return new HistoryConnection(currentUser, object);
}
@Override
protected Connection undecorate(Connection object) throws GuacamoleException {
return ((HistoryConnection) object).getWrappedConnection();
}
};
}
@Override
public ActivityRecordSet<ConnectionRecord> getConnectionHistory()
throws GuacamoleException {
return new RecordedConnectionActivityRecordSet(currentUser, super.getConnectionHistory());
}
}

View File

@@ -0,0 +1,16 @@
{
"guacamoleVersion" : "1.4.0",
"name" : "Session Recording Storage",
"namespace" : "recording-storage",
"authProviders" : [
"org.apache.guacamole.history.HistoryAuthenticationProvider"
],
"translations" : [
"translations/en.json"
]
}

View File

@@ -0,0 +1,14 @@
{
"DATA_SOURCE_RECORDING_STORAGE" : {
"NAME" : "Session Recording Storage"
},
"RECORDING_STORAGE" : {
"INFO_GUACAMOLE_SESSION_RECORDING" : "Graphical recording of remote desktop session",
"INFO_SERVER_LOG" : "Server/system log",
"INFO_TYPESCRIPT" : "Text recording of terminal session",
"INFO_TYPESCRIPT_TIMING" : "Timing information for text recording of terminal session"
}
}

View File

@@ -50,6 +50,7 @@
<module>guacamole-auth-totp</module>
<!-- Additional features -->
<module>guacamole-history-recording-storage</module>
<module>guacamole-vault</module>
</modules>

View File

@@ -0,0 +1,65 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.net.auth;
import org.apache.guacamole.language.TranslatableMessage;
/**
* Base implementation of an ActivityLog, providing storage and simple
* getters/setters for its main properties.
*/
public abstract class AbstractActivityLog implements ActivityLog {
/**
* The type of this ActivityLog.
*/
private final Type type;
/**
* A human-readable description of this log.
*/
private final TranslatableMessage description;
/**
* Creates a new AbstractActivityLog having the given type and
* human-readable description.
*
* @param type
* The type of this ActivityLog.
*
* @param description
* A human-readable message that describes this log.
*/
public AbstractActivityLog(Type type, TranslatableMessage description) {
this.type = type;
this.description = description;
}
@Override
public Type getType() {
return type;
}
@Override
public TranslatableMessage getDescription() {
return description;
}
}

View File

@@ -0,0 +1,142 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.net.auth;
import java.io.InputStream;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.language.TranslatableMessage;
/**
* An arbitrary log of an activity whose content may be exposed to a user with
* sufficient privileges. Types of content that might be exposed in this way
* include textual server logs, Guacamole session recordings, and typescripts.
*/
public interface ActivityLog {
/**
* The value returned by {@link #getSize()} if the number of available
* bytes within {@link #getContent()} is unknown.
*/
public static final long UNKNOWN_SIZE = -1;
/**
* All possible types of {@link ActivityLog}.
*/
enum Type {
/**
* A Guacamole session recording in the form of a Guacamole protocol
* dump.
*/
GUACAMOLE_SESSION_RECORDING("application/octet-stream"),
/**
* A text log from a server-side process, such as the Guacamole web
* application or guacd.
*/
SERVER_LOG("text/plain"),
/**
* A text session recording in the form of a standard typescript.
*/
TYPESCRIPT("application/octet-stream"),
/**
* The timing file related to a typescript.
*/
TYPESCRIPT_TIMING("text/plain");
/**
* The MIME type of the content of an activity log of this type.
*/
private final String contentType;
/**
* Creates a new Type that may be associated with content having the
* given MIME type.
*
* @param contentType
* The MIME type of the content of an activity log of this type.
*/
Type(String contentType) {
this.contentType = contentType;
}
/**
* Returns the MIME type of the content of an activity log of this
* type, as might be sent via the HTTP "Content-Type" header.
*
* @return
* The MIME type of the content of an activity log of this type.
*/
public String getContentType() {
return contentType;
}
}
/**
* Returns the type of this activity log. The type of an activity log
* dictates how its content should be interpreted or exposed.
*
* @return
* The type of this activity log.
*/
Type getType();
/**
* Returns a human-readable message that describes this log. This message
* should provide sufficient information for a user with access to this
* log to understand its context and/or purpose.
*
* @return
* A human-readable message that describes this log.
*/
TranslatableMessage getDescription();
/**
* Returns the number of bytes available for reading within the content of
* this log. If this value is unknown, -1 ({@link #UNKNOWN_SIZE}) should be
* returned.
*
* @return
* The number of bytes available for reading within the content of
* this log, or -1 ({@link #UNKNOWN_SIZE}) if this value is unknown.
*
* @throws GuacamoleException
* If the size of the content of this log cannot be determined due to
* an error.
*/
long getSize() throws GuacamoleException;
/**
* Returns an InputStream that allows the content of this log to be read.
* Multiple instances of this InputStream may be open at any given time. It
* is the responsibility of the caller to close the returned InputStream.
*
* @return
* An InputStream that allows the content of this log to be read.
*
* @throws GuacamoleException
* If the content of this log cannot be read due to an error.
*/
InputStream getContent() throws GuacamoleException;
}

View File

@@ -19,13 +19,16 @@
package org.apache.guacamole.net.auth;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
/**
* A logging record describing when a user started and ended a particular
* activity.
*/
public interface ActivityRecord {
public interface ActivityRecord extends ReadableAttributes {
/**
* Returns the date and time the activity began.
@@ -75,4 +78,52 @@ public interface ActivityRecord {
*/
public boolean isActive();
/**
* Returns the unique identifier assigned to this record, if any. If this
* record is not uniquely identifiable, this may be null. If provided, this
* unique identifier MUST be unique across all {@link ActivityRecord}
* objects within the same {@link ActivityRecordSet}.
*
* @return
* The unique identifier assigned to this record, or null if this
* record has no such identifier.
*/
public default String getIdentifier() {
UUID uuid = getUUID();
return uuid != null ? uuid.toString() : null;
}
/**
* Returns a UUID that uniquely identifies this record. If provided, this
* UUID MUST be deterministic and unique across all {@link ActivityRecord}
* objects within the same {@link ActivityRecordSet}, and SHOULD be unique
* across all {@link ActivityRecord} objects.
*
* @return
* A UUID that uniquely identifies this record, or null if no such
* unique identifier exists.
*/
public default UUID getUUID() {
return null;
}
/**
* Returns a Map of logs related to this record and accessible by the
* current user, such as Guacamole session recordings. Each log is
* associated with a corresponding, arbitrary, unique name. If the user
* does not have access to any logs, or if no logs are available, this may
* be an empty map.
*
* @return
* A Map of logs related to this record.
*/
public default Map<String, ActivityLog> getLogs() {
return Collections.emptyMap();
}
@Override
public default Map<String, String> getAttributes() {
return Collections.emptyMap();
}
}

View File

@@ -57,6 +57,30 @@ public interface ActivityRecordSet<RecordType extends ActivityRecord> {
*/
Collection<RecordType> asCollection() throws GuacamoleException;
/**
* Returns the record having the given unique identifier, if records within
* this set have unique identifiers. If records within this set do not have
* defined unique identifiers, this function has no effect.
*
* @param identifier
* The unique identifier of the record to retrieve.
*
* @return
* The record having the given unique identifier, or null if there is
* no such record.
*
* @throws GuacamoleException
* If an error occurs while retrieving the record.
*/
default RecordType get(String identifier) throws GuacamoleException {
return asCollection().stream()
.filter((record) -> {
String recordIdentifier = record.getIdentifier();
return recordIdentifier != null && recordIdentifier.equals(identifier);
})
.findFirst().orElse(null);
}
/**
* Returns the subset of records which contain the given value. The
* properties and semantics involved with determining whether a particular

View File

@@ -22,21 +22,10 @@ package org.apache.guacamole.net.auth;
import java.util.Map;
/**
* An object which is associated with a set of arbitrary attributes, defined
* as name/value pairs.
* An object which is associated with a set of arbitrary attributes that may
* be modifiable, defined as name/value pairs.
*/
public interface Attributes {
/**
* Returns all attributes associated with this object. The returned map
* may not be modifiable.
*
* @return
* A map of all attribute identifiers to their corresponding values,
* for all attributes associated with this object, which may not be
* modifiable.
*/
Map<String, String> getAttributes();
public interface Attributes extends ReadableAttributes {
/**
* Sets the given attributes. If an attribute within the map is not

View File

@@ -19,6 +19,9 @@
package org.apache.guacamole.net.auth;
import java.util.UUID;
import org.apache.guacamole.net.GuacamoleTunnel;
/**
* A logging record describing when a user started and ended usage of a
* particular connection.
@@ -70,4 +73,15 @@ public interface ConnectionRecord extends ActivityRecord {
*/
public String getSharingProfileName();
/**
* {@inheritDoc}
* <p>If implemented, this UUID SHOULD be identical to the UUID of the
* {@link GuacamoleTunnel} originally returned when the connection was
* established to allow extensions and/or the web application to
* automatically associate connection information with corresponding
* history records, such as log messages and session recordings.
*/
@Override
public UUID getUUID();
}

View File

@@ -0,0 +1,165 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.net.auth;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.guacamole.GuacamoleException;
/**
* ActivityRecordSet implementation which simplifies decorating the records
* within an underlying ActivityRecordSet. The decorate() function must be
* implemented to define how each record is decorated. As ActivityRecordSets
* are read-only, there is no need to define an undecorate() function as
* required by {@link DecoratingDirectory}.
*
* @param <RecordType>
* The type of records stored within this ActivityRecordSet.
*/
public abstract class DecoratingActivityRecordSet<RecordType extends ActivityRecord>
extends DelegatingActivityRecordSet<RecordType> {
/**
* Creates a new DecoratingActivityRecordSet which decorates the records
* within the given set.
*
* @param recordSet
* The ActivityRecordSet whose records are being decorated.
*/
public DecoratingActivityRecordSet(ActivityRecordSet<RecordType> recordSet) {
super(recordSet);
}
/**
* Given a record retrieved from a ActivityRecordSet which originates from
* a different AuthenticationProvider, returns an identical type of record
* optionally wrapped with additional information, functionality, etc. If
* this record set chooses to decorate the record provided, it is up to the
* implementation of that decorated record to properly pass through
* operations as appropriate. All records retrieved from this
* DecoratingActivityRecordSet will first be passed through this function.
*
* @param record
* A record from a ActivityRecordSet which originates from a different
* AuthenticationProvider.
*
* @return
* A record which may have been decorated by this
* DecoratingActivityRecordSet. If the record was not decorated, the
* original, unmodified record may be returned instead.
*
* @throws GuacamoleException
* If the provided record cannot be decorated due to an error.
*/
protected abstract RecordType decorate(RecordType record)
throws GuacamoleException;
/**
* Given an ActivityRecordSet which originates from a different
* AuthenticationProvider, returns an identical type of record set
* optionally wrapped with additional information, functionality, etc. If
* this record set chooses to decorate the record set provided, it is up to
* the implementation of that decorated record set to properly pass through
* operations as appropriate. All record sets retrieved from this
* DecoratingActivityRecordSet will first be passed through this function,
* such as those returned by {@link #limit(int)} and similar functions.
* <p>
* By default, this function will wrap any provided ActivityRecordSet in a
* simple, anonymous instance of DecoratingActivityRecordSet that delegates
* to the decorate() implementations of this DecoratingActivityRecordSet.
* <strong>This default behavior may need to be overridden if the
* DecoratingActivityRecordSet implementation maintains any internal
* state.</strong>
*
* @param recordSet
* An ActivityRecordSet which originates from a different
* AuthenticationProvider.
*
* @return
* A record set which may have been decorated by this
* DecoratingActivityRecordSet. If the record set was not decorated, the
* original, unmodified record set may be returned instead, however
* beware that this may result in records within the set no longer
* being decorated.
*
* @throws GuacamoleException
* If the provided record set cannot be decorated due to an error.
*/
protected ActivityRecordSet<RecordType> decorate(ActivityRecordSet<RecordType> recordSet)
throws GuacamoleException {
final DecoratingActivityRecordSet<RecordType> decorator = this;
return new DecoratingActivityRecordSet<RecordType>(recordSet) {
@Override
protected RecordType decorate(RecordType record) throws GuacamoleException {
return decorator.decorate(record);
}
@Override
protected ActivityRecordSet<RecordType> decorate(ActivityRecordSet<RecordType> recordSet)
throws GuacamoleException {
return decorator.decorate(recordSet);
}
};
}
@Override
public RecordType get(String string) throws GuacamoleException {
RecordType record = super.get(string);
if (record != null)
return decorate(record);
return null;
}
@Override
public ActivityRecordSet<RecordType> sort(SortableProperty property,
boolean desc) throws GuacamoleException {
return decorate(super.sort(property, desc));
}
@Override
public ActivityRecordSet<RecordType> limit(int limit) throws GuacamoleException {
return decorate(super.limit(limit));
}
@Override
public ActivityRecordSet<RecordType> contains(String value) throws GuacamoleException {
return decorate(super.contains(value));
}
@Override
public Collection<RecordType> asCollection() throws GuacamoleException {
Collection<RecordType> records = super.asCollection();
List<RecordType> decoratedRecords = new ArrayList<>(records.size());
for (RecordType record : records)
decoratedRecords.add(decorate(record));
return decoratedRecords;
}
}

View File

@@ -0,0 +1,104 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.net.auth;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
/**
* ActivityRecord implementation which simply delegates all function calls to an
* underlying ActivityRecord.
*/
public class DelegatingActivityRecord implements ActivityRecord {
/**
* The wrapped ActivityRecord.
*/
private final ActivityRecord record;
/**
* Wraps the given ActivityRecord such that all function calls against this
* DelegatingActivityRecord will be delegated to it.
*
* @param record
* The record to wrap.
*/
public DelegatingActivityRecord(ActivityRecord record) {
this.record = record;
}
/**
* Returns the underlying ActivityRecord wrapped by this
* DelegatingActivityRecord.
*
* @return
* The ActivityRecord wrapped by this DelegatingActivityRecord.
*/
protected ActivityRecord getDelegateActivityRecord() {
return record;
}
@Override
public Date getStartDate() {
return record.getStartDate();
}
@Override
public Date getEndDate() {
return record.getEndDate();
}
@Override
public String getRemoteHost() {
return record.getRemoteHost();
}
@Override
public String getUsername() {
return record.getUsername();
}
@Override
public boolean isActive() {
return record.isActive();
}
@Override
public String getIdentifier() {
return record.getIdentifier();
}
@Override
public UUID getUUID() {
return record.getUUID();
}
@Override
public Map<String, ActivityLog> getLogs() {
return record.getLogs();
}
@Override
public Map<String, String> getAttributes() {
return record.getAttributes();
}
}

View File

@@ -0,0 +1,88 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.net.auth;
import java.util.Collection;
import org.apache.guacamole.GuacamoleException;
/**
* ActivityRecordSet implementation which simply delegates all function calls
* to an underlying ActivityRecordSet.
*
* @param <RecordType>
* The type of ActivityRecord contained within this set.
*/
public class DelegatingActivityRecordSet<RecordType extends ActivityRecord>
implements ActivityRecordSet<RecordType> {
/**
* The wrapped ActivityRecordSet.
*/
private final ActivityRecordSet<RecordType> recordSet;
/**
* Wraps the given ActivityRecordSet such that all function calls against this
* DelegatingActivityRecordSet will be delegated to it.
*
* @param recordSet
* The ActivityRecordSet to wrap.
*/
public DelegatingActivityRecordSet(ActivityRecordSet<RecordType> recordSet) {
this.recordSet = recordSet;
}
/**
* Returns the underlying ActivityRecordSet wrapped by this
* DelegatingActivityRecordSet.
*
* @return
* The ActivityRecordSet wrapped by this DelegatingActivityRecordSet.
*/
protected ActivityRecordSet<RecordType> getDelegateActivityRecordSet() {
return recordSet;
}
@Override
public RecordType get(String identifier) throws GuacamoleException {
return recordSet.get(identifier);
}
@Override
public Collection<RecordType> asCollection() throws GuacamoleException {
return recordSet.asCollection();
}
@Override
public ActivityRecordSet<RecordType> contains(String value) throws GuacamoleException {
return recordSet.contains(value);
}
@Override
public ActivityRecordSet<RecordType> limit(int limit) throws GuacamoleException {
return recordSet.limit(limit);
}
@Override
public ActivityRecordSet<RecordType> sort(SortableProperty property,
boolean desc) throws GuacamoleException {
return recordSet.sort(property, desc);
}
}

View File

@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.net.auth;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
/**
* ConnectionRecord implementation which simply delegates all function calls to
* an underlying ConnectionRecord.
*/
public class DelegatingConnectionRecord extends DelegatingActivityRecord
implements ConnectionRecord {
/**
* The wrapped ConnectionRecord.
*/
private final ConnectionRecord record;
/**
* Wraps the given ConnectionRecord such that all function calls against
* this DelegatingConnectionRecord will be delegated to it.
*
* @param record
* The record to wrap.
*/
public DelegatingConnectionRecord(ConnectionRecord record) {
super(record);
this.record = record;
}
/**
* Returns the underlying ConnectionRecord wrapped by this
* DelegatingConnectionRecord.
*
* @return
* The ConnectionRecord wrapped by this DelegatingConnectionRecord.
*/
protected ConnectionRecord getDelegateConnectionRecord() {
return record;
}
@Override
public String getConnectionIdentifier() {
return record.getConnectionIdentifier();
}
@Override
public String getConnectionName() {
return record.getConnectionName();
}
@Override
public String getSharingProfileIdentifier() {
return record.getSharingProfileIdentifier();
}
@Override
public String getSharingProfileName() {
return record.getSharingProfileName();
}
}

View File

@@ -0,0 +1,74 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.net.auth;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceNotFoundException;
import org.apache.guacamole.language.TranslatableMessage;
/**
* ActivityLog implementation that exposes the content of a local file.
*/
public class FileActivityLog extends AbstractActivityLog {
/**
* The File providing the content of this log.
*/
private final File content;
/**
* Creates a new FileActivityLog that exposes the content of the given
* local file as an {@link ActivityLog}.
*
* @param type
* The type of this ActivityLog.
*
* @param description
* A human-readable message that describes this log.
*
* @param content
* The File that should be used to provide the content of this log.
*/
public FileActivityLog(Type type, TranslatableMessage description, File content) {
super(type, description);
this.content = content;
}
@Override
public long getSize() throws GuacamoleException {
return content.length();
}
@Override
public InputStream getContent() throws GuacamoleException {
try {
return new FileInputStream(content);
}
catch (FileNotFoundException e) {
throw new GuacamoleResourceNotFoundException("Associated file "
+ "does not exist or cannot be read.", e);
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.net.auth;
import java.util.Map;
/**
* An object which is associated with a set of arbitrary attributes, defined
* as name/value pairs.
*/
public interface ReadableAttributes {
/**
* Returns all attributes associated with this object. The returned map
* may not be modifiable.
*
* @return
* A map of all attribute identifiers to their corresponding values,
* for all attributes associated with this object, which may not be
* modifiable.
*/
Map<String, String> getAttributes();
}

View File

@@ -0,0 +1,89 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Service which defines the ActivityLog class.
*/
angular.module('rest').factory('ActivityLog', [function defineActivityLog() {
/**
* The object returned by REST API calls when representing a log or
* recording associated with a connection's usage history, such as a
* session recording or typescript.
*
* @constructor
* @param {ActivityLog|Object} [template={}]
* The object whose properties should be copied within the new
* ActivityLog.
*/
var ActivityLog = function ActivityLog(template) {
// Use empty object by default
template = template || {};
/**
* The type of this ActivityLog.
*
* @type {string}
*/
this.type = template.type;
/**
* A human-readable description of this log.
*
* @type {TranslatableMessage}
*/
this.description = template.description;
};
/**
* All possible types of ActivityLog.
*
* @type {!object.<string, string>}
*/
ActivityLog.Type = {
/**
* A Guacamole session recording in the form of a Guacamole protocol
* dump.
*/
GUACAMOLE_SESSION_RECORDING : 'GUACAMOLE_SESSION_RECORDING',
/**
* A text log from a server-side process, such as the Guacamole web
* application or guacd.
*/
SERVER_LOG : 'SERVER_LOG',
/**
* A text session recording in the form of a standard typescript.
*/
TYPESCRIPT : 'TYPESCRIPT',
/**
* The timing file related to a typescript.
*/
TYPESCRIPT_TIMING : 'TYPESCRIPT_TIMING'
};
return ActivityLog;
}]);

View File

@@ -38,6 +38,23 @@ angular.module('rest').factory('ConnectionHistoryEntry', [function defineConnect
// Use empty object by default
template = template || {};
/**
* An arbitrary identifier that uniquely identifies this record
* relative to other records in the same set, or null if no such unique
* identifier exists.
*
* @type {string}
*/
this.identifier = template.identifier;
/**
* A UUID that uniquely identifies this record, or null if no such
* unique identifier exists.
*
* @type {string}
*/
this.uuid = template.uuid;
/**
* The identifier of the connection associated with this history entry.
*
@@ -103,6 +120,23 @@ angular.module('rest').factory('ConnectionHistoryEntry', [function defineConnect
*/
this.active = template.active;
/**
* Arbitrary name/value pairs which further describe this history
* entry. The semantics and validity of these attributes are dictated
* by the extension which defines them.
*
* @type {!object.<string, string>}
*/
this.attributes = template.attributes;
/**
* All logs associated and accessible via this record, stored by their
* corresponding unique names.
*
* @type {!object.<string, ActivityLog>}
*/
this.logs = template.logs;
};
/**

View File

@@ -0,0 +1,74 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.rest.history;
import org.apache.guacamole.language.TranslatableMessage;
import org.apache.guacamole.net.auth.ActivityLog;
/**
* A activity log which may be exposed through the REST endpoints.
*/
public class APIActivityLog {
/**
* The type of this ActivityLog.
*/
private final ActivityLog.Type type;
/**
* A human-readable description of this log.
*/
private final TranslatableMessage description;
/**
* Creates a new APIActivityLog, copying the data from the given activity
* log.
*
* @param log
* The log to copy data from.
*/
public APIActivityLog(ActivityLog log) {
this.type = log.getType();
this.description = log.getDescription();
}
/**
* Returns the type of this activity log. The type of an activity log
* dictates how its content should be interpreted or exposed, however the
* content of a log is not directly exposed by this class.
*
* @return
* The type of this activity log.
*/
public ActivityLog.Type getType() {
return type;
}
/**
* Returns a human-readable message that describes this log.
*
* @return
* A human-readable message that describes this log.
*/
public TranslatableMessage getDescription() {
return description;
}
}

View File

@@ -20,6 +20,9 @@
package org.apache.guacamole.rest.history;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.guacamole.net.auth.ActivityRecord;
/**
@@ -55,6 +58,30 @@ public class APIActivityRecord {
*/
private final boolean active;
/**
* The unique identifier assigned to this record, or null if this record
* has no such identifier.
*/
private final String identifier;
/**
* A UUID that uniquely identifies this record, or null if no such unique
* identifier exists.
*/
private final UUID uuid;
/**
* A map of all attribute identifiers to their corresponding values, for
* all attributes associated with this record.
*/
private final Map<String, String> attributes;
/**
* A map of all logs associated and accessible via this record, associated
* with their corresponding unique names.
*/
private final Map<String, APIActivityLog> logs;
/**
* Creates a new APIActivityRecord, copying the data from the given activity
* record.
@@ -63,11 +90,21 @@ public class APIActivityRecord {
* The record to copy data from.
*/
public APIActivityRecord(ActivityRecord record) {
this.startDate = record.getStartDate();
this.endDate = record.getEndDate();
this.remoteHost = record.getRemoteHost();
this.username = record.getUsername();
this.active = record.isActive();
this.identifier = record.getIdentifier();
this.uuid = record.getUUID();
this.attributes = record.getAttributes();
this.logs = record.getLogs().entrySet().stream().collect(Collectors.toMap(
Map.Entry::getKey,
(entry) -> new APIActivityLog(entry.getValue())
));
}
/**
@@ -128,4 +165,51 @@ public class APIActivityRecord {
return active;
}
/**
* Returns the unique identifier assigned to this record, if any. If this
* record is not uniquely identifiable, this may be null.
*
* @return
* The unique identifier assigned to this record, or null if this
* record has no such identifier.
*/
public String getIdentifier() {
return identifier;
}
/**
* Returns a UUID that uniquely identifies this record. If not implemented
* by the extension exposing this history record, this may be null.
*
* @return
* A UUID that uniquely identifies this record, or null if no such
* unique identifier exists.
*/
public UUID getUUID() {
return uuid;
}
/**
* Returns all attributes associated with this record.
*
* @return
* A map of all attribute identifiers to their corresponding values,
* for all attributes associated with this record.
*/
public Map<String, String> getAttributes() {
return attributes;
}
/**
* Returns a Map of logs related to this record and accessible by the
* current user, such as Guacamole session recordings. Each log is
* associated with a corresponding, arbitrary, unique name.
*
* @return
* A Map of logs related to this record.
*/
public Map<String, APIActivityLog> getLogs() {
return logs;
}
}

View File

@@ -0,0 +1,77 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.rest.history;
import javax.ws.rs.GET;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.ActivityLog;
/**
* A REST resource which exposes the contents of a given ActivityLog.
*/
public class ActivityLogResource {
/**
* The ActivityLog whose contents are being exposed.
*/
private final ActivityLog log;
/**
* Creates a new ActivityLogResource which exposes the records within the
* given ActivityLog.
*
* @param log
* The ActivityLog whose contents should be exposed.
*/
public ActivityLogResource(ActivityLog log) {
this.log = log;
}
/**
* Returns the raw contents of the underlying ActivityLog. If the size of
* the ActivityLog is known, this size is included as the "Content-Length"
* of the response.
*
* @return
* A Response containing the raw contents of the underlying
* ActivityLog.
*
* @throws GuacamoleException
* If an error prevents retrieving the content of the log or its size.
*/
@GET
public Response getContents() throws GuacamoleException {
// Build base response exposing the raw contents of the underlying log
ResponseBuilder response = Response.ok(log.getContent(),
log.getType().getContentType());
// Include size, if known
long size = log.getSize();
if (size >= 0)
response.header("Content-Length", size);
return response.build();
}
}

View File

@@ -0,0 +1,107 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.rest.history;
import java.util.function.Function;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceNotFoundException;
import org.apache.guacamole.net.auth.ActivityLog;
import org.apache.guacamole.net.auth.ActivityRecord;
/**
* A REST resource which exposes a single ActivityRecord, allowing any
* associated and accessible logs to be retrieved.
*/
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ActivityRecordResource {
/**
* The ActivityRecord being exposed.
*/
private final ActivityRecord record;
/**
* The REST API object representing the ActivityRecord being exposed.
*/
private final APIActivityRecord externalRecord;
/**
* Creates a new ActivityRecordResource which exposes the given record.
*
* @param record
* The ActivityRecord that should be exposed.
*
* @param externalRecord
* The REST API object representing the ActivityRecord being exposed.
*/
public ActivityRecordResource(ActivityRecord record,
APIActivityRecord externalRecord) {
this.record = record;
this.externalRecord = externalRecord;
}
/**
* Returns the record represented by this ActivityRecordResource, in a
* format intended for interchange.
*
* @return
* The record that this ActivityRecordResource represents, in a format
* intended for interchange.
*/
@GET
public APIActivityRecord getRecord() {
return externalRecord;
}
/**
* Returns an ActivityLogResource representing the log associated with the
* underlying ActivityRecord and having the given name. If no such log
* can be retrieved, either because it does not exist or the current user
* does not have access, an exception is thrown.
*
* @param logName
* The unique name of the log to retrieve.
*
* @return
* An ActivityLogResource representing the log having the given name.
*
* @throws GuacamoleException
* If no such log can be retrieved.
*/
@Path("logs/{name}")
public ActivityLogResource getLog(@PathParam("name") String logName)
throws GuacamoleException {
ActivityLog log = record.getLogs().get(logName);
if (log != null)
return new ActivityLogResource(log);
throw new GuacamoleResourceNotFoundException("No such log.");
}
}

View File

@@ -23,10 +23,13 @@ import java.util.ArrayList;
import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceNotFoundException;
import org.apache.guacamole.net.auth.ActivityRecord;
import org.apache.guacamole.net.auth.ActivityRecordSet;
@@ -86,6 +89,45 @@ public abstract class ActivityRecordSetResource<InternalRecordType extends Activ
*/
protected abstract ExternalRecordType toExternalRecord(InternalRecordType record);
/**
* Applies the given search and sorting criteria to the ActivityRecordSet
* exposed by this ActivityRecordSetResource. The ActivityRecordSet stored
* as {@link #history} is modified as a result of this call.
*
* @param requiredContents
* The set of strings that each must occur somewhere within the
* returned records, whether within the associated username,
* the name of some associated object (such as a connection), or any
* associated date. If non-empty, any record not matching each of the
* strings within the collection will be excluded from the results.
*
* @param sortPredicates
* A list of predicates to apply while sorting the resulting records,
* describing the properties involved and the sort order for those
* properties.
*
* @throws GuacamoleException
* If an error occurs while applying the given filter criteria or
* sort predicates.
*/
private void applyCriteria(List<String> requiredContents,
List<APISortPredicate> sortPredicates) throws GuacamoleException {
// Restrict to records which contain the specified strings
for (String required : requiredContents) {
if (!required.isEmpty())
history = history.contains(required);
}
// Sort according to specified ordering
for (APISortPredicate predicate : sortPredicates)
history = history.sort(predicate.getProperty(), predicate.isDescending());
// Limit to maximum result size
history = history.limit(MAXIMUM_HISTORY_SIZE);
}
/**
* Retrieves the list of activity records stored within the underlying
* ActivityRecordSet which match the given, arbitrary criteria. If
@@ -118,19 +160,9 @@ public abstract class ActivityRecordSetResource<InternalRecordType extends Activ
@QueryParam("order") List<APISortPredicate> sortPredicates)
throws GuacamoleException {
// Restrict to records which contain the specified strings
for (String required : requiredContents) {
if (!required.isEmpty())
history = history.contains(required);
}
// Sort according to specified ordering
for (APISortPredicate predicate : sortPredicates)
history = history.sort(predicate.getProperty(), predicate.isDescending());
// Limit to maximum result size
history = history.limit(MAXIMUM_HISTORY_SIZE);
// Apply search/sort criteria
applyCriteria(requiredContents, sortPredicates);
// Convert record set to collection of API records
List<ExternalRecordType> apiRecords = new ArrayList<>();
for (InternalRecordType record : history.asCollection())
@@ -141,4 +173,30 @@ public abstract class ActivityRecordSetResource<InternalRecordType extends Activ
}
/**
* Retrieves record having the given identifier from among the set of
* activity records stored within the underlying ActivityRecordSet.
*
* @param identifier
* The unique identifier of the record to retrieve.
*
* @return
* A resource representing the record having the given identifier.
*
* @throws GuacamoleException
* If an error occurs while locating the requested record, or if the
* requested record cannot be found.
*/
@Path("{identifier}")
public ActivityRecordResource getRecord(@PathParam("identifier") String identifier)
throws GuacamoleException {
InternalRecordType record = history.get(identifier);
if (record == null)
throw new GuacamoleResourceNotFoundException("Not found: \"" + identifier + "\"");
return new ActivityRecordResource(record, toExternalRecord(record));
}
}