The Coarse Gate - Why Login-Level Permission Isn't Enough for a Shared Knowledge Base

The Coarse Gate - Why Login-Level Permission Isn't Enough for a Shared Knowledge Base

The problem

I built an analytics copilot for a small consulting firm that shared one knowledge base across three clients' project data: CRM notes, tickets, and deliverables, all sitting in the same index because that's genuinely how the firm's data lived. Every record carried a client scope and a list of allowed roles, right there in its metadata. The permission check that actually ran, though, happened once, at login: does this person hold the "consultant" role at all. Nothing checked the per-record scope again after that.

A consultant staffed only on Client A asked a broad question, something like what integration issues have come up across our accounts. Retrieval did exactly what retrieval does. It found the most relevant chunks regardless of which client they belonged to, and it pulled matching content from Client B and Client C right alongside Client A's own data. The coarse gate at login had already passed this person through as a legitimate consultant. Nothing downstream asked whether this specific consultant was allowed to see this specific client's confidential notes.

I measured how often that produced an actual leak. Across a 20-question eval set with requesters scoped to different clients and roles, the login-only version let through at least three permission-violating chunks per 100 queries, chunks whose client scope explicitly excluded the person asking. That's not a hypothetical exposure. That's a real consultant getting real confidential material about a client they've never been staffed on, delivered by a system that was, from the outside, working exactly as designed.

The pattern

The fix inserts a resolution step before anything reaches the assembled prompt. For every retrieved chunk, it checks that specific record's client scope and allowed roles against the requester's actual grants for that source system, and drops or redacts anything that fails the check. The part that matters most is what happens to the chunks that survive: every one carries a permission_check record attached to it, naming the source system and the exact rule that was applied, so the decision is auditable instead of something you have to take on trust.

That auditability turned out to be the part I valued most once the fix was running. On the identical 20-question eval set, the leaked-record rate dropped to zero, but the number I actually watch on a dashboard is how many per-record checks ran on a given query. Zero leaked records because every record got checked and passed is a real result. Zero leaked records because the check quietly ran on nothing is an accident wearing the same score. Logging the check count is what tells those two outcomes apart.

The reason this has to run per record, per source system, rather than once at login, is that a single coarse gate answers a question that isn't the one that matters. "Is this person a consultant" and "is this specific consultant allowed to see this specific client's data" are different questions with different failure costs, and a system that only asks the first one will pass every request that clears a bar that was never fine-grained enough to catch the actual risk. The resolution step exists specifically to ask the second question, against the specific source system each chunk came from, every time.

Design considerations

I've also built a second design for a different project, a hard, binary access gate that refuses outright the moment a grant is missing, structurally impossible to bypass rather than logged and reviewed after the fact. That's a genuinely different intent from the resolution step described here, which resolves against a source-system-keyed permission table and produces an auditable record for someone to review. Both are legitimate designs, built for different risk postures, and I've learned to be precise about which one a given system actually has. Treating an auditable resolution step as if it carries a hard gate's guarantee is exactly the kind of mistake that looks fine right up until an incident review asks which one was actually built.

The harder limitation is one I haven't fully closed, and I'd rather say so than pretend otherwise. This resolution step checks a record's client scope against whatever the permission table says at the moment the check runs. That table can be wrong the instant somebody's access changes after it was last synced and before this exact query hits it. A consultant who gets pulled off a client engagement this morning is still, technically, checked against a table that might not reflect that change until the next sync runs. Static, pre-filtered permission resolution is explicitly named, across the enterprise security literature I've read on this, as suffering exactly this kind of staleness problem, and the pairing that's converged on as best practice by 2026 combines pre-filtering like this with a live, real-time check against whatever system actually owns the grant at query time, rather than trusting the pre-filtered table alone. I haven't built that live recheck into every system I've shipped this fix in, and where I haven't, I've been explicit with clients about exactly what gap that leaves open: the resolution step catches every stale grant that was already wrong when it was tagged, and it has no mechanism for catching a grant that changed after the fact.

What convinced me this design decision matters beyond my own projects is how consistently the same shape shows up across the field once you go looking for it. Resolve identity, resolve the requester's actual grants for the specific system in question, apply the filter before retrieval runs, and log what happened, explicitly rejecting the older assumption that if a system has access to a database, the person using that system automatically does too. That's become a named, converged-upon category in enterprise security review. Seeing multiple independent sources land on the identical shape told me the per-record, per-source, auditable design was the right level of granularity to build toward, even with the staleness gap still open.