access-control-proof
Skill0 starsChanged 53 days agoMITView on GitHub
Add or audit an authorization gate — tenant scoping, role or group checks, share links, admin-only routes — and prove that it denies. Use whenever a new endpoint, resolver, query, page or link exposes data that belongs to someone, and whenever a review asks whether a gate actually works.
In this repository17 other files it gives its agents.
- agent-instructionsSkill
- contract-changeSkill
- data-change-scriptSkill
- deploy-safetySkill
- design-critiqueSkill
- evidence-layersSkill
- interface-craftSkill
- issue-ledgerSkill
- mobile-field-readinessSkill
- offline-first-syncSkill
- parallel-worktreesSkill
- requirements-elicitationSkill
- root-cause-investigationSkill
- solution-architectureSkill
- ux-state-coverageSkill
- versioned-publishingSkill
- skill-nameSkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 80
Write the list down — anonymous, each role, each tenant, the second user in a tenant, the expired session, the revoked link — and for each …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Proving an access gate
An access gate is not done when it lets the right person through. It is done when you have watched it turn the wrong person away.
1. Scope from the credential, never from the request
The tenant, org, user or account id comes out of the verified token. It never comes out of a path parameter, query string, body field or header that the caller controls.
GET /api/documents?orgId=7 <- the caller chose the org. This is the bug.
GET /api/documents <- org comes from the token claim. Correct.Where a resource id is in the path, the ownership check belongs in the query,
not in a separate if after the fetch:
SELECT * FROM documents WHERE id = :id AND org_id = :org_from_tokenTwo reasons this beats fetch-then-check. It cannot be forgotten on one of the five code paths that load a document. And it returns the same "not found" for "does not exist" and "not yours", which stops the endpoint confirming that someone else's id is real.
Make it structural. One wrapper, helper or base query that every handler goes through, so an unscoped query looks wrong on sight during review.
2. A declared gate can be silently inert
This is the failure worth checking by hand, because everything looks correct:
- A framework directive or decorator that the runtime ignores in the configuration you actually run — for example an auth directive that stops being enforced once more than one auth mode is enabled on the API.
- A middleware registered after the route it was meant to protect.
- A policy attached to a resource that a different, more permissive policy already allows.
- A guard that returns a value instead of throwing, so execution continues.
- A rule in a config file that never loads in the deployed environment.
None of these fail a build. None of them log. The route simply answers.
So: do not conclude a gate works because it is written down. Call the endpoint with the wrong identity and read the response body.
3. The deny-test must use a real, wrong identity
The commonest worthless test is "a request with no credential is rejected". Of course it is — that is the authentication layer, and it was already working. It proves nothing about authorization.
A deny-test needs a credential that is valid and wrong:
| Gate | The credential the test needs |
| Tenant scoping | A real token for tenant B, asking for tenant A's row |
| Role gate | A real token for a signed-in user not in the required role |
| Ownership | A real token for a second user in the same tenant |
| Share link | An expired token, a revoked token, a token for another resource |
Mint these once, keep them as fixtures, and assert on both the status and the body. An empty success response is not a denial — it may mean the filter ran and found nothing, which passes for the wrong reason and will keep passing after someone removes the filter.
For every gate, two tests: one allow, one deny. A gate with only an allow-test is untested.
4. Enumerate the identities before you start
Write the list down — anonymous, each role, each tenant, the second user in a tenant, the expired session, the revoked link — and for each one, what it should see on this endpoint. It takes five minutes and it is what surfaces the case nobody thought about, which is almost always the other user in the same tenant.
5. Public and shared routes are a different surface
A link that works without signing in has no identity to scope by, so everything has to be done at the edges:
- Strip fields. The public view returns a subset, built by listing what goes out. Never serialise the internal object and delete a few keys — the next field someone adds will leak by default.
- Unguessable tokens. Random and long, never derived from the resource id.
- Expiry and revocation, both enforced server-side on every request, and both covered by a test.
- Rate limit by address, because there is no account to limit by.
- Assume the URL will end up in a referrer header, a chat log and a search index.
6. Check the data layer too
Application-level scoping is undone by anything that reads around it: a reporting query, an export job, a search index, an aggregate endpoint, a webhook payload, a cache. Walk the list of everything that reads the table, not just the routes.
Cache keys deserve their own look. A cache keyed on the resource id but not the viewer will serve tenant A's response to tenant B — intermittently, which makes it very hard to believe when it is reported.
Checklist
- Every identifier used for scoping comes from the verified credential
- Ownership is a query condition, not a post-fetch check
- Same response for "not found" and "not yours"
- Gate verified by calling it, not by reading the declaration
- Deny-test per gate, using a valid credential with the wrong identity
- Status and body both asserted; an empty success is not accepted as denial
- Identity matrix written down, including a second user in the same tenant
- Public routes: explicit field allow-list, random token, server-side expiry and revocation, rate limit
- Reports, exports, search indexes and caches scoped too
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

