GUACAMOLE-1656: Fall back to user KSM config for single value fetch.

This commit is contained in:
James Muehlner
2022-08-05 00:01:12 +00:00
parent 87cd7fbe22
commit 33f2b499ef
3 changed files with 143 additions and 13 deletions

View File

@@ -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.ksm;
import org.apache.guacamole.GuacamoleException;
/**
* A class that is basically equivalent to the standard Supplier class in
* Java, except that the get() function can throw GuacamoleException, which
* is impossible with any of the standard Java lambda type classes, since
* none of them can handle checked exceptions
*/
public abstract class GuacamoleExceptionSupplier<T> {
/**
* Returns a value of the declared type.
*
* @return
* A value of the declared type.
*
* @throws GuacamoleException
* If an error occurs while attemping to calculate the return value.
*/
public abstract T get() throws GuacamoleException;
}

View File

@@ -34,7 +34,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -45,10 +44,12 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService;
import org.apache.guacamole.vault.secret.WindowsUsername;
import org.apache.guacamole.vault.ksm.GuacamoleExceptionSupplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -595,6 +596,38 @@ public class KsmClient {
* is invalid.
*/
public Future<String> getSecret(String notation) throws GuacamoleException {
return getSecret(notation, null);
}
/**
* Returns the value of the secret stored within Keeper Secrets Manager and
* represented by the given Keeper notation. Keeper notation locates the
* value of a specific field, custom field, or file associated with a
* specific record. See: https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation
* If a fallbackFunction is provided, it will be invoked to generate
* a return value in the case where no secrest is found with the given
* keeper notation.
*
* @param notation
* The Keeper notation of the secret to retrieve.
*
* @param fallbackFunction
* A function to invoke in order to produce a Future for return,
* if the requested secret is not found. If the provided Function
* is null, it will not be run.
*
* @return
* A Future which completes with the value of the secret represented by
* the given Keeper notation, or null if there is no such secret.
*
* @throws GuacamoleException
* If the requested secret cannot be retrieved or the Keeper notation
* is invalid.
*/
public Future<String> getSecret(
String notation,
@Nullable GuacamoleExceptionSupplier<Future<String>> fallbackFunction)
throws GuacamoleException {
validateCache();
cacheLock.readLock().lock();
try {
@@ -614,6 +647,11 @@ public class KsmClient {
catch (Error e) {
logger.warn("Record \"{}\" does not exist.", notation);
logger.debug("Retrieval of record by Keeper notation failed.", e);
// If the secret is not found, invoke the fallback function
if (fallbackFunction != null)
return fallbackFunction.get();
return CompletableFuture.completedFuture(null);
}

View File

@@ -48,6 +48,7 @@ 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.GuacamoleExceptionSupplier;
import org.apache.guacamole.vault.ksm.conf.KsmAttributeService;
import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService;
import org.apache.guacamole.vault.secret.VaultSecretService;
@@ -147,7 +148,24 @@ public class KsmSecretService implements VaultSecretService {
// Attempt to find a KSM config for this connection or group
String ksmConfig = getConnectionGroupKsmConfig(userContext, connectable);
return getClient(ksmConfig).getSecret(name);
return getClient(ksmConfig).getSecret(name, new GuacamoleExceptionSupplier<Future<String>>() {
@Override
public Future<String> get() throws GuacamoleException {
// Get the user-supplied KSM config, if allowed by config and
// set by the user
String userKsmConfig = getUserKSMConfig(userContext, connectable);
// If the user config happens to be the same as admin-defined one,
// don't bother trying again
if (userKsmConfig != ksmConfig)
return getClient(userKsmConfig).getSecret(name);
return CompletableFuture.completedFuture(null);
}
});
}
@Override
@@ -335,6 +353,44 @@ public class KsmSecretService implements VaultSecretService {
}
/**
* Return the KSM config blob for the current user IFF user KSM configs
* are enabled globally, and are enabled for the given connectable. If no
* KSM config exists for the given user or KSM configs are not enabled,
* null will be returned.
*
* @param userContext
* The user context from which the current user should be fetched.
*
* @param connectable
* The connectable to which the connection is being established. This
* is the conneciton which will be checked to see if user KSM configs
* are enabled.
*
* @return
* The base64 encoded KSM config blob for the current user if one
* exists, and if user KSM configs are enabled globally and for the
* provided connectable.
*
* @throws GuacamoleException
* If an error occurs while attempting to fetch the KSM config.
*/
private String getUserKSMConfig(
UserContext userContext, Connectable connectable) throws GuacamoleException {
// Check if user KSM configs are enabled globally, and for the given connectable
if (confService.getAllowUserConfig() && isKsmUserConfigEnabled(connectable))
// Return the user-specific KSM config, if one exists
return userContext.self().getAttributes().get(
KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE);
// If user-specific KSM config is disabled globally or for the given
// connectable, return null to indicate that no user config exists
return null;
}
@Override
public Map<String, Future<String>> getTokens(UserContext userContext, Connectable connectable,
GuacamoleConfiguration config, TokenFilter filter) throws GuacamoleException {
@@ -351,16 +407,9 @@ public class KsmSecretService implements VaultSecretService {
// Only use the user-specific KSM config if explicitly enabled in the global
// configuration, AND for the specific connectable being connected to
if (confService.getAllowUserConfig() && isKsmUserConfigEnabled(connectable)) {
// Find a user-specific KSM config, if one exists
String userKsmConfig = userContext.self().getAttributes().get(
KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE);
// If a user-specific config exsts, process it first
if (userKsmConfig != null && !userKsmConfig.trim().isEmpty())
ksmClients.add(0, getClient(userKsmConfig));
}
String userKsmConfig = getUserKSMConfig(userContext, connectable);
if (userKsmConfig != null && !userKsmConfig.trim().isEmpty())
ksmClients.add(0, getClient(userKsmConfig));
// Iterate through the KSM clients, processing using the user-specific
// config first (if it exists), to ensure that any admin-defined values
@@ -431,6 +480,7 @@ public class KsmSecretService implements VaultSecretService {
addRecordTokens(tokens, "KEEPER_USER_",
ksm.getRecordByLogin(filter.filter(username), null));
}
}
return tokens;