GUACAMOLE-1661: Add domain search support for KSM vault extension.

This commit is contained in:
James Muehlner
2022-08-08 23:10:41 +00:00
parent 4b4490b45a
commit aa06c81f29
2 changed files with 141 additions and 5 deletions

View File

@@ -34,6 +34,7 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -45,6 +46,8 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService;
import org.apache.guacamole.vault.secret.WindowsUsername;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -63,6 +66,12 @@ public class KsmClient {
*/ */
private static final Logger logger = LoggerFactory.getLogger(KsmClient.class); private static final Logger logger = LoggerFactory.getLogger(KsmClient.class);
/**
* Service for retrieving configuration information.
*/
@Inject
private KsmConfigurationService confService;
/** /**
* Service for retrieving data from records. * Service for retrieving data from records.
*/ */
@@ -179,6 +188,30 @@ public class KsmClient {
*/ */
private final Set<String> cachedAmbiguousUsernames = new HashSet<>(); private final Set<String> cachedAmbiguousUsernames = new HashSet<>();
/**
* All records retrieved from Keeper Secrets Manager, where each key is the
* domain of the corresponding record. The domain of a record is
* determined by {@link Login} fields, thus a record may be associated with
* multiple domains. If a record is associated with multiple domains, there
* will be multiple references to that record within this Map. The contents
* of this Map are automatically updated if {@link #validateCache()}
* refreshes the cache. This Map must not be accessed without
* {@link #cacheLock} acquired appropriately. Before using a value from
* this Map, {@link #cachedAmbiguousDomains} must first be checked to
* verify that there is indeed only one record associated with that domain.
*/
private final Map<String, KeeperRecord> cachedRecordsByDomain = new HashMap<>();
/**
* The set of all domains that are associated with multiple records, and
* thus cannot uniquely identify a record. The contents of this Set are
* automatically updated if {@link #validateCache()} refreshes the cache.
* This Set must not be accessed without {@link #cacheLock} acquired
* appropriately. This Set must be checked before using a value retrieved
* from {@link #cachedRecordsByDomain}.
*/
private final Set<String> cachedAmbiguousDomains = new HashSet<>();
/** /**
* Create a new KSM client based around the provided KSM configuration. * Create a new KSM client based around the provided KSM configuration.
* *
@@ -239,9 +272,17 @@ public class KsmClient {
cachedAmbiguousUsernames.clear(); cachedAmbiguousUsernames.clear();
cachedRecordsByUsername.clear(); cachedRecordsByUsername.clear();
// Store all records, sorting each into host-based and login-based // Clear cache of domain-based records
// buckets cachedAmbiguousDomains.clear();
records.forEach(record -> { cachedRecordsByDomain.clear();
// Store all records, sorting each into host-based, login-based,
// and domain-based buckets
Iterator<KeeperRecord> recordIterator = records.iterator();
while(recordIterator.hasNext()) {
// Go through records one at a time
KeeperRecord record = recordIterator.next();
// Store based on UID ... // Store based on UID ...
cachedRecordsByUid.put(record.getRecordUid(), record); cachedRecordsByUid.put(record.getRecordUid(), record);
@@ -250,13 +291,38 @@ public class KsmClient {
String hostname = recordService.getHostname(record); String hostname = recordService.getHostname(record);
addRecordForHost(record, hostname); addRecordForHost(record, hostname);
// ... and domain
String domain = recordService.getDomain(record);
addRecordForDomain(record, domain);
// Fetch the username
String username = recordService.getUsername(record);
// If domains should be split out from usernames
if (username != null && confService.getSplitWindowsUsernames()) {
// Attempt to split the domain of the username
WindowsUsername usernameAndDomain = (
WindowsUsername.splitWindowsUsernameFromDomain(username));
if (usernameAndDomain.hasDomain()) {
// Update the username if a domain has been stripped off
username = usernameAndDomain.getUsername();
// Use the username-split domain if not already set explicitly
if (domain == null)
addRecordForDomain(record, usernameAndDomain.getDomain());
}
}
// Store based on username ONLY if no hostname (will otherwise // Store based on username ONLY if no hostname (will otherwise
// result in ambiguous entries for servers tied to identical // result in ambiguous entries for servers tied to identical
// accounts) // accounts)
if (hostname == null) if (hostname == null)
addRecordForLogin(record, recordService.getUsername(record)); addRecordForLogin(record, username);
}); }
// Cache has been refreshed // Cache has been refreshed
this.cacheTimestamp = System.currentTimeMillis(); this.cacheTimestamp = System.currentTimeMillis();
@@ -268,6 +334,30 @@ public class KsmClient {
} }
/**
* Associates the given record with the given domain. The domain may be
* null. Both {@link #cachedRecordsByDomain} and {@link #cachedAmbiguousDomains}
* are updated appropriately. The write lock of {@link #cacheLock} must
* already be acquired before invoking this function.
*
* @param record
* The record to associate with the domains in the given field.
*
* @param domain
* The domain that the given record should be associated with.
* This may be null.
*/
private void addRecordForDomain(KeeperRecord record, String domain) {
if (domain == null)
return;
KeeperRecord existing = cachedRecordsByDomain.putIfAbsent(domain, record);
if (existing != null && record != existing)
cachedAmbiguousDomains.add(domain);
}
/** /**
* Associates the given record with the given hostname. The hostname may be * Associates the given record with the given hostname. The hostname may be
* null. Both {@link #cachedRecordsByHost} and {@link #cachedAmbiguousHosts} * null. Both {@link #cachedRecordsByHost} and {@link #cachedAmbiguousHosts}
@@ -432,6 +522,40 @@ public class KsmClient {
} }
} }
/**
* Returns the record associated with the given domain. If no such record
* exists, or there are multiple such records, null is returned.
*
* @param domain
* The domain of the record to return.
*
* @return
* The record associated with the given domain, or null if there is
* no such record or multiple such records.
*
* @throws GuacamoleException
* If an error occurs that prevents the record from being retrieved.
*/
public KeeperRecord getRecordByDomain(String domain) throws GuacamoleException {
validateCache();
cacheLock.readLock().lock();
try {
if (cachedAmbiguousDomains.contains(domain)) {
logger.debug("The domain \"{}\" is referenced by multiple "
+ "Keeper records and cannot be used to locate "
+ "individual secrets.", domain);
return null;
}
return cachedRecordsByDomain.get(domain);
}
finally {
cacheLock.readLock().unlock();
}
}
/** /**
* Returns the value of the secret stored within Keeper Secrets Manager and * Returns the value of the secret stored within Keeper Secrets Manager and
* represented by the given Keeper notation. Keeper notation locates the * represented by the given Keeper notation. Keeper notation locates the

View File

@@ -344,6 +344,18 @@ public class KsmSecretService implements VaultSecretService {
addRecordTokens(tokens, "KEEPER_GATEWAY_USER_", addRecordTokens(tokens, "KEEPER_GATEWAY_USER_",
ksm.getRecordByLogin(filter.filter(gatewayUsername))); ksm.getRecordByLogin(filter.filter(gatewayUsername)));
// Retrieve and define domain tokens, if any
String domain = parameters.get("domain");
if (domain != null && !domain.isEmpty())
addRecordTokens(tokens, "KEEPER_DOMAIN_",
ksm.getRecordByDomain(filter.filter(domain)));
// Retrieve and define gateway domain tokens, if any
String gatewayDomain = parameters.get("gateway-domain");
if (gatewayDomain != null && !gatewayDomain.isEmpty())
addRecordTokens(tokens, "KEEPER_GATEWAY_DOMAIN_",
ksm.getRecordByDomain(filter.filter(gatewayDomain)));
} }
return tokens; return tokens;