diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ActivityRecordMapper.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ActivityRecordMapper.java new file mode 100644 index 000000000..52fbf9e64 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ActivityRecordMapper.java @@ -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 + * The type of model object representing the activity records mapped by + * this mapper. + */ +public interface ActivityRecordMapper { + + /** + * 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 limit 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 search(@Param("identifier") String identifier, + @Param("recordIdentifier") String recordIdentifier, + @Param("terms") Collection terms, + @Param("sortPredicates") List sortPredicates, + @Param("limit") int limit); + + /** + * Searches for up to limit 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 searchReadable(@Param("identifier") String identifier, + @Param("user") UserModel user, + @Param("recordIdentifier") String recordIdentifier, + @Param("terms") Collection terms, + @Param("sortPredicates") List sortPredicates, + @Param("limit") int limit, + @Param("effectiveGroups") Collection effectiveGroups); + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecord.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecord.java index 95b1a256d..5c2c3285b 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecord.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecord.java @@ -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()); + + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecordSet.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecordSet.java index 60446aab5..79346f51e 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecordSet.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecordSet.java @@ -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 extends RestrictedObject implements ActivityRecordSet { + /** + * 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 retrieveHistory( - AuthenticatedUser user, + protected abstract List retrieveHistory( + AuthenticatedUser user, String recordIdentifier, Set requiredContents, List sortPredicates, int limit) throws GuacamoleException; + @Override + public RecordType get(String identifier) throws GuacamoleException { + + List 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 asCollection() throws GuacamoleException { - return retrieveHistory(getCurrentUser(), requiredContents, + return retrieveHistory(getCurrentUser(), null, requiredContents, sortPredicates, limit); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java index 720ded317..34c9cfb86 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java @@ -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 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 limit 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 search(@Param("identifier") String identifier, - @Param("terms") Collection terms, - @Param("sortPredicates") List sortPredicates, - @Param("limit") int limit); - - /** - * Searches for up to limit 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 searchReadable(@Param("identifier") String identifier, - @Param("user") UserModel user, - @Param("terms") Collection terms, - @Param("sortPredicates") List sortPredicates, - @Param("limit") int limit, - @Param("effectiveGroups") Collection effectiveGroups); - -} +public interface ConnectionRecordMapper extends ActivityRecordMapper {} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSet.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSet.java index 2f559e930..b788cab11 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSet.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSet.java @@ -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 { + /** + * 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 retrieveHistory( - AuthenticatedUser user, Set requiredContents, - List sortPredicates, int limit) - throws GuacamoleException { + protected List retrieveHistory( + AuthenticatedUser user, String recordIdentifier, + Set requiredContents, + List sortPredicates, + int limit) throws GuacamoleException { // Retrieve history from database return connectionService.retrieveHistory(identifier, getCurrentUser(), - requiredContents, sortPredicates, limit); + recordIdentifier, requiredContents, sortPredicates, limit); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java index 16d378432..b3ed89ce8 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java @@ -390,40 +390,6 @@ public class ConnectionService extends ModeledChildDirectoryObjectService retrieveHistory(ModeledAuthenticatedUser user, - ModeledConnection connection) throws GuacamoleException { - - String identifier = connection.getIdentifier(); - - // Get current active connections. - List 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 limit connection history records matching @@ -437,6 +403,11 @@ public class ConnectionService extends ModeledChildDirectoryObjectService retrieveHistory(String identifier, - ModeledAuthenticatedUser user, + ModeledAuthenticatedUser user, String recordIdentifier, Collection requiredContents, List sortPredicates, int limit) throws GuacamoleException { @@ -465,54 +436,19 @@ public class ConnectionService extends ModeledChildDirectoryObjectServicelimit 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 retrieveHistory(ModeledAuthenticatedUser user, - Collection requiredContents, - List sortPredicates, int limit) - throws GuacamoleException { - - return retrieveHistory(null, user, requiredContents, sortPredicates, limit); - - } /** * Connects to the given connection as the given user, using the given diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnectionRecord.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnectionRecord.java index 9f34385cf..9daa8f305 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnectionRecord.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnectionRecord.java @@ -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; + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java index 44b65bff5..8c16363a6 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java @@ -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 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); diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java index a150212c8..c3150ca97 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java @@ -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 diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java index fe60a5faf..1416b0b39 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java @@ -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); } } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.java index fe15f41e4..155033d6a 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.java @@ -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 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 limit 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 search(@Param("username") String username, - @Param("terms") Collection terms, - @Param("sortPredicates") List sortPredicates, - @Param("limit") int limit); - - /** - * Searches for up to limit 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 searchReadable(@Param("username") String username, - @Param("user") UserModel user, - @Param("terms") Collection terms, - @Param("sortPredicates") List sortPredicates, - @Param("limit") int limit, - @Param("effectiveGroups") Collection effectiveGroups); - -} +public interface UserRecordMapper extends ActivityRecordMapper {} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordSet.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordSet.java index c5c9f6a6c..f1c2771fb 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordSet.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordSet.java @@ -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 { + /** + * 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 { } @Override - protected Collection retrieveHistory( - AuthenticatedUser user, Set requiredContents, + protected List retrieveHistory( + AuthenticatedUser user, String recordIdentifier, + Set requiredContents, List sortPredicates, int limit) throws GuacamoleException { // Retrieve history from database return userService.retrieveHistory(identifier, getCurrentUser(), - requiredContents, sortPredicates, limit); + recordIdentifier, requiredContents, sortPredicates, limit); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java index 6f6d05625..c43aa1b01 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java @@ -546,7 +546,7 @@ public class UserService extends ModeledDirectoryObjectService 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 limit user history records matching the @@ -611,6 +585,11 @@ public class UserService extends ModeledDirectoryObjectService retrieveHistory(String username, - ModeledAuthenticatedUser user, + ModeledAuthenticatedUser user, String recordIdentifier, Collection requiredContents, List sortPredicates, int limit) throws GuacamoleException { @@ -638,52 +617,18 @@ public class UserService extends ModeledDirectoryObjectServicelimit 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 retrieveHistory(ModeledAuthenticatedUser user, - Collection requiredContents, - List sortPredicates, int limit) - throws GuacamoleException { - - return retrieveHistory(null, user, requiredContents, sortPredicates, limit); - - } } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml index 022ec97b3..11ca348dd 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml @@ -25,6 +25,7 @@ + @@ -36,30 +37,9 @@ - - - - + INSERT INTO guacamole_connection_history ( connection_id, @@ -90,10 +70,18 @@ + + + UPDATE guacamole_connection_history + SET end_date = #{record.endDate,jdbcType=TIMESTAMP} + WHERE history_id = #{record.recordID,jdbcType=INTEGER} + + 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 @@ - + + + guacamole_connection_history.history_id = #{recordIdentifier,jdbcType=VARCHAR} + + - guacamole_connection_history.connection_id IN ( + AND guacamole_connection_history.connection_id IN ( diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml index 447321a24..70742df4c 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml @@ -33,26 +33,6 @@ - - - @@ -78,18 +58,10 @@ - - + + 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} @@ -97,6 +69,7 @@ SELECT + guacamole_user_history.history_id, guacamole_user_history.remote_host, guacamole_user_history.user_id, guacamole_user_history.username, @@ -171,8 +145,8 @@ ) - - AND guacamole_entity.name = #{username,jdbcType=VARCHAR} + + AND guacamole_entity.name = #{identifier,jdbcType=VARCHAR} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml index 1dcf9506e..9857193b4 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml @@ -25,6 +25,7 @@ + @@ -36,30 +37,9 @@ - - - - + INSERT INTO guacamole_connection_history ( connection_id, @@ -90,10 +70,18 @@ + + + UPDATE guacamole_connection_history + SET end_date = #{record.endDate,jdbcType=TIMESTAMP} + WHERE history_id = #{record.recordID,jdbcType=INTEGER}::integer + + 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 @@ - + + + guacamole_connection_history.history_id = #{recordIdentifier,jdbcType=INTEGER}::integer + + - guacamole_connection_history.connection_id IN ( + AND guacamole_connection_history.connection_id IN ( diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml index 204329fdf..a372087eb 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml @@ -33,26 +33,6 @@ - - - @@ -78,18 +58,10 @@ - - + + 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 @@ -97,6 +69,7 @@ SELECT + guacamole_user_history.history_id, guacamole_user_history.remote_host, guacamole_user_history.user_id, guacamole_user_history.username, @@ -171,8 +145,8 @@ ) - - AND guacamole_entity.name = #{username,jdbcType=VARCHAR} + + AND guacamole_entity.name = #{identifier,jdbcType=VARCHAR} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml index 0ea14cc85..81f31d428 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml @@ -25,6 +25,7 @@ + @@ -36,30 +37,16 @@ - - + + + UPDATE [guacamole_connection_history] + SET end_date = #{record.endDate,jdbcType=TIMESTAMP} + WHERE history_id = #{record.recordID,jdbcType=INTEGER} + - + INSERT INTO [guacamole_connection_history] ( connection_id, @@ -94,6 +81,7 @@ 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 @@ - + + + [guacamole_connection_history].history_id = #{recordIdentifier,jdbcType=INTEGER} + + - [guacamole_connection_history].connection_id IN ( + AND [guacamole_connection_history].connection_id IN ( diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml index 8518e7097..4fb64491a 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml @@ -33,26 +33,6 @@ - - - @@ -78,18 +58,10 @@ - - + + 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} @@ -97,6 +69,7 @@ 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 @@ ) - - AND [guacamole_entity].name = #{username,jdbcType=VARCHAR} + + AND [guacamole_entity].name = #{identifier,jdbcType=VARCHAR} diff --git a/extensions/guacamole-history-recording-storage/.gitignore b/extensions/guacamole-history-recording-storage/.gitignore new file mode 100644 index 000000000..1de9633ae --- /dev/null +++ b/extensions/guacamole-history-recording-storage/.gitignore @@ -0,0 +1,3 @@ +src/main/resources/generated/ +target/ +*~ diff --git a/extensions/guacamole-history-recording-storage/.ratignore b/extensions/guacamole-history-recording-storage/.ratignore new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/guacamole-history-recording-storage/pom.xml b/extensions/guacamole-history-recording-storage/pom.xml new file mode 100644 index 000000000..27cca873d --- /dev/null +++ b/extensions/guacamole-history-recording-storage/pom.xml @@ -0,0 +1,52 @@ + + + + + 4.0.0 + org.apache.guacamole + guacamole-history-recording-storage + jar + 1.4.0 + guacamole-history-recording-storage + http://guacamole.apache.org/ + + + org.apache.guacamole + extensions + 1.4.0 + ../ + + + + + + + org.apache.guacamole + guacamole-ext + 1.4.0 + provided + + + + + diff --git a/extensions/guacamole-history-recording-storage/src/main/assembly/dist.xml b/extensions/guacamole-history-recording-storage/src/main/assembly/dist.xml new file mode 100644 index 000000000..6ee3cd8c8 --- /dev/null +++ b/extensions/guacamole-history-recording-storage/src/main/assembly/dist.xml @@ -0,0 +1,53 @@ + + + + + dist + ${project.artifactId}-${project.version} + + + + tar.gz + + + + + + + + + target/licenses + + + + + target + + + *.jar + + + + + + diff --git a/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/HistoryAuthenticationProvider.java b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/HistoryAuthenticationProvider.java new file mode 100644 index 000000000..43f4424f2 --- /dev/null +++ b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/HistoryAuthenticationProvider.java @@ -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); + } + +} diff --git a/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/HistoryConnection.java b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/HistoryConnection.java new file mode 100644 index 000000000..4f1067413 --- /dev/null +++ b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/HistoryConnection.java @@ -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 getConnectionHistory() throws GuacamoleException { + return new RecordedConnectionActivityRecordSet(currentUser, super.getConnectionHistory()); + } + +} diff --git a/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/HistoryConnectionRecord.java b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/HistoryConnectionRecord.java new file mode 100644 index 000000000..676682130 --- /dev/null +++ b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/HistoryConnectionRecord.java @@ -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 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 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 logs = new HashMap<>(super.getLogs()); + if (recording.isDirectory()) { + Arrays.asList(recording.listFiles()).stream() + .forEach((file) -> addActivityLog(logs, file)); + } + else + addActivityLog(logs, recording); + + return logs; + + } + +} diff --git a/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/RecordedConnectionActivityRecordSet.java b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/RecordedConnectionActivityRecordSet.java new file mode 100644 index 000000000..5caffdfa7 --- /dev/null +++ b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/RecordedConnectionActivityRecordSet.java @@ -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 { + + /** + * 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 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 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; + + } + +} diff --git a/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/user/HistoryUserContext.java b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/user/HistoryUserContext.java new file mode 100644 index 000000000..a103a6e5e --- /dev/null +++ b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/user/HistoryUserContext.java @@ -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 getTokens() throws GuacamoleException { + return Collections.singletonMap(HISTORY_PATH_TOKEN_NAME, + HistoryAuthenticationProvider.getRecordingSearchPath().getAbsolutePath()); + } + + @Override + protected Map getTokens(ConnectionGroup connectionGroup) + throws GuacamoleException { + return getTokens(); + } + + @Override + protected Map getTokens(Connection connection) + throws GuacamoleException { + return getTokens(); + } + + @Override + public Directory getConnectionDirectory() throws GuacamoleException { + return new DecoratingDirectory(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 getConnectionHistory() + throws GuacamoleException { + return new RecordedConnectionActivityRecordSet(currentUser, super.getConnectionHistory()); + } + +} diff --git a/extensions/guacamole-history-recording-storage/src/main/resources/guac-manifest.json b/extensions/guacamole-history-recording-storage/src/main/resources/guac-manifest.json new file mode 100644 index 000000000..51422359d --- /dev/null +++ b/extensions/guacamole-history-recording-storage/src/main/resources/guac-manifest.json @@ -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" + ] + +} diff --git a/extensions/guacamole-history-recording-storage/src/main/resources/translations/en.json b/extensions/guacamole-history-recording-storage/src/main/resources/translations/en.json new file mode 100644 index 000000000..13b7dba2e --- /dev/null +++ b/extensions/guacamole-history-recording-storage/src/main/resources/translations/en.json @@ -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" + } + +} diff --git a/extensions/pom.xml b/extensions/pom.xml index 966bf410f..de2b24556 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -50,6 +50,7 @@ guacamole-auth-totp + guacamole-history-recording-storage guacamole-vault diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractActivityLog.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractActivityLog.java new file mode 100644 index 000000000..bff1271a4 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractActivityLog.java @@ -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; + } + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActivityLog.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActivityLog.java new file mode 100644 index 000000000..f55ea493d --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActivityLog.java @@ -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; + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActivityRecord.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActivityRecord.java index 2324b0ee4..df321d71a 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActivityRecord.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActivityRecord.java @@ -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 getLogs() { + return Collections.emptyMap(); + } + + @Override + public default Map getAttributes() { + return Collections.emptyMap(); + } + } diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActivityRecordSet.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActivityRecordSet.java index 4cce03e65..f5b930558 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActivityRecordSet.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActivityRecordSet.java @@ -57,6 +57,30 @@ public interface ActivityRecordSet { */ Collection 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 diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Attributes.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Attributes.java index 050017dd7..cdb91868f 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Attributes.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Attributes.java @@ -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 getAttributes(); +public interface Attributes extends ReadableAttributes { /** * Sets the given attributes. If an attribute within the map is not diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ConnectionRecord.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ConnectionRecord.java index 21e30a98e..770e6e4b4 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ConnectionRecord.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ConnectionRecord.java @@ -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} + *

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(); + } diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DecoratingActivityRecordSet.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DecoratingActivityRecordSet.java new file mode 100644 index 000000000..3461d9d57 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DecoratingActivityRecordSet.java @@ -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 + * The type of records stored within this ActivityRecordSet. + */ +public abstract class DecoratingActivityRecordSet + extends DelegatingActivityRecordSet { + + /** + * Creates a new DecoratingActivityRecordSet which decorates the records + * within the given set. + * + * @param recordSet + * The ActivityRecordSet whose records are being decorated. + */ + public DecoratingActivityRecordSet(ActivityRecordSet 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. + *

+ * 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. + * This default behavior may need to be overridden if the + * DecoratingActivityRecordSet implementation maintains any internal + * state. + * + * @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 decorate(ActivityRecordSet recordSet) + throws GuacamoleException { + final DecoratingActivityRecordSet decorator = this; + return new DecoratingActivityRecordSet(recordSet) { + + @Override + protected RecordType decorate(RecordType record) throws GuacamoleException { + return decorator.decorate(record); + } + + @Override + protected ActivityRecordSet decorate(ActivityRecordSet 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 sort(SortableProperty property, + boolean desc) throws GuacamoleException { + return decorate(super.sort(property, desc)); + } + + @Override + public ActivityRecordSet limit(int limit) throws GuacamoleException { + return decorate(super.limit(limit)); + } + + @Override + public ActivityRecordSet contains(String value) throws GuacamoleException { + return decorate(super.contains(value)); + } + + @Override + public Collection asCollection() throws GuacamoleException { + + Collection records = super.asCollection(); + + List decoratedRecords = new ArrayList<>(records.size()); + for (RecordType record : records) + decoratedRecords.add(decorate(record)); + + return decoratedRecords; + + } + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingActivityRecord.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingActivityRecord.java new file mode 100644 index 000000000..155612b25 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingActivityRecord.java @@ -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 getLogs() { + return record.getLogs(); + } + + @Override + public Map getAttributes() { + return record.getAttributes(); + } + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingActivityRecordSet.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingActivityRecordSet.java new file mode 100644 index 000000000..84ae9541c --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingActivityRecordSet.java @@ -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 + * The type of ActivityRecord contained within this set. + */ +public class DelegatingActivityRecordSet + implements ActivityRecordSet { + + /** + * The wrapped ActivityRecordSet. + */ + private final ActivityRecordSet 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 recordSet) { + this.recordSet = recordSet; + } + + /** + * Returns the underlying ActivityRecordSet wrapped by this + * DelegatingActivityRecordSet. + * + * @return + * The ActivityRecordSet wrapped by this DelegatingActivityRecordSet. + */ + protected ActivityRecordSet getDelegateActivityRecordSet() { + return recordSet; + } + + @Override + public RecordType get(String identifier) throws GuacamoleException { + return recordSet.get(identifier); + } + + @Override + public Collection asCollection() throws GuacamoleException { + return recordSet.asCollection(); + } + + @Override + public ActivityRecordSet contains(String value) throws GuacamoleException { + return recordSet.contains(value); + } + + @Override + public ActivityRecordSet limit(int limit) throws GuacamoleException { + return recordSet.limit(limit); + } + + @Override + public ActivityRecordSet sort(SortableProperty property, + boolean desc) throws GuacamoleException { + return recordSet.sort(property, desc); + } + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnectionRecord.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnectionRecord.java new file mode 100644 index 000000000..4d953b4bb --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnectionRecord.java @@ -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(); + } + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/FileActivityLog.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/FileActivityLog.java new file mode 100644 index 000000000..df3cc31c2 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/FileActivityLog.java @@ -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); + } + } + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ReadableAttributes.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ReadableAttributes.java new file mode 100644 index 000000000..9ad5b08b0 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ReadableAttributes.java @@ -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 getAttributes(); + +} diff --git a/guacamole/src/main/frontend/src/app/rest/types/ActivityLog.js b/guacamole/src/main/frontend/src/app/rest/types/ActivityLog.js new file mode 100644 index 000000000..f74e53e05 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/rest/types/ActivityLog.js @@ -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.} + */ + 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; + +}]); diff --git a/guacamole/src/main/frontend/src/app/rest/types/ConnectionHistoryEntry.js b/guacamole/src/main/frontend/src/app/rest/types/ConnectionHistoryEntry.js index bfa9f6989..e035efc82 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/ConnectionHistoryEntry.js +++ b/guacamole/src/main/frontend/src/app/rest/types/ConnectionHistoryEntry.js @@ -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.} + */ + this.attributes = template.attributes; + + /** + * All logs associated and accessible via this record, stored by their + * corresponding unique names. + * + * @type {!object.} + */ + this.logs = template.logs; + }; /** diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/history/APIActivityLog.java b/guacamole/src/main/java/org/apache/guacamole/rest/history/APIActivityLog.java new file mode 100644 index 000000000..385f6f855 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/history/APIActivityLog.java @@ -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; + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/history/APIActivityRecord.java b/guacamole/src/main/java/org/apache/guacamole/rest/history/APIActivityRecord.java index c1a0149d6..0825c5037 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/history/APIActivityRecord.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/history/APIActivityRecord.java @@ -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 attributes; + + /** + * A map of all logs associated and accessible via this record, associated + * with their corresponding unique names. + */ + private final Map 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 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 getLogs() { + return logs; + } + } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/history/ActivityLogResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/history/ActivityLogResource.java new file mode 100644 index 000000000..e311884d0 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/history/ActivityLogResource.java @@ -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(); + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/history/ActivityRecordResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/history/ActivityRecordResource.java new file mode 100644 index 000000000..5b5faf4a6 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/history/ActivityRecordResource.java @@ -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."); + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/history/ActivityRecordSetResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/history/ActivityRecordSetResource.java index 8599a9557..b6fdc49d9 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/history/ActivityRecordSetResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/history/ActivityRecordSetResource.java @@ -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 requiredContents, + List 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 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 apiRecords = new ArrayList<>(); for (InternalRecordType record : history.asCollection()) @@ -141,4 +173,30 @@ public abstract class ActivityRecordSetResource