GUACAMOLE-641: Add generic vault support with an initial Azure Key Vault implementation.

This commit is contained in:
Michael Jumper committed 2022-01-22 22:25:04 -08:00
1 parent 0ac67b8cf8
commit 6145a79f5d
73 files changed
+2369 -1

No files matched your search

@@ -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.auth.vault;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.vault.user.VaultUserContextFactory;
import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.UserContext;
/**
* AuthenticationProvider implementation which automatically injects tokens
* containing the values of secrets retrieved from a vault.
*/
public abstract class VaultAuthenticationProvider
extends AbstractAuthenticationProvider {
/**
* Factory for creating instances of the relevant vault-specific
* UserContext implementation.
*/
private final VaultUserContextFactory userContextFactory;
/**
* Creates a new VaultAuthenticationProvider which uses the given module to
* configure dependency injection.
*
* @param module
* The module to use to configure dependency injection.
*/
protected VaultAuthenticationProvider(VaultAuthenticationProviderModule module) {
Injector injector = Guice.createInjector(module);
this.userContextFactory = injector.getInstance(VaultUserContextFactory.class);
}
@Override
public UserContext decorate(UserContext context,
AuthenticatedUser authenticatedUser, Credentials credentials)
throws GuacamoleException {
return userContextFactory.create(context);
}
}
@@ -0,0 +1,98 @@
/*
* 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.vault;
import com.google.inject.AbstractModule;
import com.google.inject.assistedinject.FactoryModuleBuilder;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.vault.user.VaultUserContext;
import org.apache.guacamole.auth.vault.user.VaultUserContextFactory;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.net.auth.UserContext;
/**
* Guice module which configures injections specific to the base support for
* key vaults. When adding support for a key vault provider, a subclass
* specific to that vault implementation will need to be created.
*
* @see AzureKeyVaultAuthenticationProviderModule
*/
public abstract class VaultAuthenticationProviderModule extends AbstractModule {
/**
* Guacamole server environment.
*/
private final Environment environment;
/**
* Creates a new VaultAuthenticationProviderModule which configures
* dependency injection for the Azure Key Vault authentication provider.
*
* @throws GuacamoleException
* If an error occurs while retrieving the Guacamole server
* environment.
*/
public VaultAuthenticationProviderModule() throws GuacamoleException {
this.environment = LocalEnvironment.getInstance();
}
/**
* Configures injections for interfaces which are implementation-specific
* to the vault service in use. Subclasses MUST provide a version of this
* function which binds concrete implementations to the following
* interfaces:
*
* - VaultConfigurationService
* - VaultSecretService
*
* @see AzureKeyVaultAuthenticationProviderModule
*/
protected abstract void configureVault();
/**
* Returns the instance of the Guacamole server environment which will be
* exposed to other classes via dependency injection.
*
* @return
* The instance of the Guacamole server environment which will be
* exposed via dependency injection.
*/
protected Environment getEnvironment() {
return environment;
}
@Override
protected void configure() {
// Bind Guacamole server environment
bind(Environment.class).toInstance(environment);
// Bind factory for creating UserContexts
install(new FactoryModuleBuilder()
.implement(UserContext.class, VaultUserContext.class)
.build(VaultUserContextFactory.class));
// Bind all other implementation-specific interfaces
configureVault();
}
}
@@ -0,0 +1,107 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.vault.conf;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.vault.VaultAuthenticationProviderModule;
import org.apache.guacamole.environment.Environment;
/**
* Base class for services which retrieve key vault configuration information.
* A concrete implementation of this class must be defined and bound for key
* vault support to work.
*
* @see VaultAuthenticationProviderModule
*/
public abstract class VaultConfigurationService {
/**
* The Guacamole server environment.
*/
@Inject
private Environment environment;
/**
* ObjectMapper for deserializing JSON.
*/
private static final ObjectMapper mapper = new ObjectMapper();
/**
* The name of the file containing a JSON mapping of Guacamole parameter
* token to vault secret name.
*/
private final String tokenMappingFilename;
/**
* Creates a new VaultConfigurationService which retrieves the token/secret
* mapping from a JSON file having the given name.
*
* @param tokenMappingFilename
* The name of the JSON file containing the token/secret mapping.
*/
protected VaultConfigurationService(String tokenMappingFilename) {
this.tokenMappingFilename = tokenMappingFilename;
}
/**
* Returns a mapping dictating the name of the secret which maps to each
* parameter token. In the returned mapping, the value of each entry is the
* name of the secret to use to populate the value of the parameter token,
* and the key of each entry is the name of the parameter token which
* should receive the value of the secret.
*
* The name of the secret may contain its own tokens, which will be
* substituted using values from the given filter. See the definition of
* VaultUserContext for the names of these tokens and the contexts in which
* they can be applied to secret names.
*
* @return
* A mapping dictating the name of the secret which maps to each
* parameter token.
*
* @throws GuacamoleException
* If the JSON file defining the token/secret mapping cannot be read.
*/
public Map<String, String> getTokenMapping() throws GuacamoleException {
// Get configuration file from GUACAMOLE_HOME
File confFile = new File(environment.getGuacamoleHome(), tokenMappingFilename);
// Deserialize token mapping from JSON
try {
return mapper.readValue(confFile, new TypeReference<Map<String, String>>() {});
}
// Fail if JSON is invalid/unreadable
catch (IOException e) {
throw new GuacamoleServerException("Unable to read token mapping "
+ "configuration file \"" + tokenMappingFilename + "\".", e);
}
}
}
@@ -0,0 +1,67 @@
/*
* 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.vault.secret;
import org.apache.guacamole.GuacamoleException;
/**
* Generic service for retrieving the value of a secret stored in a vault.
*/
public interface VaultSecretService {
/**
* Translates an arbitrary string, which may contain characters not allowed
* by the vault implementation, into a string which is a valid secret name.
* The type of transformation performed on the string, if any, will depend
* on the specific requirements of the vault provider.
*
* NOTE: It is critical that this transformation is deterministic and
* reasonably predictable for users. If an implementation must apply a
* transformation to secret names, that transformation needs to be
* documented.
*
* @param name
* An arbitrary string intended for use as a secret name, but which may
* contain characters not allowed by the vault implementation.
*
* @return
* A name containing essentially the same content as the provided
* string, but transformed deterministically such that it is acceptable
* as a secret name by the vault provider.
*/
String canonicalize(String name);
/**
* Returns the value of the secret having the given name. If no such
* secret exists, null is returned.
*
* @param name
* The name of the secret to retrieve.
*
* @return
* The value of the secret having the given name, or null if no such
* secret exists.
*
* @throws GuacamoleException
* If the secret cannot be retrieved due to an error.
*/
String getValue(String name) throws GuacamoleException;
}
@@ -0,0 +1,281 @@
/*
* 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.vault.user;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.util.HashMap;
import java.util.Map;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.vault.conf.VaultConfigurationService;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.ConnectionGroup;
import org.apache.guacamole.net.auth.TokenInjectingUserContext;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.auth.vault.secret.VaultSecretService;
import org.apache.guacamole.protocol.GuacamoleConfiguration;
import org.apache.guacamole.token.GuacamoleTokenUndefinedException;
import org.apache.guacamole.token.TokenFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* UserContext implementation which automatically injects tokens containing the
* values of secrets retrieved from a vault.
*/
public class VaultUserContext extends TokenInjectingUserContext {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(VaultUserContext.class);
/**
* The name of the token which will be replaced with the username of the
* current user if specified within the name of a secret. This token
* applies to both connections and connection groups.
*/
private static final String USERNAME_TOKEN = "GUAC_USERNAME";
/**
* The name of the token which will be replaced with the name of the
* current connection group if specified within the name of a secret. This
* token only applies only to connection groups.
*/
private static final String CONNECTION_GROUP_NAME_TOKEN = "CONNECTION_GROUP_NAME";
/**
* The name of the token which will be replaced with the identifier of the
* current connection group if specified within the name of a secret. This
* token only applies only to connection groups.
*/
private static final String CONNECTION_GROUP_IDENTIFIER_TOKEN = "CONNECTION_GROUP_ID";
/**
* The name of the token which will be replaced with the \"hostname\"
* connection parameter of the current connection if specified within the
* name of a secret. This token only applies only to connections.
*/
private static final String CONNECTION_HOSTNAME_TOKEN = "CONNECTION_HOSTNAME";
/**
* The name of the token which will be replaced with the \"username\"
* connection parameter of the current connection if specified within the
* name of a secret. This token only applies only to connections.
*/
private static final String CONNECTION_USERNAME_TOKEN = "CONNECTION_USERNAME";
/**
* The name of the token which will be replaced with the name of the
* current connection if specified within the name of a secret. This token
* only applies only to connections.
*/
private static final String CONNECTION_NAME_TOKEN = "CONNECTION_NAME";
/**
* The name of the token which will be replaced with the identifier of the
* current connection if specified within the name of a secret. This token
* only applies only to connections.
*/
private static final String CONNECTION_IDENTIFIER_TOKEN = "CONNECTION_ID";
/**
* Service for retrieving configuration information.
*/
@Inject
private VaultConfigurationService confService;
/**
* Service for retrieving the values of secrets stored in a vault.
*/
@Inject
private VaultSecretService secretService;
/**
* 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
* will receive additional tokens during the connection process.
*
* Note that this class depends on concrete implementations of the
* following classes to be provided via dependency injection:
*
* - VaultConfigurationService
* - VaultSecretService
*
* Bindings providing these concrete implementations will need to be
* provided by subclasses of VaultAuthenticationProviderModule for each
* supported vault.
*
* @param userContext
* The UserContext instance to decorate.
*/
@AssistedInject
public VaultUserContext(@Assisted UserContext userContext) {
super(userContext);
}
/**
* Creates a new TokenFilter instance with token values set for all tokens
* which are not specific to connections or connection groups. Currently,
* this is only the username token ("GUAC_USERNAME").
*
* @return
* A new TokenFilter instance with token values set for all tokens
* which are not specific to connections or connection groups.
*/
private TokenFilter createFilter() {
TokenFilter filter = new TokenFilter();
filter.setToken(USERNAME_TOKEN, self().getIdentifier());
return filter;
}
/**
* Retrieve all applicable tokens and corresponding values from the vault,
* using the given TokenFilter to filter tokens within the secret names
* prior to retrieving those secrets.
*
* @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
* and the value is the name of the secret. The name of the secret
* may contain its own tokens, which will be substituted using values
* from the given filter.
*
* @param filter
* The filter to use to substitute values for tokens in the names of
* secrets to be retrieved from the vault.
*
* @return
* The tokens which should be added to the in-progress call to
* connect().
*
* @throws GuacamoleException
* If the value for any applicable secret cannot be retrieved from the
* vault due to an error.
*/
private Map<String, String> getTokens(Map<String, String> tokenMapping,
TokenFilter filter) throws GuacamoleException {
Map<String, String> tokens = new HashMap<>();
// Populate map with tokens containing the values of all secrets
// indicated in the token mapping
for (Map.Entry<String, String> entry : tokenMapping.entrySet()) {
// Translate secret pattern into secret name, ignoring any
// secrets which cannot be translated
String secretName;
try {
secretName = secretService.canonicalize(filter.filterStrict(entry.getValue()));
}
catch (GuacamoleTokenUndefinedException e) {
logger.debug("Secret for token \"{}\" will not be retrieved. "
+ "Token \"{}\" within mapped secret name has no "
+ "defined value in the current context.",
entry.getKey(), e.getTokenName());
continue;
}
// If a value is defined for the secret in question, store that
// value under the mapped token
String tokenName = entry.getKey();
String secretValue = secretService.getValue(secretName);
if (secretValue != null) {
tokens.put(tokenName, secretValue);
logger.debug("Token \"{}\" populated with value from "
+ "secret \"{}\".", tokenName, secretName);
}
else
logger.debug("Token \"{}\" not populated. Mapped "
+ "secret \"{}\" has no value.",
tokenName, secretName);
}
return tokens;
}
@Override
protected Map<String, String> getTokens(ConnectionGroup connectionGroup)
throws GuacamoleException {
String name = connectionGroup.getName();
String identifier = connectionGroup.getIdentifier();
logger.debug("Injecting tokens from vault for connection group "
+ "\"{}\" (\"{}\").", identifier, name);
// Add general and connection-group-specific tokens
TokenFilter filter = createFilter();
filter.setToken(CONNECTION_GROUP_NAME_TOKEN, name);
filter.setToken(CONNECTION_GROUP_IDENTIFIER_TOKEN, identifier);
// Substitute tokens producing secret names, retrieving and storing
// those secrets as parameter tokens
return getTokens(confService.getTokenMapping(), filter);
}
@Override
protected Map<String, String> getTokens(Connection connection)
throws GuacamoleException {
String name = connection.getName();
String identifier = connection.getIdentifier();
logger.debug("Injecting tokens from vault for connection \"{}\" "
+ "(\"{}\").", identifier, name);
// Add general and connection-specific tokens
TokenFilter filter = createFilter();
filter.setToken(CONNECTION_NAME_TOKEN, connection.getName());
filter.setToken(CONNECTION_IDENTIFIER_TOKEN, identifier);
// Add hostname and username tokens if available (implementations are
// not required to expose connection configuration details)
GuacamoleConfiguration config = connection.getConfiguration();
String hostname = config.getParameter("hostname");
if (hostname != null)
filter.setToken(CONNECTION_HOSTNAME_TOKEN, hostname);
else
logger.debug("Hostname for connection \"{}\" (\"{}\") not "
+ "available. \"{}\" token will not be populated in "
+ "secret names.", identifier, name,
CONNECTION_HOSTNAME_TOKEN);
String username = config.getParameter("username");
if (username != null)
filter.setToken(CONNECTION_USERNAME_TOKEN, username);
else
logger.debug("Username for connection \"{}\" (\"{}\") not "
+ "available. \"{}\" token will not be populated in "
+ "secret names.", identifier, name,
CONNECTION_USERNAME_TOKEN);
// Substitute tokens producing secret names, retrieving and storing
// those secrets as parameter tokens
return getTokens(confService.getTokenMapping(), filter);
}
}
@@ -0,0 +1,46 @@
/*
* 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.vault.user;
import org.apache.guacamole.net.auth.UserContext;
/**
* Factory for creating UserContext instances which automatically inject tokens
* containing the values of secrets retrieved from a vault.
*/
public interface VaultUserContextFactory {
/**
* Returns a new instance of a UserContext implementation which
* automatically injects tokens containing values of secrets retrieved from
* a vault. The given UserContext is decorated such that connections and
* connection groups will receive additional tokens during the connection
* process.
*
* @param userContext
* The UserContext instance to decorate.
*
* @return
* A new UserContext instance which automatically injects tokens
* containing values of secrets retrieved from a vault.
*/
UserContext create(UserContext userContext);
}
@@ -0,0 +1,7 @@
{
"DATA_SOURCE_AZURE_KEYVAULT" : {
"NAME" : "Azure Key Vault"
}
}