DEV Community

Micky Irons
Micky Irons

Posted on

Classify Before You Embed: The Access Control Problem RAG Pipelines Skip

Most teams building retrieval-augmented systems over an organisation's archive treat data preparation as a parsing problem: get the PDFs and emails into text, chunk it, embed it, done. I've made that mistake myself, early on, and the system worked fine in every demo. It broke in production, three months in, when a support chatbot surfaced a severance negotiation memo in response to a completely unrelated question about expense policy. Nobody had done anything wrong. The retrieval pipeline did exactly what it was built to do: find the chunk most semantically similar to the query. The problem was that nothing in the pipeline knew that chunk was anything other than ordinary text.

That's the bit that gets missed when people frame this as a compliance exercise. Treating it as a checklist you run once to satisfy an auditor misses the point. It is an access control design problem, and if you get the sequencing wrong, you end up with a retrieval layer that holds more privilege than any single human user is meant to have, with nothing stopping that privilege leaking into a response.

Why broad retrieval access is a confused deputy, not a convenience

When you point an embedding pipeline at a file share, a SharePoint site, or an email archive, the pipeline runs under a service account. That account usually has read access to everything in scope, because that's the easiest way to get it working. The model sitting on top of the retriever doesn't have its own identity. It inherits whatever the retriever can see. So the moment end users start querying that system, you've built a confused deputy: an agent with high privilege acting on behalf of a requester with lower privilege, with no check in between.

This failure mode is concrete and specific. It isn't "the model might say something wrong." It's: a junior employee's benign question gets answered using the contents of a document they were never granted read access to in the source system, because the vector index flattened every document into one undifferentiated pool, and the only gate left is whether a chunk looks semantically relevant. Semantic relevance and authorisation are unrelated properties. Treating the first as a proxy for the second is where this goes wrong.

Start with assessment, not ingestion

Before anything gets parsed, I want an inventory: what repositories exist, who owns each one, what access control already governs it in its source system (file share ACLs, SharePoint permissions, mailbox ownership), and roughly what's in it. This sounds slow. It's the only point in the process where you can catch, for example, an HR share that holds years of disciplinary records sitting next to the induction handbook, before both get flattened into the same chunk store.

Digitisation needs to carry provenance forward, not just extract text

OCR and format conversion (Word, PDF, scanned paper, old database exports) is the part everyone budgets time for. What usually gets skipped is making sure the pipeline preserves where each piece of text came from: source system, original path, original ACL where one exists, and a reference back to the source object, not just the extracted string. Throw that away at digitisation time and you have no way to reconstruct it later. You'd be re-deriving sensitivity from content alone, which is a much weaker signal than the access control that already existed in the source system.

Classify at the chunk level, because documents aren't uniformly sensitive

A tier taxonomy (public, internal, restricted, confidential) is the easy part to decide on. The harder part is applying it below the document level. A long board pack usually has a public-facing strategy summary and a confidential remuneration annex in the same file. Tag the whole document confidential and you've blocked legitimate use of the public section. Tag it internal because that's the average and you've exposed the annex. Tag at chunk granularity instead, in the same pass that splits the document for embedding, so the sensitivity tag and the chunk boundary get decided together, by whoever or whatever is doing the splitting:

{
  "chunk_id": "doc-4471-chunk-12",
  "source_system": "sharepoint://hr-archive",
  "source_path": "/Board/2024/Remuneration_Annex.docx",
  "sensitivity_tier": "confidential",
  "compartment": ["hr", "exec"],
  "inherited_acl": ["hr-leadership", "board"],
  "classified_by": "pipeline-v2",
  "classified_at": "2026-03-11T09:02:00Z"
}
Enter fullscreen mode Exit fullscreen mode

Enforce permissioning at retrieval time, not at the prompt

The tags are inert unless something checks them before a chunk reaches the model's context window. I've seen teams try to solve this by instructing the model in its system prompt not to repeat confidential information. That doesn't hold up, because by the time the chunk is in context, the model has already read it, and a different phrasing of the question can get it to paraphrase rather than quote. The filter needs to sit between the vector search and context assembly, and it needs to be a hard filter, not a suggestion to the model:

def retrieve(query, user, top_k=8):
    candidates = vector_index.search(embed(query), k=50)
    allowed = [
        c for c in candidates
        if tier_rank(c.metadata.sensitivity_tier) <= user.clearance_rank
        and set(c.metadata.compartment) & set(user.compartments)
    ]
    return allowed[:top_k]
Enter fullscreen mode Exit fullscreen mode

Note what that function requires: every candidate chunk has to carry sensitivity_tier and compartment as queryable metadata, which only exists because classification happened before indexing, not after. If you index everything first and plan to add permissions later, you're not adding a feature. You're re-running the entire classification and re-indexing pass over whatever has accumulated in the meantime, under time pressure, probably after an incident rather than before one.

Where this actually lands

This is ordinary compartmentalisation thinking that's existed in information security for decades, applied to a retrieval mechanism that happens to be very good at finding things you didn't mean to expose. I've done this properly on a production system, and the part that took the longest wasn't the classification taxonomy, it was getting chunk-level metadata to survive every transformation a document goes through before it reaches an index. The only genuinely new wrinkle semantic search introduces is that relevant and accessible start to feel like the same property when they aren't, and that's exactly where the failure mode above lives.

I haven't found a clean answer for one part of this: tier inheritance when a chunk gets reused across derived documents, summaries, translations, re-exports, that sort of thing. The sensitivity of the source is clear enough. What the derived version should inherit is less obvious. Has anyone settled on a pattern for propagating sensitivity metadata through a document that's been transformed rather than simply copied?

Top comments (0)