👋 はじめに
社内向けのRAGを作るとき一番最初に上司から飛んでくる質問
「で、経営陣しか見ちゃダメな資料はどうすんの?」
これです。100%これ。検索精度がどうとかチャンクサイズがどうとかそういう楽しい話をする前に権限の話が来ます。
そんな悩みに応えるべくAmazon BedrockManagedKnowledgeBase(以下ManagedKB)にはACL-aware retrievalという機能があります。ドキュメントごとにアクセス権を持たせて検索時にユーザー情報を渡すとそのユーザーが見ていいものだけ返ってくる。これで完璧ですね。...で、意気揚々と公式ドキュメントを開いたわけですが
Typeの説明にはこう書いてありました。
Must be
USER.
ん?……USERのみ?![]()
つまり現時点のManagedKBのACLはユーザー(メールアドレス)単位でしか権限を指定できません。「営業部」のような部署単位で制御する仕組みは用意されていないようです。
うちは部署単位で管理したいんですけどぉ...営業部の全員のメアドを文書ごとに列挙しろってこと?異動のたびに書き直すの?
というわけでユーザー単位の仕組みしかないManagedKBで部署単位の権限管理をどう実現するかを考えてみました。
※この記事は「S3に置いた文書をManagedKBで使いたい」という(たぶん一番よくある)ケースを前提としております
🤔 そもそもManagedKBのACLって何をしてるの?
ドキュメントを取り込むときこういう情報を一緒に渡します
"metadata": {
"type": "IN_LINE_ATTRIBUTE",
"accessControlList": [
{ "name": "alice@example.com", "type": "USER", "access": "ALLOW" }
]
}
そして検索時はこのようなパラメータで検索しに行きます
{
"knowledgeBaseId": "KB12345678",
"retrievalQuery": { "text": "有給の繰越は?" },
"userContext": { "userId": "alice@example.com" }
}
一言で言うと文書ごとに「見てOKな人の名簿」を貼っておいて検索時に名乗った名前が名簿にある文書だけを返す仕組みです。上のJSONだとaccessControlListが名簿でuserContext.userIdが名乗りにあたります。突き合わせは文字列の完全一致です。
💡 案1 nameに部署IDを突っ込む
TypeはUSER固定。でもよく考えてみるとマッチングは単なる文字列の完全一致です。
ということはnameにメアドじゃなくて部署IDを書いて、検索時にuserIdとして部署IDを渡せば部署単位の制御になります。実際に試したところ、取り込みも検索も問題なく動きました![]()
まず取込み側。文書をManagedKBに取り込むとき、文書と一緒にACLをメタデータとして渡します。下のJSONはCUSTOMデータソースで使うIngestKnowledgeBaseDocuments APIのリクエストの一部です。S3データソースなら同じ内容を.metadata.jsonとしてS3に置きます。nameにメールアドレスではなく部署IDを入れているのがポイントです。
"accessControlList": [
{ "name": "grp-sales", "type": "USER", "access": "ALLOW" }
]
次に検索側。アプリがRetrieve APIを呼ぶとき、リクエストのuserContextにログインユーザーの所属部署IDを入れます。(本来はメールアドレスを渡す場所です)
"userContext": { "userId": "grp-sales" }
ManagedKBはこのuserIdの文字列とACLのnameを突き合わせて一致した文書だけを返します。逆にuserIdを渡し忘れると0件になります。これで営業部の人には見えてシステム部の人(grp-it)には見えないACLができました。
※ACLの細かい設定はここでは省きます。公式ドキュメントを見てください。
🏢 文書側は複数の部署を並べられる
エントリは1つだけでなく複数書けてORで効きます。つまり1つのファイルに複数の部署IDを並べられます。
社内文書ってきれいに1部署で閉じてないんですよね。予算関連は経営管理部と各事業部が見る、就業規則は全社が見る、みたいな。そういう「またがり」を表現するのにALLOWを並べるだけでいいのは素直で助かります。
"accessControlList": [
{ "name": "grp-sales", "type": "USER", "access": "ALLOW" },
{ "name": "grp-finance", "type": "USER", "access": "ALLOW" },
{ "name": "grp-it", "type": "USER", "access": "ALLOW" }
]
注意したいのはACLを何も付けなかった文書が「誰でも見える」にはならないことです。ACLを有効にしたデータソースでは、ACLのない文書はエラーにもならず取り込みの時点で飛ばされ、そもそも索引に入りません。就業規則のように全社で見せたい文書でも、上の例のように全部署のIDを並べる必要があります。
同期は成功しているのに検索しても出てこないときは、まずここを疑ってみてください。
🙅 ユーザー側は1つの部署しか名乗れない
ここで気をつけたいのは「複数並べられる」があくまで文書側の話だということです。userContext.userIdは単一の文字列なので1回のRetrieveで名乗れる部署は1つだけになります。
これが響いてくるのが「兼務」です。営業部と経営企画を兼ねている人がgrp-salesで検索すると経営企画の文書は返らずgrp-planningで検索すると営業の文書が返りません。両方を1回で取る手段はないので所属部署の数だけRetrieveを投げて結果を自分でマージすることになります。
つまり案1は兼務者が一人でもいる組織ではそのままでは使えません。
⚡ メリット 人事異動があってもACLを書き換えなくて済む
文書に登録されているのは「営業部が見てよい」という変化の遅い情報だけで「この人はどの部署か」という変化の速い情報はアプリ側の人事マスタが持ちます。メアド方式だと1人の異動で営業部の全ファイルを再取り込みですが、部署ID方式ならBedrock側は何も触りません。
💣 デメリット 権限を変えるたびに該当文書を全部再取り込み
ACLだけを更新する手段はなくACLは常に取り込みの付随属性です。「営業部の資料をシステム部にも見せたい」と言われたら営業部の全ファイルを再取り込み。しんどい...![]()
⚠️ 公式の想定外の使い方
もう一つ気になるのはこれが公式の想定外の使い方だという点です。
ここにはこう書かれています。
The
userIdis always the user's universal email address associated with the underlying data source.(userIdは常に、基となるデータソースに関連付けられたユーザーのユニバーサルメールアドレスです。)
現状メール形式のバリデーションはかかっておらず部署IDのような文字列でも取り込みと検索は通ります。
🗂️ 案2 ACLを使わずカテゴリ属性(meta)+外部の権限ストア
もう一つの選択肢です。こちらは発想を変えて文書には「誰が見てよいか」ではなく「これは何の文書か」だけを持たせます。
"inlineAttributes": [
{ "key": "category", "value": { "type": "STRING", "stringValue": "sales-manual" } }
]
そして「どの部署がどのカテゴリを見てよいか」はDynamoDBなどの外部ストアで管理します。
| PK (group_id) | categories |
|---|---|
| grp-sales | ["sales-manual", "general"] |
| grp-finance | ["finance-report", "sales-manual", "general"] |
| grp-it | ["general"] |
検索時の流れはこんな感じ
- アプリがユーザーを認証し、部署IDを特定
- DynamoDBに部署IDを投げて、参照可能なカテゴリのリストを取得
- そのリストを
inフィルタとしてRetrieveに渡す
"retrievalConfiguration": {
"managedSearchConfiguration": {
"filter": {
"in": {
"key": "category",
"value": ["sales-manual", "general"]
}
}
}
}
⚡ メリット 権限変更はDynamoDBの1行更新で終わり再インデックス不要
「システム部にも営業マニュアルを見せたい」ならgrp-itのcategoriesにsales-manualを足すだけです。ファイルが何千件あっても関係なく、案1と比べると桁違いに安く済みます。
💣 デメリット フィルタを渡し忘れた瞬間に全件返る
ACLのfail closedがないので、実装ミスが即漏洩につながります。対策はRetrieveを叩くラッパーを1本に絞ってフィルタ必須にすることです。生のbedrock-agent-runtimeクライアントをアプリ各所から直接叩かせないようにします。
もう一つinに並べられる値には上限があります。ユーザーが見てよいカテゴリが増えるとここに当たります↓
inに何件まで並べられるか公式に書かれていなかったので実際に試してみました。ひとつのinに書ける値は9件までで、10件目でValidationExceptionになりました。
⚖️ 使い分けの整理
さて、2つ紹介しました。どっちを選ぶべきか迷いますね。
| 案1 ACLに部署ID | 案2 カテゴリ属性+外部ストア | |
|---|---|---|
| 文書に持たせるもの | 誰が見てよいか | 何の文書か |
| 権限の正 | インデックス内 | DynamoDBなど |
| 権限変更のコスト | 該当文書の再取り込み | 1行UPDATE |
| フィルタを組むのは | サービス | アプリ |
| 実装ミス時 | 0件(fail closed) | 全件(fail open) |
権限変更/部署追加がまれなら案1。兼務や横断ロールが多くて権限変更が日常的なら案2が向いています。
ちなみに2つは排他ではありません。ACLで部署の壁を張っておいてその中をカテゴリのフィルタでさらに絞る、というハイブリッドもできます。両方使うと「ACLも通ってフィルタも通った文書」だけが返ってきます。
🍜 締め
「認可」とタイトルに入れて恐縮ですが、ManagedKBのACLは厳密には認可ではありません。公式ドキュメントにもはっきり書かれています。
ACL awareness is not authorization
この機能はお客様が指定したIDに基づいて結果をフィルタリングしますが、真の認可を構成するものではありません。
BedrockはuserIdが本人のものかどうかを一切確かめないので、他人の部署IDを名乗れば他部署の文書が普通に出てきます。名簿は用意してくれるけど、入口で身分証を確認するのはこっちの仕事。ログインとその人の部署IDを正しく組み立てるところだけは、案1でも案2でもサボれません。
そしてその名簿の欄に「Must be USER」と書いてあるのを承知で部署IDを流し込んでいるわけで、グレー感は否めません。将来バリデーションが入ったらこの記事ごと成仏します
そのときは素直にAWSが用意した方法に乗り換えますので、どうか穏便にお願いします。
とはいえ、ManagedKBだけで全部きれいに済ませようとしなくてもいいのかなと思っています。DynamoDBでも何でも周りのサービスを組み合わせれば選択肢はいくらでも増えますし、正直そうやってあれこれ試しているときが一番楽しいです。「うちはこうやった」「もっといい手がある」という方はぜひ教えてください。
この記事の内容は2026年9月時点の公式ドキュメントをもとにしています


