Permission-Aware RAG: Access Control, Metadata Filters and a Cache That Cannot Leak
Hands-on lab · IDE in your browser

Permission-Aware RAG: Access Control, Metadata Filters and a Cache That Cannot Leak

Enforce document permissions in a retrieval-augmented assistant: write allow, deny and clearance rules, filter by region and effective dates, search only what each user may see, answer with citations and an exact refusal, run leak probes, and build an answer cache keyed so it can never serve one user's answer to another.

Time
50 min
Checked steps
5
Level
Intermediate
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

Lab cockpit50 min · 5 stepsSession running
1 / 5 steps passingWhich documents apply today · step 2 of 5
access.py▶ Run✓ Check
# ---------- Step 2: which documents apply ----------def in_scope(user_id, doc, today=harness.TODAY):    """True if the document applies to the user today: their region (or "all"), already in force and not expired."""     
TerminalOutput

The job

Kestrel Software's internal assistant answers from the company knowledge base, which holds everyday policies next to salary bands, a restructuring plan, an acquisition and a security incident report. Nine colleagues with different groups, regions and clearance will try it. You make sure each of them gets the policy that applies to them and nothing they are not allowed to read, including through the cache.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Who may read a document

    Kestrel Software is building an internal assistant over its knowledge base (docs.json): expense and leave policies, engineering and sales guides, and a few documents that must stay closed, such as salary bands, a restructuring plan and board minutes.

    You writecan_read()
  2. 2

    Which documents apply today

    Being allowed to read a document does not make it the right one to answer from.

    You writein_scope()
  3. 3

    Search inside the fence

    There are two ways to combine search with filters.

    You writesecure_search()
  4. 4

    Answer without leaking

    Now the assistant answers.

    You writeanswer()
  5. 5

    A cache that cannot leak

    Many people ask the same questions, so the team wants to cache answers.

    You writecache_key()

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Who may read a document

Kestrel Software is building an internal assistant over its knowledge base (docs.json): expense and leave policies, engineering and sales guides, and a few documents that must stay closed, such as salary bands, a restructuring plan and board minutes. Nine people in users.json will test it.

Permissions have to be enforced before the model sees anything. A model told "don't reveal confidential documents" can still be talked into it; a model that never receives them cannot leak them.

Each document's meta says who may read it:

  • allow: groups that may read it, or a single person as "user:<id>".
  • deny: groups that may never read it, even if another of their groups is allowed. Deny wins.
  • confidential: readable only by users with clearance, on top of allow.
Do this

1. Write can_read(user_id, doc) with the three rules in this order: deny, then confidential, then allow (by group or by user:<id>). Return True or False.

2. Run to print the access matrix, one row per user, and look at Dev (a contractor in the eng group) and Zoe (an exec without clearance).

access.py, the file you edit64 lines
"""Permission-aware retrieval for Kestrel's internal assistant. Users are ids from users.json ("ana", "ben", ...)."""
import hashlib

import harness


# ---------- Step 1: who may read a document ----------
def can_read(user_id, doc):
    """True if the user may read the document: deny wins, confidential needs clearance, then allow by group or user."""
    # TODO (Step 1): user = harness.USERS[user_id], m = doc["meta"].
    # 1. If any of the user's groups is in m["deny"], return False (deny wins).
    # 2. If m["confidential"] and the user has no clearance, return False.
    # 3. Otherwise True if the user shares a group with m["allow"], or "user:<user_id>" is in m["allow"].
    raise NotImplementedError("Step 1: write can_read()")


# ---------- Step 2: which documents apply ----------
def in_scope(user_id, doc, today=harness.TODAY):
    """True if the document applies to the user today: their region (or "all"), already in force and not expired."""
    raise NotImplementedError("in_scope() arrives in Step 2")


def visible(user_id, today=harness.TODAY):
    """Every document the user can read that is in scope for them today."""
    return [d for d in harness.DOCS if can_read(user_id, d) and in_scope(user_id, d, today)]


# ---------- Step 3: search inside the fence ----------
def secure_search(user_id, question, k=3):
    """The k best documents for the question among those visible to the user."""
    raise NotImplementedError("secure_search() arrives in Step 3")


# ---------- Step 4: answer from what the user may see ----------
REFUSAL = "I can't find that in the documents you have access to."
ANSWER_PROMPT = """You answer Kestrel employees' questions using only the documents below.
Cite the document id in square brackets after each fact, for example [d04].
If the documents do not answer the question, reply with exactly this sentence and nothing else:
""" + REFUSAL + """

Documents:
{documents}"""


def answer(user_id, question):
    """{"text": the model's answer, "sources": the ids of the documents it was given}."""
    raise NotImplementedError("answer() arrives in Step 4")


# ---------- Step 5: a cache that cannot leak ----------
_answers = {}


def cache_key(user_id, question):
    """Same key only when the answer would be the same: same visible documents and same question."""
    raise NotImplementedError("cache_key() arrives in Step 5")


def cached_answer(user_id, question):
    """answer(), reused whenever cache_key() matches an earlier call."""
    key = cache_key(user_id, question)
    if key not in _answers:
        _answers[key] = answer(user_id, question)
    return _answers[key]
Provided for you:docs.jsonevaluate.pyharness.pyprobes.jsonquestions.jsonusers.json

Frequently asked questions

How do you enforce permissions in RAG?

Filter documents by the user's permissions before or inside the search, and build the prompt only from what survives. Instructions telling the model not to reveal documents are not a control, because the model can be talked out of them.

Should you filter before or after vector search?

Before, or inside the search as a metadata filter. Ranking everything and filtering afterwards is safe but often leaves one result or none; in the lab it starves most questions of context.

What is a deny rule?

A group that may never read a document even when another of the user's groups is allowed. Deny is checked first and wins, which is how the lab keeps contractors in the engineering group away from engineering pay documents.

Can a response cache leak data between users?

Yes, if it is keyed on the question alone. The lab keys the cache on the question plus the set of documents the user can see, so people with identical access share answers and nobody else does.

Permissions and metadata filters in RAG, tested with leak probes

An internal RAG assistant is only as safe as the documents it puts in the prompt. Permissions belong in retrieval: filter what each user may read before ranking, and the model cannot leak what it never saw. In this lab you implement allow, deny and clearance rules, region and effective-date filters, filter-then-rank search, cited answers with an exact refusal sentence, and leak probes that must never surface a secret. The last step builds an answer cache whose key is the set of documents a user can see, so it shares answers safely.