GUACAMOLE-2130: Support for Linked Records in KSM.

This commit is contained in:
Stephen Schiffli committed 2025-09-03 11:42:46 -07:00
1 parent 3426a706da
commit 06e34878a8
11 files changed
+142 -14

No files matched your search

@@ -1,2 +0,0 @@
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:jar:1.7.2
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:jar:1.7.2
@@ -1,7 +1,7 @@
Kotlin Serialization (https://github.com/Kotlin/kotlinx.serialization) Kotlin Serialization (https://github.com/Kotlin/kotlinx.serialization)
---------------------------------------------------------------------- ----------------------------------------------------------------------
Version: 1.7.2 Version: 1.9.0
From: 'JetBrains s.r.o and respective authors and developers' From: 'JetBrains s.r.o and respective authors and developers'
License(s): License(s):
Apache v2.0 Apache v2.0
@@ -0,0 +1,2 @@
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:jar:1.9.0
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:jar:1.9.0
@@ -1 +0,0 @@
com.keepersecurity.secrets-manager:core:jar:17.0.0
File renamed without changes.
@@ -2,8 +2,8 @@ Keeper Secrets Manager Java SDK
(https://github.com/Keeper-Security/secrets-manager) (https://github.com/Keeper-Security/secrets-manager)
---------------------------------------------------- ----------------------------------------------------
Version: 17.0.0 Version: 17.1.1
From: 'Keeper Security' (https://www.keepersecurity.com/) From: 'Keeper Security' (https://www.keepersecurity.com/)
License(s): License(s):
MIT (bundled/ksm-sdk-17.0.0/LICENSE) MIT (bundled/ksm-sdk-17.1.1/LICENSE)
@@ -0,0 +1 @@
com.keepersecurity.secrets-manager:core:jar:17.1.1
@@ -60,7 +60,7 @@
<dependency> <dependency>
<groupId>com.keepersecurity.secrets-manager</groupId> <groupId>com.keepersecurity.secrets-manager</groupId>
<artifactId>core</artifactId> <artifactId>core</artifactId>
<version>17.0.0</version> <version>17.1.1</version>
<!-- Correct version conflict (different versions across transitive <!-- Correct version conflict (different versions across transitive
dependencies) --> dependencies) -->
@@ -24,9 +24,11 @@ import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject; import com.google.inject.assistedinject.AssistedInject;
import com.keepersecurity.secretsManager.core.Hosts; import com.keepersecurity.secretsManager.core.Hosts;
import com.keepersecurity.secretsManager.core.KeeperRecord; import com.keepersecurity.secretsManager.core.KeeperRecord;
import com.keepersecurity.secretsManager.core.KeeperRecordLink;
import com.keepersecurity.secretsManager.core.KeeperSecrets; import com.keepersecurity.secretsManager.core.KeeperSecrets;
import com.keepersecurity.secretsManager.core.Login; import com.keepersecurity.secretsManager.core.Login;
import com.keepersecurity.secretsManager.core.Notation; import com.keepersecurity.secretsManager.core.Notation;
import com.keepersecurity.secretsManager.core.QueryOptions;
import com.keepersecurity.secretsManager.core.SecretsManager; import com.keepersecurity.secretsManager.core.SecretsManager;
import com.keepersecurity.secretsManager.core.SecretsManagerOptions; import com.keepersecurity.secretsManager.core.SecretsManagerOptions;
@@ -156,6 +158,22 @@ public class KsmClient {
*/ */
private final Map<String, KeeperRecord> cachedRecordsByHost = new HashMap<>(); private final Map<String, KeeperRecord> cachedRecordsByHost = new HashMap<>();
/**
* All linked admin records retrieved from Keeper Secrets Manager, where each key is a
* KeeperRecord and the value is its associated linked admin KeeperRecord. 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.
*/
private final Map<KeeperRecord, KeeperRecord> cachedLinkedAdminRecords = new HashMap<>();
/**
* All linked launch records retrieved from Keeper Secrets Manager, where each key is a
* KeeperRecord and the value is its associated linked launch KeeperRecord. 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.
*/
private final Map<KeeperRecord, KeeperRecord> cachedLinkedLaunchRecords = new HashMap<>();
/** /**
* The set of all hostnames or IP addresses that are associated with * The set of all hostnames or IP addresses that are associated with
* multiple records, and thus cannot uniquely identify a record. The * multiple records, and thus cannot uniquely identify a record. The
@@ -264,9 +282,16 @@ public class KsmClient {
if (currentTime - cacheTimestamp < cacheInterval) if (currentTime - cacheTimestamp < cacheInterval)
return; return;
// Enable GraphSync
QueryOptions queryOptions = new QueryOptions(
Collections.emptyList(),
Collections.emptyList(),
true
);
// Attempt to pull all records first, allowing that operation to // Attempt to pull all records first, allowing that operation to
// succeed/fail BEFORE we clear out the last cached success // succeed/fail BEFORE we clear out the last cached success
KeeperSecrets secrets = SecretsManager.getSecrets(ksmConfig); KeeperSecrets secrets = SecretsManager.getSecrets2(ksmConfig, queryOptions);
List<KeeperRecord> records = secrets.getRecords(); List<KeeperRecord> records = secrets.getRecords();
// Store all secrets within cache // Store all secrets within cache
@@ -275,6 +300,10 @@ public class KsmClient {
// Clear unambiguous cache of all records by UID // Clear unambiguous cache of all records by UID
cachedRecordsByUid.clear(); cachedRecordsByUid.clear();
// Clear cache of all linked records
cachedLinkedAdminRecords.clear();
cachedLinkedLaunchRecords.clear();
// Clear cache of host-based records // Clear cache of host-based records
cachedAmbiguousHosts.clear(); cachedAmbiguousHosts.clear();
cachedRecordsByHost.clear(); cachedRecordsByHost.clear();
@@ -339,6 +368,9 @@ public class KsmClient {
}); });
// Run through again and build a cache of linked records
records.forEach(record -> cacheLinkedRecords(record));
// Cache has been refreshed // Cache has been refreshed
this.cacheTimestamp = System.currentTimeMillis(); this.cacheTimestamp = System.currentTimeMillis();
@@ -397,6 +429,42 @@ public class KsmClient {
} }
/**
* Populates {@link #cachedLinkedAdminRecords} and {@link #cachedLinkedLaunchRecords}
* with any linked admin or launch records from the provided record. The write lock of
* {@link #cacheLock} must already be acquired before invoking this function.
*
* @param record
* The record whose linked records should be processed and cached.
*/
private void cacheLinkedRecords(KeeperRecord record) {
List<KeeperRecordLink> links = record.getLinks();
if (links != null && !links.isEmpty()) {
for (KeeperRecordLink link : links) {
KeeperRecord linkedRecord = cachedRecordsByUid.get(link.getRecordUid());
if (linkedRecord != null && link.isAdminUser()) {
if (cachedLinkedAdminRecords.containsKey(record)) {
logger.warn("Multiple 'admin' linked records exist "
+ "for record UID: {}", record.getRecordUid());
}
cachedLinkedAdminRecords.put(record, linkedRecord);
}
if (linkedRecord != null && link.isLaunchCredential()) {
if (cachedLinkedLaunchRecords.containsKey(record)) {
logger.warn("Multiple 'launch' linked records exist "
+ "for record UID: {}", record.getRecordUid());
}
cachedLinkedLaunchRecords.put(record, linkedRecord);
}
}
}
}
/** /**
* Associates the given record with the given user, and optional domain. * Associates the given record with the given user, and optional domain.
* The given username or domain may be null. Both {@link #cachedRecordsByUser} * The given username or domain may be null. Both {@link #cachedRecordsByUser}
@@ -587,6 +655,54 @@ public class KsmClient {
} }
} }
/**
* Returns the linked admin record for the given Keeper record, if one exists.
* If no such linked admin record exists, null is returned.
*
* @param record
* The KeeperRecord for which to retrieve the linked admin record.
*
* @return
* The linked KeeperRecord, or null if there is no such record.
*
* @throws GuacamoleException
* If an error occurs that prevents the linked record from being retrieved.
*/
public KeeperRecord getLinkedAdminRecord(KeeperRecord record) throws GuacamoleException {
validateCache();
cacheLock.readLock().lock();
try {
return cachedLinkedAdminRecords.get(record);
}
finally {
cacheLock.readLock().unlock();
}
}
/**
* Returns the linked launch record for the given Keeper record, if one exists.
* If no such linked launch record exists, null is returned.
*
* @param record
* The KeeperRecord for which to retrieve the linked launch record.
*
* @return
* The linked KeeperRecord, or null if there is no such record.
*
* @throws GuacamoleException
* If an error occurs that prevents the linked record from being retrieved.
*/
public KeeperRecord getLinkedLaunchRecord(KeeperRecord record) throws GuacamoleException {
validateCache();
cacheLock.readLock().lock();
try {
return cachedLinkedLaunchRecords.get(record);
}
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
@@ -370,7 +370,7 @@ public class KsmSecretService implements VaultSecretService {
* *
* @param connectable * @param connectable
* The connectable to which the connection is being established. This * The connectable to which the connection is being established. This
* is the conneciton which will be checked to see if user KSM configs * is the connection which will be checked to see if user KSM configs
* are enabled. * are enabled.
* *
* @return * @return
@@ -435,18 +435,30 @@ public class KsmSecretService implements VaultSecretService {
// Retrieve and define server-specific tokens, if any // Retrieve and define server-specific tokens, if any
String hostname = parameters.get("hostname"); String hostname = parameters.get("hostname");
if (hostname != null && !hostname.isEmpty()) if (hostname != null && !hostname.isEmpty()) {
KeeperRecord record = ksm.getRecordByHost(filter.filter(hostname));
KeeperRecord adminRecord = ksm.getLinkedAdminRecord(record);
KeeperRecord launchRecord = ksm.getLinkedLaunchRecord(record);
addRecordTokens(tokens, "KEEPER_SERVER_", addRecordTokens(tokens, "KEEPER_SERVER_",
ksm.getRecordByHost(filter.filter(hostname))); adminRecord != null ? adminRecord : record);
addRecordTokens(tokens, "KEEPER_SERVER_ADMIN_", adminRecord);
addRecordTokens(tokens, "KEEPER_SERVER_LAUNCH_", launchRecord);
}
// Tokens specific to RDP // Tokens specific to RDP
if ("rdp".equals(config.getProtocol())) { if ("rdp".equals(config.getProtocol())) {
// Retrieve and define gateway server-specific tokens, if any // Retrieve and define gateway server-specific tokens, if any
String gatewayHostname = parameters.get("gateway-hostname"); String gatewayHostname = parameters.get("gateway-hostname");
if (gatewayHostname != null && !gatewayHostname.isEmpty()) if (gatewayHostname != null && !gatewayHostname.isEmpty()) {
KeeperRecord record = ksm.getRecordByHost(filter.filter(gatewayHostname));
KeeperRecord adminRecord = ksm.getLinkedAdminRecord(record);
KeeperRecord launchRecord = ksm.getLinkedLaunchRecord(record);
addRecordTokens(tokens, "KEEPER_GATEWAY_", addRecordTokens(tokens, "KEEPER_GATEWAY_",
ksm.getRecordByHost(filter.filter(gatewayHostname))); adminRecord != null ? adminRecord : record);
addRecordTokens(tokens, "KEEPER_GATEWAY_ADMIN_", adminRecord);
addRecordTokens(tokens, "KEEPER_GATEWAY_LAUNCH_", launchRecord);
}
// Retrieve and define domain tokens, if any // Retrieve and define domain tokens, if any
String domain = parameters.get("domain"); String domain = parameters.get("domain");
@@ -518,7 +530,7 @@ public class KsmSecretService implements VaultSecretService {
config, getClient(userKsmConfig), tokens, parameters, filter); config, getClient(userKsmConfig), tokens, parameters, filter);
// Add connection group or globally defined tokens after the user-specific // Add connection group or globally defined tokens after the user-specific
// ones to ensure that the user config will be overriden on collision // ones to ensure that the user config will be overridden on collision
String ksmConfig = getConnectionGroupKsmConfig(userContext, connectable); String ksmConfig = getConnectionGroupKsmConfig(userContext, connectable);
addConnectableTokens( addConnectableTokens(
config, getClient(ksmConfig), tokens, parameters, filter); config, getClient(ksmConfig), tokens, parameters, filter);