Unit 09.01: Enforcing access at retrieval time
Access control in a RAG system is a set intersection performed before ranking. Anything else is a request to a component with no obligation to comply.
A filter you can state the guarantee for
Each chunk carries the groups permitted to see it. Each request carries the groups its user belongs to. Eligible chunks are those where the two sets intersect - computed before similarity, so a restricted chunk never competes for a slot.
The example below runs the same query as three different users.
CHUNKS AND WHO MAY SEE THEM
c1 Refunds within 7 days. public
c2 Escalate refunds over 500. staff
c3 Q3 refund cost forecast. finance
QUERY "refund", ASKED BY WHAT COMES BACK
a public user c1
a public + staff user c1, c2
a public + staff + finance user c1, c2, c3
Same query, same corpus, three different result sets. The difference is a
comparison of two lists of groups, made before ranking -- deterministic,
checkable, and independent of the model behaving.
The public user sees c1. Adding staff reveals c2. Adding finance reveals c3. Same query, same corpus, three different result sets, and the difference is a set intersection you can read in one line.
Three properties follow, and all three matter to whoever audits this. It is deterministic - the same inputs always give the same result. It is testable - you can assert on it in CI. And it does not depend on the model behaving, because the model never sees the excluded chunks.
The mistake this prevents
The mistake is appending "do not reveal finance documents" to the system prompt and calling it access control. By the time the prompt is read, the finance document is already in the context window; whether it appears in the output is then a matter of probability. You cannot state a guarantee for that, which means you cannot pass an audit with it.
Takeaway
Store permitted groups on the chunk, intersect with the user's groups before ranking, and treat any restricted chunk reaching the context window as an incident rather than a near miss.
