Watch a restricted chunk reach the wrong user, then filter by identity before retrieval
Task
Reproduce the lesson's HR assistant failure on your own app. Ask about pay as an ordinary user and watch the restricted salary chunk, canary and all, reach the prompt. Then put the control where the lesson says it belongs: in the retrieval layer, filtering on the requesting user's identity before anything reaches the model. Show that the filter blocks the ordinary user, fails closed for an unknown one, and still serves the user who is entitled to see the chunk.
Steps
- As
alice, ask three pay questions, for examplepython app/ask.py --user alice "What are the salary bands for engineers?". Check the latest log records:retrieved_idsshould include arestricted/chunk, and the canary should be in the logged prompt and possibly in the response. - Write
~/secai-lab/app/users.jsonmapping users to the classifications they may read, using the course's users:alice,bobanddavegetpublicandinternal;carol, who works in HR, getspublic,internalandrestricted. Any user not in the file getspubliconly, so the control fails closed. Later labs add their test users to this file. - Change
retrieve(question, user)inapp/rag.pyso the classification filter for that user goes into the vector store query itself, so restricted chunks are never candidates. In Chroma that is the query'swhereargument on theclassificationmetadata; with FAISS, over-fetch and drop disallowed chunks using your metadata before building the prompt for now (lesson 11's lab replaces that with one index per classification). Filtering the model's answer afterwards is not this control. Restart the gateway so it loads the change: the gateway callsretrievewith the request'suser, so this is the retrieval layer filtering on identity. - Mark the moment the filter went live:
date +%Y-%m-%dT%H:%M:%S%z > ~/secai-lab/lab/rag-filter-on.txt. The format matches the gateway'sts, so the two compare as strings. - Ask the same three questions as
alice, ascaroland as an unknown user,visitor. Then write two sentences in~/secai-lab/lab/rag-notes.mdon why the system prompt was not the control, and on what is still in the log from before the fix.
Verify
cd ~/secai-lab
python - <<'EOF'
import json
on = open("lab/rag-filter-on.txt").read().strip()
rows = [json.loads(l) for l in open("logs/gateway.jsonl", encoding="utf-8") if l.strip()]
def restricted(r):
return any(i.startswith("restricted/") for i in r["retrieved_ids"])
before = [r for r in rows if r["ts"] < on and r["user"] == "alice"]
after = [r for r in rows if r["ts"] >= on]
print("alice before the filter:", sum(map(restricted, before)), "restricted retrieval(s)")
print("alice after:", sum(restricted(r) for r in after if r["user"] == "alice"))
print("visitor after:", sum(restricted(r) for r in after if r["user"] == "visitor"))
print("carol after:", sum(restricted(r) for r in after if r["user"] == "carol"))
def canary(r):
return any(m in r["prompt"] + r["response"] for m in ("CANARY-7Q4X-SECAI", "[CANARY]"))
leak = sum(canary(r) for r in after if r["user"] in ("alice", "visitor"))
print("canary in alice's or visitor's prompt or response after:", leak)
EOF
grep -c '"user": "visitor"' logs/gateway.jsonl
Alice before the filter: at least 1. Alice after: 0. Visitor after: 0. Carol after: at least 1, which shows the filter is selective rather than blocking everything. The canary count after the fix: 0. The visitor count is at least 3. If carol also shows 0, the filter is broken in the other direction, and the HR team would route round it.
Notes
The user field is still whatever the caller claims, so this filter is only as good as the identity it is given. The lessons on model and data access replace the claimed name with an authenticated one. The pre-fix records still hold the canary, which is the logging problem lesson 15 takes up.
This is an independent study companion for CompTIA SecAI+ CY0-001 and is not produced by or endorsed by CompTIA.