Merge 1.5.0 changes back to master.
Please enter a commit message to explain why this merge is necessary,
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/AuthenticationProviderService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/AuthenticationProviderService.java
index afdaf33..5fedd23 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/AuthenticationProviderService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/AuthenticationProviderService.java
@@ -110,4 +110,34 @@
UserContext context, AuthenticatedUser authenticatedUser,
Credentials credentials) throws GuacamoleException;
+ /**
+ * Decorates a UserContext instance for the given already-authenticated user.
+ * If no decoration is required, the original UserContext will be returned.
+ *
+ * @param authenticationProvider
+ * The AuthenticationProvider on behalf of which the UserContext is
+ * being decorated.
+ *
+ * @param context
+ * The UserContext to decorate.
+ *
+ * @param authenticatedUser
+ * The AuthenticatedUser associated with the UserContext being decorated.
+ *
+ * @param credentials
+ * The credentials most recently submitted by the user. These
+ * credentials are not guaranteed to be the same as the credentials
+ * already associated with the AuthenticatedUser.
+ *
+ * @return
+ * A decorated UserContext instance for the user identified by the given
+ * credentials, or the original user context if no decoration is required.
+ *
+ * @throws GuacamoleException
+ * If the an error occurs during decoration of the UserContext.
+ */
+ public UserContext decorateUserContext(AuthenticationProvider authenticationProvider,
+ UserContext context, AuthenticatedUser authenticatedUser,
+ Credentials credentials) throws GuacamoleException;
+
}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java
new file mode 100644
index 0000000..f25620b
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java
@@ -0,0 +1,124 @@
+/*
+ * 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;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordMapper;
+import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel;
+import org.apache.guacamole.auth.jdbc.connection.ModeledConnectionRecord;
+import org.apache.guacamole.net.GuacamoleTunnel;
+import org.apache.guacamole.net.auth.Connection;
+import org.apache.guacamole.net.auth.DelegatingConnection;
+import org.apache.guacamole.net.auth.User;
+import org.apache.guacamole.protocol.GuacamoleClientInformation;
+
+/**
+ * Connection implementation that creates a history record when the connection
+ * is established, and returns a HistoryTrackingTunnel to automatically set the
+ * end date when the connection is closed.
+ */
+public class HistoryTrackingConnection extends DelegatingConnection {
+
+ /**
+ * The current Guacamole user.
+ */
+ private final User currentUser;
+
+ /**
+ * The remote host that the user connected from.
+ */
+ private final String remoteHost;
+
+ /**
+ * The connection record mapper to use when writing history entries for
+ * established connections.
+ */
+ private final ConnectionRecordMapper connectionRecordMapper;
+
+ /**
+ * Creates a new HistoryConnection that wraps the given connection,
+ * automatically creating a history record when the connection is
+ * established, and returning a HistoryTrackingTunnel to set the end
+ * date on the history entry when the connection is closed.
+ *
+ * @param currentUser
+ * The current Guacamole user.
+ *
+ * @param remoteHost
+ * The remote host that the user connected from.
+ *
+ * @param connection
+ * The connection to wrap.
+ *
+ * @param connectionRecordMapper
+ * The connection record mapper that will be used to write the connection history records.
+ */
+ public HistoryTrackingConnection(User currentUser, String remoteHost, Connection connection, ConnectionRecordMapper connectionRecordMapper) {
+ super(connection);
+
+ this.currentUser = currentUser;
+ this.remoteHost = remoteHost;
+ this.connectionRecordMapper = connectionRecordMapper;
+ }
+
+ @Override
+ public GuacamoleTunnel connect(GuacamoleClientInformation info,
+ Map<String, String> tokens) throws GuacamoleException {
+
+ // Create a connection record model, starting at the current date/time
+ ConnectionRecordModel connectionRecordModel = new ConnectionRecordModel();
+ connectionRecordModel.setStartDate(new Date());
+
+ // Set the user information
+ connectionRecordModel.setUsername(this.currentUser.getIdentifier());
+ connectionRecordModel.setRemoteHost(this.remoteHost);
+
+ // Set the connection information
+ connectionRecordModel.setConnectionName(this.getDelegateConnection().getName());
+
+ // Insert the connection history record to mark the start of this connection
+ connectionRecordMapper.insert(connectionRecordModel);
+
+ // Include history record UUID as token
+ ModeledConnectionRecord modeledRecord = new ModeledConnectionRecord(connectionRecordModel);
+ Map<String, String> updatedTokens = new HashMap<>(tokens);
+ updatedTokens.put("HISTORY_UUID", modeledRecord.getUUID().toString());
+
+ // Connect, and wrap the tunnel for return
+ GuacamoleTunnel tunnel = super.connect(info, updatedTokens);
+ return new HistoryTrackingTunnel(
+ tunnel, this.connectionRecordMapper, connectionRecordModel);
+ }
+
+ /**
+ * Get the Connection wrapped by this HistoryTrackingConnection.
+ *
+ * @return
+ * The wrapped Connection.
+ */
+ public Connection getWrappedConnection() {
+ return getDelegateConnection();
+ }
+
+}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnectionDirectory.java
new file mode 100644
index 0000000..ddc3afb
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnectionDirectory.java
@@ -0,0 +1,96 @@
+/*
+ * 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;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordMapper;
+import org.apache.guacamole.net.auth.Connection;
+import org.apache.guacamole.net.auth.DecoratingDirectory;
+import org.apache.guacamole.net.auth.Directory;
+import org.apache.guacamole.net.auth.User;
+
+/**
+ * A connection directory that returns HistoryTrackingConnection-wrapped connections
+ * when queried.
+ */
+public class HistoryTrackingConnectionDirectory extends DecoratingDirectory<Connection> {
+
+ /**
+ * The connection record mapper to use when writing history entries for
+ * established connections.
+ */
+ private final ConnectionRecordMapper connectionRecordMapper;
+
+ /**
+ * The user that directory operations are being performed for.
+ */
+ private final User user;
+
+ /**
+ * The remote host that the user connected from.
+ */
+ private final String remoteHost;
+
+ /**
+ * Create a new history tracking connection directory. Any connection retrieved from this
+ * directory will be wrapped in a HistoryTrackingConnection, enabling connection history
+ * records to be written with the provided connection record mapper.
+ *
+ * @param directory
+ * The connection directory to wrap.
+ *
+ * @param user
+ * The user associated with the connection directory.
+ *
+ * @param remoteHost
+ * The remote host that the user connected from.
+ *
+ * @param connectionRecordMapper
+ * The connection record mapper that will be used to write the connection history records.
+ */
+ public HistoryTrackingConnectionDirectory(Directory<Connection> directory, User user, String remoteHost, ConnectionRecordMapper connectionRecordMapper) {
+ super(directory);
+
+ this.user = user;
+ this.remoteHost = remoteHost;
+ this.connectionRecordMapper = connectionRecordMapper;
+ }
+
+ @Override
+ protected Connection decorate(Connection connection) throws GuacamoleException {
+
+ // Wrap the connection in a history-tracking layer
+ return new HistoryTrackingConnection(
+ this.user, this.remoteHost, connection, this.connectionRecordMapper);
+ }
+
+ @Override
+ protected Connection undecorate(Connection connection) throws GuacamoleException {
+
+ // If the connection was wrapped, unwrap it
+ if (connection instanceof HistoryTrackingConnection) {
+ return ((HistoryTrackingConnection) connection).getWrappedConnection();
+ }
+
+ // Otherwise, return the unwrapped connection directly
+ return connection;
+ }
+
+}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingTunnel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingTunnel.java
new file mode 100644
index 0000000..aeccc3a
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingTunnel.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.auth.jdbc;
+
+import java.util.Date;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordMapper;
+import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel;
+import org.apache.guacamole.net.DelegatingGuacamoleTunnel;
+import org.apache.guacamole.net.GuacamoleTunnel;
+
+/**
+ * Tunnel implementation which automatically writes an end date for the
+ * provided connection history record model using the provided connection
+ * history mapper, when the tunnel is closed.
+ */
+public class HistoryTrackingTunnel extends DelegatingGuacamoleTunnel {
+
+ /**
+ * The connection for which this tunnel was established.
+ */
+ private final ConnectionRecordMapper connectionRecordMapper;
+
+ /**
+ * The user for which this tunnel was established.
+ */
+ private final ConnectionRecordModel connectionRecordModel;
+
+ /**
+ * Creates a new HistoryTrackingTunnel that wraps the given tunnel,
+ * automatically setting the end date for the provided connection history records,
+ * using the provided connection history record mapper.
+ *
+ * @param tunnel
+ * The tunnel to wrap.
+ *
+ * @param connectionRecordMapper
+ * The mapper to use when writing connection history records.
+ *
+ * @param connectionRecordModel
+ * The connection history record model representing the in-progress connection.
+ */
+ public HistoryTrackingTunnel(GuacamoleTunnel tunnel,
+ ConnectionRecordMapper connectionRecordMapper, ConnectionRecordModel connectionRecordModel) {
+
+ super(tunnel);
+
+ // Store the connection record mapper and model for history tracking
+ this.connectionRecordMapper = connectionRecordMapper;
+ this.connectionRecordModel = connectionRecordModel;
+ }
+
+ @Override
+ public void close() throws GuacamoleException {
+
+ // Set the end date to complete the connection history record
+ this.connectionRecordModel.setEndDate(new Date());
+ this.connectionRecordMapper.updateEndDate(this.connectionRecordModel);
+
+ super.close();
+ }
+
+}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingUserContext.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingUserContext.java
new file mode 100644
index 0000000..2c13a7e
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingUserContext.java
@@ -0,0 +1,75 @@
+/*
+ * 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;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordMapper;
+import org.apache.guacamole.net.auth.Connection;
+import org.apache.guacamole.net.auth.DelegatingUserContext;
+import org.apache.guacamole.net.auth.Directory;
+import org.apache.guacamole.net.auth.UserContext;
+
+/**
+ * DelegatingUserContext implementation which writes connection history records
+ * when connections are established and closed.
+ */
+public class HistoryTrackingUserContext extends DelegatingUserContext {
+
+ /**
+ * The remote host that the user associated with the user context
+ * connected from.
+ */
+ private final String remoteHost;
+
+ /**
+ * The connection record mapper to use when writing history entries for
+ * established connections.
+ */
+ private final ConnectionRecordMapper connectionRecordMapper;
+
+ /**
+ * Creates a new HistoryTrackingUserContext which wraps the given
+ * UserContext, allowing for tracking of connection history external to
+ * this authentication provider.
+ *
+ * @param userContext
+ * The UserContext to wrap.
+ *
+ * @param remoteHost
+ * The host that the user associated with the given user context connected from.
+ *
+ * @param connectionRecordMapper
+ * The mapper to use when writing connection history entries to the DB.
+ */
+ public HistoryTrackingUserContext(UserContext userContext, String remoteHost, ConnectionRecordMapper connectionRecordMapper) {
+ super(userContext);
+
+ this.remoteHost = remoteHost;
+ this.connectionRecordMapper = connectionRecordMapper;
+ }
+
+ @Override
+ public Directory<Connection> getConnectionDirectory() throws GuacamoleException {
+ return new HistoryTrackingConnectionDirectory(
+ super.getConnectionDirectory(), self(),
+ this.remoteHost, this.connectionRecordMapper);
+ }
+
+}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/InjectedAuthenticationProvider.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/InjectedAuthenticationProvider.java
index fddb204..8ae2eea 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/InjectedAuthenticationProvider.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/InjectedAuthenticationProvider.java
@@ -90,4 +90,12 @@
authenticatedUser, credentials);
}
+ @Override
+ public UserContext decorate(UserContext context,
+ AuthenticatedUser authenticatedUser, Credentials credentials)
+ throws GuacamoleException {
+ return authProviderService.decorateUserContext(this, context,
+ authenticatedUser, credentials);
+ }
+
}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java
index 2f38ebe..d2576ec 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java
@@ -22,6 +22,7 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordMapper;
import org.apache.guacamole.auth.jdbc.security.PasswordPolicyService;
import org.apache.guacamole.auth.jdbc.sharing.user.SharedAuthenticatedUser;
import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser;
@@ -68,6 +69,12 @@
@Inject
private Provider<ModeledUserContext> userContextProvider;
+ /**
+ * Mapper for writing connection history.
+ */
+ @Inject
+ private ConnectionRecordMapper connectionRecordMapper;
+
@Override
public AuthenticatedUser authenticateUser(AuthenticationProvider authenticationProvider,
Credentials credentials) throws GuacamoleException {
@@ -99,7 +106,7 @@
ModeledUser user = userService.retrieveUser(authenticationProvider, authenticatedUser);
ModeledUserContext context = userContextProvider.get();
if (user != null && !user.isDisabled()) {
-
+
// Enforce applicable account restrictions
if (databaseRestrictionsApplicable) {
@@ -125,18 +132,18 @@
}
}
-
+
// If no user account is found, and database-specific account
// restrictions do not apply, get a skeleton user.
else if (!databaseRestrictionsApplicable) {
user = userService.retrieveSkeletonUser(authenticationProvider, authenticatedUser);
-
+
// If auto account creation is enabled, add user to DB.
if (environment.autoCreateAbsentAccounts()) {
ModeledUser createdUser = userService.createObject(new PrivilegedModeledAuthenticatedUser(user.getCurrentUser()), user);
user.setModel(createdUser.getModel());
}
-
+
}
// Veto authentication result only if database-specific account
@@ -144,7 +151,7 @@
else
throw new GuacamoleInvalidCredentialsException("Invalid login",
CredentialsInfo.USERNAME_PASSWORD);
-
+
// Initialize the UserContext with the user account and return it.
context.init(user.getCurrentUser());
context.recordUserLogin();
@@ -162,4 +169,18 @@
}
+ @Override
+ public UserContext decorateUserContext(AuthenticationProvider authenticationProvider,
+ UserContext context, AuthenticatedUser authenticatedUser,
+ Credentials credentials) throws GuacamoleException {
+
+ // Track connection history only for external connections, and only if enabled in the config
+ if (environment.trackExternalConnectionHistory() && context.getAuthenticationProvider() != authenticationProvider) {
+ return new HistoryTrackingUserContext(context, credentials.getRemoteHostname(), connectionRecordMapper);
+ }
+
+ return context;
+
+ }
+
}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java
index 1954444..763793d 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java
@@ -30,7 +30,7 @@
* intended for use within JDBC based authentication providers.
*/
public abstract class JDBCEnvironment extends DelegatingEnvironment {
-
+
/**
* Constructs a new JDBCEnvironment using an underlying LocalEnviroment to
* read properties from the file system.
@@ -68,12 +68,12 @@
public abstract int getAbsoluteMaxConnections() throws GuacamoleException;
/**
- * Returns the default maximum number of concurrent connections to allow to
- * any one connection, unless specified differently on an individual
+ * Returns the default maximum number of concurrent connections to allow to
+ * any one connection, unless specified differently on an individual
* connection. Zero denotes unlimited.
- *
+ *
* @return
- * The default maximum allowable number of concurrent connections
+ * The default maximum allowable number of concurrent connections
* to any connection.
*
* @throws GuacamoleException
@@ -82,10 +82,10 @@
public abstract int getDefaultMaxConnections() throws GuacamoleException;
/**
- * Returns the default maximum number of concurrent connections to allow to
- * any one connection group, unless specified differently on an individual
+ * Returns the default maximum number of concurrent connections to allow to
+ * any one connection group, unless specified differently on an individual
* connection group. Zero denotes unlimited.
- *
+ *
* @return
* The default maximum allowable number of concurrent connections
* to any connection group.
@@ -95,12 +95,12 @@
*/
public abstract int getDefaultMaxGroupConnections()
throws GuacamoleException;
-
+
/**
- * Returns the default maximum number of concurrent connections to allow to
+ * Returns the default maximum number of concurrent connections to allow to
* any one connection by an individual user, unless specified differently on
* an individual connection. Zero denotes unlimited.
- *
+ *
* @return
* The default maximum allowable number of concurrent connections to
* any connection by an individual user.
@@ -110,12 +110,12 @@
*/
public abstract int getDefaultMaxConnectionsPerUser()
throws GuacamoleException;
-
+
/**
- * Returns the default maximum number of concurrent connections to allow to
- * any one connection group by an individual user, unless specified
+ * Returns the default maximum number of concurrent connections to allow to
+ * any one connection group by an individual user, unless specified
* differently on an individual connection group. Zero denotes unlimited.
- *
+ *
* @return
* The default maximum allowable number of concurrent connections to
* any connection group by an individual user.
@@ -149,19 +149,19 @@
* true if the database supports recursive queries, false otherwise.
*/
public abstract boolean isRecursiveQuerySupported(SqlSession session);
-
+
/**
* Returns a boolean value representing whether or not the JDBC module
* should automatically create accounts within the database for users that
* are successfully authenticated via other extensions. Returns true if
* accounts should be auto-created, otherwise returns false.
- *
+ *
* @return
* true if user accounts should be automatically created within the
* database when authentication succeeds from another extension;
* otherwise false.
- *
- * @throws GuacamoleException
+ *
+ * @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public abstract boolean autoCreateAbsentAccounts() throws GuacamoleException;
@@ -212,4 +212,19 @@
}
}
+ /**
+ * Returns a boolean value representing whether or not the JDBC module
+ * should automatically track connection history for external connections,
+ * i.e. connections not originated from within the JDBC auth provider
+ * itself.
+ *
+ * @return
+ * true if connection history should be tracked for connections that
+ * do not originate from within this JDBC auth provider, false otherwise.
+ *
+ * @throws GuacamoleException
+ * If guacamole.properties cannot be parsed.
+ */
+ public abstract boolean trackExternalConnectionHistory() throws GuacamoleException;
+
}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/SharedAuthenticationProviderService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/SharedAuthenticationProviderService.java
index 40df1e3..8cd6edc 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/SharedAuthenticationProviderService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/SharedAuthenticationProviderService.java
@@ -109,4 +109,13 @@
}
+ @Override
+ public UserContext decorateUserContext(AuthenticationProvider authenticationProvider,
+ UserContext context, AuthenticatedUser authenticatedUser,
+ Credentials credentials) {
+
+ // There's no need to decorate the user context here
+ return context;
+ }
+
}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java
index ee3fd8a..d8344ec 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java
@@ -99,7 +99,7 @@
* allowed to any one connection group.
*/
private final int DEFAULT_MAX_GROUP_CONNECTIONS = 0;
-
+
/**
* The default SSL mode for connecting to MySQL servers.
*/
@@ -108,8 +108,8 @@
/**
* Constructs a new MySQLEnvironment, providing access to MySQL-specific
* configuration options.
- *
- * @throws GuacamoleException
+ *
+ * @throws GuacamoleException
* If an error occurs while setting up the underlying JDBCEnvironment
* or while parsing legacy MySQL configuration options.
*/
@@ -177,12 +177,12 @@
* database server hosting the Guacamole database. If unspecified, the
* installed MySQL driver will be automatically detected by inspecting the
* classes available in the classpath.
- *
+ *
* @return
* The MySQL driver that will be used to communicate with the MySQL-
* compatible server.
- *
- * @throws GuacamoleException
+ *
+ * @throws GuacamoleException
* If guacamole.properties cannot be parsed, or if no MySQL-compatible
* JDBC driver is present.
*/
@@ -210,15 +210,15 @@
throw new GuacamoleServerException("No JDBC driver for MySQL/MariaDB is installed.");
}
-
+
/**
* Returns the hostname of the MySQL server hosting the Guacamole
* authentication tables. If unspecified, this will be "localhost".
- *
+ *
* @return
* The URL of the MySQL server.
*
- * @throws GuacamoleException
+ * @throws GuacamoleException
* If an error occurs while retrieving the property value.
*/
public String getMySQLHostname() throws GuacamoleException {
@@ -227,30 +227,30 @@
DEFAULT_HOSTNAME
);
}
-
+
/**
* Returns the port number of the MySQL server hosting the Guacamole
* authentication tables. If unspecified, this will be the default MySQL
* port of 3306.
- *
+ *
* @return
* The port number of the MySQL server.
*
- * @throws GuacamoleException
+ * @throws GuacamoleException
* If an error occurs while retrieving the property value.
*/
public int getMySQLPort() throws GuacamoleException {
return getProperty(MySQLGuacamoleProperties.MYSQL_PORT, DEFAULT_PORT);
}
-
+
/**
- * Returns the name of the MySQL database containing the Guacamole
+ * Returns the name of the MySQL database containing the Guacamole
* authentication tables.
- *
+ *
* @return
* The name of the MySQL database.
*
- * @throws GuacamoleException
+ * @throws GuacamoleException
* If an error occurs while retrieving the property value, or if the
* value was not set, as this property is required.
*/
@@ -262,7 +262,7 @@
public String getUsername() throws GuacamoleException {
return getRequiredProperty(MySQLGuacamoleProperties.MYSQL_USERNAME);
}
-
+
@Override
public String getPassword() throws GuacamoleException {
return getRequiredProperty(MySQLGuacamoleProperties.MYSQL_PASSWORD);
@@ -303,15 +303,15 @@
}
}
-
+
/**
* Return the MySQL SSL mode as configured in guacamole.properties, or the
* default value of PREFERRED if not configured.
- *
+ *
* @return
* The SSL mode to use when connecting to the MySQL server.
- *
- * @throws GuacamoleException
+ *
+ * @throws GuacamoleException
* If an error occurs retrieving the property value.
*/
public MySQLSSLMode getMySQLSSLMode() throws GuacamoleException {
@@ -319,71 +319,71 @@
MySQLGuacamoleProperties.MYSQL_SSL_MODE,
DEFAULT_SSL_MODE);
}
-
+
/**
* Returns the File where the trusted certificate store is located as
* configured in guacamole.properties, or null if no value has been
* configured. The trusted certificate store is used to validate server
* certificates when making SSL connections to MySQL servers.
- *
+ *
* @return
* The File where the trusted certificate store is located, or null
* if the value has not been configured.
- *
+ *
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public File getMySQLSSLTrustStore() throws GuacamoleException {
return getProperty(MySQLGuacamoleProperties.MYSQL_SSL_TRUST_STORE);
}
-
+
/**
* Returns the password used to access the trusted certificate store as
* configured in guacamole.properties, or null if no password has been
* specified.
- *
+ *
* @return
* The password used to access the trusted certificate store.
- *
- * @throws GuacamoleException
+ *
+ * @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getMySQLSSLTrustPassword() throws GuacamoleException {
return getProperty(MySQLGuacamoleProperties.MYSQL_SSL_TRUST_PASSWORD);
}
-
+
/**
* Returns the File used to store the client SSL certificate as configured
* in guacamole.properties, or null if no value has been specified. This
* file will be used to load the client certificate used for SSL connections
* to MySQL servers, if the SSL connection is so configured to require
* client certificate authentication.
- *
+ *
* @return
* The File where the client SSL certificate is stored.
- *
- * @throws GuacamoleException
+ *
+ * @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public File getMySQLSSLClientStore() throws GuacamoleException {
return getProperty(MySQLGuacamoleProperties.MYSQL_SSL_CLIENT_STORE);
}
-
+
/**
* Returns the password used to access the client certificate store as
* configured in guacamole.properties, or null if no value has been
* specified.
- *
+ *
* @return
* The password used to access the client SSL certificate store.
- *
- * @throws GuacamoleException
+ *
+ * @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getMYSQLSSLClientPassword() throws GuacamoleException {
return getProperty(MySQLGuacamoleProperties.MYSQL_SSL_CLIENT_PASSWORD);
}
-
+
@Override
public boolean autoCreateAbsentAccounts() throws GuacamoleException {
return getProperty(MySQLGuacamoleProperties.MYSQL_AUTO_CREATE_ACCOUNTS,
@@ -393,15 +393,23 @@
/**
* Return the server timezone if configured in guacamole.properties, or
* null if the configuration option is not present.
- *
+ *
* @return
* The server timezone as configured in guacamole.properties.
- *
- * @throws GuacamoleException
+ *
+ * @throws GuacamoleException
* If an error occurs retrieving the configuration value.
*/
public TimeZone getServerTimeZone() throws GuacamoleException {
return getProperty(MySQLGuacamoleProperties.SERVER_TIMEZONE);
}
+ @Override
+ public boolean trackExternalConnectionHistory() throws GuacamoleException {
+
+ // Track external connection history unless explicitly disabled
+ return getProperty(MySQLGuacamoleProperties.MYSQL_TRACK_EXTERNAL_CONNECTION_HISTORY,
+ true);
+ }
+
}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java
index 925f82a..6a5944f 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java
@@ -35,7 +35,7 @@
* This class should not be instantiated.
*/
private MySQLGuacamoleProperties() {}
-
+
/**
* The JDBC driver that should be used to talk to MySQL-compatible servers.
*/
@@ -48,7 +48,7 @@
};
/**
- * The hostname of the MySQL server hosting the Guacamole authentication
+ * The hostname of the MySQL server hosting the Guacamole authentication
* tables.
*/
public static final StringGuacamoleProperty MYSQL_HOSTNAME = new StringGuacamoleProperty() {
@@ -59,7 +59,7 @@
};
/**
- * The port number of the MySQL server hosting the Guacamole authentication
+ * The port number of the MySQL server hosting the Guacamole authentication
* tables.
*/
public static final IntegerGuacamoleProperty MYSQL_PORT = new IntegerGuacamoleProperty() {
@@ -70,7 +70,7 @@
};
/**
- * The name of the MySQL database containing the Guacamole authentication
+ * The name of the MySQL database containing the Guacamole authentication
* tables.
*/
public static final StringGuacamoleProperty MYSQL_DATABASE = new StringGuacamoleProperty() {
@@ -179,19 +179,19 @@
public String getName() { return "mysql-default-max-group-connections-per-user"; }
};
-
+
/**
* The SSL mode used to connect to the MySQL Server. By default the driver
* will attempt SSL connections and fall back to plain-text if SSL fails.
*/
public static final EnumGuacamoleProperty<MySQLSSLMode> MYSQL_SSL_MODE =
new EnumGuacamoleProperty<MySQLSSLMode>(MySQLSSLMode.class) {
-
+
@Override
public String getName() { return "mysql-ssl-mode" ; }
-
+
};
-
+
/**
* The File where trusted SSL certificate authorities and server certificates
* are stored. By default no file is specified, and the default Java
@@ -199,24 +199,24 @@
*/
public static final FileGuacamoleProperty MYSQL_SSL_TRUST_STORE =
new FileGuacamoleProperty() {
-
+
@Override
public String getName() { return "mysql-ssl-trust-store"; }
-
+
};
-
+
/**
* The password to use to access the mysql-ssl-trust-store, if required. By
* default no password will be used to attempt to access the store.
*/
public static final StringGuacamoleProperty MYSQL_SSL_TRUST_PASSWORD =
new StringGuacamoleProperty() {
-
+
@Override
public String getName() { return "mysql-ssl-trust-password"; }
-
+
};
-
+
/**
* The File used to store the client certificate for configurations where
* a client certificate is required for authentication. By default no
@@ -224,24 +224,24 @@
*/
public static final FileGuacamoleProperty MYSQL_SSL_CLIENT_STORE =
new FileGuacamoleProperty() {
-
+
@Override
public String getName() { return "mysql-ssl-client-store"; }
-
+
};
-
+
/**
* The password to use to access the mysql-ssl-client-store file. By
* default no password will be used to attempt to access the file.
*/
public static final StringGuacamoleProperty MYSQL_SSL_CLIENT_PASSWORD =
new StringGuacamoleProperty() {
-
+
@Override
public String getName() { return "mysql-ssl-client-password"; }
-
+
};
-
+
/**
* Whether or not to automatically create accounts in the MySQL database for
* users who successfully authenticate through another extension. By default
@@ -249,7 +249,7 @@
*/
public static final BooleanGuacamoleProperty MYSQL_AUTO_CREATE_ACCOUNTS =
new BooleanGuacamoleProperty() {
-
+
@Override
public String getName() { return "mysql-auto-create-accounts"; }
};
@@ -259,10 +259,23 @@
*/
public static final TimeZoneGuacamoleProperty SERVER_TIMEZONE =
new TimeZoneGuacamoleProperty() {
-
+
@Override
public String getName() { return "mysql-server-timezone"; }
-
+
+ };
+
+ /**
+ * Whether or not to track connection history for connections that do not originate
+ * from within the MySQL database. By default, external connection history will be
+ * tracked.
+ */
+ public static final BooleanGuacamoleProperty MYSQL_TRACK_EXTERNAL_CONNECTION_HISTORY =
+ new BooleanGuacamoleProperty() {
+
+ @Override
+ public String getName() { return "mysql-track-external-connection-history"; }
+
};
}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java
index 2ead15b..ac08e0a 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java
@@ -110,7 +110,7 @@
* the values that should be used in the absence of the correct properties.
*/
private final int DEFAULT_MAX_GROUP_CONNECTIONS = 0;
-
+
/**
* The default value to use for SSL mode if none is explicitly configured.
*/
@@ -119,8 +119,8 @@
/**
* Constructs a new PostgreSQLEnvironment, providing access to PostgreSQL-specific
* configuration options.
- *
- * @throws GuacamoleException
+ *
+ * @throws GuacamoleException
* If an error occurs while setting up the underlying JDBCEnvironment
* or while parsing legacy PostgreSQL configuration options.
*/
@@ -186,11 +186,11 @@
/**
* Returns the hostname of the PostgreSQL server hosting the Guacamole
* authentication tables. If unspecified, this will be "localhost".
- *
+ *
* @return
* The URL of the PostgreSQL server.
*
- * @throws GuacamoleException
+ * @throws GuacamoleException
* If an error occurs while retrieving the property value.
*/
public String getPostgreSQLHostname() throws GuacamoleException {
@@ -199,16 +199,16 @@
DEFAULT_HOSTNAME
);
}
-
+
/**
* Returns the port number of the PostgreSQL server hosting the Guacamole
* authentication tables. If unspecified, this will be the default
* PostgreSQL port of 5432.
- *
+ *
* @return
* The port number of the PostgreSQL server.
*
- * @throws GuacamoleException
+ * @throws GuacamoleException
* If an error occurs while retrieving the property value.
*/
public int getPostgreSQLPort() throws GuacamoleException {
@@ -217,15 +217,15 @@
DEFAULT_PORT
);
}
-
+
/**
* Returns the name of the PostgreSQL database containing the Guacamole
* authentication tables.
- *
+ *
* @return
* The name of the PostgreSQL database.
*
- * @throws GuacamoleException
+ * @throws GuacamoleException
* If an error occurs while retrieving the property value, or if the
* value was not set, as this property is required.
*/
@@ -242,16 +242,16 @@
public String getPassword() throws GuacamoleException {
return getRequiredProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_PASSWORD);
}
-
+
/**
* Returns the defaultStatementTimeout set for PostgreSQL connections.
* If unspecified, this will default to 0,
* and should not be passed through to the backend.
- *
+ *
* @return
* The statement timeout (in seconds)
*
- * @throws GuacamoleException
+ * @throws GuacamoleException
* If an error occurs while retrieving the property value.
*/
public int getPostgreSQLDefaultStatementTimeout() throws GuacamoleException {
@@ -260,15 +260,15 @@
DEFAULT_STATEMENT_TIMEOUT
);
}
-
+
/**
* Returns the socketTimeout property to set on PostgreSQL connections.
* If unspecified, this will default to 0 (no timeout)
- *
+ *
* @return
* The socketTimeout to use when waiting on read operations (in seconds)
*
- * @throws GuacamoleException
+ * @throws GuacamoleException
* If an error occurs while retrieving the property value.
*/
public int getPostgreSQLSocketTimeout() throws GuacamoleException {
@@ -282,85 +282,93 @@
public boolean isRecursiveQuerySupported(SqlSession session) {
return true; // All versions of PostgreSQL support recursive queries through CTEs
}
-
+
/**
* Get the SSL mode to use to make the JDBC connection to the PostgreSQL
* server. If unspecified this will default to PREFER, attempting SSL
* and falling back to plain-text if SSL fails.
- *
+ *
* @return
* The enum value of the SSL mode to use to make the JDBC connection
* to the server.
- *
- * @throws GuacamoleException
+ *
+ * @throws GuacamoleException
* If an error occurs retrieving the value from guacamole.properties.
*/
public PostgreSQLSSLMode getPostgreSQLSSLMode() throws GuacamoleException {
return getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_SSL_MODE,
DEFAULT_SSL_MODE);
}
-
+
/**
* Return the SSL client certificate file to use to make the connection
* to the PostgreSQL server.
- *
+ *
* @return
* The SSL client certificate file to use for the PostgreSQL connection.
- *
+ *
* @throws GuacamoleException
* If an error occurs retrieving the value from guacamole.properties.
*/
public File getPostgreSQLSSLClientCertFile() throws GuacamoleException {
return getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_SSL_CERT_FILE);
}
-
+
/**
* Return the SSL client private key file to use to make the connection to the
* PostgreSQL server.
- *
+ *
* @return
* The SSL client private key file to use for the PostgreSQL connection.
- * @throws GuacamoleException
+ * @throws GuacamoleException
* If an error occurs retrieving the value from guacamole.properties.
*/
public File getPostgreSQLSSLClientKeyFile() throws GuacamoleException {
return getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_SSL_KEY_FILE);
}
-
+
/**
* Return the SSL client root certificate file to use to make the connection
* to the PostgreSQL server.
- *
+ *
* @return
* The SSL client root certificate file to use to make the connection
* to the PostgreSQL server.
- *
- * @throws GuacamoleException
+ *
+ * @throws GuacamoleException
* If an error occurs retrieving the value from guacamole.properties.
*/
public File getPostgreSQLSSLClientRootCertFile() throws GuacamoleException {
return getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_SSL_ROOT_CERT_FILE);
}
-
+
/**
* Return the password to use to decrypt the private SSL key file when making
* the connection to the PostgreSQL server.
- *
+ *
* @return
* The password to use to decrypt the private SSL key file when making
* the connection to the PostgreSQL server.
- *
- * @throws GuacamoleException
+ *
+ * @throws GuacamoleException
* If an error occurs retrieving the value from guacamole.properties.
*/
public String getPostgreSQLSSLClientKeyPassword() throws GuacamoleException {
return getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_SSL_KEY_PASSWORD);
}
-
+
@Override
public boolean autoCreateAbsentAccounts() throws GuacamoleException {
return getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_AUTO_CREATE_ACCOUNTS,
false);
}
-
+
+ @Override
+ public boolean trackExternalConnectionHistory() throws GuacamoleException {
+
+ // Track external connection history unless explicitly disabled
+ return getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_TRACK_EXTERNAL_CONNECTION_HISTORY,
+ true);
+ }
+
}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java
index 271d9c0..dfa00b6 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java
@@ -111,7 +111,7 @@
* The number of seconds to wait for socket read operations.
* If reading from the server takes longer than this value, the
* connection will be closed. This can be used to handle network problems
- * such as a dropped connection to the database. Similar to
+ * such as a dropped connection to the database. Similar to
* postgresql-default-statement-timeout, it will have the effect of
* aborting queries that take too long.
* A value of 0 (the default) means the timeout is disabled.
@@ -202,7 +202,7 @@
public String getName() { return "postgresql-default-max-group-connections-per-user"; }
};
-
+
/**
* The SSL mode that should be used by the JDBC driver when making
* connections to the remote server. By default SSL will be attempted but
@@ -210,60 +210,60 @@
*/
public static final EnumGuacamoleProperty<PostgreSQLSSLMode> POSTGRESQL_SSL_MODE =
new EnumGuacamoleProperty<PostgreSQLSSLMode>(PostgreSQLSSLMode.class) {
-
+
@Override
public String getName() { return "postgresql-ssl-mode"; }
-
+
};
-
+
/**
* The client SSL certificate file used by the JDBC driver to make the
* SSL connection.
*/
public static final FileGuacamoleProperty POSTGRESQL_SSL_CERT_FILE =
new FileGuacamoleProperty() {
-
+
@Override
public String getName() { return "postgresql-ssl-cert-file"; }
-
+
};
-
+
/**
* The client SSL private key file used by the JDBC driver to make the
* SSL connection.
*/
public static final FileGuacamoleProperty POSTGRESQL_SSL_KEY_FILE =
new FileGuacamoleProperty() {
-
+
@Override
public String getName() { return "postgresql-ssl-key-file"; }
-
+
};
-
+
/**
* The client SSL root certificate file used by the JDBC driver to validate
* certificates when making the SSL connection.
*/
public static final FileGuacamoleProperty POSTGRESQL_SSL_ROOT_CERT_FILE =
new FileGuacamoleProperty() {
-
+
@Override
public String getName() { return "postgresql-ssl-root-cert-file"; }
-
+
};
-
+
/**
* The password of the SSL private key used by the JDBC driver to make
* the SSL connection to the PostgreSQL server.
*/
public static final StringGuacamoleProperty POSTGRESQL_SSL_KEY_PASSWORD =
new StringGuacamoleProperty() {
-
+
@Override
public String getName() { return "postgresql-ssl-key-password"; }
-
+
};
-
+
/**
* Whether or not to automatically create accounts in the PostgreSQL
* database for users who successfully authenticate through another
@@ -271,10 +271,23 @@
*/
public static final BooleanGuacamoleProperty POSTGRESQL_AUTO_CREATE_ACCOUNTS =
new BooleanGuacamoleProperty() {
-
+
@Override
public String getName() { return "postgresql-auto-create-accounts"; }
-
+
};
-
+
+ /**
+ * Whether or not to track connection history for connections that do not originate
+ * from within the Postgres database. By default, external connection history will be
+ * tracked.
+ */
+ public static final BooleanGuacamoleProperty POSTGRESQL_TRACK_EXTERNAL_CONNECTION_HISTORY =
+ new BooleanGuacamoleProperty() {
+
+ @Override
+ public String getName() { return "postgresql-track-external-connection-history"; }
+
+ };
+
}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java
index 0e15543..0b69836 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java
@@ -91,8 +91,8 @@
/**
* Constructs a new SQLServerEnvironment, providing access to SQLServer-specific
* configuration options.
- *
- * @throws GuacamoleException
+ *
+ * @throws GuacamoleException
* If an error occurs while setting up the underlying JDBCEnvironment
* or while parsing legacy SQLServer configuration options.
*/
@@ -158,11 +158,11 @@
/**
* Returns the hostname of the SQLServer server hosting the Guacamole
* authentication tables. If unspecified, this will be "localhost".
- *
+ *
* @return
* The URL of the SQLServer server.
*
- * @throws GuacamoleException
+ * @throws GuacamoleException
* If an error occurs while retrieving the property value.
*/
public String getSQLServerHostname() throws GuacamoleException {
@@ -171,15 +171,15 @@
DEFAULT_HOSTNAME
);
}
-
+
/**
* Returns the instance name of the SQL Server installation hosting the
* Guacamole database, if any. If unspecified it will be null.
- *
+ *
* @return
* The instance name of the SQL Server install hosting the Guacamole
* database, or null if undefined.
- *
+ *
* @throws GuacamoleException
* If an error occurs reading guacamole.properties.
*/
@@ -188,16 +188,16 @@
SQLServerGuacamoleProperties.SQLSERVER_INSTANCE
);
}
-
+
/**
* Returns the port number of the SQLServer server hosting the Guacamole
* authentication tables. If unspecified, this will be the default
* SQLServer port of 5432.
- *
+ *
* @return
* The port number of the SQLServer server.
*
- * @throws GuacamoleException
+ * @throws GuacamoleException
* If an error occurs while retrieving the property value.
*/
public int getSQLServerPort() throws GuacamoleException {
@@ -206,15 +206,15 @@
DEFAULT_PORT
);
}
-
+
/**
* Returns the name of the SQLServer database containing the Guacamole
* authentication tables.
- *
+ *
* @return
* The name of the SQLServer database.
*
- * @throws GuacamoleException
+ * @throws GuacamoleException
* If an error occurs while retrieving the property value, or if the
* value was not set, as this property is required.
*/
@@ -226,7 +226,7 @@
public String getUsername() throws GuacamoleException {
return getRequiredProperty(SQLServerGuacamoleProperties.SQLSERVER_USERNAME);
}
-
+
@Override
public String getPassword() throws GuacamoleException {
return getRequiredProperty(SQLServerGuacamoleProperties.SQLSERVER_PASSWORD);
@@ -253,11 +253,19 @@
public boolean isRecursiveQuerySupported(SqlSession session) {
return true; // All versions of SQL Server support recursive queries through CTEs
}
-
+
@Override
public boolean autoCreateAbsentAccounts() throws GuacamoleException {
return getProperty(SQLServerGuacamoleProperties.SQLSERVER_AUTO_CREATE_ACCOUNTS,
false);
}
+ @Override
+ public boolean trackExternalConnectionHistory() throws GuacamoleException {
+
+ // Track external connection history unless explicitly disabled
+ return getProperty(SQLServerGuacamoleProperties.SQLSERVER_TRACK_EXTERNAL_CONNECTION_HISTORY,
+ true);
+ }
+
}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java
index df63c53..432454e 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java
@@ -44,7 +44,7 @@
public String getName() { return "sqlserver-hostname"; }
};
-
+
/**
* The instance name of the SQL Server where the Guacamole database is running.
*/
@@ -53,7 +53,7 @@
@Override
public String getName() { return "sqlserver-instance"; }
-
+
};
/**
@@ -193,7 +193,7 @@
public String getName() { return "sqlserver-driver"; }
};
-
+
/**
* Whether or not to automatically create accounts in the SQL Server
* database for users who successfully authenticate through another
@@ -201,10 +201,23 @@
*/
public static final BooleanGuacamoleProperty SQLSERVER_AUTO_CREATE_ACCOUNTS =
new BooleanGuacamoleProperty() {
-
+
@Override
public String getName() { return "sqlserver-auto-create-accounts"; }
-
+
+ };
+
+ /**
+ * Whether or not to track connection history for connections that do not originate
+ * from within the SQL Server database. By default, external connection history will be
+ * tracked.
+ */
+ public static final BooleanGuacamoleProperty SQLSERVER_TRACK_EXTERNAL_CONNECTION_HISTORY =
+ new BooleanGuacamoleProperty() {
+
+ @Override
+ public String getName() { return "sqlserver-track-external-connection-history"; }
+
};
}
diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java
index 0487737..95862bd 100644
--- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java
+++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java
@@ -24,7 +24,12 @@
import com.onelogin.saml2.settings.Saml2Settings;
import com.onelogin.saml2.settings.SettingsBuilder;
import com.onelogin.saml2.util.Constants;
+
+import java.io.File;
+import java.io.IOException;
import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.core.UriBuilder;
@@ -32,6 +37,7 @@
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.properties.BooleanGuacamoleProperty;
+import org.apache.guacamole.properties.FileGuacamoleProperty;
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
import org.apache.guacamole.properties.StringGuacamoleProperty;
import org.apache.guacamole.properties.URIGuacamoleProperty;
@@ -162,6 +168,30 @@
};
/**
+ * The file containing the X.509 cert to use when signing or encrypting
+ * requests to the SAML IdP.
+ */
+ private static final FileGuacamoleProperty SAML_X509_CERT_PATH =
+ new FileGuacamoleProperty() {
+
+ @Override
+ public String getName() { return "saml-x509-cert-path"; }
+
+ };
+
+ /**
+ * The file containing the private key to use when signing or encrypting
+ * requests to the SAML IdP.
+ */
+ private static final FileGuacamoleProperty SAML_PRIVATE_KEY_PATH =
+ new FileGuacamoleProperty() {
+
+ @Override
+ public String getName() { return "saml-private-key-path"; }
+
+ };
+
+ /**
* The Guacamole server environment.
*/
@Inject
@@ -330,6 +360,70 @@
}
/**
+ * Returns the file containing the X.509 certificate to use when signing
+ * requests to the SAML IdP. If the property is not set, null will be
+ * returned.
+ *
+ * @return
+ * The file containing the X.509 certificate to use when signing
+ * requests to the SAML IdP, or null if not defined.
+ *
+ * @throws GuacamoleException
+ * If the X.509 certificate cannot be parsed.
+ */
+ public File getCertificateFile() throws GuacamoleException {
+ return environment.getProperty(SAML_X509_CERT_PATH);
+ }
+
+ /**
+ * Returns the file containing the private key to use when signing
+ * requests to the SAML IdP. If the property is not set, null will be
+ * returned.
+ *
+ * @return
+ * The file containing the private key to use when signing
+ * requests to the SAML IdP, or null if not defined.
+ *
+ * @throws GuacamoleException
+ * If the private key file cannot be parsed.
+ */
+ public File getPrivateKeyFile() throws GuacamoleException {
+ return environment.getProperty(SAML_PRIVATE_KEY_PATH);
+ }
+
+ /**
+ * Returns the contents of a small file, such as a private key or certificate into
+ * a String. If the file does not exist, or cannot be read for any reason, an exception
+ * will be thrown with the details of the failure.
+ *
+ * @param file
+ * The file to read into a string.
+ *
+ * @param name
+ * A human-readable name for the file, to be used when formatting log messages.
+ *
+ * @return
+ * The contents of the file having the given path.
+ *
+ * @throws GuacamoleException
+ * If the provided file does not exist, or cannot be read for any reason.
+ */
+ private String readFileContentsIntoString(File file, String name) throws GuacamoleException {
+
+ // Attempt to read the file directly into a String
+ try {
+ return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
+ }
+
+ // If the file cannot be read, log a warning and treat it as if it does not exist
+ catch (IOException e) {
+ throw new GuacamoleServerException(
+ name + " at \"" + file.getAbsolutePath() + "\" could not be read.", e);
+ }
+
+ }
+
+ /**
* Returns the collection of SAML settings used to initialize the client.
*
* @return
@@ -380,6 +474,18 @@
UriBuilder.fromUri(getCallbackUrl()).path("api/ext/saml/callback").build().toString());
}
+ // If a private key file is set, load the value into the builder now
+ File privateKeyFile = getPrivateKeyFile();
+ if (privateKeyFile != null)
+ samlMap.put(SettingsBuilder.SP_PRIVATEKEY_PROPERTY_KEY,
+ readFileContentsIntoString(privateKeyFile, "Private Key"));
+
+ // If a certificate file is set, load the value into the builder now
+ File certificateFile = getCertificateFile();
+ if (certificateFile != null)
+ samlMap.put(SettingsBuilder.SP_X509CERT_PROPERTY_KEY,
+ readFileContentsIntoString(certificateFile, "X.509 Certificate"));
+
SettingsBuilder samlBuilder = new SettingsBuilder();
Saml2Settings samlSettings = samlBuilder.fromValues(samlMap).build();
samlSettings.setStrict(getStrict());
diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java
index d50e3a6..0c6f567 100644
--- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java
+++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java
@@ -45,17 +45,26 @@
public static final String TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME = "guac-totp-key-confirmed";
/**
- * The name of the field used to trigger a reset of the TOTP data.
+ * The name of the user attribute defines whether the TOTP key has been
+ * generated for the user, regardless of whether that key has been
+ * confirmed. This attribute is not stored, but is instead exposed
+ * dynamically in lieu of exposing the actual TOTP key.
*/
- public static final String TOTP_KEY_SECRET_RESET_FIELD = "guac-totp-reset";
+ public static final String TOTP_KEY_SECRET_GENERATED_ATTRIBUTE_NAME = "guac-totp-key-generated";
+
+ /**
+ * The string value used by TOTP user attributes to represent the boolean
+ * value "true".
+ */
+ private static final String TRUTH_VALUE = "true";
/**
* The form which contains all configurable properties for this user.
*/
- public static final Form TOTP_CONFIG_FORM = new Form("totp-config-form",
+ public static final Form TOTP_ENROLLMENT_STATUS = new Form("totp-enrollment-status",
Arrays.asList(
- new BooleanField(TOTP_KEY_SECRET_RESET_FIELD, "true"),
- new BooleanField(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME, "true")
+ new BooleanField(TOTP_KEY_SECRET_GENERATED_ATTRIBUTE_NAME, TRUTH_VALUE),
+ new BooleanField(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME, TRUTH_VALUE)
)
);
@@ -86,17 +95,12 @@
// Create independent, mutable copy of attributes
Map<String, String> attributes = new HashMap<>(super.getAttributes());
-
- // Protect the secret value by removing it
+
+ // Replace secret key with simple boolean attribute representing
+ // whether a key has been generated at all
String secret = attributes.remove(TOTP_KEY_SECRET_ATTRIBUTE_NAME);
-
- // If secret is null or empty, mark the reset as true.
- if (secret == null || secret.isEmpty())
- attributes.put(TOTP_KEY_SECRET_RESET_FIELD, "true");
-
- // If secret has a value, mark the reset as false.
- else
- attributes.put(TOTP_KEY_SECRET_RESET_FIELD, "false");
+ if (secret != null && !secret.isEmpty())
+ attributes.put(TOTP_KEY_SECRET_GENERATED_ATTRIBUTE_NAME, TRUTH_VALUE);
return attributes;
@@ -107,15 +111,13 @@
// Create independent, mutable copy of attributes
attributes = new HashMap<>(attributes);
-
- // Do not expose any TOTP secret attribute outside this extension
+
+ // Do not allow TOTP secret to be directly manipulated
attributes.remove(TOTP_KEY_SECRET_ATTRIBUTE_NAME);
-
- // Pull off the boolean reset field
- String reset = attributes.remove(TOTP_KEY_SECRET_RESET_FIELD);
-
- // If reset has been set to true, clear the secret.
- if (reset != null && reset.equals("true")) {
+
+ // Reset TOTP status entirely if requested
+ String generated = attributes.remove(TOTP_KEY_SECRET_GENERATED_ATTRIBUTE_NAME);
+ if (generated != null && !generated.equals(TRUTH_VALUE)) {
attributes.put(TOTP_KEY_SECRET_ATTRIBUTE_NAME, null);
attributes.put(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME, null);
}
diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java
index f478519..a7a97b0 100644
--- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java
+++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java
@@ -68,7 +68,7 @@
@Override
public Collection<Form> getUserAttributes() {
Collection<Form> userAttrs = new HashSet<>(super.getUserAttributes());
- userAttrs.add(TOTPUser.TOTP_CONFIG_FORM);
+ userAttrs.add(TOTPUser.TOTP_ENROLLMENT_STATUS);
return Collections.unmodifiableCollection(userAttrs);
}
diff --git a/extensions/guacamole-auth-totp/src/main/resources/translations/en.json b/extensions/guacamole-auth-totp/src/main/resources/translations/en.json
index 55bd69a..80bdaf0 100644
--- a/extensions/guacamole-auth-totp/src/main/resources/translations/en.json
+++ b/extensions/guacamole-auth-totp/src/main/resources/translations/en.json
@@ -33,10 +33,10 @@
"USER_ATTRIBUTES" : {
- "FIELD_HEADER_GUAC_TOTP_RESET" : "Clear TOTP secret:",
- "FIELD_HEADER_GUAC_TOTP_KEY_CONFIRMED" : "TOTP key confirmed:",
+ "FIELD_HEADER_GUAC_TOTP_KEY_GENERATED" : "Secret key generated:",
+ "FIELD_HEADER_GUAC_TOTP_KEY_CONFIRMED" : "Authentication device confirmed:",
- "SECTION_HEADER_TOTP_CONFIG_FORM" : "Configure TOTP"
+ "SECTION_HEADER_TOTP_ENROLLMENT_STATUS" : "TOTP Enrollment Status"
}
diff --git a/extensions/guacamole-display-statistics/.gitignore b/extensions/guacamole-display-statistics/.gitignore
new file mode 100644
index 0000000..e55f47f
--- /dev/null
+++ b/extensions/guacamole-display-statistics/.gitignore
@@ -0,0 +1 @@
+src/main/resources/generated/
diff --git a/extensions/guacamole-display-statistics/.ratignore b/extensions/guacamole-display-statistics/.ratignore
new file mode 100644
index 0000000..da318d1
--- /dev/null
+++ b/extensions/guacamole-display-statistics/.ratignore
@@ -0,0 +1 @@
+src/main/resources/html/*.html
diff --git a/extensions/guacamole-display-statistics/pom.xml b/extensions/guacamole-display-statistics/pom.xml
new file mode 100644
index 0000000..403d974
--- /dev/null
+++ b/extensions/guacamole-display-statistics/pom.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/maven-v4_0_0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.apache.guacamole</groupId>
+ <artifactId>guacamole-display-statistics</artifactId>
+ <packaging>jar</packaging>
+ <version>1.4.0</version>
+ <name>guacamole-display-statistics</name>
+ <url>http://guacamole.apache.org/</url>
+
+ <parent>
+ <groupId>org.apache.guacamole</groupId>
+ <artifactId>extensions</artifactId>
+ <version>1.4.0</version>
+ <relativePath>../</relativePath>
+ </parent>
+
+ <build>
+ <plugins>
+
+ <!-- Pre-cache Angular templates with maven-angular-plugin -->
+ <plugin>
+ <groupId>com.keithbranton.mojo</groupId>
+ <artifactId>angular-maven-plugin</artifactId>
+ <version>0.3.4</version>
+ <executions>
+ <execution>
+ <phase>generate-resources</phase>
+ <goals>
+ <goal>html2js</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <sourceDir>${basedir}/src/main/resources</sourceDir>
+ <include>**/*.html</include>
+ <target>${basedir}/src/main/resources/generated/templates-main/templates.js</target>
+ <prefix>app/ext/display-stats</prefix>
+ </configuration>
+ </plugin>
+
+ <!-- JS/CSS Minification Plugin -->
+ <plugin>
+ <groupId>com.github.buckelieg</groupId>
+ <artifactId>minify-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>default-cli</id>
+ <configuration>
+ <charset>UTF-8</charset>
+
+ <webappSourceDir>${basedir}/src/main/resources</webappSourceDir>
+ <webappTargetDir>${project.build.directory}/classes</webappTargetDir>
+
+ <cssSourceDir>/</cssSourceDir>
+ <cssTargetDir>/</cssTargetDir>
+ <cssFinalFile>display-stats.css</cssFinalFile>
+
+ <cssSourceFiles>
+ <cssSourceFile>license.txt</cssSourceFile>
+ </cssSourceFiles>
+
+ <cssSourceIncludes>
+ <cssSourceInclude>**/*.css</cssSourceInclude>
+ </cssSourceIncludes>
+
+ <jsSourceDir>/</jsSourceDir>
+ <jsTargetDir>/</jsTargetDir>
+ <jsFinalFile>display-stats.js</jsFinalFile>
+
+ <jsSourceFiles>
+ <jsSourceFile>license.txt</jsSourceFile>
+ </jsSourceFiles>
+
+ <jsSourceIncludes>
+ <jsSourceInclude>**/*.js</jsSourceInclude>
+ </jsSourceIncludes>
+
+ <!-- Do not minify and include tests -->
+ <jsSourceExcludes>
+ <jsSourceExclude>**/*.test.js</jsSourceExclude>
+ </jsSourceExcludes>
+ <jsEngine>CLOSURE</jsEngine>
+
+ <!-- Disable warnings for JSDoc annotations -->
+ <closureWarningLevels>
+ <misplacedTypeAnnotation>OFF</misplacedTypeAnnotation>
+ <nonStandardJsDocs>OFF</nonStandardJsDocs>
+ </closureWarningLevels>
+
+ </configuration>
+ <goals>
+ <goal>minify</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+
+ </plugins>
+ </build>
+
+</project>
diff --git a/extensions/guacamole-display-statistics/src/main/assembly/dist.xml b/extensions/guacamole-display-statistics/src/main/assembly/dist.xml
new file mode 100644
index 0000000..0b16a71
--- /dev/null
+++ b/extensions/guacamole-display-statistics/src/main/assembly/dist.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<assembly
+ xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+
+ <id>dist</id>
+ <baseDirectory>${project.artifactId}-${project.version}</baseDirectory>
+
+ <!-- Output tar.gz -->
+ <formats>
+ <format>tar.gz</format>
+ </formats>
+
+ <!-- Include licenses and extension .jar -->
+ <fileSets>
+
+ <!-- Include licenses -->
+ <fileSet>
+ <outputDirectory></outputDirectory>
+ <directory>target/licenses</directory>
+ </fileSet>
+
+ <!-- Include extension .jar -->
+ <fileSet>
+ <directory>target</directory>
+ <outputDirectory></outputDirectory>
+ <includes>
+ <include>*.jar</include>
+ </includes>
+ </fileSet>
+
+ </fileSets>
+
+</assembly>
diff --git a/extensions/guacamole-display-statistics/src/main/resources/directives/guacClientStatistics.js b/extensions/guacamole-display-statistics/src/main/resources/directives/guacClientStatistics.js
new file mode 100644
index 0000000..4dc6b8a
--- /dev/null
+++ b/extensions/guacamole-display-statistics/src/main/resources/directives/guacClientStatistics.js
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+
+/**
+ * A directive which displays frame rendering performance statistics for a
+ * Guacamole client.
+ */
+angular.module('client').directive('guacClientStatistics', [function guacClientStatistics() {
+
+ const directive = {
+ restrict: 'E',
+ templateUrl: 'app/ext/display-stats/templates/guacClientStatistics.html',
+ };
+
+ directive.scope = {
+
+ /**
+ * The Guacamole client to display frame rendering statistics for.
+ *
+ * @type ManagedClient
+ */
+ client : '='
+
+ };
+
+ directive.controller = ['$scope', function guacClientStatisticsController($scope) {
+
+ /**
+ * Updates the displayed frame rendering statistics to the values
+ * within the given statistics object.
+ *
+ * @param {!Guacamole.Display.Statistics} stats
+ * An object containing general rendering performance statistics for
+ * the remote desktop, Guacamole server, and Guacamole client.
+ */
+ var updateStatistics = function updateStatistics(stats) {
+ $scope.$apply(function statisticsChanged() {
+ $scope.statistics = stats;
+ });
+ };
+
+ /**
+ * Returns whether the given value is a defined value that should be
+ * rendered within the statistics toolbar.
+ *
+ * @param {number} value
+ * The value to test.
+ *
+ * @returns {!boolean}
+ * true if the given value should be rendered within the statistics
+ * toolbar, false otherwise.
+ */
+ $scope.hasValue = function hasValue(value) {
+ return value || value === 0;
+ };
+
+ /**
+ * Rounds the given numeric value to the nearest hundredth (two decimal places).
+ *
+ * @param {!number} value
+ * The value to round.
+ *
+ * @param {!number}
+ * The given value, rounded to the nearest hundredth.
+ */
+ $scope.round = function round(value) {
+ return Math.round(value * 100) / 100;
+ };
+
+ // Assign/remove onstatistics handlers to track the statistics of the
+ // current client
+ $scope.$watch('client', function clientChanged(client, oldClient) {
+
+ if (oldClient)
+ oldClient.managedDisplay.display.onstatistics = null;
+
+ client.managedDisplay.display.statisticWindow = 1000;
+ client.managedDisplay.display.onstatistics = updateStatistics;
+
+ });
+
+ // Clear onstatistics handler when directive is being unloaded
+ $scope.$on('$destroy', function scopeDestroyed() {
+ if ($scope.client)
+ $scope.client.managedDisplay.display.onstatistics = null;
+ });
+
+ }];
+
+ return directive;
+
+}]);
diff --git a/extensions/guacamole-display-statistics/src/main/resources/guac-manifest.json b/extensions/guacamole-display-statistics/src/main/resources/guac-manifest.json
new file mode 100644
index 0000000..71c84b3
--- /dev/null
+++ b/extensions/guacamole-display-statistics/src/main/resources/guac-manifest.json
@@ -0,0 +1,28 @@
+{
+
+ "guacamoleVersion" : "1.4.0",
+
+ "name" : "Display Statistic Toolbar",
+ "namespace" : "display-stats",
+
+ "translations" : [
+ "translations/en.json"
+ ],
+
+ "js" : [
+ "display-stats.min.js"
+ ],
+
+ "css" : [
+ "display-stats.min.css"
+ ],
+
+ "html" : [
+ "html/add-statistics.html"
+ ],
+
+ "resources" : {
+ "templates/guacClientStatistics.html" : "text/html"
+ }
+
+}
diff --git a/extensions/guacamole-display-statistics/src/main/resources/html/add-statistics.html b/extensions/guacamole-display-statistics/src/main/resources/html/add-statistics.html
new file mode 100644
index 0000000..5978f7f
--- /dev/null
+++ b/extensions/guacamole-display-statistics/src/main/resources/html/add-statistics.html
@@ -0,0 +1,4 @@
+<meta name="after" content=".client-tile guac-client">
+
+<!-- Guacamole display statistics -->
+<guac-client-statistics client="client"></guac-client-statistics>
diff --git a/extensions/guacamole-display-statistics/src/main/resources/license.txt b/extensions/guacamole-display-statistics/src/main/resources/license.txt
new file mode 100644
index 0000000..042f3ce
--- /dev/null
+++ b/extensions/guacamole-display-statistics/src/main/resources/license.txt
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
diff --git a/extensions/guacamole-display-statistics/src/main/resources/styles/clientStatistics.css b/extensions/guacamole-display-statistics/src/main/resources/styles/clientStatistics.css
new file mode 100644
index 0000000..ba46614
--- /dev/null
+++ b/extensions/guacamole-display-statistics/src/main/resources/styles/clientStatistics.css
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+guac-client-statistics {
+ font-size: 13px;
+ color: white;
+ background: #111;
+}
+
+guac-client-statistics dl.client-statistics {
+ display: table;
+ margin: 0;
+ padding: 0.25em;
+}
+
+guac-client-statistics dl.client-statistics dt,
+guac-client-statistics dl.client-statistics dd {
+ display: table-cell;
+ padding: 0.25em;
+}
+
+guac-client-statistics dl.client-statistics dt {
+ padding-right: 0.5em;
+ padding-left: 1em;
+}
+
+guac-client-statistics dl.client-statistics dt:first-child {
+ padding-left: 0.5em;
+}
+
+guac-client-statistics dl.client-statistics dd {
+ min-width: 6em;
+ border: 1px solid rgba(255, 255, 255, 0.125);
+ border-radius: 3px;
+ background: black;
+}
+
+guac-client-statistics dl.client-statistics dd.no-value::before {
+ color: #888;
+ content: '-';
+}
diff --git a/extensions/guacamole-display-statistics/src/main/resources/templates/guacClientStatistics.html b/extensions/guacamole-display-statistics/src/main/resources/templates/guacClientStatistics.html
new file mode 100644
index 0000000..ae7537c
--- /dev/null
+++ b/extensions/guacamole-display-statistics/src/main/resources/templates/guacClientStatistics.html
@@ -0,0 +1,39 @@
+<dl class="client-statistics">
+
+ <dt class="client-statistic desktop-fps">
+ {{ 'CLIENT.FIELD_HEADER_DESKTOP_FRAMERATE' | translate }}
+ </dt>
+ <dd ng-class="{ 'no-value' : !hasValue(statistics.desktopFps) }">
+ <span ng-show="hasValue(statistics.desktopFps)"
+ translate="CLIENT.INFO_FRAMERATE"
+ translate-values="{ VALUE : round(statistics.desktopFps) }"></span>
+ </dd>
+
+ <dt class="client-statistic server-fps">
+ {{ 'CLIENT.FIELD_HEADER_SERVER_FRAMERATE' | translate }}
+ </dt>
+ <dd ng-class="{ 'no-value' : !hasValue(statistics.serverFps) }">
+ <span ng-show="hasValue(statistics.serverFps)"
+ translate="CLIENT.INFO_FRAMERATE"
+ translate-values="{ VALUE : round(statistics.serverFps) }"></span>
+ </dd>
+
+ <dt class="client-statistic client-fps">
+ {{ 'CLIENT.FIELD_HEADER_CLIENT_FRAMERATE' | translate }}
+ </dt>
+ <dd ng-class="{ 'no-value' : !hasValue(statistics.clientFps) }">
+ <span ng-show="hasValue(statistics.clientFps)"
+ translate="CLIENT.INFO_FRAMERATE"
+ translate-values="{ VALUE : round(statistics.clientFps) }"></span>
+ </dd>
+
+ <dt class="client-statistic drop-rate">
+ {{ 'CLIENT.FIELD_HEADER_DROP_FRAMERATE' | translate }}
+ </dt>
+ <dd ng-class="{ 'no-value' : !hasValue(statistics.dropRate) }">
+ <span ng-show="hasValue(statistics.dropRate)"
+ translate="CLIENT.INFO_FRAMERATE"
+ translate-values="{ VALUE : round(statistics.dropRate) }"></span>
+ </dd>
+
+</dl>
\ No newline at end of file
diff --git a/extensions/guacamole-display-statistics/src/main/resources/translations/en.json b/extensions/guacamole-display-statistics/src/main/resources/translations/en.json
new file mode 100644
index 0000000..ef42908
--- /dev/null
+++ b/extensions/guacamole-display-statistics/src/main/resources/translations/en.json
@@ -0,0 +1,12 @@
+{
+ "CLIENT" : {
+
+ "FIELD_HEADER_CLIENT_FRAMERATE" : "Guacamole (Client):",
+ "FIELD_HEADER_DESKTOP_FRAMERATE" : "Remote Desktop:",
+ "FIELD_HEADER_DROP_FRAMERATE" : "Drop:",
+ "FIELD_HEADER_SERVER_FRAMERATE" : "Guacamole (Server):",
+
+ "INFO_FRAMERATE" : "{VALUE} fps"
+
+ }
+}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/pom.xml b/extensions/guacamole-vault/modules/guacamole-vault-base/pom.xml
index 96fe118..a8137b5 100644
--- a/extensions/guacamole-vault/modules/guacamole-vault-base/pom.xml
+++ b/extensions/guacamole-vault/modules/guacamole-vault-base/pom.xml
@@ -59,6 +59,13 @@
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
+ <!-- JUnit -->
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+
<!-- Guice -->
<dependency>
<groupId>com.google.inject</groupId>
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java
new file mode 100644
index 0000000..2bd08cd
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java
@@ -0,0 +1,42 @@
+/*
+ * 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.vault.conf;
+
+import java.util.Collection;
+
+import org.apache.guacamole.form.Form;
+
+/**
+ * A service that exposes attributes for the admin UI, specific to the vault
+ * implementation. Any vault implementation will need to expose the attributes
+ * necessary for that implementation.
+ */
+public interface VaultAttributeService {
+
+ /**
+ * Return all custom connection group attributes to be exposed through the
+ * admin UI for the current vault implementation.
+ *
+ * @return
+ * All custom connection group attributes to be exposed through the
+ * admin UI for the current vault implementation.
+ */
+ public Collection<Form> getConnectionGroupAttributes();
+}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultConfigurationService.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultConfigurationService.java
index a666a7b..490da68 100644
--- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultConfigurationService.java
+++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultConfigurationService.java
@@ -55,7 +55,7 @@
@Inject
private VaultSecretService secretService;
-
+
/**
* ObjectMapper for deserializing YAML.
*/
@@ -127,7 +127,7 @@
return Collections.emptyMap();
return mapping;
-
+
}
// Fail if YAML is invalid/unreadable
@@ -169,7 +169,7 @@
String secretName = super.getProperty(name);
if (secretName == null)
return null;
-
+
return secretService.getValue(secretName).get();
}
@@ -177,7 +177,7 @@
if (e.getCause() instanceof GuacamoleException)
throw (GuacamoleException) e;
-
+
throw new GuacamoleServerException(String.format("Property "
+ "\"%s\" could not be retrieved from the vault.", name), e);
}
@@ -187,4 +187,23 @@
}
+ /**
+ * Return whether Windows domains should be split out from usernames when
+ * fetched from the vault.
+ *
+ * For example: "DOMAIN\\user" or "user@DOMAIN" should both
+ * be split into seperate username and domain tokens if this configuration
+ * is true. If false, no domain token should be created and the above values
+ * should be stored directly in the username token.
+ *
+ * @return
+ * true if windows domains should be split out from usernames, false
+ * otherwise.
+ *
+ * @throws GuacamoleException
+ * If the value specified within guacamole.properties cannot be
+ * parsed.
+ */
+ public abstract boolean getSplitWindowsUsernames() throws GuacamoleException;
+
}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/VaultSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/VaultSecretService.java
index 76349ba..81204be 100644
--- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/VaultSecretService.java
+++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/VaultSecretService.java
@@ -22,6 +22,8 @@
import java.util.Map;
import java.util.concurrent.Future;
import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.auth.Connectable;
+import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.protocol.GuacamoleConfiguration;
import org.apache.guacamole.token.TokenFilter;
@@ -55,7 +57,9 @@
/**
* Returns a Future which eventually completes with the value of the secret
* having the given name. If no such secret exists, the Future will be
- * completed with null.
+ * completed with null. The secrets retrieved from this method are independent
+ * of the context of the particular connection being established, or any
+ * associated user context.
*
* @param name
* The name of the secret to retrieve.
@@ -73,6 +77,35 @@
Future<String> getValue(String name) throws GuacamoleException;
/**
+ * Returns a Future which eventually completes with the value of the secret
+ * having the given name. If no such secret exists, the Future will be
+ * completed with null. The connection or connection group, as well as the
+ * user context associated with the request are provided for additional context.
+ *
+ * @param userContext
+ * The user context associated with the connection or connection group for
+ * which the secret is being retrieved.
+ *
+ * @param connectable
+ * The connection or connection group for which the secret is being retrieved.
+ *
+ * @param name
+ * The name of the secret to retrieve.
+ *
+ * @return
+ * A Future which completes with value of the secret having the given
+ * name. If no such secret exists, the Future will be completed with
+ * null. If an error occurs asynchronously which prevents retrieval of
+ * the secret, that error will be exposed through an ExecutionException
+ * when an attempt is made to retrieve the value from the Future.
+ *
+ * @throws GuacamoleException
+ * If the secret cannot be retrieved due to an error.
+ */
+ Future<String> getValue(UserContext userContext, Connectable connectable,
+ String name) throws GuacamoleException;
+
+ /**
* Returns a map of token names to corresponding Futures which eventually
* complete with the value of that token, where each token is dynamically
* defined based on connection parameters. If a vault implementation allows
@@ -80,6 +113,12 @@
* function should be implemented to provide automatic tokens for those
* secrets and remove the need for manual mapping via YAML.
*
+ * @param userContext
+ * The user context from which the connectable originated.
+ *
+ * @param connectable
+ * The connection or connection group for which the tokens are being replaced.
+ *
* @param config
* The configuration of the Guacamole connection for which tokens are
* being generated. This configuration may be empty or partial,
@@ -99,7 +138,7 @@
* If an error occurs producing the tokens and values required for the
* given configuration.
*/
- Map<String, Future<String>> getTokens(GuacamoleConfiguration config,
- TokenFilter filter) throws GuacamoleException;
+ Map<String, Future<String>> getTokens(UserContext userContext, Connectable connectable,
+ GuacamoleConfiguration config, TokenFilter filter) throws GuacamoleException;
}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/WindowsUsername.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/WindowsUsername.java
new file mode 100644
index 0000000..36d8a1b
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/WindowsUsername.java
@@ -0,0 +1,157 @@
+/*
+ * 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.vault.secret;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nonnull;
+
+/**
+ * A class representing a Windows username, which may optionally also include
+ * a domain. This class can be used to parse the username and domain out of a
+ * username from a vault.
+ */
+public class WindowsUsername {
+
+ /**
+ * A pattern for matching a down-level logon name containing a Windows
+ * domain and username - e.g. domain\\user. For more information, see
+ * https://docs.microsoft.com/en-us/windows/win32/secauthn/user-name-formats#down-level-logon-name
+ */
+ private static final Pattern DOWN_LEVEL_LOGON_NAME_PATTERN = Pattern.compile(
+ "(?<domain>[^@\\\\]+)\\\\(?<username>[^@\\\\]+)");
+
+ /**
+ * A pattern for matching a user principal name containing a Windows
+ * domain and username - e.g. user@domain. For more information, see
+ * https://docs.microsoft.com/en-us/windows/win32/secauthn/user-name-formats#user-principal-name
+ */
+ private static final Pattern USER_PRINCIPAL_NAME_PATTERN = Pattern.compile(
+ "(?<username>[^@\\\\]+)@(?<domain>[^@\\\\]+)");
+
+ /**
+ * The username associated with the potential Windows domain/username
+ * value. If no domain is found, the username field will contain the
+ * entire value as read from the vault.
+ */
+ private final String username;
+
+ /**
+ * The dinaun associated with the potential Windows domain/username
+ * value. If no domain is found, this will be null.
+ */
+ private final String domain;
+
+ /**
+ * Create a WindowsUsername record with no associated domain.
+ *
+ * @param username
+ * The username, which should be the entire value as extracted
+ * from the vault.
+ */
+ private WindowsUsername(@Nonnull String username) {
+ this.username = username;
+ this.domain = null;
+ }
+
+ /**
+ * Create a WindowsUsername record with a username and a domain.
+ *
+ * @param username
+ * The username portion of the field value from the vault.
+ *
+ * @param domain
+ * The domain portion of the field value from the vault.
+ */
+ private WindowsUsername(
+ @Nonnull String username, @Nonnull String domain) {
+ this.username = username;
+ this.domain = domain;
+ }
+
+ /**
+ * Return the value of the username as extracted from the vault field.
+ * If the domain is null, this will be the entire field value.
+ *
+ * @return
+ * The username value as extracted from the vault field.
+ */
+ public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Return the value of the domain as extracted from the vault field.
+ * If this is null, it means that no domain was found in the vault field.
+ *
+ * @return
+ * The domain value as extracted from the vault field.
+ */
+ public String getDomain() {
+ return domain;
+ }
+
+ /**
+ * Return true if a domain was found in the vault field, false otherwise.
+ *
+ * @return
+ * true if a domain was found in the vault field, false otherwise.
+ */
+ public boolean hasDomain() {
+ return this.domain != null;
+ }
+
+ /**
+ * Strip off a Windows domain from the provided username, if one is
+ * present. For example: "DOMAIN\\user" or "user@DOMAIN" will both
+ * be stripped to just "user". Note: neither the '@' or '\\' characters
+ * are valid in Windows usernames.
+ *
+ * @param vaultField
+ * The raw field value as retrieved from the vault. This might contain
+ * a Windows domain.
+ *
+ * @return
+ * The provided username with the Windows domain stripped off, if one
+ * is present.
+ */
+ public static WindowsUsername splitWindowsUsernameFromDomain(String vaultField) {
+
+ // If it's the down-level logon format, return the extracted username and domain
+ Matcher downLevelLogonMatcher = DOWN_LEVEL_LOGON_NAME_PATTERN.matcher(vaultField);
+ if (downLevelLogonMatcher.matches())
+ return new WindowsUsername(
+ downLevelLogonMatcher.group("username"),
+ downLevelLogonMatcher.group("domain"));
+
+ // If it's the user principal format, return the extracted username and domain
+ Matcher userPrincipalMatcher = USER_PRINCIPAL_NAME_PATTERN.matcher(vaultField);
+ if (userPrincipalMatcher.matches())
+ return new WindowsUsername(
+ userPrincipalMatcher.group("username"),
+ userPrincipalMatcher.group("domain"));
+
+ // If none of the expected formats matched, return the username with do domain
+ return new WindowsUsername(vaultField);
+
+ }
+
+}
\ No newline at end of file
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultDirectoryService.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultDirectoryService.java
new file mode 100644
index 0000000..700d9d3
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultDirectoryService.java
@@ -0,0 +1,140 @@
+/*
+ * 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.vault.user;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.auth.ActiveConnection;
+import org.apache.guacamole.net.auth.Connection;
+import org.apache.guacamole.net.auth.ConnectionGroup;
+import org.apache.guacamole.net.auth.Directory;
+import org.apache.guacamole.net.auth.SharingProfile;
+import org.apache.guacamole.net.auth.User;
+import org.apache.guacamole.net.auth.UserGroup;
+
+/**
+ * A service that allows a vault implementation to override the directory
+ * for any entity that a user context may return.
+ */
+public abstract class VaultDirectoryService {
+
+ /**
+ * Given an existing User Directory, return a new Directory for
+ * this vault implementation.
+ *
+ * @return
+ * A new User Directory based on the provided Directory.
+ *
+ * @throws GuacamoleException
+ * If an error occurs while creating the Directory.
+ */
+ public Directory<User> getUserDirectory(
+ Directory<User> underlyingDirectory) throws GuacamoleException {
+
+ // By default, the provided directly will be returned unchanged
+ return underlyingDirectory;
+ }
+
+ /**
+ * Given an existing UserGroup Directory, return a new Directory for
+ * this vault implementation.
+ *
+ * @return
+ * A new UserGroup Directory based on the provided Directory.
+ *
+ * @throws GuacamoleException
+ * If an error occurs while creating the Directory.
+ */
+ public Directory<UserGroup> getUserGroupDirectory(
+ Directory<UserGroup> underlyingDirectory) throws GuacamoleException {
+
+ // Unless overriden in the vault implementation, the underlying directory
+ // will be returned directly
+ return underlyingDirectory;
+ }
+
+ /**
+ * Given an existing Connection Directory, return a new Directory for
+ * this vault implementation.
+ *
+ * @return
+ * A new Connection Directory based on the provided Directory.
+ *
+ * @throws GuacamoleException
+ * If an error occurs while creating the Directory.
+ */
+ public Directory<Connection> getConnectionDirectory(
+ Directory<Connection> underlyingDirectory) throws GuacamoleException {
+
+ // By default, the provided directly will be returned unchanged
+ return underlyingDirectory;
+ }
+
+ /**
+ * Given an existing ConnectionGroup Directory, return a new Directory for
+ * this vault implementation.
+ *
+ * @return
+ * A new ConnectionGroup Directory based on the provided Directory.
+ *
+ * @throws GuacamoleException
+ * If an error occurs while creating the Directory.
+ */
+ public Directory<ConnectionGroup> getConnectionGroupDirectory(
+ Directory<ConnectionGroup> underlyingDirectory) throws GuacamoleException {
+
+ // By default, the provided directly will be returned unchanged
+ return underlyingDirectory;
+ }
+
+ /**
+ * Given an existing ActiveConnection Directory, return a new Directory for
+ * this vault implementation.
+ *
+ * @return
+ * A new ActiveConnection Directory based on the provided Directory.
+ *
+ * @throws GuacamoleException
+ * If an error occurs while creating the Directory.
+ */
+ public Directory<ActiveConnection> getActiveConnectionDirectory(
+ Directory<ActiveConnection> underlyingDirectory) throws GuacamoleException {
+
+ // By default, the provided directly will be returned unchanged
+ return underlyingDirectory;
+ }
+
+ /**
+ * Given an existing SharingProfile Directory, return a new Directory for
+ * this vault implementation.
+ *
+ * @return
+ * A new SharingProfile Directory based on the provided Directory.
+ *
+ * @throws GuacamoleException
+ * If an error occurs while creating the Directory.
+ */
+ public Directory<SharingProfile> getSharingProfileDirectory(
+ Directory<SharingProfile> underlyingDirectory) throws GuacamoleException {
+
+ // By default, the provided directly will be returned unchanged
+ return underlyingDirectory;
+ }
+
+}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java
index 5390148..8e8f668 100644
--- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java
+++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java
@@ -22,19 +22,33 @@
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
+
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.form.Form;
+import org.apache.guacamole.net.auth.ActiveConnection;
+import org.apache.guacamole.net.auth.Connectable;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.ConnectionGroup;
+import org.apache.guacamole.net.auth.Directory;
+import org.apache.guacamole.net.auth.SharingProfile;
import org.apache.guacamole.net.auth.TokenInjectingUserContext;
+import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.UserContext;
+import org.apache.guacamole.net.auth.UserGroup;
import org.apache.guacamole.protocol.GuacamoleConfiguration;
import org.apache.guacamole.token.GuacamoleTokenUndefinedException;
import org.apache.guacamole.token.TokenFilter;
+import org.apache.guacamole.vault.conf.VaultAttributeService;
import org.apache.guacamole.vault.conf.VaultConfigurationService;
import org.apache.guacamole.vault.secret.VaultSecretService;
import org.slf4j.Logger;
@@ -122,6 +136,20 @@
private VaultSecretService secretService;
/**
+ * Service for retrieving any custom attributes defined for the
+ * current vault implementation.
+ */
+ @Inject
+ private VaultAttributeService attributeService;
+
+ /**
+ * Service for modifying any underlying directories for the current
+ * vault implementation.
+ */
+ @Inject
+ private VaultDirectoryService directoryService;
+
+ /**
* Creates a new VaultUserContext which automatically injects tokens
* containing values of secrets retrieved from a vault. The given
* UserContext is decorated such that connections and connection groups
@@ -182,6 +210,10 @@
* corresponding values from the vault, using the given TokenFilter to
* filter tokens within the secret names prior to retrieving those secrets.
*
+ * @param connectable
+ * The connection or connection group to which the connection is being
+ * established.
+ *
* @param tokenMapping
* The mapping dictating the name of the secret which maps to each
* parameter token, where the key is the name of the parameter token
@@ -211,7 +243,8 @@
* If the value for any applicable secret cannot be retrieved from the
* vault due to an error.
*/
- private Map<String, Future<String>> getTokens(Map<String, String> tokenMapping,
+ private Map<String, Future<String>> getTokens(
+ Connectable connectable, Map<String, String> tokenMapping,
TokenFilter secretNameFilter, GuacamoleConfiguration config,
TokenFilter configFilter) throws GuacamoleException {
@@ -236,14 +269,16 @@
// Initiate asynchronous retrieval of the token value
String tokenName = entry.getKey();
- Future<String> secret = secretService.getValue(secretName);
+ Future<String> secret = secretService.getValue(
+ this, connectable, secretName);
pendingTokens.put(tokenName, secret);
}
// Additionally include any dynamic, parameter-based tokens
- pendingTokens.putAll(secretService.getTokens(config, configFilter));
-
+ pendingTokens.putAll(secretService.getTokens(
+ this, connectable, config, configFilter));
+
return pendingTokens;
}
@@ -318,7 +353,8 @@
// Substitute tokens producing secret names, retrieving and storing
// those secrets as parameter tokens
- tokens.putAll(resolve(getTokens(confService.getTokenMapping(), filter,
+ tokens.putAll(resolve(getTokens(
+ connectionGroup, confService.getTokenMapping(), filter,
null, new TokenFilter(tokens))));
}
@@ -398,9 +434,62 @@
// Substitute tokens producing secret names, retrieving and storing
// those secrets as parameter tokens
- tokens.putAll(resolve(getTokens(confService.getTokenMapping(), filter,
- config, new TokenFilter(tokens))));
+ tokens.putAll(resolve(getTokens(connection, confService.getTokenMapping(),
+ filter, config, new TokenFilter(tokens))));
}
+ @Override
+ public Collection<Form> getConnectionGroupAttributes() {
+
+ // Add any custom attributes to any previously defined attributes
+ return Collections.unmodifiableCollection(Stream.concat(
+ super.getConnectionGroupAttributes().stream(),
+ attributeService.getConnectionGroupAttributes().stream()
+ ).collect(Collectors.toList()));
+
+ }
+
+ @Override
+ public Directory<User> getUserDirectory() throws GuacamoleException {
+
+ // Defer to the vault-specific directory service
+ return directoryService.getUserDirectory(super.getUserDirectory());
+ }
+
+ @Override
+ public Directory<UserGroup> getUserGroupDirectory() throws GuacamoleException {
+
+ // Defer to the vault-specific directory service
+ return directoryService.getUserGroupDirectory(super.getUserGroupDirectory());
+ }
+
+ @Override
+ public Directory<Connection> getConnectionDirectory() throws GuacamoleException {
+
+ // Defer to the vault-specific directory service
+ return directoryService.getConnectionDirectory(super.getConnectionDirectory());
+ }
+
+ @Override
+ public Directory<ConnectionGroup> getConnectionGroupDirectory() throws GuacamoleException {
+
+ // Defer to the vault-specific directory service
+ return directoryService.getConnectionGroupDirectory(super.getConnectionGroupDirectory());
+ }
+
+ @Override
+ public Directory<ActiveConnection> getActiveConnectionDirectory() throws GuacamoleException {
+
+ // Defer to the vault-specific directory service
+ return directoryService.getActiveConnectionDirectory(super.getActiveConnectionDirectory());
+ }
+
+ @Override
+ public Directory<SharingProfile> getSharingProfileDirectory() throws GuacamoleException {
+
+ // Defer to the vault-specific directory service
+ return directoryService.getSharingProfileDirectory(super.getSharingProfileDirectory());
+ }
+
}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/test/java/org/apache/guacamole/vault/secret/WindowsUsernameTest.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/test/java/org/apache/guacamole/vault/secret/WindowsUsernameTest.java
new file mode 100644
index 0000000..61f4546
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/test/java/org/apache/guacamole/vault/secret/WindowsUsernameTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.vault.secret;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Class to test the parsing functionality of the WindowsUsername class.
+ */
+public class WindowsUsernameTest {
+
+ /**
+ * Verify that the splitWindowsUsernameFromDomain() method correctly strips Windows
+ * domains from provided usernames that include them, and does not modify
+ * usernames that do not have Windows domains.
+ */
+ @Test
+ public void testSplitWindowsUsernameFromDomain() {
+
+ WindowsUsername usernameAndDomain;
+
+ // If no Windows domain is present in the provided field, the username should
+ // contain the entire field, and no domain should be returned
+ usernameAndDomain = WindowsUsername.splitWindowsUsernameFromDomain("bob");
+ assertEquals(usernameAndDomain.getUsername(), "bob");
+ assertFalse(usernameAndDomain.hasDomain());
+
+ // It should parse down-level logon name style domains
+ usernameAndDomain = WindowsUsername.splitWindowsUsernameFromDomain("localhost\\bob");
+ assertEquals("bob", usernameAndDomain.getUsername(), "bob");
+ assertTrue(usernameAndDomain.hasDomain());
+ assertEquals("localhost", usernameAndDomain.getDomain());
+
+ // It should parse user principal name style domains
+ usernameAndDomain = WindowsUsername.splitWindowsUsernameFromDomain("bob@localhost");
+ assertEquals("bob", usernameAndDomain.getUsername(), "bob");
+ assertTrue(usernameAndDomain.hasDomain());
+ assertEquals("localhost", usernameAndDomain.getDomain());
+
+ // It should not match if there are an invalid number of separators
+ List<String> invalidSeparators = Arrays.asList(
+ "bob@local@host", "local\\host\\bob",
+ "bob\\local@host", "local@host\\bob");
+ invalidSeparators.stream().forEach(
+ invalidSeparator -> {
+
+ // An invalid number of separators means that the parse failed -
+ // there should be no detected domain, and the entire field value
+ // should be returned as the username
+ WindowsUsername parseOutput =
+ WindowsUsername.splitWindowsUsernameFromDomain(invalidSeparator);
+ assertFalse(parseOutput.hasDomain());
+ assertEquals(invalidSeparator, parseOutput.getUsername());
+
+ });
+
+ }
+
+}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java
index bcc5a78..3c28553 100644
--- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java
@@ -21,12 +21,19 @@
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.vault.VaultAuthenticationProviderModule;
+import org.apache.guacamole.vault.ksm.conf.KsmAttributeService;
import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService;
import org.apache.guacamole.vault.ksm.secret.KsmSecretService;
+import org.apache.guacamole.vault.ksm.user.KsmDirectoryService;
+import org.apache.guacamole.vault.conf.VaultAttributeService;
import org.apache.guacamole.vault.conf.VaultConfigurationService;
import org.apache.guacamole.vault.ksm.secret.KsmClient;
+import org.apache.guacamole.vault.ksm.secret.KsmClientFactory;
import org.apache.guacamole.vault.ksm.secret.KsmRecordService;
import org.apache.guacamole.vault.secret.VaultSecretService;
+import org.apache.guacamole.vault.user.VaultDirectoryService;
+
+import com.google.inject.assistedinject.FactoryModuleBuilder;
/**
* Guice module which configures injections specific to Keeper Secrets
@@ -49,10 +56,16 @@
protected void configureVault() {
// Bind services specific to Keeper Secrets Manager
- bind(KsmClient.class);
bind(KsmRecordService.class);
+ bind(VaultAttributeService.class).to(KsmAttributeService.class);
bind(VaultConfigurationService.class).to(KsmConfigurationService.class);
bind(VaultSecretService.class).to(KsmSecretService.class);
+ bind(VaultDirectoryService.class).to(KsmDirectoryService.class);
+
+ // Bind factory for creating KSM Clients
+ install(new FactoryModuleBuilder()
+ .implement(KsmClient.class, KsmClient.class)
+ .build(KsmClientFactory.class));
}
}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java
new file mode 100644
index 0000000..83ab9c4
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java
@@ -0,0 +1,63 @@
+/*
+ * 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.vault.ksm.conf;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.apache.guacamole.form.Form;
+import org.apache.guacamole.form.TextField;
+import org.apache.guacamole.vault.conf.VaultAttributeService;
+
+import com.google.inject.Singleton;
+
+/**
+ * A service that exposes KSM-specific attributes, allowing setting KSM
+ * configuration through the admin interface.
+ */
+@Singleton
+public class KsmAttributeService implements VaultAttributeService {
+
+ /**
+ * The name of the attribute which can contain a KSM configuration blob
+ * associated with a connection group.
+ */
+ public static final String KSM_CONFIGURATION_ATTRIBUTE = "ksm-config";
+
+ /**
+ * All attributes related to configuring the KSM vault on a
+ * per-connection-group basis.
+ */
+ public static final Form KSM_CONFIGURATION_FORM = new Form("ksm-config",
+ Arrays.asList(new TextField(KSM_CONFIGURATION_ATTRIBUTE)));
+
+ /**
+ * All KSM-specific connection group attributes, organized by form.
+ */
+ public static final Collection<Form> KSM_CONNECTION_GROUP_ATTRIBUTES =
+ Collections.unmodifiableCollection(Arrays.asList(KSM_CONFIGURATION_FORM));
+
+ @Override
+ public Collection<Form> getConnectionGroupAttributes() {
+ return KSM_CONNECTION_GROUP_ATTRIBUTES;
+ }
+
+}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfig.java
similarity index 65%
rename from extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java
rename to extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfig.java
index aaddb0d..54aaec7 100644
--- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfig.java
@@ -23,21 +23,28 @@
import com.keepersecurity.secretsManager.core.KeyValueStorage;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
-import org.apache.guacamole.properties.GuacamoleProperty;
/**
- * A GuacamoleProperty whose value is Keeper Secrets Manager {@link KeyValueStorage}
- * object. The value of this property must be base64-encoded JSON, as output by
- * the Keeper Commander CLI tool via the "sm client add" command.
+ * A utility for parsing base64-encoded JSON, as output by the Keeper Commander
+ * CLI tool via the "sm client add" command into a Keeper Secrets Manager
+ * {@link KeyValueStorage} object.
*/
-public abstract class KsmConfigProperty implements GuacamoleProperty<KeyValueStorage> {
+public class KsmConfig {
- @Override
- public KeyValueStorage parseValue(String value) throws GuacamoleException {
-
- // If no property provided, return null.
- if (value == null)
- return null;
+ /**
+ * Given a base64-encoded JSON KSM configuration, parse and return a
+ * KeyValueStorage object.
+ *
+ * @param value
+ * The base64-encoded JSON KSM configuration to parse.
+ *
+ * @return
+ * The KeyValueStorage that is a result of the parsing operation
+ *
+ * @throws GuacamoleException
+ * If the provided value is not valid base-64 encoded JSON KSM configuration.
+ */
+ public static KeyValueStorage parseKsmConfig(String value) throws GuacamoleException {
// Parse base64 value as KSM config storage
try {
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java
index 38bcaae..3ef02b8 100644
--- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java
@@ -21,10 +21,18 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
+
+import javax.annotation.Nonnull;
+
import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.properties.BooleanGuacamoleProperty;
+import org.apache.guacamole.properties.StringGuacamoleProperty;
import org.apache.guacamole.vault.conf.VaultConfigurationService;
+
+import com.keepersecurity.secretsManager.core.InMemoryStorage;
+import com.keepersecurity.secretsManager.core.KeyValueStorage;
import com.keepersecurity.secretsManager.core.SecretsManagerOptions;
/**
@@ -57,7 +65,7 @@
* The base64-encoded configuration information generated by the Keeper
* Commander CLI tool.
*/
- private static final KsmConfigProperty KSM_CONFIG = new KsmConfigProperty() {
+ private static final StringGuacamoleProperty KSM_CONFIG = new StringGuacamoleProperty() {
@Override
public String getName() {
@@ -77,6 +85,18 @@
};
/**
+ * Whether windows domains should be stripped off from usernames that are
+ * read from the KSM vault.
+ */
+ private static final BooleanGuacamoleProperty STRIP_WINDOWS_DOMAINS = new BooleanGuacamoleProperty() {
+
+ @Override
+ public String getName() {
+ return "ksm-strip-windows-domains";
+ }
+ };
+
+ /**
* Creates a new KsmConfigurationService which reads the configuration
* from "ksm-token-mapping.yml" and properties from
* "guacamole.properties.ksm". The token mapping is a YAML file which lists
@@ -105,22 +125,73 @@
return environment.getProperty(ALLOW_UNVERIFIED_CERT, false);
}
+ @Override
+ public boolean getSplitWindowsUsernames() throws GuacamoleException {
+ return environment.getProperty(STRIP_WINDOWS_DOMAINS, false);
+ }
+
+
+ /**
+ * Return the globally-defined base-64-encoded JSON KSM configuration blob
+ * as a string.
+ *
+ * @return
+ * The globally-defined base-64-encoded JSON KSM configuration blob
+ * as a string.
+ *
+ * @throws GuacamoleException
+ * If the value specified within guacamole.properties cannot be
+ * parsed or does not exist.
+ */
+ public String getKsmConfig() throws GuacamoleException {
+ return environment.getRequiredProperty(KSM_CONFIG);
+ }
+
+ /**
+ * Given a base64-encoded JSON KSM configuration, parse and return a
+ * KeyValueStorage object.
+ *
+ * @param value
+ * The base64-encoded JSON KSM configuration to parse.
+ *
+ * @return
+ * The KeyValueStorage that is a result of the parsing operation
+ *
+ * @throws GuacamoleException
+ * If the provided value is not valid base-64 encoded JSON KSM configuration.
+ */
+ private static KeyValueStorage parseKsmConfig(String value) throws GuacamoleException {
+
+ // Parse base64 value as KSM config storage
+ try {
+ return new InMemoryStorage(value);
+ }
+ catch (IllegalArgumentException e) {
+ throw new GuacamoleServerException("Invalid base64 configuration "
+ + "for Keeper Secrets Manager.", e);
+ }
+
+ }
+
/**
* Returns the options required to authenticate with Keeper Secrets Manager
* when retrieving secrets. These options are read from the contents of
* base64-encoded JSON configuration data generated by the Keeper Commander
- * CLI tool.
+ * CLI tool. This configuration data must be passed directly as an argument.
+ *
+ * @param ksmConfig
+ * The KSM configuration blob to parse.
*
* @return
* The options that should be used when connecting to Keeper Secrets
* Manager when retrieving secrets.
*
* @throws GuacamoleException
- * If required properties are not specified within
- * guacamole.properties or cannot be parsed.
+ * If an invalid ksmConfig parameter is provided.
*/
- public SecretsManagerOptions getSecretsManagerOptions() throws GuacamoleException {
- return new SecretsManagerOptions(environment.getRequiredProperty(KSM_CONFIG), null,
- getAllowUnverifiedCertificate());
+ public SecretsManagerOptions getSecretsManagerOptions(@Nonnull String ksmConfig) throws GuacamoleException {
+
+ return new SecretsManagerOptions(
+ parseKsmConfig(ksmConfig), null, getAllowUnverifiedCertificate());
}
}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java
index 2372dcb..5572ef7 100644
--- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java
@@ -20,14 +20,16 @@
package org.apache.guacamole.vault.ksm.secret;
import com.google.inject.Inject;
-import com.google.inject.Singleton;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
import com.keepersecurity.secretsManager.core.Hosts;
-import com.keepersecurity.secretsManager.core.KeeperFile;
import com.keepersecurity.secretsManager.core.KeeperRecord;
import com.keepersecurity.secretsManager.core.KeeperSecrets;
import com.keepersecurity.secretsManager.core.Login;
import com.keepersecurity.secretsManager.core.Notation;
import com.keepersecurity.secretsManager.core.SecretsManager;
+import com.keepersecurity.secretsManager.core.SecretsManagerOptions;
+
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -41,8 +43,8 @@
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+
import org.apache.guacamole.GuacamoleException;
-import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -54,7 +56,6 @@
* information), it's not possible for the server to perform a search of
* content on the client's behalf. The client has to perform its own search.
*/
-@Singleton
public class KsmClient {
/**
@@ -63,12 +64,6 @@
private static final Logger logger = LoggerFactory.getLogger(KsmClient.class);
/**
- * Service for retrieving configuration information.
- */
- @Inject
- private KsmConfigurationService confService;
-
- /**
* Service for retrieving data from records.
*/
@Inject
@@ -96,6 +91,11 @@
private static final long CACHE_INTERVAL = 5000;
/**
+ * The KSM configuration associated with this client instance.
+ */
+ private final SecretsManagerOptions ksmConfig;
+
+ /**
* Read/write lock which guards access to all cached data, including the
* timestamp recording the last time the cache was refreshed. Readers of
* the cache must first acquire (and eventually release) the read lock, and
@@ -119,7 +119,7 @@
* {@link #cacheLock} acquired appropriately.
*/
private KeeperSecrets cachedSecrets = null;
-
+
/**
* All records retrieved from Keeper Secrets Manager, where each key is the
* UID of the corresponding record. The contents of this Map are
@@ -180,6 +180,17 @@
private final Set<String> cachedAmbiguousUsernames = new HashSet<>();
/**
+ * Create a new KSM client based around the provided KSM configuration.
+ *
+ * @param ksmConfig
+ * The KSM configuration to use when retrieving properties from KSM.
+ */
+ @AssistedInject
+ public KsmClient(@Assisted SecretsManagerOptions ksmConfig) {
+ this.ksmConfig = ksmConfig;
+ }
+
+ /**
* Validates that all cached data is current with respect to
* {@link #CACHE_INTERVAL}, refreshing data from the server as needed.
*
@@ -211,12 +222,12 @@
// Attempt to pull all records first, allowing that operation to
// succeed/fail BEFORE we clear out the last cached success
- KeeperSecrets secrets = SecretsManager.getSecrets(confService.getSecretsManagerOptions());
+ KeeperSecrets secrets = SecretsManager.getSecrets(ksmConfig);
List<KeeperRecord> records = secrets.getRecords();
// Store all secrets within cache
cachedSecrets = secrets;
-
+
// Clear unambiguous cache of all records by UID
cachedRecordsByUid.clear();
@@ -398,7 +409,7 @@
* The record associated with the given username, or null if there is
* no such record or multiple such records.
*
- * @throws GuacamoleException
+ * @throws GuacamoleException
* If an error occurs that prevents the record from being retrieved.
*/
public KeeperRecord getRecordByLogin(String username) throws GuacamoleException {
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClientFactory.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClientFactory.java
new file mode 100644
index 0000000..f8220c1
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClientFactory.java
@@ -0,0 +1,45 @@
+/*
+ * 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.vault.ksm.secret;
+
+import javax.annotation.Nonnull;
+
+import com.keepersecurity.secretsManager.core.SecretsManagerOptions;
+
+/**
+ * Factory for creating KsmClient instances.
+ */
+public interface KsmClientFactory {
+
+ /**
+ * Returns a new instance of a KsmClient instance associated with
+ * the provided KSM configuration options.
+ *
+ * @param ksmConfigOptions
+ * The KSM config options to use when constructing the KsmClient
+ * object.
+ *
+ * @return
+ * A new KsmClient instance associated with the provided KSM config
+ * options.
+ */
+ KsmClient create(@Nonnull SecretsManagerOptions ksmConfigOptions);
+
+}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmRecordService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmRecordService.java
index d67b581..a3d79b4 100644
--- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmRecordService.java
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmRecordService.java
@@ -41,6 +41,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+
/**
* Service for automatically parsing out secrets and data from Keeper records.
*/
@@ -49,6 +50,13 @@
/**
* Regular expression which matches the labels of custom fields containing
+ * domains.
+ */
+ private static final Pattern DOMAIN_LABEL_PATTERN =
+ Pattern.compile("domain", Pattern.CASE_INSENSITIVE);
+
+ /**
+ * Regular expression which matches the labels of custom fields containing
* hostnames/addresses.
*/
private static final Pattern HOSTNAME_LABEL_PATTERN =
@@ -143,13 +151,13 @@
* empty or contains multiple values, null is returned. Note that null will
* also be returned if the mapping transformation returns null for the
* single value stored in the list.
- *
+ *
* @param <T>
* The type of object stored in the list.
- *
+ *
* @param <R>
* The type of object to return.
- *
+ *
* @param values
* The list to retrieve a single value from.
*
@@ -168,7 +176,7 @@
return null;
return mapper.apply(value);
-
+
}
/**
@@ -271,7 +279,7 @@
* multiple such fields, null is returned. Both standard and custom fields
* are searched. As standard fields do not have labels, any given label
* pattern is ignored for standard fields.
- *
+ *
* @param <T>
* The type of field to return.
*
@@ -339,7 +347,7 @@
return null;
foundFile = file;
-
+
}
return foundFile;
@@ -362,7 +370,7 @@
if (file == null)
return CompletableFuture.completedFuture(null);
-
+
return CompletableFuture.supplyAsync(() -> {
return new String(SecretsManager.downloadFile(file), StandardCharsets.UTF_8);
});
@@ -446,6 +454,38 @@
}
/**
+ * Returns the single domain associated with the given record. If the
+ * record has no associated domain, or multiple domains, null is
+ * returned. Domains are retrieved from "Text" and "Hidden" fields
+ * that have the label "domain" (case-insensitive).
+ *
+ * @param record
+ * The record to retrieve the domain from.
+ *
+ * @return
+ * The domain associated with the given record, or null if the record
+ * has no associated domain or multiple domains.
+ */
+ public String getDomain(KeeperRecord record) {
+
+ KeeperRecordData data = record.getData();
+ List<KeeperRecordField> custom = data.getCustom();
+
+ // First check text "domain" custom field ...
+ Text textField = getField(custom, Text.class, DOMAIN_LABEL_PATTERN);
+ if (textField != null)
+ return getSingleStringValue(textField.getValue());
+
+ // ... or hidden "domain" custom field if that's not found
+ HiddenField hiddenField = getField(custom, HiddenField.class, DOMAIN_LABEL_PATTERN);
+ if (hiddenField != null)
+ return getSingleStringValue(hiddenField.getValue());
+
+ return null;
+
+ }
+
+ /**
* Returns the password associated with the given record. Both standard and
* custom fields are searched. Only "Password" and "Hidden" field types are
* considered. Custom fields must additionally have the label "password"
@@ -555,7 +595,7 @@
// a pair of custom hidden fields for the private key and passphrase:
// the standard password field of the "Login" record refers to the
// user's own password, if any, not the passphrase of their key)
-
+
// Use password "private key" custom field as fallback ...
Password passwordField = getField(custom, Password.class, PASSPHRASE_LABEL_PATTERN);
if (passwordField != null)
@@ -567,7 +607,7 @@
return getSingleStringValue(hiddenField.getValue());
return null;
-
+
}
}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java
index 824f9e5..d8168dc 100644
--- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java
@@ -22,29 +22,49 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.keepersecurity.secretsManager.core.KeeperRecord;
+import com.keepersecurity.secretsManager.core.SecretsManagerOptions;
+
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Future;
+import javax.annotation.Nonnull;
+
import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.auth.Connectable;
+import org.apache.guacamole.net.auth.Connection;
+import org.apache.guacamole.net.auth.ConnectionGroup;
+import org.apache.guacamole.net.auth.Directory;
+import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.protocol.GuacamoleConfiguration;
import org.apache.guacamole.token.TokenFilter;
+import org.apache.guacamole.vault.ksm.conf.KsmAttributeService;
+import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService;
import org.apache.guacamole.vault.secret.VaultSecretService;
+import org.apache.guacamole.vault.secret.WindowsUsername;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Service which retrieves secrets from Keeper Secrets Manager.
+ * The configuration used to connect to KSM can be set at a global
+ * level using guacamole.properties, or using a connection group
+ * attribute.
*/
@Singleton
public class KsmSecretService implements VaultSecretService {
/**
- * Client for retrieving records and secrets from Keeper Secrets Manager.
+ * Logger for this class.
*/
- @Inject
- private KsmClient ksm;
+ private static final Logger logger = LoggerFactory.getLogger(VaultSecretService.class);
/**
* Service for retrieving data from records.
@@ -52,6 +72,57 @@
@Inject
private KsmRecordService recordService;
+ /**
+ * Service for retrieving configuration information.
+ */
+ @Inject
+ private KsmConfigurationService confService;
+
+ /**
+ * Factory for creating KSM client instances.
+ */
+ @Inject
+ private KsmClientFactory ksmClientFactory;
+
+ /**
+ * A map of base-64 encoded JSON KSM config blobs to associated KSM client instances.
+ * A distinct KSM client will exist for every KSM config.
+ */
+ private final ConcurrentMap<String, KsmClient> ksmClientMap = new ConcurrentHashMap<>();
+
+ /**
+ * Create and return a KSM client for the provided KSM config if not already
+ * present in the client map, otherwise return the existing client entry.
+ *
+ * @param ksmConfig
+ * The base-64 encoded JSON KSM config blob associated with the client entry.
+ * If an associated entry does not already exist, it will be created using
+ * this configuration.
+ *
+ * @return
+ * A KSM client for the provided KSM config if not already present in the
+ * client map, otherwise the existing client entry.
+ *
+ * @throws GuacamoleException
+ * If an error occurs while creating the KSM client.
+ */
+ private KsmClient getClient(@Nonnull String ksmConfig)
+ throws GuacamoleException {
+
+ // If a client already exists for the provided config, use it
+ KsmClient ksmClient = ksmClientMap.get(ksmConfig);
+ if (ksmClient != null)
+ return ksmClient;
+
+ // Create and store a new KSM client instance for the provided KSM config blob
+ SecretsManagerOptions options = confService.getSecretsManagerOptions(ksmConfig);
+ ksmClient = ksmClientFactory.create(options);
+ KsmClient prevClient = ksmClientMap.putIfAbsent(ksmConfig, ksmClient);
+
+ // If the client was already set before this thread got there, use the existing one
+ return prevClient != null ? prevClient : ksmClient;
+ }
+
@Override
public String canonicalize(String nameComponent) {
try {
@@ -67,8 +138,20 @@
}
@Override
+ public Future<String> getValue(UserContext userContext, Connectable connectable,
+ String name) throws GuacamoleException {
+
+ // Attempt to find a KSM config for this connection or group
+ String ksmConfig = getConnectionGroupKsmConfig(userContext, connectable);
+
+ return getClient(ksmConfig).getSecret(name);
+ }
+
+ @Override
public Future<String> getValue(String name) throws GuacamoleException {
- return ksm.getSecret(name);
+
+ // Use the default KSM configuration from guacamole.properties
+ return getClient(confService.getKsmConfig()).getSecret(name);
}
/**
@@ -86,17 +169,48 @@
* @param record
* The record to retrieve secrets from when generating tokens. This may
* be null.
+ *
+ * @throws GuacamoleException
+ * If configuration details in guacamole.properties cannot be parsed.
*/
private void addRecordTokens(Map<String, Future<String>> tokens, String prefix,
- KeeperRecord record) {
+ KeeperRecord record) throws GuacamoleException {
if (record == null)
return;
+ // Domain of server-related record
+ String domain = recordService.getDomain(record);
+ if (domain != null)
+ tokens.put(prefix + "DOMAIN", CompletableFuture.completedFuture(domain));
+
// Username of server-related record
String username = recordService.getUsername(record);
- if (username != null)
- tokens.put(prefix + "USERNAME", CompletableFuture.completedFuture(username));
+ if (username != null) {
+
+ // If the record had no directly defined domain, but there is a
+ // username, and the configuration is enabled to split Windows
+ // domains out of usernames, attempt to split the domain out now
+ if (domain == null && confService.getSplitWindowsUsernames()) {
+ WindowsUsername usernameAndDomain =
+ WindowsUsername.splitWindowsUsernameFromDomain(username);
+
+ // Always store the username token
+ tokens.put(prefix + "USERNAME", CompletableFuture.completedFuture(
+ usernameAndDomain.getUsername()));
+
+ // Only store the domain if one is detected
+ if (usernameAndDomain.hasDomain())
+ tokens.put(prefix + "DOMAIN", CompletableFuture.completedFuture(
+ usernameAndDomain.getDomain()));
+
+ }
+
+ // If splitting is not enabled, store the whole value in the USERNAME token
+ else {
+ tokens.put(prefix + "USERNAME", CompletableFuture.completedFuture(username));
+ }
+ }
// Password of server-related record
String password = recordService.getPassword(record);
@@ -113,14 +227,96 @@
tokens.put(prefix + "KEY", privateKey);
}
-
+
+ /**
+ * Search for a KSM configuration attribute, recursing up the connection group tree
+ * until a connection group with the appropriate attribute is found. If the KSM config
+ * is found, it will be returned. If not, the default value from the config file will
+ * be returned.
+ *
+ * @param userContext
+ * The userContext associated with the connection or connection group.
+ *
+ * @param connectable
+ * A connection or connection group for which the tokens are being replaced.
+ *
+ * @return
+ * The value of the KSM configuration attribute if found in the tree, the default
+ * KSM config blob defined in guacamole.properties otherwise.
+ *
+ * @throws GuacamoleException
+ * If an error occurs while attempting to retrieve the KSM config attribute, or if
+ * no KSM config is found in the connection group tree, and the value is also not
+ * defined in the config file.
+ */
+ private String getConnectionGroupKsmConfig(
+ UserContext userContext, Connectable connectable) throws GuacamoleException {
+
+ // Check to make sure it's a usable type before proceeding
+ if (
+ !(connectable instanceof Connection)
+ && !(connectable instanceof ConnectionGroup)) {
+ logger.warn(
+ "Unsupported Connectable type: {}; skipping KSM config lookup.",
+ connectable.getClass());
+
+ // Use the default value if searching is impossible
+ return confService.getKsmConfig();
+ }
+
+ // For connections, start searching the parent group for the KSM config
+ // For connection groups, start searching the group directly
+ String parentIdentifier = (connectable instanceof Connection)
+ ? ((Connection) connectable).getParentIdentifier()
+ : ((ConnectionGroup) connectable).getIdentifier();
+
+ // Keep track of all group identifiers seen while recursing up the tree
+ // in case there's a cycle - if the same identifier is ever seen twice,
+ // the search is over.
+ Set<String> observedIdentifiers = new HashSet<>();
+ observedIdentifiers.add(parentIdentifier);
+
+ Directory<ConnectionGroup> connectionGroupDirectory = userContext.getConnectionGroupDirectory();
+ while (true) {
+
+ // Fetch the parent group, if one exists
+ ConnectionGroup group = connectionGroupDirectory.get(parentIdentifier);
+ if (group == null)
+ break;
+
+ // If the current connection group has the KSM configuration attribute
+ // set to a non-empty value, return immediately
+ String ksmConfig = group.getAttributes().get(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE);
+ if (ksmConfig != null && !ksmConfig.trim().isEmpty())
+ return ksmConfig;
+
+ // Otherwise, keep searching up the tree until an appropriate configuration is found
+ parentIdentifier = group.getParentIdentifier();
+
+ // If the parent is a group that's already been seen, this is a cycle, so there's no
+ // need to search any further
+ if (!observedIdentifiers.add(parentIdentifier))
+ break;
+ }
+
+ // If no KSM configuration was ever found, use the default value
+ return confService.getKsmConfig();
+
+ }
+
@Override
- public Map<String, Future<String>> getTokens(GuacamoleConfiguration config,
- TokenFilter filter) throws GuacamoleException {
+ public Map<String, Future<String>> getTokens(UserContext userContext, Connectable connectable,
+ GuacamoleConfiguration config, TokenFilter filter) throws GuacamoleException {
Map<String, Future<String>> tokens = new HashMap<>();
Map<String, String> parameters = config.getParameters();
+ // Attempt to find a KSM config for this connection or group
+ String ksmConfig = getConnectionGroupKsmConfig(userContext, connectable);
+
+ // Get a client instance for this KSM config
+ KsmClient ksm = getClient(ksmConfig);
+
// Retrieve and define server-specific tokens, if any
String hostname = parameters.get("hostname");
if (hostname != null && !hostname.isEmpty())
@@ -135,7 +331,7 @@
// Tokens specific to RDP
if ("rdp".equals(config.getProtocol())) {
-
+
// Retrieve and define gateway server-specific tokens, if any
String gatewayHostname = parameters.get("gateway-hostname");
if (gatewayHostname != null && !gatewayHostname.isEmpty())
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java
new file mode 100644
index 0000000..397c42f
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java
@@ -0,0 +1,78 @@
+/*
+ * 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.vault.ksm.user;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.guacamole.net.auth.ConnectionGroup;
+import org.apache.guacamole.net.auth.DelegatingConnectionGroup;
+import org.apache.guacamole.vault.ksm.conf.KsmAttributeService;
+
+ /**
+ * A KSM-specific connection group implementation that always exposes
+ * the KSM_CONFIGURATION_ATTRIBUTE attribute, even when no value is set.
+ * This ensures that the attribute will always show up in the UI, even
+ * for connection groups that don't already have it set.
+ */
+ public class KsmConnectionGroup extends DelegatingConnectionGroup {
+
+ /**
+ * Create a new KsmConnectionGroup instance, wrapping the provided
+ * ConnectionGroup.
+ *
+ * @param connectionGroup
+ * The ConnectionGroup instance to wrap.
+ */
+ public KsmConnectionGroup(ConnectionGroup connectionGroup) {
+
+ // Wrap the provided connection group
+ super(connectionGroup);
+ }
+
+ @Override
+ public Map<String, String> getAttributes() {
+
+ // All attributes defined on the underlying connection group
+ Map<String, String> attributes = super.getAttributes();
+
+ // If the attribute is already present, there's no need to add it - return
+ // the existing attributes as they are
+ if (attributes.containsKey(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE))
+ return attributes;
+
+ // Make a copy of the existing attributes and add KSM_CONFIGURATION_ATTRIBUTE
+ attributes = new HashMap<>(attributes);
+ attributes.put(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, null);
+ return attributes;
+
+ }
+
+ /**
+ * Return the underlying ConnectionGroup that's wrapped by this KsmConnectionGroup.
+ *
+ * @return
+ * The underlying ConnectionGroup that's wrapped by this KsmConnectionGroup.
+ */
+ ConnectionGroup getUnderlyConnectionGroup() {
+ return getDelegateConnectionGroup();
+ }
+
+ }
\ No newline at end of file
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java
new file mode 100644
index 0000000..b924bc5
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java
@@ -0,0 +1,272 @@
+/*
+ * 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.vault.ksm.user;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.language.TranslatableGuacamoleClientException;
+import org.apache.guacamole.net.auth.Attributes;
+import org.apache.guacamole.net.auth.ConnectionGroup;
+import org.apache.guacamole.net.auth.DecoratingDirectory;
+import org.apache.guacamole.net.auth.Directory;
+import org.apache.guacamole.vault.ksm.conf.KsmAttributeService;
+import org.apache.guacamole.vault.ksm.conf.KsmConfig;
+import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService;
+import org.apache.guacamole.vault.user.VaultDirectoryService;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.inject.Inject;
+import com.keepersecurity.secretsManager.core.InMemoryStorage;
+import com.keepersecurity.secretsManager.core.SecretsManager;
+import com.keepersecurity.secretsManager.core.SecretsManagerOptions;
+
+/**
+ * A KSM-specific vault directory service that wraps the connection group directory
+ * to enable automatic translation of KSM one-time tokens into base64-encoded JSON
+ * config bundles.
+ */
+public class KsmDirectoryService extends VaultDirectoryService {
+
+ /**
+ * Service for retrieving KSM configuration details.
+ */
+ @Inject
+ private KsmConfigurationService configurationService;
+
+ /**
+ * A singleton ObjectMapper for converting a Map to a JSON string when
+ * generating a base64-encoded JSON KSM config blob.
+ */
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * All expected fields in the KSM configuration JSON blob.
+ */
+ private static final List<String> EXPECTED_KSM_FIELDS = (
+ Collections.unmodifiableList(Arrays.asList(
+ SecretsManager.KEY_HOSTNAME,
+ SecretsManager.KEY_CLIENT_ID,
+ SecretsManager.KEY_PRIVATE_KEY,
+ SecretsManager.KEY_CLIENT_KEY,
+ SecretsManager.KEY_APP_KEY,
+ SecretsManager.KEY_OWNER_PUBLIC_KEY,
+ SecretsManager.KEY_SERVER_PUBIC_KEY_ID
+ )));
+
+ /**
+ * Return true if the provided input is a valid base64-encoded string,
+ * false otherwise.
+ *
+ * @param input
+ * The string to check if base-64 encoded.
+ *
+ * @return
+ * true if the provided input is a valid base64-encoded string,
+ * false otherwise.
+ */
+ private static boolean isBase64(String input) {
+
+ try {
+ Base64.getDecoder().decode(input);
+ return true;
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Given an attributes-enabled entity, check for the presence of the
+ * KSM_CONFIGURATION_ATTRIBUTE attribute. If it's set, check if it's a valid
+ * KSM one-time token. If so, attempt to translate it to a base-64-encoded
+ * json KSM config blob, and set it back to the provided entity.
+ * If it's already a KSM config blob, validate it as config blob. If either
+ * validation fails, a GuacamoleException will be thrown.
+ *
+ * @param entity
+ * The attributes-enabled entity for which the KSM configuration
+ * attribute parsing/validation should be performed.
+ *
+ * @throws GuacamoleException
+ * If the KSM_CONFIGURATION_ATTRIBUTE is set, but fails to validate as
+ * either a KSM one-time-token, or a KSM base64-encoded JSON config blob.
+ */
+ public void processAttributes(Attributes entity) throws GuacamoleException {
+
+ // By default, if the KSM config attribute isn't being set, pass the
+ // provided attributes through without any changes
+ Map<String, String> attributes = entity.getAttributes();
+
+ // Get the value of the KSM config attribute in the provided map
+ String ksmConfigValue = attributes.get(
+ KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE);
+
+ // Check if the attribute is set to a non-empty value
+ if (ksmConfigValue != null && !ksmConfigValue.trim().isEmpty()) {
+
+ // If it's already base64-encoded, it's a KSM configuration blob,
+ // so validate it immediately
+ if (isBase64(ksmConfigValue)) {
+
+ // Attempt to validate the config as a base64-econded KSM config blob
+ try {
+ KsmConfig.parseKsmConfig(ksmConfigValue);
+
+ // If it validates, the entity can be left alone - it's already valid
+ return;
+ }
+
+ catch (GuacamoleException exception) {
+
+ // If the parsing attempt fails, throw a translatable error for display
+ // on the frontend
+ throw new TranslatableGuacamoleClientException(
+ "Invalid base64-encoded JSON KSM config provided for "
+ + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE + " attribute",
+ "CONNECTION_GROUP_ATTRIBUTES.ERROR_INVALID_KSM_CONFIG_BLOB",
+ exception);
+ }
+ }
+
+ // It wasn't a valid base64-encoded string, it should be a one-time token, so
+ // attempt to validat it as such, and if valid, update the attribute to the
+ // base64 config blob generated by the token
+ try {
+
+ // Create an initially empty storage to be populated using the one-time token
+ InMemoryStorage storage = new InMemoryStorage();
+
+ // Populate the in-memory storage using the one-time-token
+ SecretsManager.initializeStorage(storage, ksmConfigValue, null);
+
+ // Create an options object using the values we extracted from the one-time token
+ SecretsManagerOptions options = new SecretsManagerOptions(
+ storage, null,
+ configurationService.getAllowUnverifiedCertificate());
+
+ // Attempt to fetch secrets using the options we created. This will both validate
+ // that the configuration works, and potentially populate missing fields that the
+ // initializeStorage() call did not set.
+ SecretsManager.getSecrets(options);
+
+ // Create a map to store the extracted values from the KSM storage
+ Map<String, String> configMap = new HashMap<>();
+
+ // Go through all the expected fields, extract from the KSM storage,
+ // and write to the newly created map
+ EXPECTED_KSM_FIELDS.forEach(configKey -> {
+
+ // Only write the value into the new map if non-null
+ String value = storage.getString(configKey);
+ if (value != null)
+ configMap.put(configKey, value);
+
+ });
+
+ // JSON-encode the value, and then base64 encode that to get the format
+ // that KSM would expect
+ String jsonString = objectMapper.writeValueAsString(configMap);
+ String base64EncodedJson = Base64.getEncoder().encodeToString(
+ jsonString.getBytes(StandardCharsets.UTF_8));
+
+ // Finally, try to parse the newly generated token as a KSM config. If this
+ // works, the config should be fully functional
+ KsmConfig.parseKsmConfig(base64EncodedJson);
+
+ // Make a copy of the existing attributes, modifying just the value for
+ // KSM_CONFIGURATION_ATTRIBUTE
+ attributes = new HashMap<>(attributes);
+ attributes.put(
+ KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, base64EncodedJson);
+
+ // Set the newly updated attributes back to the original object
+ entity.setAttributes(attributes);
+
+ }
+
+ // The KSM SDK only throws raw Exceptions, so we can't be more specific
+ catch (Exception exception) {
+
+ // If the parsing attempt fails, throw a translatable error for display
+ // on the frontend
+ throw new TranslatableGuacamoleClientException(
+ "Invalid one-time KSM token provided for "
+ + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE + " attribute",
+ "CONNECTION_GROUP_ATTRIBUTES.ERROR_INVALID_KSM_ONE_TIME_TOKEN",
+ exception);
+ }
+ }
+
+ }
+
+ @Override
+ public Directory<ConnectionGroup> getConnectionGroupDirectory(
+ Directory<ConnectionGroup> underlyingDirectory) throws GuacamoleException {
+
+ // A ConnectionGroup directory that will intercept add and update calls to
+ // validate KSM configurations, and translate one-time-tokens, if possible,
+ // as well as ensuring that all ConnectionGroups returned include the
+ // KSM_CONFIGURATION_ATTRIBUTE attribute, so it will be available in the UI.
+ return new DecoratingDirectory<ConnectionGroup>(underlyingDirectory) {
+
+ @Override
+ public void add(ConnectionGroup connectionGroup) throws GuacamoleException {
+
+ // Check for the KSM config attribute and translate the one-time token
+ // if possible before adding
+ processAttributes(connectionGroup);
+ super.add(connectionGroup);
+ }
+
+ @Override
+ public void update(ConnectionGroup connectionGroup) throws GuacamoleException {
+
+ // Check for the KSM config attribute and translate the one-time token
+ // if possible before updating
+ processAttributes(connectionGroup);
+ super.update(connectionGroup);
+ }
+
+ @Override
+ protected ConnectionGroup decorate(ConnectionGroup connectionGroup) throws GuacamoleException {
+
+ // Wrap the existing connection group in a KsmConnection to ensure the presence of the
+ // KSM_CONFIGURATION_ATTRIBUTE attribute
+ return new KsmConnectionGroup(connectionGroup);
+
+ }
+
+ @Override
+ protected ConnectionGroup undecorate(ConnectionGroup connectionGroup) throws GuacamoleException {
+
+ // Return the underlying connection group that the KsmConnectionGroup wraps
+ return ((KsmConnectionGroup) connectionGroup).getUnderlyConnectionGroup();
+
+ }
+
+ };
+ }
+}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json
new file mode 100644
index 0000000..4601a13
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json
@@ -0,0 +1,15 @@
+{
+
+ "DATA_SOURCE_KEEPER_SECRETS_MANAGER" : {
+ "NAME" : "Keeper Secrets Manager"
+ },
+
+ "CONNECTION_GROUP_ATTRIBUTES" : {
+ "SECTION_HEADER_KSM_CONFIG" : "Keeper Secrets Manager",
+ "FIELD_HEADER_KSM_CONFIG" : "KSM Service Configuration ",
+
+ "ERROR_INVALID_KSM_CONFIG_BLOB" : "The provided base64-encoded KSM configuration blob is not valid. Please ensure that you have copied the entire blob.",
+ "ERROR_INVALID_KSM_ONE_TIME_TOKEN" : "The provided configuration is not a valid KSM one-time token or base64-encoded configuration blob. Please ensure that you have copied the entire token value."
+ }
+
+}
diff --git a/extensions/pom.xml b/extensions/pom.xml
index de2b245..3bab332 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -53,6 +53,9 @@
<module>guacamole-history-recording-storage</module>
<module>guacamole-vault</module>
+ <!-- Utility extensions -->
+ <module>guacamole-display-statistics</module>
+
</modules>
<build>
diff --git a/guacamole-common-js/pom.xml b/guacamole-common-js/pom.xml
index 5d7585b..41cc790 100644
--- a/guacamole-common-js/pom.xml
+++ b/guacamole-common-js/pom.xml
@@ -118,7 +118,7 @@
<plugin>
<groupId>com.github.searls</groupId>
<artifactId>jasmine-maven-plugin</artifactId>
- <version>2.2</version>
+ <version>3.0-beta-02</version>
<executions>
<execution>
<goals>
diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js
index 83d89ab..2cc9c28 100644
--- a/guacamole-common-js/src/main/webapp/modules/Client.js
+++ b/guacamole-common-js/src/main/webapp/modules/Client.js
@@ -840,6 +840,12 @@
* @event
* @param {!number} timestamp
* The timestamp associated with the sync instruction.
+ *
+ * @param {!number} frames
+ * The number of frames that were considered or combined to produce the
+ * frame associated with this sync instruction, or zero if this value
+ * is not known or the remote desktop server provides no concept of
+ * frames.
*/
this.onsync = null;
@@ -1530,6 +1536,7 @@
"sync": function(parameters) {
var timestamp = parseInt(parameters[0]);
+ var frames = parameters[1] ? parseInt(parameters[1]) : 0;
// Flush display, send sync when done
display.flush(function displaySyncComplete() {
@@ -1547,7 +1554,7 @@
currentTimestamp = timestamp;
}
- });
+ }, timestamp, frames);
// If received first update, no longer waiting.
if (currentState === STATE_WAITING)
@@ -1555,7 +1562,7 @@
// Call sync handler if defined
if (guac_client.onsync)
- guac_client.onsync(timestamp);
+ guac_client.onsync(timestamp, frames);
},
diff --git a/guacamole-common-js/src/main/webapp/modules/Display.js b/guacamole-common-js/src/main/webapp/modules/Display.js
index 8baa6b7..374a762 100644
--- a/guacamole-common-js/src/main/webapp/modules/Display.js
+++ b/guacamole-common-js/src/main/webapp/modules/Display.js
@@ -113,6 +113,17 @@
this.cursorY = 0;
/**
+ * The number of milliseconds over which display rendering statistics
+ * should be gathered, dispatching {@link #onstatistics} events as those
+ * statistics are available. If set to zero, no statistics will be
+ * gathered.
+ *
+ * @default 0
+ * @type {!number}
+ */
+ this.statisticWindow = 0;
+
+ /**
* Fired when the default layer (and thus the entire Guacamole display)
* is resized.
*
@@ -143,6 +154,18 @@
this.oncursor = null;
/**
+ * Fired whenever performance statistics are available for recently-
+ * rendered frames. This event will fire only if {@link #statisticWindow}
+ * is non-zero.
+ *
+ * @event
+ * @param {!Guacamole.Display.Statistics} stats
+ * An object containing general rendering performance statistics for
+ * the remote desktop, Guacamole server, and Guacamole client.
+ */
+ this.onstatistics = null;
+
+ /**
* The queue of all pending Tasks. Tasks will be run in order, with new
* tasks added at the end of the queue and old tasks removed from the
* front of the queue (FIFO). These tasks will eventually be grouped
@@ -163,11 +186,33 @@
var frames = [];
/**
- * Flushes all pending frames.
+ * The ID of the animation frame request returned by the last call to
+ * requestAnimationFrame(). This value will only be set if the browser
+ * supports requestAnimationFrame(), if a frame render is currently
+ * pending, and if the current browser tab is currently focused (likely to
+ * handle requests for animation frames). In all other cases, this will be
+ * null.
+ *
+ * @private
+ * @type {number}
+ */
+ var inProgressFrame = null;
+
+ /**
+ * Flushes all pending frames synchronously. This function will block until
+ * all pending frames have rendered. If a frame is currently blocked by an
+ * asynchronous operation like an image load, this function will return
+ * after reaching that operation and the flush operation will
+ * automamtically resume after that operation completes.
+ *
* @private
*/
- function __flush_frames() {
+ var syncFlush = function syncFlush() {
+ var localTimestamp = 0;
+ var remoteTimestamp = 0;
+
+ var renderedLogicalFrames = 0;
var rendered_frames = 0;
// Draw all pending frames, if ready
@@ -178,6 +223,10 @@
break;
frame.flush();
+
+ localTimestamp = frame.localTimestamp;
+ remoteTimestamp = frame.remoteTimestamp;
+ renderedLogicalFrames += frame.logicalFrames;
rendered_frames++;
}
@@ -185,6 +234,172 @@
// Remove rendered frames from array
frames.splice(0, rendered_frames);
+ if (rendered_frames)
+ notifyFlushed(localTimestamp, remoteTimestamp, renderedLogicalFrames);
+
+ };
+
+ /**
+ * Flushes all pending frames asynchronously. This function returns
+ * immediately, relying on requestAnimationFrame() to dictate when each
+ * frame should be flushed.
+ *
+ * @private
+ */
+ var asyncFlush = function asyncFlush() {
+
+ var continueFlush = function continueFlush() {
+
+ // We're no longer waiting to render a frame
+ inProgressFrame = null;
+
+ // Nothing to do if there are no frames remaining
+ if (!frames.length)
+ return;
+
+ // Flush the next frame only if it is ready (not awaiting
+ // completion of some asynchronous operation like an image load)
+ if (frames[0].isReady()) {
+ var frame = frames.shift();
+ frame.flush();
+ notifyFlushed(frame.localTimestamp, frame.remoteTimestamp, frame.logicalFrames);
+ }
+
+ // Request yet another animation frame if frames remain to be
+ // flushed
+ if (frames.length)
+ inProgressFrame = window.requestAnimationFrame(continueFlush);
+
+ };
+
+ // Begin flushing frames if not already waiting to render a frame
+ if (!inProgressFrame)
+ inProgressFrame = window.requestAnimationFrame(continueFlush);
+
+ };
+
+ /**
+ * Recently-gathered display render statistics, as made available by calls
+ * to notifyFlushed(). The contents of this array will be trimmed to
+ * contain only up to {@link #statisticWindow} milliseconds of statistics.
+ *
+ * @private
+ * @type {Guacamole.Display.Statistics[]}
+ */
+ var statistics = [];
+
+ /**
+ * Notifies that one or more frames have been successfully rendered
+ * (flushed) to the display.
+ *
+ * @private
+ * @param {!number} localTimestamp
+ * The local timestamp of the point in time at which the most recent,
+ * flushed frame was received by the display, in milliseconds since the
+ * Unix Epoch.
+ *
+ * @param {!number} remoteTimestamp
+ * The remote timestamp of sync instruction associated with the most
+ * recent, flushed frame received by the display. This timestamp is in
+ * milliseconds, but is arbitrary, having meaning only relative to
+ * other timestamps in the same connection.
+ *
+ * @param {!number} logicalFrames
+ * The number of remote desktop frames that were flushed.
+ */
+ var notifyFlushed = function notifyFlushed(localTimestamp, remoteTimestamp, logicalFrames) {
+
+ // Ignore if statistics are not being gathered
+ if (!guac_display.statisticWindow)
+ return;
+
+ var current = new Date().getTime();
+
+ // Find the first statistic that is still within the configured time
+ // window
+ for (var first = 0; first < statistics.length; first++) {
+ if (current - statistics[first].timestamp <= guac_display.statisticWindow)
+ break;
+ }
+
+ // Remove all statistics except those within the time window
+ statistics.splice(0, first - 1);
+
+ // Record statistics for latest frame
+ statistics.push({
+ localTimestamp : localTimestamp,
+ remoteTimestamp : remoteTimestamp,
+ timestamp : current,
+ frames : logicalFrames
+ });
+
+ // Determine the actual time interval of the available statistics (this
+ // will not perfectly match the configured interval, which is an upper
+ // bound)
+ var statDuration = (statistics[statistics.length - 1].timestamp - statistics[0].timestamp) / 1000;
+
+ // Determine the amount of time that elapsed remotely (within the
+ // remote desktop)
+ var remoteDuration = (statistics[statistics.length - 1].remoteTimestamp - statistics[0].remoteTimestamp) / 1000;
+
+ // Calculate the number of frames that have been rendered locally
+ // within the configured time interval
+ var localFrames = statistics.length;
+
+ // Calculate the number of frames actually received from the remote
+ // desktop by the Guacamole server
+ var remoteFrames = statistics.reduce(function sumFrames(prev, stat) {
+ return prev + stat.frames;
+ }, 0);
+
+ // Calculate the number of frames that the Guacamole server had to
+ // drop or combine with other frames
+ var drops = statistics.reduce(function sumDrops(prev, stat) {
+ return prev + Math.max(0, stat.frames - 1);
+ }, 0);
+
+ // Produce lag and FPS statistics from above raw measurements
+ var stats = new Guacamole.Display.Statistics({
+ processingLag : current - localTimestamp,
+ desktopFps : (remoteDuration && remoteFrames) ? remoteFrames / remoteDuration : null,
+ clientFps : statDuration ? localFrames / statDuration : null,
+ serverFps : remoteDuration ? localFrames / remoteDuration : null,
+ dropRate : remoteDuration ? drops / remoteDuration : null
+ });
+
+ // Notify of availability of new statistics
+ if (guac_display.onstatistics)
+ guac_display.onstatistics(stats);
+
+ };
+
+ // Switch from asynchronous frame handling to synchronous frame handling if
+ // requestAnimationFrame() is unlikely to be usable (browsers may not
+ // invoke the animation frame callback if the relevant tab is not focused)
+ window.addEventListener('blur', function switchToSyncFlush() {
+ if (inProgressFrame && !document.hasFocus()) {
+
+ // Cancel pending asynchronous processing of frame ...
+ window.cancelAnimationFrame(inProgressFrame);
+ inProgressFrame = null;
+
+ // ... and instead process it synchronously
+ syncFlush();
+
+ }
+ }, true);
+
+ /**
+ * Flushes all pending frames.
+ * @private
+ */
+ function __flush_frames() {
+
+ if (window.requestAnimationFrame && document.hasFocus())
+ asyncFlush();
+ else
+ syncFlush();
+
}
/**
@@ -198,8 +413,43 @@
*
* @param {!Task[]} tasks
* The set of tasks which must be executed to render this frame.
+ *
+ * @param {number} [timestamp]
+ * The remote timestamp of sync instruction associated with this frame.
+ * This timestamp is in milliseconds, but is arbitrary, having meaning
+ * only relative to other remote timestamps in the same connection. If
+ * omitted, a compatible but local timestamp will be used instead.
+ *
+ * @param {number} [logicalFrames=0]
+ * The number of remote desktop frames that were combined to produce
+ * this frame, or zero if this value is unknown or inapplicable.
*/
- function Frame(callback, tasks) {
+ var Frame = function Frame(callback, tasks, timestamp, logicalFrames) {
+
+ /**
+ * The local timestamp of the point in time at which this frame was
+ * received by the display, in milliseconds since the Unix Epoch.
+ *
+ * @type {!number}
+ */
+ this.localTimestamp = new Date().getTime();
+
+ /**
+ * The remote timestamp of sync instruction associated with this frame.
+ * This timestamp is in milliseconds, but is arbitrary, having meaning
+ * only relative to other remote timestamps in the same connection.
+ *
+ * @type {!number}
+ */
+ this.remoteTimestamp = timestamp || this.localTimestamp;
+
+ /**
+ * The number of remote desktop frames that were combined to produce
+ * this frame. If unknown or not applicable, this will be zero.
+ *
+ * @type {!number}
+ */
+ this.logicalFrames = logicalFrames || 0;
/**
* Cancels rendering of this frame and all associated tasks. The
@@ -254,7 +504,7 @@
};
- }
+ };
/**
* A container for an task handler. Each operation which must be ordered
@@ -431,11 +681,20 @@
* @param {function} [callback]
* The function to call when this frame is flushed. This may happen
* immediately, or later when blocked tasks become unblocked.
+ *
+ * @param {number} timestamp
+ * The remote timestamp of sync instruction associated with this frame.
+ * This timestamp is in milliseconds, but is arbitrary, having meaning
+ * only relative to other remote timestamps in the same connection.
+ *
+ * @param {number} logicalFrames
+ * The number of remote desktop frames that were combined to produce
+ * this frame.
*/
- this.flush = function(callback) {
+ this.flush = function(callback, timestamp, logicalFrames) {
// Add frame, reset tasks
- frames.push(new Frame(callback, tasks));
+ frames.push(new Frame(callback, tasks, timestamp, logicalFrames));
tasks = [];
// Attempt flush
@@ -1855,3 +2114,79 @@
* @type {!number}
*/
Guacamole.Display.VisibleLayer.__next_id = 0;
+
+/**
+ * A set of Guacamole display performance statistics, describing the speed at
+ * which the remote desktop, Guacamole server, and Guacamole client are
+ * rendering frames.
+ *
+ * @constructor
+ * @param {Guacamole.Display.Statistics|Object} [template={}]
+ * The object whose properties should be copied within the new
+ * Guacamole.Display.Statistics.
+ */
+Guacamole.Display.Statistics = function Statistics(template) {
+
+ template = template || {};
+
+ /**
+ * The amount of time that the Guacamole client is taking to render
+ * individual frames, in milliseconds, if known. If this value is unknown,
+ * such as if the there are insufficient frame statistics recorded to
+ * calculate this value, this will be null.
+ *
+ * @type {?number}
+ */
+ this.processingLag = template.processingLag;
+
+ /**
+ * The framerate of the remote desktop currently being viewed within the
+ * relevant Gucamole.Display, independent of Guacamole, in frames per
+ * second. This represents the speed at which the remote desktop is
+ * producing frame data for the Guacamole server to consume. If this
+ * value is unknown, such as if the remote desktop server does not actually
+ * define frame boundaries, this will be null.
+ *
+ * @type {?number}
+ */
+ this.desktopFps = template.desktopFps;
+
+ /**
+ * The rate at which the Guacamole server is generating frames for the
+ * Guacamole client to consume, in frames per second. If the Guacamole
+ * server is correctly adjusting for variance in client/browser processing
+ * power, this rate should closely match the client rate, and should remain
+ * independent of any network latency. If this value is unknown, such as if
+ * the there are insufficient frame statistics recorded to calculate this
+ * value, this will be null.
+ *
+ * @type {?number}
+ */
+ this.serverFps = template.serverFps;
+
+ /**
+ * The rate at which the Guacamole client is consuming frames generated by
+ * the Guacamole server, in frames per second. If the Guacamole server is
+ * correctly adjusting for variance in client/browser processing power,
+ * this rate should closely match the server rate, regardless of any
+ * latency on the network between the server and client. If this value is
+ * unknown, such as if the there are insufficient frame statistics recorded
+ * to calculate this value, this will be null.
+ *
+ * @type {?number}
+ */
+ this.clientFps = template.clientFps;
+
+ /**
+ * The rate at which the Guacamole server is dropping or combining frames
+ * received from the remote desktop server to compensate for variance in
+ * client/browser processing power, in frames per second. This value may
+ * also be non-zero if the server is compensating for variances in its own
+ * processing power, or relative slowness in image compression vs. the rate
+ * that inbound frames are received. If this value is unknown, such as if
+ * the remote desktop server does not actually define frame boundaries,
+ * this will be null.
+ */
+ this.dropRate = template.dropRate;
+
+};
diff --git a/guacamole-common-js/src/main/webapp/modules/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js
index 23657cd..fa886f5 100644
--- a/guacamole-common-js/src/main/webapp/modules/Tunnel.js
+++ b/guacamole-common-js/src/main/webapp/modules/Tunnel.js
@@ -1094,7 +1094,7 @@
if (tunnel.uuid === null) {
// Associate tunnel UUID if received
- if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE)
+ if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE && elements.length === 1)
tunnel.setUUID(elements[0]);
// Tunnel is now open and UUID is available
@@ -1236,12 +1236,17 @@
tunnel.onstatechange = chained_tunnel.onstatechange;
tunnel.oninstruction = chained_tunnel.oninstruction;
tunnel.onerror = chained_tunnel.onerror;
- tunnel.onuuid = chained_tunnel.onuuid;
// Assign UUID if already known
if (tunnel.uuid)
chained_tunnel.setUUID(tunnel.uuid);
+ // Assign any future received UUIDs such that they are
+ // accessible from the main uuid property of the chained tunnel
+ tunnel.onuuid = function uuidReceived(uuid) {
+ chained_tunnel.setUUID(uuid);
+ };
+
committedTunnel = tunnel;
}
diff --git a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json
index badefb1..c51f762 100644
--- a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json
+++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json
@@ -100,6 +100,7 @@
"failsafe",
"fr-be-azerty",
"fr-fr-azerty",
+ "fr-ca-qwerty",
"fr-ch-qwertz",
"hu-hu-qwertz",
"it-it-qwerty",
@@ -297,6 +298,11 @@
"name" : "disable-glyph-caching",
"type" : "BOOLEAN",
"options" : [ "true" ]
+ },
+ {
+ "name" : "disable-gfx",
+ "type" : "BOOLEAN",
+ "options" : [ "true" ]
}
]
},
diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js
index e1a08eb..03344b6 100644
--- a/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js
+++ b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js
@@ -34,16 +34,16 @@
/**
* The client whose status should be displayed.
- *
+ *
* @type ManagedClient
*/
client : '='
-
+
};
directive.controller = ['$scope', '$injector', '$element',
function guacClientNotificationController($scope, $injector, $element) {
-
+
// Required types
const ManagedClient = $injector.get('ManagedClient');
const ManagedClientState = $injector.get('ManagedClientState');
@@ -53,6 +53,7 @@
const $location = $injector.get('$location');
const authenticationService = $injector.get('authenticationService');
const guacClientManager = $injector.get('guacClientManager');
+ const guacTranslate = $injector.get('guacTranslate');
const requestService = $injector.get('requestService');
const userPageService = $injector.get('userPageService');
@@ -66,26 +67,6 @@
$scope.status = false;
/**
- * All client error codes handled and passed off for translation. Any error
- * code not present in this list will be represented by the "DEFAULT"
- * translation.
- */
- const CLIENT_ERRORS = {
- 0x0201: true,
- 0x0202: true,
- 0x0203: true,
- 0x0207: true,
- 0x0208: true,
- 0x0209: true,
- 0x020A: true,
- 0x020B: true,
- 0x0301: true,
- 0x0303: true,
- 0x0308: true,
- 0x031D: true
- };
-
- /**
* All error codes for which automatic reconnection is appropriate when a
* client error occurs.
*/
@@ -98,26 +79,7 @@
0x0301: true,
0x0308: true
};
-
- /**
- * All tunnel error codes handled and passed off for translation. Any error
- * code not present in this list will be represented by the "DEFAULT"
- * translation.
- */
- const TUNNEL_ERRORS = {
- 0x0201: true,
- 0x0202: true,
- 0x0203: true,
- 0x0204: true,
- 0x0205: true,
- 0x0207: true,
- 0x0208: true,
- 0x0301: true,
- 0x0303: true,
- 0x0308: true,
- 0x031D: true
- };
-
+
/**
* All error codes for which automatic reconnection is appropriate when a
* tunnel error occurs.
@@ -239,7 +201,7 @@
// Get any associated status code
const status = $scope.client.clientState.statusCode;
- // Connecting
+ // Connecting
if (connectionState === ManagedClientState.ConnectionState.CONNECTING
|| connectionState === ManagedClientState.ConnectionState.WAITING) {
$scope.status = {
@@ -254,44 +216,58 @@
// Client error
else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) {
- // Determine translation name of error
- const errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
+ // Translation IDs for this error code
+ const errorPrefix = "CLIENT.ERROR_CLIENT_";
+ const errorId = errorPrefix + status.toString(16).toUpperCase();
+ const defaultErrorId = errorPrefix + "DEFAULT";
// Determine whether the reconnect countdown applies
const countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
- // Show error status
- notifyConnectionClosed({
- className : "error",
- title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
- text : {
- key : "CLIENT.ERROR_CLIENT_" + errorName
- },
- countdown : countdown,
- actions : actions
- });
+ // Use the guacTranslate service to determine if there is a translation for
+ // this error code; if not, use the default
+ guacTranslate(errorId, defaultErrorId).then(
+
+ // Show error status
+ translationResult => notifyConnectionClosed({
+ className : "error",
+ title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
+ text : {
+ key : translationResult.id
+ },
+ countdown : countdown,
+ actions : actions
+ })
+ );
}
// Tunnel error
else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) {
- // Determine translation name of error
- const errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
+ // Translation IDs for this error code
+ const errorPrefix = "CLIENT.ERROR_TUNNEL_";
+ const errorId = errorPrefix + status.toString(16).toUpperCase();
+ const defaultErrorId = errorPrefix + "DEFAULT";
// Determine whether the reconnect countdown applies
const countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
- // Show error status
- notifyConnectionClosed({
- className : "error",
- title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
- text : {
- key : "CLIENT.ERROR_TUNNEL_" + errorName
- },
- countdown : countdown,
- actions : actions
- });
+ // Use the guacTranslate service to determine if there is a translation for
+ // this error code; if not, use the default
+ guacTranslate(errorId, defaultErrorId).then(
+
+ // Show error status
+ translationResult => notifyConnectionClosed({
+ className : "error",
+ title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
+ text : {
+ key : translationResult.id
+ },
+ countdown : countdown,
+ actions : actions
+ })
+ );
}
diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js b/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js
index a9c09bc..d016a72 100644
--- a/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js
+++ b/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js
@@ -30,7 +30,7 @@
/**
* The file transfer to display.
- *
+ *
* @type ManagedFileUpload|ManagedFileDownload
*/
transfer : '='
@@ -40,28 +40,13 @@
templateUrl: 'app/client/templates/guacFileTransfer.html',
controller: ['$scope', '$injector', function guacFileTransferController($scope, $injector) {
+ // Required services
+ const guacTranslate = $injector.get('guacTranslate');
+
// Required types
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
/**
- * All upload error codes handled and passed off for translation.
- * Any error code not present in this list will be represented by
- * the "DEFAULT" translation.
- */
- var UPLOAD_ERRORS = {
- 0x0100: true,
- 0x0201: true,
- 0x0202: true,
- 0x0203: true,
- 0x0204: true,
- 0x0205: true,
- 0x0301: true,
- 0x0303: true,
- 0x0308: true,
- 0x031D: true
- };
-
- /**
* Returns the unit string that is most appropriate for the
* number of bytes transferred thus far - either 'gb', 'mb', 'kb',
* or 'b'.
@@ -193,7 +178,7 @@
return;
// Save file
- saveAs($scope.transfer.blob, $scope.transfer.filename);
+ saveAs($scope.transfer.blob, $scope.transfer.filename);
};
@@ -210,23 +195,20 @@
return $scope.transfer.transferState.streamState === ManagedFileTransferState.StreamState.ERROR;
};
- /**
- * Returns the text of the current error as a translation string.
- *
- * @returns {String}
- * The name of the translation string containing the text
- * associated with the current error.
- */
- $scope.getErrorText = function getErrorText() {
+ // The translated error message for the current status code
+ $scope.translatedErrorMessage = '';
+
+ $scope.$watch('transfer.transferState.statusCode', function statusCodeChanged(statusCode) {
// Determine translation name of error
- var status = $scope.transfer.transferState.statusCode;
- var errorName = (status in UPLOAD_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
+ const errorName = 'CLIENT.ERROR_UPLOAD_' + statusCode.toString(16).toUpperCase();
- // Return translation string
- return 'CLIENT.ERROR_UPLOAD_' + errorName;
+ // Use translation string, or the default if no translation is found for this error code
+ guacTranslate(errorName, 'CLIENT.ERROR_UPLOAD_DEFAULT').then(
+ translationResult => $scope.translatedErrorMessage = translationResult.message
+ );
- };
+ });
}] // end file transfer controller
diff --git a/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js b/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js
new file mode 100644
index 0000000..c7fe8e9
--- /dev/null
+++ b/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+/**
+ * A wrapper around the angular-translate $translate service that offers a
+ * convenient way to fall back to a default translation if the requested
+ * translation is not available.
+ */
+ angular.module('client').factory('guacTranslate', ['$injector', function guacTranslate($injector) {
+
+ // Required services
+ const $q = $injector.get('$q');
+ const $translate = $injector.get('$translate');
+
+ // Required types
+ const TranslationResult = $injector.get('TranslationResult');
+
+ /**
+ * Returns a promise that will be resolved with a TranslationResult containg either the
+ * requested ID and message (if translated), or the default ID and message if translated,
+ * or the literal value of `defaultTranslationId` for both the ID and message if neither
+ * is translated.
+ *
+ * @param {String} translationId
+ * The requested translation ID, which may or may not be translated.
+ *
+ * @param {Sting} defaultTranslationId
+ * The translation ID that will be used if no translation is found for `translationId`.
+ *
+ * @returns {Promise.<TranslationResult>}
+ * A promise which resolves with a TranslationResult containing the results from
+ * the translation attempt.
+ */
+ var translateWithFallback = function translateWithFallback(translationId, defaultTranslationId) {
+ const deferredTranslation = $q.defer();
+
+ // Attempt to translate the requested translation ID
+ $translate(translationId).then(
+
+ // If the requested translation is available, use that
+ translation => deferredTranslation.resolve(new TranslationResult({
+ id: translationId, message: translation
+ })),
+
+ // Otherwise, try the default translation ID
+ () => $translate(defaultTranslationId).then(
+
+ // Default translation worked, so use that
+ defaultTranslation =>
+ deferredTranslation.resolve(new TranslationResult({
+ id: defaultTranslationId, message: defaultTranslation
+ })),
+
+ // Neither translation is available; as a fallback, return default ID for both
+ () => deferredTranslation.resolve(new TranslationResult({
+ id: defaultTranslationId, message: defaultTranslationId
+ })),
+ )
+ );
+
+ return deferredTranslation.promise;
+ };
+
+ return translateWithFallback;
+
+}]);
diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html b/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html
index dd96baa..32ead84 100644
--- a/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html
+++ b/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html
@@ -10,7 +10,7 @@
</div>
<!-- Error text -->
- <p class="error-text">{{getErrorText() | translate}}</p>
+ <p class="error-text">{{translatedErrorMessage}}</p>
</div>
diff --git a/guacamole/src/main/frontend/src/app/client/types/TranslationResult.js b/guacamole/src/main/frontend/src/app/client/types/TranslationResult.js
new file mode 100644
index 0000000..0a81511
--- /dev/null
+++ b/guacamole/src/main/frontend/src/app/client/types/TranslationResult.js
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+/**
+ * Provides the TranslationResult class used by the guacTranslate service. This class contains
+ * both the translated message and the translation ID that generated the message, in the case
+ * where it's unknown whether a translation is defined or not.
+ */
+ angular.module('client').factory('TranslationResult', [function defineTranslationResult() {
+
+ /**
+ * Object which represents the result of a translation as returned from
+ * the guacTranslate service.
+ *
+ * @constructor
+ * @param {TranslationResult|Object} [template={}]
+ * The object whose properties should be copied within the new
+ * TranslationResult.
+ */
+ const TranslationResult = function TranslationResult(template) {
+
+ // Use empty object by default
+ template = template || {};
+
+ /**
+ * The translation ID.
+ *
+ * @type {String}
+ */
+ this.id = template.id;
+
+ /**
+ * The translated message.
+ *
+ * @type {String}
+ */
+ this.message = template.message;
+
+ };
+
+ return TranslationResult;
+
+}]);
\ No newline at end of file
diff --git a/guacamole/src/main/frontend/src/app/home/templates/home.html b/guacamole/src/main/frontend/src/app/home/templates/home.html
index b2b3db5..06c0f22 100644
--- a/guacamole/src/main/frontend/src/app/home/templates/home.html
+++ b/guacamole/src/main/frontend/src/app/home/templates/home.html
@@ -4,14 +4,14 @@
<!-- The recent connections for this user -->
<div class="header">
- <h2>{{'HOME.SECTION_HEADER_RECENT_CONNECTIONS' | translate}}</h2>
+ <h2 id="section-header-recent-connections">{{'HOME.SECTION_HEADER_RECENT_CONNECTIONS' | translate}}</h2>
<guac-user-menu></guac-user-menu>
</div>
<guac-recent-connections root-groups="rootConnectionGroups"></guac-recent-connections>
<!-- All connections for this user -->
<div class="header">
- <h2>{{'HOME.SECTION_HEADER_ALL_CONNECTIONS' | translate}}</h2>
+ <h2 id="section-header-all-connections">{{'HOME.SECTION_HEADER_ALL_CONNECTIONS' | translate}}</h2>
<guac-group-list-filter connection-groups="rootConnectionGroups"
filtered-connection-groups="filteredRootConnectionGroups"
placeholder="'HOME.FIELD_PLACEHOLDER_FILTER' | translate"
diff --git a/guacamole/src/main/frontend/src/app/rest/services/patchService.js b/guacamole/src/main/frontend/src/app/rest/services/patchService.js
index 991a54a..ea06e5b 100644
--- a/guacamole/src/main/frontend/src/app/rest/services/patchService.js
+++ b/guacamole/src/main/frontend/src/app/rest/services/patchService.js
@@ -25,17 +25,16 @@
// Required services
var requestService = $injector.get('requestService');
- var authenticationService = $injector.get('authenticationService');
var cacheService = $injector.get('cacheService');
var service = {};
-
+
/**
* Makes a request to the REST API to get the list of patches, returning
* a promise that provides the array of all applicable patches if
* successful. Each patch is a string of raw HTML with meta information
* describing the patch operation stored within meta tags.
- *
+ *
* @returns {Promise.<String[]>}
* A promise which will resolve with an array of HTML patches upon
* success.
@@ -43,14 +42,14 @@
service.getPatches = function getPatches() {
// Retrieve all applicable HTML patches
- return authenticationService.request({
+ return requestService({
cache : cacheService.patches,
method : 'GET',
url : 'api/patches'
});
};
-
+
return service;
}]);
diff --git a/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js b/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js
index 6a637ff..5f9a21a 100644
--- a/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js
+++ b/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js
@@ -323,7 +323,7 @@
// Parse and reject with resulting JSON error
else if (xhr.getResponseHeader('Content-Type') === 'application/json')
- deferred.reject(angular.fromJson(xhr.responseText));
+ deferred.reject(new Error(angular.fromJson(xhr.responseText)));
// Warn of lack of permission of a proxy rejects the upload
else if (xhr.status >= 400 && xhr.status < 500)
diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json
index 6896d0a..05a9fa2 100644
--- a/guacamole/src/main/frontend/src/translations/en.json
+++ b/guacamole/src/main/frontend/src/translations/en.json
@@ -494,6 +494,7 @@
"FIELD_HEADER_DISABLE_BITMAP_CACHING" : "Disable bitmap caching:",
"FIELD_HEADER_DISABLE_OFFSCREEN_CACHING" : "Disable off-screen caching:",
"FIELD_HEADER_DISABLE_GLYPH_CACHING" : "Disable glyph caching:",
+ "FIELD_HEADER_DISABLE_GFX" : "Disable Graphics Pipeline Extension:",
"FIELD_HEADER_ENABLE_PRINTING" : "Enable printing:",
"FIELD_HEADER_ENABLE_SFTP" : "Enable SFTP:",
"FIELD_HEADER_ENABLE_THEMING" : "Enable theming:",
@@ -582,6 +583,7 @@
"FIELD_OPTION_SERVER_LAYOUT_ES_LATAM_QWERTY" : "Latin American (Qwerty)",
"FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode",
"FIELD_OPTION_SERVER_LAYOUT_FR_BE_AZERTY" : "Belgian French (Azerty)",
+ "FIELD_OPTION_SERVER_LAYOUT_FR_CA_QWERTY" : "Canadian French (Qwerty)",
"FIELD_OPTION_SERVER_LAYOUT_FR_CH_QWERTZ" : "Swiss French (Qwertz)",
"FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "French (Azerty)",
"FIELD_OPTION_SERVER_LAYOUT_HU_HU_QWERTZ" : "Hungarian (Qwertz)",
diff --git a/guacamole/src/main/frontend/src/translations/fr.json b/guacamole/src/main/frontend/src/translations/fr.json
index 110bbe0..3c8dc27 100644
--- a/guacamole/src/main/frontend/src/translations/fr.json
+++ b/guacamole/src/main/frontend/src/translations/fr.json
@@ -550,6 +550,7 @@
"FIELD_OPTION_SERVER_LAYOUT_ES_LATAM_QWERTY" : "Latino-Américain (Qwerty)",
"FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode",
"FIELD_OPTION_SERVER_LAYOUT_FR_BE_AZERTY" : "Français Belge (Azerty)",
+ "FIELD_OPTION_SERVER_LAYOUT_FR_CA_QWERTY" : "Français Canada (Qwerty)",
"FIELD_OPTION_SERVER_LAYOUT_FR_CH_QWERTZ" : "Français Suisse (Qwertz)",
"FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "Français (Azerty)",
"FIELD_OPTION_SERVER_LAYOUT_HU_HU_QWERTZ" : "Hongrois (Qwertz)",
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupObjectTranslator.java b/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupObjectTranslator.java
index 721156a..88000b0 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupObjectTranslator.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupObjectTranslator.java
@@ -57,7 +57,7 @@
throws GuacamoleException {
// Filter object attributes by defined schema
- object.setAttributes(filterAttributes(userContext.getUserAttributes(),
+ object.setAttributes(filterAttributes(userContext.getUserGroupAttributes(),
object.getAttributes()));
}