Stop a retriever from returning another tenant’s documents

A search helper sends every matching document to a model and asks the model to respect permissions. Repair filtering before any content reaches the model.

Requires Node.js 22 or newer. No package installation or server needed. To work in a container instead, open the folder with VS Code Dev Containers or GitHub Codespaces: .devcontainer/devcontainer.json uses the Dev Containers Node.js 24 image, pinned by digest.
1. Unzip all files into one folder.
2. In that folder, run: node --test exercise.test.mjs
3. Edit retrieval.mjs, then run the same command again.

The starter deliberately fails some tests. A successful repair passes every case, including cases that catch common wrong fixes. The browser runs this same cases.mjs with the same fixtures; locally Node runs your code directly. If you completed the optional first stage in the browser, Download my work adds your input as stage1-input.json and the tests include it. Only run code you wrote or reviewed.

1. Observe the leak
Run the same-query case for two tenants. The starter returns North’s confidential matching document to a South user.

2. Apply access before relevance
Filter on trusted tenant, active state and a group intersection before selecting matching text. Do not let instructions inside a document change this policy.

3. Handle permission changes
An inactive document and a removed group membership must disappear. Do not retain a globally cached result merely because the query is identical.

4. Preserve useful retrieval
A permitted document with matching text must remain in the sorted result. A blanket empty result is not a useful repair.

SUPPLIED CONTRACT
retrieve(user, documents, query) returns a sorted array of document IDs. The user’s tenant and groups are trusted identity data. Each stored document has tenant, groups, active and text. A document is readable only when active is true, tenant matches and at least one group overlaps. Match the query case-insensitively in text after authorization. Missing users receive no results.

REFERENCE
reference.mjs contains an explained solution: For North/staff and query invoice, only n-public passes active=true, tenant equality and group overlap. n-payroll fails the group check; s-report fails tenant; n-retired fails active; n-injection fails group despite text that asks for broader access. South/staff receives only s-report. North/finance with INVOICE receives n-payroll because relevance is case-insensitive after authorization. Removing all groups returns no documents. These result ids stand in for the content that a real retriever would pass onward; no model is invoked.

SCOPE
The exercise uses exact substring relevance and supplied authorization data. It does not run embeddings or a model. A real vector store must enforce equivalent permissions, scoped caches and revocation behavior across indexes, rerankers, citations and logs.

NEXT TASK
Which cache keys and indexing updates carry your authorization scope and permission revision?

SOURCE
https://genai.owasp.org/
