GUACAMOLE-1643: Validate/translate KSM configs and one-time tokens on connection group save.
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 dbfbbb9..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
@@ -35,11 +35,16 @@
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;
@@ -138,6 +143,13 @@
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
@@ -438,4 +450,46 @@
}
+ @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-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 6a7a70c..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
@@ -24,12 +24,14 @@
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;
@@ -58,6 +60,7 @@
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()
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..fc4a962
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java
@@ -0,0 +1,253 @@
+/*
+ * 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.DelegatingDirectory;
+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
+ return new DelegatingDirectory<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);
+ }
+
+ };
+ }
+}
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
index abda16c..4601a13 100644
--- 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
@@ -6,7 +6,10 @@
"CONNECTION_GROUP_ATTRIBUTES" : {
"SECTION_HEADER_KSM_CONFIG" : "Keeper Secrets Manager",
- "FIELD_HEADER_KSM_CONFIG" : "KSM Service Configuration "
+ "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."
}
}