| /* |
| * 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 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 { |
| |
| /** |
| * Logger for this class. |
| */ |
| private static final Logger logger = LoggerFactory.getLogger(VaultSecretService.class); |
| |
| /** |
| * Service for retrieving data from records. |
| */ |
| @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 { |
| |
| // As Keeper notation is essentially a URL, encode all components |
| // using standard URL escaping |
| return URLEncoder.encode(nameComponent, "UTF-8"); |
| |
| } |
| catch (UnsupportedEncodingException e) { |
| throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e); |
| } |
| } |
| |
| @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 { |
| |
| // Use the default KSM configuration from guacamole.properties |
| return getClient(confService.getKsmConfig()).getSecret(name); |
| } |
| |
| /** |
| * Adds contextual parameter tokens for the secrets in the given record to |
| * the given map of existing tokens. The values of each token are |
| * determined from secrets within the record. Depending on the record, this |
| * will be a subset of the username, password, private key, and passphrase. |
| * |
| * @param tokens |
| * The map of parameter tokens that any new tokens should be added to. |
| * |
| * @param prefix |
| * The prefix that should be prepended to each added token. |
| * |
| * @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) 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) { |
| |
| // 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); |
| if (password != null) |
| tokens.put(prefix + "PASSWORD", CompletableFuture.completedFuture(password)); |
| |
| // Key passphrase of server-related record |
| String passphrase = recordService.getPassphrase(record); |
| if (passphrase != null) |
| tokens.put(prefix + "PASSPHRASE", CompletableFuture.completedFuture(passphrase)); |
| |
| // Private key of server-related record |
| Future<String> privateKey = recordService.getPrivateKey(record); |
| 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(); |
| |
| 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 no KSM configuration was ever found, use the default value |
| return confService.getKsmConfig(); |
| |
| } |
| |
| @Override |
| 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()) |
| addRecordTokens(tokens, "KEEPER_SERVER_", |
| ksm.getRecordByHost(filter.filter(hostname))); |
| |
| // Retrieve and define user-specific tokens, if any |
| String username = parameters.get("username"); |
| if (username != null && !username.isEmpty()) |
| addRecordTokens(tokens, "KEEPER_USER_", |
| ksm.getRecordByLogin(filter.filter(username))); |
| |
| // 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()) |
| addRecordTokens(tokens, "KEEPER_GATEWAY_", |
| ksm.getRecordByHost(filter.filter(gatewayHostname))); |
| |
| // Retrieve and define gateway user-specific tokens, if any |
| String gatewayUsername = parameters.get("gateway-username"); |
| if (gatewayUsername != null && !gatewayUsername.isEmpty()) |
| addRecordTokens(tokens, "KEEPER_GATEWAY_USER_", |
| ksm.getRecordByLogin(filter.filter(gatewayUsername))); |
| |
| } |
| |
| return tokens; |
| |
| } |
| |
| } |