Practice page · Week 3
ACL before rank
Access control is a filter on the candidate set, applied before anything is scored. If you rank first and filter later, the forbidden text either takes the slots the right answer needed or reaches the prompt.
Extracted from Week 3: Search and retrieval. Northline and Mill City Permits are fictional practice. Portfolio policy.
The example
Northline's handbook has two documents. Parental leave is visible to every employee. Compensation bands are visible to managers. Chunk leave-2 says "Parental leave is 16 weeks for every employee," with ACL employee and manager. Chunk comp-1 says "Compensation band L5 starts at 180000," with ACL manager only. An individual contributor asks about leave. In a sloppy index, the compensation chunk may be the nearest neighbor. It must not be in the result list, and it must not be in the prompt "just in case the model is careful."
The order of operations
- Take the caller's groups from the authenticated session. "I am a manager" in the question text is not a group membership.
- Build the candidate set: chunks that are not tombstoned and whose ACL intersects the caller's groups.
- Rank inside that set.
- Pass the top results to the model, and accept only citations that are chunk ids actually returned for this caller.
In the lab this is Index.query in retrieve.py:
allowed = set(groups)
live = [
row
for row in self.chunks.values()
if not row["tombstoned"] and allowed.intersection(row["acl"])
]
if not live:
return []
groups is keyword-only, so idx.query(text, groups=["employee"]) is the call. An empty ACL is rejected at upsert with ValueError: a chunk with no groups is a chunk you cannot authorize.
Why "filter later" fails twice
After the model. Filtering the answer after the model has written it is too late. The model has already seen the compensation text. A sentence in the prompt that says "only use chunks this user may see" is the same mistake in a politer form.
After the top 20. Taking the global top 20 by similarity and then dropping forbidden chunks is also too late. If those 20 are compensation bands and the leave policy was candidate 40, the leave policy never enters the window. The forbidden chunks used up the slots even though they were removed.
Where the ACL comes from
Stamp the groups at ingest from the source system, not from the document's own text. A PDF that says "managers only," or "ACL: public," is a claim. The source of truth is the group list on the ingestion job, set by the application from the folder the file was uploaded into. Someone who can edit a document must not be able to change who can retrieve it by typing a line.
The corpus decision comes first
ACL before rank protects what is in the store. It does not fix a store that holds the wrong things. In Mill City Permits, Ruth Ellison owns a named list of guide pages. Applicant files, draft decisions, and fee and pay pages are out, even when they share a folder. "Index the drive and filter at query time" would put pay in the store before any filter ran. On Northline the equivalent wrong call is ingesting comp-1 because it sat beside leave. The employee filter test still passes. The corpus decision was already wrong.
What the tests lock
test_employee_does_not_see_compensation_or_old_leave: an employee asking about leave getsleave-2, notcomp-1and not the tombstonedleave-1.test_manager_can_see_compensation: the same index, a different session, a different result.test_empty_acl_rejected: no groups, no upsert.
These are three of the 31 checks in python3 test_all.py. They prove the filter and its order. They do not prove your corpus list is right.
Related
- Tombstones in retrieval, the other half of the candidate filter.
- Reciprocal rank fusion, the rank that runs inside the filtered set.
- Identity and audit on Week 3, where Harbor Lane billing stays out by a test.
Last technically reviewed: October 2026. Tested with: Python 3, stdlib, SQLite, stand-in model. Not a live provider.