Linked Credentials on PAM Records
Description of each accessible field type on PAM Resource Records
SDK Version Required: 17.1.1 or higher
The document lists out the full list of Property methods and advanced use cases when interacting with the linked credentials on PAM Resource Records:
All Property Methods
for (KeeperRecordLink link : record.getLinks()) {
// Basic properties
String targetUid = link.getRecordUid();
String linkPath = link.getPath(); // e.g., "pamUser", "ai_settings", "jit_settings"
String rawData = link.getData(); // Base64-encoded data
// User privilege methods
boolean isAdmin = link.isAdminUser();
boolean isLaunchCredential = link.isLaunchCredential();
// Permission methods
boolean allowsRotation = link.allowsRotation();
boolean allowsConnections = link.allowsConnections();
boolean allowsPortForwards = link.allowsPortForwards();
boolean allowsSessionRecording = link.allowsSessionRecording();
boolean allowsTypescriptRecording = link.allowsTypescriptRecording();
boolean allowsRemoteBrowserIsolation = link.allowsRemoteBrowserIsolation();
// Settings methods
boolean rotatesOnTermination = link.rotatesOnTermination();
Integer dataVersion = link.getLinkDataVersion();
// Data analysis methods
boolean hasReadableData = link.hasReadableData();
boolean hasEncryptedData = link.hasEncryptedData();
boolean mightBeEncrypted = link.mightBeEncrypted();
System.out.println("Link Analysis for " + targetUid + ":");
System.out.println(" Path: " + linkPath);
System.out.println(" Admin: " + isAdmin);
System.out.println(" Launch Credential: " + isLaunchCredential);
System.out.println(" Allows Rotation: " + allowsRotation);
System.out.println(" Allows Connections: " + allowsConnections);
System.out.println(" Has Encrypted Data: " + hasEncryptedData);
}Advanced Use Cases
Advanced Data Access Patterns
Encrypted Data Handling
public void handleEncryptedLinkData(KeeperRecord record) {
for (KeeperRecordLink link : record.getLinks()) {
if (link.getData() != null) {
System.out.println("\nAnalyzing link data for: " + link.getRecordUid());
System.out.println(" Path: " + (link.getPath() != null ? link.getPath() : "null"));
// Check encryption status
boolean mightBeEncrypted = link.mightBeEncrypted();
boolean hasEncryptedData = link.hasEncryptedData();
boolean hasReadableData = link.hasReadableData();
System.out.println(" Encryption Analysis:");
System.out.println(" mightBeEncrypted(): " + mightBeEncrypted);
System.out.println(" hasEncryptedData(): " + hasEncryptedData);
System.out.println(" hasReadableData(): " + hasReadableData);
try {
if (hasEncryptedData || mightBeEncrypted) {
// Method 1: Use getDecryptedData for encrypted content
String decryptedData = link.getDecryptedData(record.getRecordKey());
System.out.println(" Decrypted Data: " + decryptedData);
// Method 2: Use getLinkData for structured access
Map<String, Object> linkData = link.getLinkData(record.getRecordKey());
if (linkData != null) {
System.out.println(" Structured Data:");
for (Map.Entry<String, Object> entry : linkData.entrySet()) {
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
}
} else {
// Plain base64 data
String plainData = link.getDecodedData();
System.out.println(" Plain Data: " + plainData);
}
} catch (Exception e) {
System.out.println(" Error processing data: " + e.getMessage());
System.out.println(" Raw Base64: " + link.getData());
}
}
}
}Settings-Specific Access Methods
public void demonstrateSettingsDataMethods(SecretsManagerOptions options) throws Exception {
QueryOptions queryOptions = new QueryOptions(
Collections.emptyList(),
Collections.emptyList(),
true
);
KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);
System.out.println("Settings Data Methods Demo:");
boolean foundSettings = false;
for (KeeperRecord record : secrets.getRecords()) {
if (record.getLinks() != null && !record.getLinks().isEmpty()) {
System.out.println("\nRecord: " + record.getTitle() + " (" + record.getType() + ")");
for (KeeperRecordLink link : record.getLinks()) {
String linkPath = link.getPath();
System.out.println(" Link to " + link.getRecordUid() + " (path: " + linkPath + ")");
// Test AI Settings method
if ("ai_settings".equals(linkPath)) {
foundSettings = true;
System.out.println(" Testing getAiSettingsData():");
try {
Map<String, Object> aiSettings = link.getAiSettingsData(record.getRecordKey());
if (aiSettings != null) {
System.out.println(" AI Settings found:");
for (Map.Entry<String, Object> entry : aiSettings.entrySet()) {
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
} else {
System.out.println(" AI Settings data could not be parsed");
}
} catch (Exception e) {
System.out.println(" Error parsing AI settings: " + e.getMessage());
}
}
// Test JIT Settings method
if ("jit_settings".equals(linkPath)) {
foundSettings = true;
System.out.println(" Testing getJitSettingsData():");
try {
Map<String, Object> jitSettings = link.getJitSettingsData(record.getRecordKey());
if (jitSettings != null) {
System.out.println(" JIT Settings found:");
for (Map.Entry<String, Object> entry : jitSettings.entrySet()) {
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
} else {
System.out.println(" JIT Settings data could not be parsed");
}
} catch (Exception e) {
System.out.println(" Error parsing JIT settings: " + e.getMessage());
}
}
// Test generic getSettingsForPath method
if (linkPath != null && linkPath.endsWith("_settings")) {
foundSettings = true;
System.out.println(" Testing getSettingsForPath(\"" + linkPath + "\"):");
try {
Map<String, Object> settings = link.getSettingsForPath(linkPath, record.getRecordKey());
if (settings != null) {
System.out.println(" Settings found for path '" + linkPath + "':");
for (Map.Entry<String, Object> entry : settings.entrySet()) {
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
} else {
System.out.println(" Settings data could not be parsed");
}
} catch (Exception e) {
System.out.println(" Error parsing settings: " + e.getMessage());
}
}
}
}
}
if (!foundSettings) {
System.out.println(" No settings paths found in current records.");
System.out.println(" Settings paths to look for: ai_settings, jit_settings, *_settings");
}
}Complex Relationship Analysis
public class ComprehensiveDagAnalyzer {
public static void analyzeLinkingPatterns(SecretsManagerOptions options) throws Exception {
QueryOptions queryOptions = new QueryOptions(
Collections.emptyList(),
Collections.emptyList(),
true // requestLinks
);
KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);
// Build relationship map
Map<String, List<String>> relationshipMap = new HashMap<>();
Map<String, String> recordTitles = new HashMap<>();
Map<String, String> recordTypes = new HashMap<>();
int totalLinks = 0;
for (KeeperRecord record : secrets.getRecords()) {
recordTitles.put(record.getRecordUid(), record.getTitle());
recordTypes.put(record.getRecordUid(), record.getType());
List<KeeperRecordLink> links = record.getLinks();
if (links != null && !links.isEmpty()) {
List<String> targets = new ArrayList<>();
for (KeeperRecordLink link : links) {
targets.add(link.getRecordUid());
totalLinks++;
}
relationshipMap.put(record.getRecordUid(), targets);
}
}
System.out.println("DAG Analysis Report:");
System.out.println("===================");
System.out.println("Total records: " + secrets.getRecords().size());
System.out.println("Records with outgoing links: " + relationshipMap.size());
System.out.println("Total links: " + totalLinks);
// Find records that are targets (have incoming links)
Set<String> targets = new HashSet<>();
for (List<String> linkTargets : relationshipMap.values()) {
targets.addAll(linkTargets);
}
System.out.println("Records that are link targets: " + targets.size());
// Show detailed relationship patterns
if (!relationshipMap.isEmpty()) {
System.out.println("\nDetailed Relationship Patterns:");
for (Map.Entry<String, List<String>> entry : relationshipMap.entrySet()) {
String sourceTitle = recordTitles.get(entry.getKey());
String sourceType = recordTypes.get(entry.getKey());
System.out.println(" " + sourceTitle + " [" + sourceType + "] → " + entry.getValue().size() + " record(s)");
for (String targetUid : entry.getValue()) {
String targetTitle = recordTitles.get(targetUid);
String targetType = recordTypes.get(targetUid);
System.out.println(" → " + (targetTitle != null ? targetTitle : "Unknown record: " + targetUid) +
" [" + (targetType != null ? targetType : "Unknown") + "]");
}
}
} else {
System.out.println(" No links found in current records.");
}
}
}Advanced PAM User Management
public static List<UserStatus> getPamMachineUsers(String pamMachineUid, InMemoryStorage storage) {
List<UserStatus> users = new ArrayList<>();
try {
QueryOptions queryOptions = new QueryOptions(
Collections.emptyList(),
Collections.emptyList(),
true // requestLinks
);
SecretsManagerOptions options = new SecretsManagerOptions(storage);
KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);
// Find the PAM Machine record
KeeperRecord pamMachine = null;
for (KeeperRecord record : secrets.getRecords()) {
if (record.getRecordUid().equals(pamMachineUid) && "pamMachine".equals(record.getType())) {
pamMachine = record;
break;
}
}
if (pamMachine != null && pamMachine.getLinks() != null) {
System.out.println("PAM Machine: " + pamMachine.getTitle());
System.out.println("Number of users associated with this PAM machine: " + pamMachine.getLinks().size());
for (KeeperRecordLink link : pamMachine.getLinks()) {
// Find the linked user record
for (KeeperRecord record : secrets.getRecords()) {
if (record.getRecordUid().equals(link.getRecordUid()) && "pamUser".equals(record.getType())) {
// Use SDK utility method to check admin status
boolean isAdmin = link.isAdminUser();
boolean isLaunchCredential = link.isLaunchCredential();
users.add(new UserStatus(record.getTitle(), record.getRecordUid(), isAdmin, isLaunchCredential));
System.out.println(" " + record.getTitle() + " - [" + (isAdmin ? "IS ADMIN" : "IS NOT ADMIN") + "] [" +
(isLaunchCredential ? "IS LAUNCH CREDENTIAL" : "IS NOT LAUNCH CREDENTIAL") + "]");
break;
}
}
}
// Validate PAM configuration (from your test logic)
long adminUsersCount = users.stream().filter(UserStatus::isAdmin).count();
long launchCredentialUsersCount = users.stream().filter(UserStatus::isLaunchCredential).count();
System.out.println("Summary:");
System.out.println(" Admin users: " + adminUsersCount);
System.out.println(" Launch credential users: " + launchCredentialUsersCount);
if (adminUsersCount > 1) {
System.out.println("Warning: Multiple admin users found - should typically be 1");
}
if (launchCredentialUsersCount > 1) {
System.out.println("Warning: Multiple launch credentials - should typically be 1");
}
}
} catch (Exception e) {
System.err.println("Error retrieving PAM machine users: " + e.getMessage());
}
return users;
}
public static class UserStatus {
private final String userName;
private final String userUid;
private final boolean isAdmin;
private final boolean isLaunchCredential;
public UserStatus(String userName, String userUid, boolean isAdmin, boolean isLaunchCredential) {
this.userName = userName;
this.userUid = userUid;
this.isAdmin = isAdmin;
this.isLaunchCredential = isLaunchCredential;
}
public String getUserName() { return userName; }
public String getUserUid() { return userUid; }
public boolean isAdmin() { return isAdmin; }
public boolean isLaunchCredential() { return isLaunchCredential; }
@Override
public String toString() {
return userName + " (" + userUid + ") - [" +
(isAdmin ? "IS ADMIN" : "IS NOT ADMIN") + "] [" +
(isLaunchCredential ? "IS LAUNCH CREDENTIAL" : "IS NOT LAUNCH CREDENTIAL") + "]";
}
}Comprehensive Linked Record Data Analysis
public static void analyzeLinkDataStructure(SecretsManagerOptions options) throws Exception {
QueryOptions queryOptions = new QueryOptions(
Collections.emptyList(),
Collections.emptyList(),
true
);
KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);
System.out.println("Link Data Structure Analysis:");
for (KeeperRecord record : secrets.getRecords()) {
if (record.getLinks() != null && !record.getLinks().isEmpty()) {
System.out.println("\nRecord: " + record.getTitle() + " (" + record.getType() + ")");
System.out.println("UID: " + record.getRecordUid());
for (int i = 0; i < record.getLinks().size(); i++) {
KeeperRecordLink link = record.getLinks().get(i);
System.out.println(" Link " + (i + 1) + ":");
System.out.println(" Target UID: " + link.getRecordUid());
System.out.println(" Path: " + (link.getPath() != null ? link.getPath() : "null"));
if (link.getData() != null) {
try {
if (link.hasEncryptedData() || link.mightBeEncrypted()) {
String decodedData = link.getDecryptedData(record.getRecordKey());
System.out.println(" Decoded Data: " + decodedData);
} else {
String decodedData = link.getDecodedData();
System.out.println(" Decoded Data: " + decodedData);
}
} catch (Exception e) {
System.out.println(" Raw Base64: " + link.getData());
System.out.println(" Decode Error: " + e.getMessage());
}
} else {
System.out.println(" Data: null");
}
}
}
}
}Complete Utility Methods Example
public static void demonstrateLinkUtilityMethods(SecretsManagerOptions options) throws Exception {
QueryOptions queryOptions = new QueryOptions(
Collections.emptyList(),
Collections.emptyList(),
true
);
KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);
System.out.println("Link Utility Methods Demo:");
for (KeeperRecord record : secrets.getRecords()) {
if (record.getLinks() != null && !record.getLinks().isEmpty()) {
System.out.println("\nRecord: " + record.getTitle() + " (" + record.getType() + ")");
for (int i = 0; i < record.getLinks().size(); i++) {
KeeperRecordLink link = record.getLinks().get(i);
System.out.println(" Link " + (i + 1) + " to " + link.getRecordUid() + ":");
System.out.println(" Path: " + (link.getPath() != null ? link.getPath() : "null"));
// User-related utilities
if (link.isAdminUser()) {
System.out.println(" Admin user");
}
if (link.isLaunchCredential()) {
System.out.println(" Launch credential");
}
// Permission utilities
if (link.allowsRotation()) {
System.out.println(" Allows rotation");
}
if (link.allowsConnections()) {
System.out.println(" Allows connections");
}
if (link.allowsPortForwards()) {
System.out.println(" Allows port forwards");
}
if (link.allowsSessionRecording()) {
System.out.println(" Allows session recording");
}
if (link.allowsTypescriptRecording()) {
System.out.println(" Allows typescript recording");
}
if (link.allowsRemoteBrowserIsolation()) {
System.out.println(" Allows remote browser isolation");
}
// Settings utilities
if (link.rotatesOnTermination()) {
System.out.println(" Rotates on termination");
}
Integer version = link.getLinkDataVersion();
if (version != null) {
System.out.println(" Data version: " + version);
}
if (link.hasReadableData()) {
System.out.println(" Has readable JSON data");
} else if (link.getData() != null) {
System.out.println(" Has encrypted/binary data");
} else {
System.out.println(" No data");
}
}
}
}
}Full Method Reference
getRecordUid()
String
Target record UID
getPath()
String
Link metadata type
getData()
String
Raw Base64-encoded link data
isAdminUser()
boolean
User has admin privileges
isLaunchCredential()
boolean
This is a launch credential
allowsRotation()
boolean
Password rotation allowed
allowsConnections()
boolean
Connections allowed
allowsPortForwards()
boolean
Port forwarding allowed
allowsSessionRecording()
boolean
Session recording enabled
allowsTypescriptRecording()
boolean
Typescript recording enabled
allowsRemoteBrowserIsolation()
boolean
Remote browser isolation allowed
rotatesOnTermination()
boolean
Password rotates on session termination
getLinkDataVersion()
Integer
Data format version number
hasReadableData()
boolean
Data is readable JSON format
hasEncryptedData()
boolean
Data is encrypted
mightBeEncrypted()
boolean
Data might be encrypted (heuristic check)
getDecryptedData(byte[])
String
Decrypt data using record key
getDecodedData()
String
Base64 decode without decryption
getLinkData(byte[])
Map<String, Object>
Generic encrypted data access
getAiSettingsData(byte[])
Map<String, Object>
AI settings specific access
getJitSettingsData(byte[])
Map<String, Object>
JIT settings specific access
getSettingsForPath(String, byte[])
Map<String, Object>
Generic settings access by path
DAG Concepts for Infrastructure Management
Understanding Directed Acyclic Graphs
GraphSync implements a Directed Acyclic Graph structure where:
DIRECTED: Links have direction (A → B is different from B → A)
ACYCLIC: No circular references (A → B → C → A is NOT allowed)
GRAPH: Records (nodes) connected by links (edges)
Benefits:
✅ Track dependencies ("this server needs this database")
✅ Organize related credentials
✅ Understand infrastructure relationships
✅ Maintain security boundaries
Performance Optimization
Efficient Processing Strategies
public class OptimizedGraphOperations {
// Strategy 1: Selective retrieval
public void processSpecificRecords(List<String> recordUids, SecretsManagerOptions options) throws Exception {
// Only get specific records with links to reduce bandwidth
QueryOptions filtered = new QueryOptions(
recordUids, // Only these records
Collections.emptyList(),
true
);
KeeperSecrets secrets = SecretsManager.getSecrets2(options, filtered);
// Process only what you need
}
// Strategy 2: Caching for multiple operations
private Map<String, KeeperRecord> recordCache = new HashMap<>();
private Map<String, List<KeeperRecord>> typeCache = new HashMap<>();
public void initializeCache(SecretsManagerOptions options) throws Exception {
QueryOptions queryOptions = new QueryOptions(null, null, true);
KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);
// Build efficient lookup structures
for (KeeperRecord record : secrets.getRecords()) {
recordCache.put(record.getRecordUid(), record);
typeCache.computeIfAbsent(record.getType(), k -> new ArrayList<>()).add(record);
}
}
public List<KeeperRecord> getPamMachines() {
return typeCache.getOrDefault("pamMachine", new ArrayList<>());
}
public KeeperRecord getRecord(String uid) {
return recordCache.get(uid);
}
}Error Handling Best Practices
public void robustLinkProcessing(SecretsManagerOptions options) {
try {
QueryOptions queryOptions = new QueryOptions(null, null, true);
KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);
for (KeeperRecord record : secrets.getRecords()) {
try {
List<KeeperRecordLink> links = record.getLinks();
// Handle null links (requestLinks was false)
if (links == null) {
System.err.println("Warning: Links not available for " + record.getTitle() +
" - ensure requestLinks=true in QueryOptions");
continue;
}
// Process each link safely
for (KeeperRecordLink link : links) {
try {
// Safely access encrypted data
if (link.hasEncryptedData()) {
Map<String, Object> data = link.getLinkData(record.getRecordKey());
if (data != null) {
// Successfully decrypted and processed
System.out.println("Processed encrypted data for " + link.getRecordUid());
} else {
System.out.println("Could not decrypt data for link to " + link.getRecordUid());
}
}
// Access link properties safely
boolean isAdmin = link.isAdminUser();
boolean allowsRotation = link.allowsRotation();
// ... other properties
} catch (Exception linkError) {
System.err.println("Error processing link to " + link.getRecordUid() + ": " + linkError.getMessage());
}
}
} catch (Exception recordError) {
System.err.println("Error processing record " + record.getRecordUid() + ": " + recordError.getMessage());
}
}
} catch (Exception e) {
System.err.println("Failed to retrieve records with links: " + e.getMessage());
e.printStackTrace();
}
}Important Implementation Notes
Critical Understanding
Links vs Files: The
linksToRemoveparameter in UpdateOptions removes FILES, not record linksNull vs Empty: Links field is
nullwhenrequestLinks=false, empty list whenrequestLinks=truebut no links existPerformance Impact: Requesting links significantly increases response size and processing time
Encryption: Link data may be encrypted and requires the record's key to decrypt
Security Considerations
Key Management: Always use the source record's key for decrypting link data
Access Control: Link properties indicate what operations are permitted
Validation: Always check link properties before performing operations
Best Practices from Test Implementation
Only request links when needed - Use
requestLinks=truejudiciously for performanceFilter records when possible - Use
recordsFilterto limit data retrievalCache results - Build lookup maps for multiple operations on the same data
Handle errors gracefully - Link data decryption and access may fail
Validate assumptions - Check link properties match expected permissions
Test configurations - Verify PAM setups have correct admin/launch credential counts
Kotlin Support
// Kotlin implementation with enhanced syntax
fun analyzeAdvancedGraphRelationships(options: SecretsManagerOptions) {
val queryOptions = QueryOptions(
recordsFilter = emptyList(),
foldersFilter = emptyList(),
requestLinks = true
)
val secrets = getSecrets2(options, queryOptions)
secrets.records.forEach { record ->
record.links?.forEach { link ->
println("${record.title} → ${link.recordUid}")
// Advanced property checking with Kotlin's concise syntax
when {
link.isAdminUser() -> println(" Admin privileges")
link.isLaunchCredential() -> println(" Launch credential")
link.allowsRotation() -> println(" Rotation allowed")
link.allowsConnections() -> println(" Connections allowed")
link.allowsSessionRecording() -> println(" Recording enabled")
}
// Settings data access with safe calls
when (link.path) {
"ai_settings" -> {
link.getAiSettingsData(record.recordKey)?.let { aiData ->
println(" AI: ${aiData["aiEnabled"]} - Model: ${aiData["aiModel"]}")
}
}
"jit_settings" -> {
link.getJitSettingsData(record.recordKey)?.let { jitData ->
println(" JIT: ${jitData["enabled"]} - TTL: ${jitData["ttl"]}s")
}
}
}
// Generic encrypted data access
link.getLinkData(record.recordKey)?.let { data ->
data.forEach { (key, value) ->
println(" $key: $value")
}
}
}
}
}Last updated
Was this helpful?

