RAG Access Control: Permission-Aware Retrieval for Internal Documents
RAG access control fails when authentication stops at the chat endpoint. The employee is signed in, but the retrieval layer searches every indexed chunk and sends restricted text to the model before the application removes an unsafe citation. That is already a disclosure. The text can survive in model input, traces, caches, or generated prose.
A secure design carries source-system permissions through ingestion, retrieval, generation, and logging. This guide defines that contract, shows where to enforce it, and gives you tests that prove an unauthorized user cannot retrieve or infer a protected document. The payoff is an internal assistant that follows the same document boundaries as SharePoint, Google Drive, or your knowledge platform instead of creating a second, weaker permission system.
Why endpoint authentication is not enough
Authentication answers who sent the request. It does not answer which chunks that person may retrieve. A typical pipeline authenticates the user at an API gateway, embeds the question, runs a nearest-neighbor search across a shared index, and only then tries to trim results. The dangerous step has already happened: retrieval selected content without authorization.
This mismatch appears because document permissions and vector search evolve independently. Source systems usually grant access through users, groups, folders, inheritance, link sharing, and exceptions. Vector indexes usually store chunks with a flat metadata object. If ingestion copies text but drops the source document ID, group ACL, or permission version, the retrieval service cannot reproduce the source decision.
Microsoft's secure multitenant RAG architecture makes the boundary explicit: orchestration can apply security filtering so retrieval returns only data the user is authorized to access. Azure AI Search separately documents document-level access control. The cloud feature matters less than where the check runs. Authorization must constrain retrieval before any text reaches the model.
Post-generation filtering cannot repair this boundary. A model may paraphrase protected text without citing it. A trace may capture the retrieved chunk. A semantic cache may store the answer under a key that omits the caller's access set. Unauthorized text must never enter model context.
Define one authorization contract
Write the contract before selecting a vector database or connector. It should state:
- Every chunk maps to one stable source document and one current permission version.
- The server derives the caller identity and groups from a trusted identity provider. The prompt and client request cannot supply them.
- Retrieval evaluates permissions before returning chunk text.
- Missing, malformed, or stale permission metadata causes a deny decision.
- Permission removal reaches the retrieval index within a declared revocation window.
- Caches, traces, citations, and exports preserve the same boundary.
- A test user who lacks access cannot retrieve, infer, cite, or observe the protected document through any supported interface.
A current Azure GPT-RAG issue calls for end-to-end document authorization tests with two users, including chat, citations, tools, and logs. It also requires a failed authorization test to block promotion rather than silently fall back. The same acceptance pattern works with any stack.
The contract also needs a scope. Organization-level tenant isolation is not enough for an internal assistant. Two employees in the same company may have different access to legal, finance, HR, and customer documents. Keep tenant identity as an outer boundary, then enforce document permissions inside it.
Preserve permissions during ingestion
Ingestion should produce an authorization envelope beside every chunk. Do not reduce permissions to a comma-separated list of usernames if the source uses groups and inherited rules. Large per-user lists become expensive to update and easy to truncate. Pinecone's multitenancy guidance recommends access-control groups, namespaces, or metadata filtering instead of large lists of individual user IDs.
A practical chunk record needs these fields:
{
"chunk_id": "doc-482:7",
"source_document_id": "doc-482",
"source_system": "knowledge-base",
"content": "...",
"tenant_id": "org-19",
"allowed_group_ids": ["finance", "executive"],
"allowed_user_ids": [],
"permission_version": 41,
"source_updated_at": "2026-08-06T09:30:00Z"
}
The identifiers above are examples, not a universal schema. Across stores, keep these rules:
- Use an immutable source-document identity so every chunk can be deleted or reauthorized together.
- Store group identifiers from the identity system, not display names that people can rename.
- Record a permission version or source change token so stale ACLs are detectable.
- Separate tenant, group, and user exceptions instead of overloading one field.
- Apply the same authorization metadata to derived summaries, tables, images, and child chunks.
- Make indexing idempotent so an ACL-only update can replace permission metadata without duplicating content.
Google Cloud documents data-source access control for enterprise search, while Elastic supports document-level security queries associated with roles and the authenticated user. The syntax differs, but the placement does not: searchable content and access policy meet inside retrieval.
Do not accept a partially indexed document. Write content and permission metadata as one versioned operation where the store permits it. Otherwise mark the document unavailable until both parts are committed. Availability loss is preferable to a window where new text is globally searchable.
Enforce RAG access control at query time
Resolve the caller's access set on the server for every request, or from a short-lived server-side cache with a documented expiry. Then combine the semantic query with mandatory authorization predicates. The model never writes, removes, or edits those predicates.
def retrieve(question, authenticated_user):
principal = identity_service.resolve(authenticated_user)
if not principal.tenant_id:
return deny("missing tenant")
access_filter = {
"tenant_id": principal.tenant_id,
"any_of": {
"allowed_group_ids": principal.group_ids,
"allowed_user_ids": [principal.user_id],
"is_public": True,
},
}
hits = vector_store.search(
embedding=embed(question),
mandatory_filter=access_filter,
limit=12,
)
verified = [hit for hit in hits if permission_is_current(hit)]
return verified[:6]
This pseudocode leaves storage syntax open. Your store may use a role query, namespace, pre-filter, or native source-aware access control. In every case, the vector store returns only candidates inside the caller's allowed set.
Avoid client-side post-filtering when the backend can enforce the filter. Post-filtering can produce empty or low-quality result sets because the nearest neighbors were selected from unauthorized content first. More importantly, restricted records still crossed the trust boundary into application memory. If a store cannot enforce your authorization model during retrieval, put a trusted retrieval service in front of it or choose a store that can.
Run a second permission-version check before prompt assembly. This is not a substitute for filtered retrieval. It is a defense against lag between source ACL changes and index updates. If the current source version cannot be confirmed within your latency budget, fail closed for sensitive collections.
Handle revocation, caching, and logs
Permission grants and removals have different risk. A delayed grant frustrates a user. A delayed removal exposes data. Design the change pipeline around revocation.
Subscribe to source change events when available and run a reconciliation job as a backup. An ACL change should update every chunk for the document. A deletion should remove chunks, summaries, cached answers, and any derived artifacts that contain the document. Measure the time from source revocation to retrieval denial, then set an alert against the promised window.
Cache keys must include the authorization context. A safe answer cache might include tenant ID, a stable digest of effective groups, retrieval-policy version, and source permission epoch. Do not use the raw question alone. Purge or invalidate entries when a document ACL changes. For highly sensitive collections, disable shared semantic answer caching until you can prove that invalidation works.
Logs should capture the authorization decision without copying protected text. Record the request identity, tenant, policy version, selected document IDs, permission versions, decision result, and correlation ID. Redact query and chunk content according to your logging policy. The goal is to reconstruct why a document was allowed without making the log a second document repository.
Citations need authorization too. Generate them from the already authorized hit set, and route citation clicks through the source system or an access-checking proxy. Never expose a raw object URL merely because the model cited it.
Test the boundary with two users
A useful test fixture has one allowed user, one denied user, one shared document, and one restricted document. Put a unique harmless marker in the restricted document so tests can detect paraphrase and indirect leakage without using real confidential data.
Run these checks against the deployed path:
- The allowed user retrieves the restricted document and receives its authorized citation.
- The denied user cannot retrieve it by exact title, marker, quotation, synonym, or broad semantic question.
- The denied user cannot reveal it through a tool call, conversation follow-up, exported transcript, or citation endpoint.
- A cache warmed by the allowed user does not answer the denied user from the restricted document.
- Removing the allowed user's group blocks retrieval within the revocation objective.
- Re-adding the group restores access without duplicating chunks.
- Missing ACL metadata, identity lookup failure, and stale permission versions all deny access.
- Logs prove the denial without containing the restricted marker or document text.
Also test result quality. Authorization filters can reduce the candidate pool, especially for users with narrow access. Track authorized recall using a test set where the expected source is known. Increase candidate depth only within the authorized set. Do not weaken the filter to improve relevance.
The Azure GPT-RAG authorization issue makes one point: test every route that can retrieve content, not just the original chat handler. A new agent gateway, tool layer, batch job, or alternate UI can bypass an authorization step that seemed central in the old architecture.
Common implementation mistakes
The first mistake is trusting the model with authorization. Instructions such as "do not show finance documents to non-finance users" are not access control. The model should never receive forbidden text.
The second is attaching ACLs to documents but not chunks. Retrieval operates on chunks, so every searchable derivative needs the document identity and policy fields.
The third is treating ingestion as finished after the first sync. Group membership, folder inheritance, sharing links, and user status continue to change. Permission synchronization is an operating process, not a migration task.
The fourth is using one service account's broad source permissions as the user's effective permissions. A connector may need broad read access to index content, but query-time retrieval must still evaluate the employee's narrower authority.
The fifth is testing only positive access. A green test that proves finance staff can find finance documents says nothing about leakage. Negative users, stale ACLs, cache reuse, citation routes, and revoked groups are the tests that establish the boundary.
Implementation sequence
Start with one permissioned collection rather than every company document. Map its source ACL model, including inheritance and exceptions. Define the chunk authorization envelope and deny behavior. Choose a retrieval mechanism that can enforce the required predicate before returning text. Then build ACL-only updates and revocation measurement before connecting the model.
Next, add prompt assembly, answer generation, and citations using only authorized hits. Partition or key caches by authorization context. Add structured decision logs without document text. Finally, run the two-user test matrix through every deployed route and make any denial failure block release.
RAG access control is complete only when the denied user cannot retrieve or infer the protected marker after an allowed user has queried it and after access has been revoked. Pick one restricted document today, trace its identity and ACL through every chunk, and run that negative test against the real retrieval endpoint. Any place where the permission disappears is the next engineering task.
References
- Azure AI Search document-level access control supports document trimming and retrieval-time access-control patterns.
- Microsoft secure multitenant RAG architecture supports identity-aware security filtering in RAG orchestration.
- Google Cloud data-source access control supports source-aware access control for enterprise search data.
- Elastic document-level security supports role queries that limit searchable documents for an authenticated user.
- Pinecone multitenancy guidance supports namespaces, metadata filters, and access-control groups for vector retrieval isolation.
- Azure GPT-RAG issue 591 supplies the practitioner requirement for positive and negative end-to-end authorization tests across hosted chat, citations, tools, and logs.