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

Method
Returns
Description

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 linksToRemove parameter in UpdateOptions removes FILES, not record links

  • Null vs Empty: Links field is null when requestLinks=false, empty list when requestLinks=true but no links exist

  • Performance 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

  1. Only request links when needed - Use requestLinks=true judiciously for performance

  2. Filter records when possible - Use recordsFilter to limit data retrieval

  3. Cache results - Build lookup maps for multiple operations on the same data

  4. Handle errors gracefully - Link data decryption and access may fail

  5. Validate assumptions - Check link properties match expected permissions

  6. 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?