Merry Christmas & Happy 2027! Less downtime. More good times. Celebrating 13 years of sorlavit.com
Skip to content
Sorlavit KimIT Support, Network & Infrastructure Services Let’s talk

ปุ่มแปลไทยจะเปิดบทความผ่าน Google Translate เพื่อแปลอัตโนมัติ โดยคงโค้ดและคำสั่งไว้ตามต้นฉบับ

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 · เริ่มภาคปฏิบัติ ↓

Compiled for Sorlavit Kim · Published · Updated · 3 min read

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.

Evidence to record in your worksheet
  • 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
  1. 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 documents
    Expected 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.

  2. 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.

  3. 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 context
    Expected 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.

The included self-contained Python fixture was executed successfully in this Linux environment. External products and production infrastructure were not exercised.

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.

Source & further reading

Python bound SQLite queries

Independently written summary reviewed on 2026-11-04. The example illustrates a possible application; it is not a client case study.

Share this article

Back to results