AI Security / 2026 Essentials
Your AI Search Must Respect Document Permissions
Enforce user-to-document authorization before retrieving text and prove that a highly relevant private document is absent from an unauthorized result.
Start hands-on practice · เริ่มภาคปฏิบัติ ↓Practical guide · คู่มือลงมือทำ
Download worksheet · ดาวน์โหลดขั้นตอนDetailed practical guide
Prepare your workspace and inputs
Save as rag_acl.py and run python3 rag_acl.py. The store has a public procedure and an HR-only marker. Alice is an employee; Helen has the HR role. Roles are held in application configuration, not supplied in a search prompt.
Platform context: Python 3.10+ with sqlite3; in-memory document store. The example tests a retrieval authorization boundary without a model or embedding service.
Scenario to reproduce or investigate
A private document matches a query perfectly. Removing its citation after generation is too late if its text already entered the model context.
- Actual target and platform release
- Exact inputs or substituted values
- Baseline result
- Per-step expected and observed output
- Failure encountered and corrective action
- Final acceptance and recovery state
-
Write the visibility matrix before indexing
Mark the procedure visible to employee and HR roles and the case note visible only to HR. Treat unknown identities as having no retrieval access. In production, resolve these roles from the authenticated session and current identity source.
Command, configuration or input
alice → employee → procedure only helen → hr → procedure and HR note unknown → no role → no documentsExpected result · ผลที่ควรเห็นThe expected retrieved documents are explicit for each identity, including the negative case.
If it fails · เมื่อไม่ผ่านIf the model can choose its own role or tenant in a tool argument, move that selection into the authenticated backend context before searching.
-
Apply the ACL inside the retrieval query
Run the complete program. It filters permitted rows before returning bodies. The query uses bound values, and the protected marker is checked directly in returned text, not just in a model answer.
Save and run this example
import sqlite3 db = sqlite3.connect(":memory:") db.executescript(""" CREATE TABLE documents(id TEXT PRIMARY KEY, body TEXT); CREATE TABLE access(document_id TEXT, role TEXT); """) db.executemany("INSERT INTO documents VALUES (?, ?)", [ ("procedure", "The support procedure explains leave requests."), ("hr-note", "Leave investigation marker HR_ONLY_7391."), ]) db.executemany("INSERT INTO access VALUES (?, ?)", [ ("procedure", "employee"), ("procedure", "hr"), ("hr-note", "hr"), ]) roles = {"alice": "employee", "helen": "hr"} def retrieve(user, term): role = roles.get(user) if role is None: return [] return db.execute(""" SELECT DISTINCT d.id, d.body FROM documents AS d JOIN access AS a ON a.document_id = d.id WHERE a.role = ? AND instr(lower(d.body), lower(?)) > 0 ORDER BY d.id """, (role, term)).fetchall() alice = retrieve("alice", "leave") helen = retrieve("helen", "leave") assert [row[0] for row in alice] == ["procedure"] assert {row[0] for row in helen} == {"procedure", "hr-note"} assert "HR_ONLY_7391" not in repr(alice) assert retrieve("unknown", "leave") == [] roles["helen"] = "employee" assert "HR_ONLY_7391" not in repr(retrieve("helen", "leave")) print("PASS: restricted text excluded before context; role removal takes effect") db.close()Expected result · ผลที่ควรเห็นThe program prints PASS: restricted text excluded before context; role removal takes effect. Neither Alice nor the downgraded Helen receives the protected marker.
If it fails · เมื่อไม่ผ่านIf a private row appears, inspect the query filter and identity source. Masking the marker in the final answer does not repair the retrieval leak.
-
Apply the same rule to vector search and caches
Use the equivalent current-user and tenant filters in your actual retrieval backend before fetching document bodies. Add the principal and effective permission revision to cache isolation, and invalidate cached context when permissions change. Inspect retrieved chunks, citations, reranker input and generated output for the private marker in a staging integration test.
Command, configuration or input
Authenticated identity → current ACL filter → permitted chunk retrieval → reranking → model contextExpected result · ผลที่ควรเห็นNo restricted body reaches any downstream stage, including the reranker and reused cached context.
If it fails · เมื่อไม่ผ่านIf a backend cannot apply the required ACL before returning content, redesign the retrieval boundary or keep that collection out of the assistant; do not rely on a prompt to suppress the leak.
Check the result before marking this passed
Allowed retrieval works, unknown users receive nothing, restricted text is excluded before context construction and a role downgrade immediately removes access in the test. Production caches and document deletion must pass equivalent tests.
Record the observed result and evidence location in the downloaded worksheet. Mark unresolved or untested conditions explicitly.
Recovery and cleanup
The example database is in memory. For the actual retrieval service, retain the prior index and ACL configuration so you can disable the candidate integration if it exposes a protected chunk.
Apply to actual work and known limits
Replace the lexical match with your actual search while keeping identity resolution and permission enforcement outside the model. The test is about authorization order, not retrieval ranking quality.
Observed local fixture output · ผลรันตัวอย่าง
Apply the ACL inside the retrieval query
PASS: restricted text excluded before context; role removal takes effect
Background, explanations and original notes
Prepare the exact environment
Save as rag_acl.py and run python3 rag_acl.py. The store has a public procedure and an HR-only marker. Alice is an employee; Helen has the HR role. Roles are held in application configuration, not supplied in a search prompt.
Write the visibility matrix before indexing
Mark the procedure visible to employee and HR roles and the case note visible only to HR. Treat unknown identities as having no retrieval access. In production, resolve these roles from the authenticated session and current identity source.
alice → employee → procedure only
helen → hr → procedure and HR note
unknown → no role → no documents
Apply the ACL inside the retrieval query
Run the complete program. It filters permitted rows before returning bodies. The query uses bound values, and the protected marker is checked directly in returned text, not just in a model answer.
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE documents(id TEXT PRIMARY KEY, body TEXT);
CREATE TABLE access(document_id TEXT, role TEXT);
""")
db.executemany("INSERT INTO documents VALUES (?, ?)", [
("procedure", "The support procedure explains leave requests."),
("hr-note", "Leave investigation marker HR_ONLY_7391."),
])
db.executemany("INSERT INTO access VALUES (?, ?)", [
("procedure", "employee"), ("procedure", "hr"), ("hr-note", "hr"),
])
roles = {"alice": "employee", "helen": "hr"}
def retrieve(user, term):
role = roles.get(user)
if role is None:
return []
return db.execute("""
SELECT DISTINCT d.id, d.body FROM documents AS d
JOIN access AS a ON a.document_id = d.id
WHERE a.role = ? AND instr(lower(d.body), lower(?)) > 0
ORDER BY d.id
""", (role, term)).fetchall()
alice = retrieve("alice", "leave")
helen = retrieve("helen", "leave")
assert [row[0] for row in alice] == ["procedure"]
assert {row[0] for row in helen} == {"procedure", "hr-note"}
assert "HR_ONLY_7391" not in repr(alice)
assert retrieve("unknown", "leave") == []
roles["helen"] = "employee"
assert "HR_ONLY_7391" not in repr(retrieve("helen", "leave"))
print("PASS: restricted text excluded before context; role removal takes effect")
db.close()
Apply the same rule to vector search and caches
Use the equivalent current-user and tenant filters in your actual retrieval backend before fetching document bodies. Add the principal and effective permission revision to cache isolation, and invalidate cached context when permissions change. Inspect retrieved chunks, citations, reranker input and generated output for the private marker in a staging integration test.
Authenticated identity → current ACL filter → permitted chunk retrieval → reranking → model context
Acceptance check
Allowed retrieval works, unknown users receive nothing, restricted text is excluded before context construction and a role downgrade immediately removes access in the test. Production caches and document deletion must pass equivalent tests.
Recovery and cleanup
The example database is in memory. For the actual retrieval service, retain the prior index and ACL configuration so you can disable the candidate integration if it exposes a protected chunk.
Apply to actual work and known limits
Replace the lexical match with your actual search while keeping identity resolution and permission enforcement outside the model. The test is about authorization order, not retrieval ranking quality.