Resources and inheritance¶
Every grant and every check in perms.io names a resource URI. A resource URI is a /-separated path that you
choose to identify something in your application. Your URIs don't need to match your app's real URLs; they only
need to be consistent.
/documents
/documents/42
/documents/42/comments/7
/tenants/acme/projects/web-app
Rules for URIs¶
A resource URI:
- starts with
/; - has no empty segments, so no
//and no trailing/; - contains no whitespace or control characters;
- is at most 2048 bytes long;
- contains none of these characters in any segment:
( ) [ ] { } * + ? | ^ $ \.
A request that breaks these rules is rejected with InvalidArgument. If your IDs can contain reserved characters,
percent-encode each segment. The TypeScript SDK's resource() helper does
this for you.
Grants apply to everything beneath them¶
A grant on a URI applies to that URI and every URI below it. Matching works segment by segment, so a grant on
/documents covers /documents/42 but not /documents-archive.
/documents ← grant document.read to alice here
├── /documents/42 ✔ alice can read
│ └── /documents/42/comments/7 ✔ alice can read
└── /documents/43 ✔ alice can read
/invoices/1 ✘ not covered
| Grant on | Check on | Result |
|---|---|---|
/documents |
/documents |
allowed |
/documents |
/documents/42 |
allowed |
/documents |
/documents/42/comments/7 |
allowed |
/documents/42 |
/documents |
denied (grants don't flow upwards) |
/documents |
/invoices/1 |
denied |
Inheritance also covers resources that didn't exist when the grant was made. Grant on a tenant or a folder once, and everything you later create beneath it is covered.
The _ wildcard segment¶
A segment that is exactly _ in a stored grant matches exactly one segment, whatever its value:
| Grant on | Check on | Result |
|---|---|---|
/documents/_/comments |
/documents/42/comments |
allowed |
/documents/_/comments |
/documents/42/comments/7 |
allowed (beneath a match) |
/documents/_/comments |
/documents/42 |
denied (the grant is deeper) |
/documents/_/comments |
/documents/42/attachments |
denied |
In a URI being checked, _ is literal. It is not a wildcard, and it only matches grants made with _ in that
position. You can't widen a check by sending _.
How a check is decided¶
A check asks: does principal P hold every permission in a list on every resource in a list, within one project?
- A permission counts if the principal holds it directly, or through a role, in a grant on the resource or on any
ancestor of it (including
_matches). - If every permission is held on every resource, the check passes (
passed: true). - Otherwise it fails (
passed: false) andmissing_permissionslists, per resource, the permissions that weren't found.
A failed check is a normal response, not an error. Checks fail closed: a URI with no applicable grant is denied.
Grants belong to a project. A check in one project never sees grants made in another.
Designing your URIs¶
- Put the thing you grant on at the top. If you usually grant per tenant, start URIs with the tenant:
/tenants/{tenantId}/.... - Use stable IDs, not names that can change.
- Build URIs the same way everywhere. A grant on
/Documents/42doesn't cover/documents/42. - Check the most specific URI for the object being accessed. Inheritance takes care of broader grants.
/tenants/acme ← tenant admins
/tenants/acme/projects/web-app ← project members
/tenants/acme/projects/web-app/documents/42 ← what your code checks