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 000000000..2bd08cde6 --- /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
getConnectionGroupAttributes(); +} 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 76349bad9..81204beb5 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 @@ package org.apache.guacamole.vault.secret; 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 @@ public interface VaultSecretService { /** * 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. @@ -72,6 +76,35 @@ public interface VaultSecretService { */ Future 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 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 @@ -80,6 +113,12 @@ public interface VaultSecretService { * 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 @@ public interface VaultSecretService { * If an error occurs producing the tokens and values required for the * given configuration. */ - Map> getTokens(GuacamoleConfiguration config, - TokenFilter filter) throws GuacamoleException; + Map> 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/user/VaultUserContext.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java index 53901483e..dbfbbb9cb 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,12 +22,20 @@ package org.apache.guacamole.vault.user; 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.Connectable; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; import org.apache.guacamole.net.auth.TokenInjectingUserContext; @@ -35,6 +43,7 @@ import org.apache.guacamole.net.auth.UserContext; 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; @@ -121,6 +130,13 @@ public class VaultUserContext extends TokenInjectingUserContext { @Inject private VaultSecretService secretService; + /** + * Service for retrieving any custom attributes defined for the + * current vault implementation. + */ + @Inject + private VaultAttributeService attributeService; + /** * Creates a new VaultUserContext which automatically injects tokens * containing values of secrets retrieved from a vault. The given @@ -182,6 +198,10 @@ public class VaultUserContext extends TokenInjectingUserContext { * 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 +231,8 @@ public class VaultUserContext extends TokenInjectingUserContext { * If the value for any applicable secret cannot be retrieved from the * vault due to an error. */ - private Map> getTokens(Map tokenMapping, + private Map> getTokens( + Connectable connectable, Map tokenMapping, TokenFilter secretNameFilter, GuacamoleConfiguration config, TokenFilter configFilter) throws GuacamoleException { @@ -236,14 +257,16 @@ public class VaultUserContext extends TokenInjectingUserContext { // Initiate asynchronous retrieval of the token value String tokenName = entry.getKey(); - Future secret = secretService.getValue(secretName); + Future 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 +341,8 @@ public class VaultUserContext extends TokenInjectingUserContext { // 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,8 +422,19 @@ public class VaultUserContext extends TokenInjectingUserContext { // 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 getConnectionGroupAttributes() { + + // Add any custom attributes to any previously defined attributes + return Collections.unmodifiableCollection(Stream.concat( + super.getConnectionGroupAttributes().stream(), + attributeService.getConnectionGroupAttributes().stream() + ).collect(Collectors.toList())); } 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 bcc5a784e..6a7a70c83 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,13 +21,18 @@ package org.apache.guacamole.vault.ksm; 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.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 com.google.inject.assistedinject.FactoryModuleBuilder; + /** * Guice module which configures injections specific to Keeper Secrets * Manager support. @@ -49,10 +54,15 @@ public class KsmAuthenticationProviderModule 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 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 000000000..83ab9c4a3 --- /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 KSM_CONNECTION_GROUP_ATTRIBUTES = + Collections.unmodifiableCollection(Arrays.asList(KSM_CONFIGURATION_FORM)); + + @Override + public Collection 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 aaddb0de0..54aaec753 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.InMemoryStorage; 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 { +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 0bd3f40da..3ef02b8b9 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 @@ package org.apache.guacamole.vault.ksm.conf; 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 @@ public class KsmConfigurationService extends VaultConfigurationService { * 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() { @@ -122,22 +130,68 @@ public class KsmConfigurationService extends VaultConfigurationService { 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 fa177014b..5572ef71d 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,13 +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.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; @@ -40,8 +43,8 @@ import java.util.concurrent.locks.ReadWriteLock; 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; @@ -53,7 +56,6 @@ import org.slf4j.LoggerFactory; * 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 { /** @@ -61,12 +63,6 @@ public class KsmClient { */ private static final Logger logger = LoggerFactory.getLogger(KsmClient.class); - /** - * Service for retrieving configuration information. - */ - @Inject - private KsmConfigurationService confService; - /** * Service for retrieving data from records. */ @@ -94,6 +90,11 @@ public class KsmClient { */ 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 @@ -178,6 +179,17 @@ public class KsmClient { */ private final Set 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. @@ -210,7 +222,7 @@ public class KsmClient { // 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 records = secrets.getRecords(); // Store all secrets within cache 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 000000000..f8220c16f --- /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/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index 21d7c91ac..2436a4e62 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,31 +22,47 @@ package org.apache.guacamole.vault.ksm.secret; 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.Map; 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. @@ -60,6 +76,51 @@ public class KsmSecretService implements VaultSecretService { @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 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 { @@ -74,9 +135,21 @@ public class KsmSecretService implements VaultSecretService { } } + @Override + public Future 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 getValue(String name) throws GuacamoleException { - return ksm.getSecret(name); + + // Use the default KSM configuration from guacamole.properties + return getClient(confService.getKsmConfig()).getSecret(name); } /** @@ -153,13 +226,82 @@ public class KsmSecretService implements VaultSecretService { } + /** + * 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(); + + Directory 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, return immediately + String ksmConfig = group.getAttributes().get(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE); + if (ksmConfig != null) + return ksmConfig; + + // Otherwise, keep searching up the tree until an appropriate configuration is found + parentIdentifier = group.getParentIdentifier(); + } + + // If no KSM configuration was ever found, use the default value + return confService.getKsmConfig(); + } + @Override - public Map> getTokens(GuacamoleConfiguration config, - TokenFilter filter) throws GuacamoleException { + public Map> getTokens(UserContext userContext, Connectable connectable, + GuacamoleConfiguration config, TokenFilter filter) throws GuacamoleException { Map> tokens = new HashMap<>(); Map 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()) 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 000000000..abda16cef --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json @@ -0,0 +1,12 @@ +{ + + "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 " + } + +}