The problem: a correct answer can be an unauthorized answer
The fixture contains five documents in two tenants. Alice can read alpha’s finance document; Bob cannot. Carol can read beta’s finance document. The query “forecast” is the same for all three callers, but the allowed evidence differs. Access is applied before lexical ranking and context assembly.
The example follows a basic authorization principle: the application checks the requested resource for the current trusted caller. A query string or model output does not establish the caller’s tenant or group membership.[1]
A second scenario caches Alice’s answer, revokes her finance-document access and asks again. The cached answer cannot survive the changed dependency. A direct citation request also checks current access rather than bypassing the retrieval path.
Run and inspect the fixture
Python standard library only. Includes source, behavioral tests, recorded results and an MIT license.
Read the exact Python source included in the archive.
Deterministic output captured from the packaged source.
Reuse terms for this synthetic example. The archive includes the same license.
SHA-256 of the published source archive.
python3 -B -m unittest -v
python3 -B example.pyUse Python 3.10 or later. The example uses an in-memory corpus and the standard library. There is no model, vector service, network access or credential. The Principal objects represent already-authenticated caller context supplied by a trusted application; constructing one is not an authentication mechanism.
Version 2 prevents callers from changing cached source lists through an edited response. It also tests discovery after a new access grant when the previous answer had no accessible evidence. The original versioned download remains available for reproducibility; use this revised version for the current example.
How the boundaries are represented
| Component | Behavior |
|---|---|
| Principal | Immutable tenant and user fields supplied by the trusted test harness. |
| Document | Tenant, allowed readers, content and version. |
| Search | Check access before scoring and selecting document IDs. |
| Citation | Recheck current access before returning source text. |
| Answer cache | Key by caller/query and record source-version dependencies. |
| Revocation | Remove the reader and increment the source version. |
On cache reuse, every source must still exist, remain readable and match the recorded version. Otherwise the answer is rebuilt from currently accessible evidence. Empty answers are not cached, so newly accessible evidence can be discovered after an earlier no-answer result.
Captured result from this source
{
"alice_after_revocation": {
"cached": false,
"sources": [],
"text": "No accessible evidence"
},
"alice_before_revocation": {
"cached": false,
"sources": [
"a-finance"
],
"text": "Confidential revenue forecast"
},
"alice_cached_read": {
"cached": true,
"sources": [
"a-finance"
],
"text": "Confidential revenue forecast"
},
"beta_result": {
"cached": false,
"sources": [
"b-finance"
],
"text": "Beta revenue forecast"
},
"bob_result": {
"cached": false,
"sources": [],
"text": "No accessible evidence"
},
"fixture": "synthetic five-document lexical corpus; no model"
}Alice initially receives a-finance and can reuse that cached result. Bob receives no accessible evidence. Carol receives only b-finance. After revocation, Alice receives no accessible evidence and the previous cached text is not returned. These are deterministic fixture outcomes, not a retrieval-quality benchmark.
What the nine tests establish
- The same query returns tenant-scoped results.
- A different reader in the same tenant cannot receive restricted content.
- Revocation invalidates cached evidence.
- Direct citation access follows current permissions.
- A deleted source cannot survive through a cached answer.
- A changed source version rebuilds the answer.
- An unknown principal receives no evidence.
- Edits to returned source lists cannot alter cached evidence.
- A previously empty answer does not conceal a new access grant.
These tests inspect returned content and source identifiers, not only a boolean authorization response. A system can deny a document endpoint while still leaking its content through a snippet, summary or cached answer.
What a live design still needs
The fixture has synchronous in-memory policy and data. Real ingestion, identity, index and cache systems can have different update delays. Product-specific document-permission features may require source synchronization or explicit refresh; they should not be described as immediate revocation without verifying that contract.[2]
The example checks cached dependencies. Discovery of newly relevant documents, concurrent policy changes, distributed caches, ingestion attacks, prompt injection and generator output controls are each designed and evaluated as their own requirements.
Use the code to inspect a bounded principle: access is enforced before content is returned, and reused evidence is revalidated. Extend it only after defining the actual authority source, freshness requirement and failure behavior.
Apply the principle to source ACLs, query filters, derived content, caches and citations.
Sources and scope
Technical references inform the cited statements. The decision frameworks and synthetic examples are TeqEngine’s editorial guidance.
- OWASP: Authorization Cheat Sheethttps://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
- Microsoft: Document-level access control in Azure AI Searchhttps://learn.microsoft.com/en-us/azure/search/search-document-level-access-overview