Rules that read the subject
Not on npm yet
npm install @evanion/acl does not resolve. The package is private: true, so a release run versions and tags it without publishing: npm cannot configure a trusted publisher for a package that does not exist on the registry, and the first version has to go up by hand.
Inside this repository the package resolves from source like any other workspace package, so the example below runs as written.
The concept. A subject-only rule reads the subject the server resolved and
nothing else.
What you get. Every permission answered with no database read in front of
the call.
Why you want it. A role test written by hand in each handler answers
differently per handler once somebody edits one of them.
How the library gets you there. policy() writes the matrix at startup, and
can reads it on every question.
What a subject-only policy covers
A subject-only rule reads the subject the server resolved and nothing else, so
can answers it with no database read in front of it. Baize’s stock report
carries two such rules: any bookseller may read the report, and only the owner
may export it.
The matrix holds both permissions, the server resolves the subject once per request, and one call answers yes or no for any permission in the document.
policy() writes the matrix and hands back an access object over it. can
answers one question, authorize binds the subject, and capabilities answers
the whole document at once.
You write the twentieth role check by hand and four handlers now disagree: one
tests roles.includes('owner'), another tests user.isOwner, a third was
copied before the role was renamed, and the export endpoint tests nothing at
all.
Install acl
@evanion/acl installs as one package and brings nothing with it:
npm install @evanion/aclESM only, Node 20 or newer. No framework reaches the import graph.
Write the acl policy
policy() builds a policy from three calls:
policy<Subject, Objects>()names the subject type and the object kinds..for(key, block)writes one kind’s rules..build()hands back the evaluator.
A block holds allow rules and a rule holds conditions. The engine ANDs the
conditions, so a rule matches when every condition in it holds. p is the
condition helper the block hands you, and p.contains(path, value) reads the
array at subject.roles and asks whether the role is in it. Every condition
below reads the subject, so capabilities answers the whole document at once:
import { } from '@evanion/acl';
import type { } from '@evanion/acl';
type = { : string; : string[] };
// One policy, every object kind the app has. A `.for()` per kind, and the
// permissions they flatten to live in the same flat list.
const = <
,
{ : { : string }; : { : string } },
{ : | 'export' }
>()
.('report', () =>
.('read', .('subject.roles', 'bookseller'))
.('export', .('subject.roles', 'owner')),
)
.('listing', () => .('read', .('subject.roles', 'owner')))
.();
const = .({ : 'u1', : ['bookseller'] });
.(); // -> ['report.read', 'report.export', 'listing.read']
['report.read']?.; // -> true
['report.export']?.; // -> 'no-rule-matched'
['listing.read']?.; // -> 'no-rule-matched'One policy holds every object kind your app has. Chain a .for() per kind, and
do not build a policy per resource.
Nothing is allowed without an allow rule that matched. report.export carries
one such rule, and the bookseller’s roles do not satisfy it, so the decision is
no-rule-matched. Default deny is the floor of the engine and you configure
nothing to get it.
access.matrix is what the blocks flattened to: a JSON envelope over a flat
list of permissions, each carrying a key of exactly
`${object}.${action}`. That document is what crosses to a client, where
hydratePolicy turns it back into an evaluator.
Ask acl a question
can takes the subject, the object kind (report) and the action (read), and
returns one decision:
const decision = access.can(
{ id: 'u1', roles: ['bookseller'] },
'report',
'read',
);
// { key: 'report.read', allowed: true, reason: 'allow', rule: '#0' }allowed is the answer. Gate on it and nothing else. reason is output only,
and a handler that branches on the reason string grants what the engine refused.
Bind the subject to one acl handle
authorize takes the subject once and returns a handle that asks many questions
about it, over the same report-and-listing policy:
import { , type } from '@evanion/acl';
type = { : string; : string[] };
// One policy, every object kind the app has. A `.for()` per kind, and one
// bound handle answers for all of them.
type = { : { : string }; : { : string } };
// `report` grants a verb outside the default CRUD set, so it names its own
// vocabulary. `listing` omits one and takes `Action`.
const = <, , { : | 'export' }>()
.('report', () =>
.('read', .('subject.roles', 'bookseller'))
.('export', .('subject.roles', 'owner')),
)
.('listing', () => .('read', .('subject.roles', 'owner')))
.();
const = .({ : 'u1', : ['bookseller'] });
.('report', 'read').; // -> true
.('report', 'export').; // -> 'no-rule-matched'The handle carries can, canMany, canFields and capabilities with the
subject dropped from each signature, which makes the guarded call the shortest
one to write. That is all the library can do about complete
mediation.
This decision is authoritative only where it is made
The same call in a browser toggles what a user sees and enforces nothing. Hiding a button hides the button, not the request the button would have sent. Read the security contract before this reaches production.
Where subject-only rules stop
The engine settles every permission in a subject-only policy, and two things follow:
- The server resolves the subject whole before the call, so every permission
answers
trueorfalseand nothing answersunevaluable. - Nothing has to be fetched to ask a question, so
cantakes three arguments andcapabilitiesanswers the whole matrix at once.
Write one rule that reads the object, the customer who asked may edit their own question, and both break at once. Rules that read the object picks up there.
Where to go next
- Rules that read the object. The typed builder, field permissions and the guarded save.
- Can this user do this?. The decision shape, and asking about a list.
- What can they do at all?. How a menu draws itself from one call.