[アップデート] Amazon Bedrock Managed Knowledge Baseでドキュメントのアクセス権を調べるAPIが追加されたので試してみた

[アップデート] Amazon Bedrock Managed Knowledge Baseでドキュメントのアクセス権を調べるAPIが追加されたので試してみた

Amazon Bedrock Managed Knowledge Base に追加された、ドキュメントのアクセス権を確認できる新しいAPIを試してみました!ちょっとニッチなアップデートですね・・・!
2026.09.14

はじめに

こんにちは、スーパーマーケットが好きなコンサル部の神野です。

2026年9月9日、Amazon Bedrock Managed Knowledge Baseにドキュメントのアクセス権を確認できるAPIが追加されました!

https://aws.amazon.com/jp/about-aws/whats-new/2026/09/amazon-bedrock-knowledge-base-debugging-document-access-control/

Managed Knowledge Baseで、ACL(Access Control List)を使ってユーザーごとに検索できるドキュメントを分ける機能は、以前の記事でも試していますが今回はこのACL周りでアップデートがありました。

https://dev.classmethod.jp/articles/bedrock-managed-knowledge-base-retrieve-agentic-retrieval/

まずは追加されたAPIを紹介しつつ、実際に動かして試してみたいと思います!

アップデートで追加された2つのAPI

今回追加されたAPI(CheckIngestedDocumentAcl、GetIngestedDocumentAcl)は、ACLでドキュメントごとの閲覧制限をかけている環境で、権限まわりのトラブルシューティングをするときに役立ちます。

API 確認できること
CheckIngestedDocumentAcl 指定したユーザーが、そのドキュメントにアクセスできるか。hasAccessに真偽値が返却される
GetIngestedDocumentAcl そのドキュメントに取り込まれたACL。許可・拒否に含まれるユーザーなどが返却される

このAPIの利用シーンとしては、社内のVPN手順や各種申請ルールをManaged Knowledge Baseで検索できるようにしていて、ユーザーごとに閲覧できるドキュメントを分けているケースを考えてみます。

例えば、佐藤さんから「田中さんと同じように検索しているのに、自分だけVPN手順が出てこない」と問い合わせが来たとしましょう。
ドキュメント自体が取り込まれていないのか、検索クエリの揺らぎなのか、それとも佐藤さんの閲覧権限が原因なのか、検索結果が0件というだけでは管理者側でもパッと判断がつきにくいですよね。

今回のAPIは、まさにこの「アクセス権の設定」をピンポイントで調べるためのものです。対象のドキュメントIDと佐藤さんの情報を指定すれば、閲覧が許可されているか、どんな許可・拒否ルールが取り込まれているかを直接確認できます。

2つのAPIでユーザーのアクセス可否と取り込み済みACLを調べる

どんな場面で活用するか

公式ドキュメントでは、管理者によるアクセス権の調査や監査用途として紹介されています。

社内向けの運用ツールに組み込んで、ドキュメントIDと対象ユーザーを入力したらこの2つのAPIを呼んでアクセス判定とACLを一覧表示してくれるような仕組みを作る、もしくはコンソールから直接確認することで、問い合わせ対応に使えそうですね。

ちょっとニッチなアップデートな気もしますが、特定のケースで効果を発揮しそうだなと思いました。

早速試してみる

バージニア北部(us-east-1)に、今回の検証用 Managed Knowledge Base を作成していきます。

ドキュメント ID を vpn-guide とし、架空の社内 VPN 手順として次のテキストを取り込みます。

取り込むドキュメント
ハナミズキ社のVPN接続手順。VPNエラーHANA-042が発生したら、社内IT窓口の内線8420へ連絡してください。受付時間は平日9時から17時です。

このドキュメントに対して、ユーザーごとに次のような ACL を設定してみます。

ユーザー 検証で使うユーザーID ドキュメントへ設定するACL
田中さん tanaka@example.com ALLOW
佐藤さん sato@example.com DENY
鈴木さん suzuki@example.com ALLOWとDENYの両方
高橋さん takahashi@example.com 登録しない

通常の ALLOW / DENY に加えて、両方が重複して設定されたケースや未登録ユーザーの挙動も確認していきます。

カスタムデータソース側では aclEnabledtrue に設定しておき、ドキュメントの取り込み時に metadata.accessControlList を指定します。該当するパラメーターの指定内容は以下のとおりです。

ACLの設定
# データソースのconnectorParameters
{
    "type": "CUSTOM",
    "version": "1",
    "aclEnabled": True,
}

# 取り込むドキュメントのmetadata.accessControlList
[
    {"name": "tanaka@example.com", "type": "USER", "access": "ALLOW"},
    {"name": "sato@example.com", "type": "USER", "access": "DENY"},
    {"name": "suzuki@example.com", "type": "USER", "access": "ALLOW"},
    {"name": "suzuki@example.com", "type": "USER", "access": "DENY"},
]

設定パラメーターの詳細については、公式ドキュメントもあわせて確認してみてください。

https://docs.aws.amazon.com/bedrock/latest/userguide/kb-managed-ds-custom-acl.html

検証環境を用意する

今回の検証で使ったバージョンは Python 3.14.6、Boto3 / Botocore 1.43.91 です。

まずはプロジェクト用のディレクトリを作成してセットアップします。

環境のセットアップ
mkdir kb-acl-debug
cd kb-acl-debug
uv init --bare --no-workspace --python 3.14
uv add boto3==1.43.91

続いて、以下のコードを demo.py として保存します。環境作成からドキュメントの取り込み、ACL と検索結果の確認、後片付けまでをサブコマンド形式で実行できるようにまとめています。

demo.pyの全体
demo.py
import json
import sys
import time
from pathlib import Path
import boto3

REGION = "us-east-1"
STATE = Path("state.json")
agent = boto3.client("bedrock-agent", region_name=REGION)
runtime = boto3.client("bedrock-agent-runtime", region_name=REGION)
iam = boto3.client("iam", region_name=REGION)

def save(name, data):
    Path("evidence").mkdir(exist_ok=True)
    Path("evidence", name + ".json").write_text(
        json.dumps(data, ensure_ascii=False, indent=2, default=str)
    )

def setup():
    if STATE.exists():
        raise RuntimeError("state.json already exists")
    account = boto3.client("sts").get_caller_identity()["Account"]
    name = "kb-acl-debug-" + str(int(time.time()))
    trust = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {"Service": "bedrock.amazonaws.com"},
                "Action": "sts:AssumeRole",
                "Condition": {
                    "StringEquals": {"aws:SourceAccount": account},
                    "ArnLike": {
                        "aws:SourceArn": f"arn:aws:bedrock:{REGION}:{account}:knowledge-base/*"
                    },
                },
            }
        ],
    }
    role = iam.create_role(RoleName=name, AssumeRolePolicyDocument=json.dumps(trust))[
        "Role"
    ]
    state = {"roleName": name}
    STATE.write_text(json.dumps(state))
    time.sleep(15)
    kb = agent.create_knowledge_base(
        name=name,
        roleArn=role["Arn"],
        knowledgeBaseConfiguration={
            "type": "MANAGED",
            "managedKnowledgeBaseConfiguration": {"embeddingModelType": "MANAGED"},
        },
    )["knowledgeBase"]
    state["knowledgeBaseId"] = kb["knowledgeBaseId"]
    STATE.write_text(json.dumps(state))
    save("created-kb", kb)
    while (
        agent.get_knowledge_base(knowledgeBaseId=state["knowledgeBaseId"])[
            "knowledgeBase"
        ]["status"]
        != "ACTIVE"
    ):
        time.sleep(10)
    ds = agent.create_data_source(
        knowledgeBaseId=state["knowledgeBaseId"],
        name="custom-acl",
        dataSourceConfiguration={
            "type": "MANAGED_KNOWLEDGE_BASE_CONNECTOR",
            "managedKnowledgeBaseConnectorConfiguration": {
                "connectorParameters": {
                    "type": "CUSTOM",
                    "version": "1",
                    "aclEnabled": True,
                }
            },
        },
    )["dataSource"]
    state["dataSourceId"] = ds["dataSourceId"]
    STATE.write_text(json.dumps(state))
    save("created-ds", ds)
    print(json.dumps(state))

def ingest():
    state = json.loads(STATE.read_text())
    acl = [
        {"name": u + "@example.com", "type": "USER", "access": a}
        for u, a in [
            ("tanaka", "ALLOW"),
            ("sato", "DENY"),
            ("suzuki", "ALLOW"),
            ("suzuki", "DENY"),
        ]
    ]
    document = {
        "content": {
            "dataSourceType": "CUSTOM",
            "custom": {
                "customDocumentIdentifier": {"id": "vpn-guide"},
                "sourceType": "IN_LINE",
                "inlineContent": {
                    "type": "TEXT",
                    "textContent": {
                        "data": "ハナミズキ社のVPN接続手順。VPNエラーHANA-042が発生したら、社内IT窓口の内線8420へ連絡してください。受付時間は平日9時から17時です。"
                    },
                },
            },
        },
        "metadata": {
            "type": "IN_LINE_ATTRIBUTE",
            "inlineAttributes": [
                {"key": "department", "value": {"type": "STRING", "stringValue": "IT"}}
            ],
            "accessControlList": acl,
        },
    }
    params = {k: state[k] for k in ["knowledgeBaseId", "dataSourceId"]}
    save("ingest-input", document)
    result = agent.ingest_knowledge_base_documents(**params, documents=[document])
    save("ingest", result)
    print(json.dumps(result, default=str))

def check():
    state = json.loads(STATE.read_text())
    params = {k: state[k] for k in ["knowledgeBaseId", "dataSourceId"]}
    for _ in range(60):
        status = agent.get_knowledge_base_documents(
            **params,
            documentIdentifiers=[
                {"dataSourceType": "CUSTOM", "custom": {"id": "vpn-guide"}}
            ],
        )
        save("document-status", status)
        current = status["documentDetails"][0]["status"]
        print("status", current, flush=True)
        if current == "INDEXED":
            break
        if current in (
            "FAILED",
            "IGNORED",
            "PARTIALLY_INDEXED",
            "METADATA_UPDATE_FAILED",
        ):
            raise RuntimeError(status["documentDetails"])
        time.sleep(10)
    else:
        raise RuntimeError("Document indexing timed out")
    params["documentId"] = "vpn-guide"
    acl = runtime.get_ingested_document_acl(**params)
    save("acl", acl)
    print("acl", json.dumps(acl["documentAcl"]))
    for user in ["tanaka", "sato", "suzuki", "takahashi"]:
        context = {"userId": user + "@example.com"}
        result = runtime.check_ingested_document_acl(**params, userContext=context)
        save("check-" + user, result)
        retrieval = runtime.retrieve(
            knowledgeBaseId=state["knowledgeBaseId"],
            retrievalQuery={"text": "VPNエラーHANA-042の問い合わせ先は?"},
            userContext=context,
        )
        save("retrieve-" + user, retrieval)
        print(
            user,
            "hasAccess=",
            result["hasAccess"],
            "results=",
            len(retrieval["retrievalResults"]),
        )

def cleanup():
    state = json.loads(STATE.read_text())
    if "knowledgeBaseId" in state:
        agent.delete_knowledge_base(knowledgeBaseId=state["knowledgeBaseId"])
        for _ in range(180):
            try:
                agent.get_knowledge_base(knowledgeBaseId=state["knowledgeBaseId"])
            except agent.exceptions.ResourceNotFoundException:
                break
            time.sleep(5)
        else:
            raise RuntimeError("Knowledge Base deletion timed out")
    iam.delete_role(RoleName=state["roleName"])
    save("cleanup", {"knowledgeBaseDeleted": True, "roleDeleted": True})
    STATE.unlink()
    print("Knowledge Base and IAM role deleted")

if __name__ == "__main__":
    {"setup": setup, "ingest": ingest, "check": check, "cleanup": cleanup}[
        sys.argv[1]
    ]()

スクリプトを使って検証用のリソースを作成し、ドキュメントを取り込みます。

リソースの作成とドキュメントの取り込み
uv run demo.py setup
uv run demo.py ingest

ここまで完了し、次のコマンドを実行すると、ドキュメントのステータスが INDEXED になるのを待ってから ACL と検索結果の確認に進みます。

ACLと検索結果の確認
uv run demo.py check
uv run demo.py check の実行結果

検証時に保存したレスポンスを、スクリプトの出力形式で再表示しています。

実行結果
status INDEXED
acl {"allowList": {"conditions": [{"conditionOperator": "OR", "users": [{"id": "tanaka@example.com", "type": "KNOWLEDGE_BASE"}]}], "memberRelation": "AND"}, "denyList": {"conditions": [{"conditionOperator": "OR", "users": [{"id": "sato@example.com", "type": "KNOWLEDGE_BASE"}, {"id": "suzuki@example.com", "type": "KNOWLEDGE_BASE"}]}], "memberRelation": "AND"}}
tanaka hasAccess= True results= 1
sato hasAccess= False results= 0
suzuki hasAccess= False results= 0
takahashi hasAccess= False results= 0

この実行結果を深掘りしていきます!

佐藤さんにドキュメントが出ない理由を調べる

先ほどのスクリプトで、同じドキュメントに対して、4人のユーザーでアクセス権の有無と検索結果を確かめてみました。
検索クエリには「VPNエラーHANA-042の問い合わせ先は?」を指定しています。

4人のACL判定と検索結果

田中さんにはVPN手順が1件返ってきて、本文の結果もしっかり取れていますね!
一方で佐藤さんには、同じ質問を投げてもドキュメントが返ってきません。

ここで、佐藤さんがそのドキュメントへのアクセス権を持っているかどうかを直接確かめてみます。CheckIngestedDocumentAcl API にドキュメントIDと佐藤さんのメールアドレスを渡して実行します。

demo.py(アクセス判定の抜粋)
result = runtime.check_ingested_document_acl(
    knowledgeBaseId=state['knowledgeBaseId'],
    dataSourceId=state['dataSourceId'],
    documentId='vpn-guide',
    userContext={'userId': 'sato@example.com'},
)
print(result['hasAccess'])
実行結果
False

佐藤さんの判定結果はしっかり False になっていました。取り込み時に設定したACLの通り、佐藤さんからのアクセスがブロックされていることが確認できますね。

取り込まれたACLで拒否の設定を確認する

続いて、なぜアクセスが拒否されたのか実際の中身を確かめてみます。GetIngestedDocumentAcl に Knowledge Base ID、データソースID、ドキュメントID を渡すことで、そのドキュメントに取り込まれている ACL をごそっと取得できます。

demo.py(ACL取得の抜粋)
acl = runtime.get_ingested_document_acl(
    knowledgeBaseId=state['knowledgeBaseId'],
    dataSourceId=state['dataSourceId'],
    documentId='vpn-guide',
)
print(json.dumps(acl['documentAcl'], indent=2))

返ってきたレスポンスから documentAcl の部分を抜粋してみます。

実行結果(documentAcl)
{
  "allowList": {
    "conditions": [
      {
        "conditionOperator": "OR",
        "users": [
          {
            "id": "tanaka@example.com",
            "type": "KNOWLEDGE_BASE"
          }
        ]
      }
    ],
    "memberRelation": "AND"
  },
  "denyList": {
    "conditions": [
      {
        "conditionOperator": "OR",
        "users": [
          {
            "id": "sato@example.com",
            "type": "KNOWLEDGE_BASE"
          },
          {
            "id": "suzuki@example.com",
            "type": "KNOWLEDGE_BASE"
          }
        ]
      }
    ],
    "memberRelation": "AND"
  }
}

拒否条件である denyList 側にしっかり佐藤さんが入っていますね! ドキュメント側で佐藤さんを拒否する設定がきちんと取り込まれていることが分かります。

本番運用でも、ドキュメントが見つからないといった問い合わせ時の原因調査や、登録直後にアクセス制御が想定どおり反映されているかサッと確かめたい場面で重宝しそうです。

コンソールからも確認してみる

ここまでの内容はマネジメントコンソールからも確認できます。ナレッジベースのデータソース詳細画面を開くと、「Document access control」という項目が用意されています。

まず「Check document access」を試してみます。Document ID に vpn-guide、User email に佐藤さんの検証用IDを入力して「Check access」をクリックすると、アクセス権がない旨が表示されました。

コンソールのアクセス判定:佐藤さんは閲覧不可

田中さんの検証用IDに切り替えて実行してみると、今度はアクセス権があると表示されます。先ほど API 経由で確認した通りの結果ですね。

コンソールのアクセス判定:田中さんは閲覧可能

また、「Get document access list」を開き、Document ID を入力して「Get access list」を実行すると、ACL の一覧を取得できます。結果を見ると田中さんが Allow、佐藤さんと鈴木さんが Deny になっていました。

コンソールのACL一覧:田中さんはAllow、佐藤さんと鈴木さんはDeny

調査用のコードを書かなくても、コンソールから手軽に各ユーザーの許可・拒否を確かめられるのは便利な気がします!

調べた結果をどう使う?

調査で得られた判定結果をもとに、ドキュメントの共有方針と突き合わせながら次のアクションを決めていくイメージを膨らませてみましょう。

確認したこと 次に行うこと
アクセス不可で、権限の設定も意図どおり 閲覧対象外のドキュメントであることを利用者へ説明する。閲覧が必要なら、ドキュメントの管理者へ権限変更を相談する
アクセス不可だが、本来は閲覧できるはず ユーザーIDの指定、許可への登録漏れ、意図しない拒否を確認し、誤っている設定を修正する
取り込み済みACLではアクセス可 実際の検索にも同じユーザー情報が渡っているかを確認し、検索条件などを調べる

今回は検証用に佐藤さんのアクセスをわざと拒否させましたが、実際の運用で「本当は佐藤さんにも見せるべきドキュメントなのに誤って拒否してしまっていた」と分かった場合は、カスタムデータソースへ渡すACLを修正したうえで、同じドキュメントIDで再取り込みすることになります。データが反映されたら、アクセス判定と検索の両方を改めて確認するイメージです。

後片付け

最後に、今回使った検証用のKnowledge BaseとIAMロールも忘れずに削除しておきます!

後片付け
uv run demo.py cleanup

おわりに

ドキュメントIDと対象ユーザーのIDさえ分かれば、検索クエリをあれこれ試行錯誤する前にサクッと原因の切り分けができるアップデートでした!ちょっとニッチですが、厳密に権限を振り分けたいケースやプレチェックしておきたい場合などに活用できるのではないでしょうか・・・!

本記事が少しでも参考になりましたら幸いです!最後までご覧いただきありがとうございましたー!

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事