# PAMレコードのリンク済み認証情報

**必要なSDKバージョン:** 17.1.1以上

このドキュメントでは、PAMリソースレコードのリンク済み認証情報を操作する際に利用できるプロパティメソッドの一覧と、高度な活用事例を紹介しています。

* [すべてのプロパティメソッド](#subetenopuropatimesoddo)
* [高度な活用事例](#na)
* [メソッド完全リファレンス](#mesoddorifarensu)

## すべてのプロパティメソッド

```java
for (KeeperRecordLink link : record.getLinks()) {
    // 基本プロパティ
    String targetUid = link.getRecordUid();
    String linkPath = link.getPath(); // 例: "pamUser", "ai_settings", "jit_settings"
    String rawData = link.getData(); // Base64エンコードされたデータ

    // ユーザー権限メソッド
    boolean isAdmin = link.isAdminUser();
    boolean isLaunchCredential = link.isLaunchCredential();

    // 権限メソッド
    boolean allowsRotation = link.allowsRotation();
    boolean allowsConnections = link.allowsConnections();
    boolean allowsPortForwards = link.allowsPortForwards();
    boolean allowsSessionRecording = link.allowsSessionRecording();
    boolean allowsTypescriptRecording = link.allowsTypescriptRecording();
    boolean allowsRemoteBrowserIsolation = link.allowsRemoteBrowserIsolation();

    // 設定関連メソッド
    boolean rotatesOnTermination = link.rotatesOnTermination();
    Integer dataVersion = link.getLinkDataVersion();

    // データ解析メソッド
    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);
}
```

***

## 高度な活用事例

### 高度なデータアクセスパターン

#### 暗号化データの処理

```java
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"));

            // 暗号化状態を確認
            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) {
                    // 方法1: getDecryptedDataを使用して暗号化コンテンツを復号
                    String decryptedData = link.getDecryptedData(record.getRecordKey());
                    System.out.println("    Decrypted Data: " + decryptedData);

                    // 方法2: getLinkDataを使用して構造化データにアクセス
                    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 {
                    // プレーンなBase64データ
                    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());
            }
        }
    }
}
```

### 設定データ関連メソッドのデモ

```java
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 + ")");

                // AI設定メソッドをテスト
                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());
                    }
                }

                // JIT設定メソッドをテスト
                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());
                    }
                }

                // 一般的な getSettingsForPath メソッドをテスト
                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");
    }
}
```

### 複雑な関係性の分析 (DAG解析)

```java
public class ComprehensiveDagAnalyzer {

    public static void analyzeLinkingPatterns(SecretsManagerOptions options) throws Exception {
        QueryOptions queryOptions = new QueryOptions(
            Collections.emptyList(),
            Collections.emptyList(),
            true // リンクを取得
        );

        KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);

        // 関係マップを構築
        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);

        // 参照対象 (被リンク) になっているレコードを抽出
        Set<String> targets = new HashSet<>();
        for (List<String> linkTargets : relationshipMap.values()) {
            targets.addAll(linkTargets);
        }
        System.out.println("Records that are link targets: " + targets.size());

        // 詳細な関係パターンを表示
        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.");
        }
    }
}
```

### 高度なPAMユーザー管理

```java
public static List<UserStatus> getPamMachineUsers(String pamMachineUid, InMemoryStorage storage) {
    List<UserStatus> users = new ArrayList<>();

    try {
        QueryOptions queryOptions = new QueryOptions(
            Collections.emptyList(),
            Collections.emptyList(),
            true // リンクを取得
        );

        SecretsManagerOptions options = new SecretsManagerOptions(storage);
        KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);

        // PAMマシンレコードを検索
        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("このPAMマシンに関連付けられたユーザー数: " + pamMachine.getLinks().size());

            for (KeeperRecordLink link : pamMachine.getLinks()) {
                // リンクされたユーザーレコードを検索
                for (KeeperRecord record : secrets.getRecords()) {
                    if (record.getRecordUid().equals(link.getRecordUid()) && "pamUser".equals(record.getType())) {
                        // SDKユーティリティメソッドで管理者ステータスを確認
                        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;
                    }
                }
            }

            // PAM構成を検証（テストロジックに基づく）
            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: 複数の管理者ユーザーが検出されました（通常は1人であるべきです）");
            }
            if (launchCredentialUsersCount > 1) {
                System.out.println("Warning: 複数の起動用認証情報が検出されました（通常は1人であるべきです）");
            }
        }

    } catch (Exception e) {
        System.err.println("PAMマシンユーザーの取得中にエラーが発生しました: " + 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") + "]";
    }
}
```

### 包括的なリンク済みレコードデータ分析

```java
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");
                }
            }
        }
    }
}
```

### ユーティリティメソッドの完全な例

```java
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"));

                // ユーザー関連ユーティリティ
                if (link.isAdminUser()) {
                    System.out.println("     Admin user");
                }
                if (link.isLaunchCredential()) {
                    System.out.println("     Launch credential");
                }

                // 権限関連ユーティリティ
                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");
                }

                // 設定関連ユーティリティ
                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");
                }
            }
        }
    }
}
```

### メソッド完全リファレンス

<table><thead><tr><th>メソッド</th><th width="145.4375">戻り値</th><th>説明</th></tr></thead><tbody><tr><td><code>getRecordUid()</code></td><td><code>String</code></td><td>対象レコードのUID</td></tr><tr><td><code>getPath()</code></td><td><code>String</code></td><td>リンクメタデータのタイプ</td></tr><tr><td><code>getData()</code></td><td><code>String</code></td><td>Base64エンコードされた生データ</td></tr><tr><td><code>isAdminUser()</code></td><td><code>boolean</code></td><td>ユーザーが管理者権限を持つか</td></tr><tr><td><code>isLaunchCredential()</code></td><td><code>boolean</code></td><td>起動用認証情報かどうか</td></tr><tr><td><code>allowsRotation()</code></td><td><code>boolean</code></td><td>パスワードローテーションが許可されているか</td></tr><tr><td><code>allowsConnections()</code></td><td><code>boolean</code></td><td>接続が許可されているか</td></tr><tr><td><code>allowsPortForwards()</code></td><td><code>boolean</code></td><td>ポートフォワーディングが許可されているか</td></tr><tr><td><code>allowsSessionRecording()</code></td><td><code>boolean</code></td><td>セッション録画が有効か</td></tr><tr><td><code>allowsTypescriptRecording()</code></td><td><code>boolean</code></td><td>Typescript録画が有効か</td></tr><tr><td><code>allowsRemoteBrowserIsolation()</code></td><td><code>boolean</code></td><td>リモートブラウザ分離が許可されているか</td></tr><tr><td><code>rotatesOnTermination()</code></td><td><code>boolean</code></td><td>セッション終了時にパスワードがローテーションされるか</td></tr><tr><td><code>getLinkDataVersion()</code></td><td><code>Integer</code></td><td>データ形式のバージョン番号</td></tr><tr><td><code>hasReadableData()</code></td><td><code>boolean</code></td><td>データが可読なJSON形式か</td></tr><tr><td><code>hasEncryptedData()</code></td><td>boolean</td><td>データが暗号化されているか</td></tr><tr><td><code>mightBeEncrypted()</code></td><td><code>boolean</code></td><td>データが暗号化されている可能性があるか (ヒューリスティック検出)</td></tr><tr><td><code>getDecryptedData(byte[])</code></td><td><code>String</code></td><td>レコードキーを使ってデータを復号</td></tr><tr><td><code>getDecodedData()</code></td><td><code>String</code></td><td>暗号化なしでBase64デコード</td></tr><tr><td><code>getLinkData(byte[])</code></td><td><code>Map&#x3C;String, Object></code></td><td>汎用的な暗号化データへのアクセス</td></tr><tr><td><code>getAiSettingsData(byte[])</code></td><td><code>Map&#x3C;String, Object></code></td><td>AI設定専用のアクセス</td></tr><tr><td><code>getJitSettingsData(byte[])</code></td><td><code>Map&#x3C;String, Object></code></td><td>JIT設定専用のアクセス</td></tr><tr><td><code>getSettingsForPath(String, byte[])</code></td><td><code>Map&#x3C;String, Object></code></td><td>パス指定による汎用的な設定アクセス</td></tr></tbody></table>

### インフラ管理のためのDAG概念

#### 有向非巡回グラフ (Directed Acyclic Graphs) の理解

GraphSyncは有向非巡回グラフ (DAG) 構造を実装しています。

* **有向 (DIRECTED):** リンクには方向があります (A → BはB → Aと同じではありません)
* **非巡回 (ACYCLIC):** 循環参照は存在しません (A → B → C → Aは許可されません)
* **グラフ (GRAPH):** レコード (ノード) がリンク (エッジ) で接続されています

#### 利点

* ✅ 依存関係の追跡 (「このサーバーはこのデータベースを必要とする」)
* ✅ 関連する認証情報の整理
* ✅ インフラの関係性を把握
* ✅ セキュリティ境界の維持

***

### パフォーマンス最適化

#### 効率的な処理戦略

```java
public class OptimizedGraphOperations {

    // 戦略1: 選択的な取得
    public void processSpecificRecords(List<String> recordUids, SecretsManagerOptions options) throws Exception {
        // 特定のレコードのみリンク付きで取得し、帯域幅を削減
        QueryOptions filtered = new QueryOptions(
            recordUids,  // このレコードだけ
            Collections.emptyList(),
            true
        );

        KeeperSecrets secrets = SecretsManager.getSecrets2(options, filtered);
        // 必要なものだけを処理
    }

    // 戦略2: 複数の操作に備えたキャッシュ
    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);

        // 効率的な検索構造を構築
        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);
    }
}
```

### エラーハンドリングのベストプラクティス

```java
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();

                // nullリンクへの対応（requestLinks=false の場合）
                if (links == null) {
                    System.err.println("Warning: Links not available for " + record.getTitle() +
                                     " - ensure requestLinks=true in QueryOptions");
                    continue;
                }

                // 各リンクを安全に処理
                for (KeeperRecordLink link : links) {
                    try {
                        // 暗号化データへ安全にアクセス
                        if (link.hasEncryptedData()) {
                            Map<String, Object> data = link.getLinkData(record.getRecordKey());
                            if (data != null) {
                                // 復号と処理に成功
                                System.out.println("Processed encrypted data for " + link.getRecordUid());
                            } else {
                                System.out.println("Could not decrypt data for link to " + link.getRecordUid());
                            }
                        }

                        // リンクのプロパティへ安全にアクセス
                        boolean isAdmin = link.isAdminUser();
                        boolean allowsRotation = link.allowsRotation();
                        // ... 他のプロパティ

                    } 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();
    }
}
```

### 実装上の重要な注意点

#### 重要な理解事項

* **リンクとファイルの違い:** `UpdateOptions` の `linksToRemove` パラメータは**レコードリンクではなくファイル**を削除します
* **Nullと空の違い:** `requestLinks=false` の場合、`links` フィールドは**null**。`requestLinks=true` でもリンクが存在しない場合は**空リスト**になります
* **パフォーマンスへの影響:** リンクをリクエストするとレスポンスサイズと処理時間が大幅に増加します
* **暗号化:** リンクデータは暗号化されている可能性があり、復号にはレコードキーが必要です

#### セキュリティ上の考慮事項

* **キー管理:** リンクデータを復号する際は、常にソースレコードのキーを使用すること
* **アクセス制御:** リンクのプロパティは、どの操作が許可されているかを示します
* **検証:** 操作を行う前に必ずリンクプロパティを確認すること

#### テスト実装からのベストプラクティス

* **必要なときのみリンクをリクエスト:** パフォーマンスのために `requestLinks=true` は慎重に使用する
* **可能な限りレコードをフィルタ:** `recordsFilter` を利用してデータ取得を制限する
* **結果をキャッシュ:** 同じデータに対して複数の操作を行う場合はルックアップマップを構築する
* **エラーを適切に処理:** リンクデータの復号やアクセスは失敗する可能性がある
* **要件を検証:** リンクプロパティが期待どおりの権限を持っているか確認する
* **構成をテスト:** PAMのセットアップで管理者や起動用認証情報の数が正しいことを検証する

### Kotlin対応

```kotlin
// Kotlin実装（構文を強化）
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}")
            
            // Kotlinの簡潔な構文を使った高度なプロパティチェック
            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")
            }
            
            // セーフコールを用いた設定データアクセス
            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")
                    }
                }
            }
            
            // 汎用的な暗号化データアクセス
            link.getLinkData(record.recordKey)?.let { data ->
                data.forEach { (key, value) ->
                    println("  $key: $value")
                }
            }
        }
    }
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.keeper.io/keeperpam/jp/secrets-manager/developer-sdk-library/java-sdk/linked-credentials-on-pam-records.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
