Rules about who the user is
@evanion/acl 0.1.0 and its patch releases up to 0.1.2, the version on npm. Values on this page were produced by running the source at 40b8f84, a later commit than the 0.1.0 tag, and are not re-executed.
In a real application, authorization is rarely as simple as “all staff can do X.” You often need to handle exceptions—such as preventing a suspended employee from accessing sensitive reports—or differentiate between different levels of staff.
When these rules depend only on the user’s properties (like their roles or account status), they are called subject-only rules. Because they don’t need to know which specific object is being accessed, they are extremely fast and can be evaluated without any database reads.
Handling exceptions with deny rules
While p.allow grants permission, you often need a way to explicitly revoke it. @evanion/acl provides p.deny for this purpose. A deny rule is a “hard stop”: if a deny rule matches, the permission is refused, even if an allow rule also matches.
Consider the Baize shop’s stock report. We want any staff member with the bookseller role to read the report, unless their account has been suspended.
import { } from '@evanion/acl';
// Anyone signed in to Baize: a customer, or a bookseller on the shop's staff.
type = { : string; : string[] };
type = { : string; : string };
type = { : string };
// Baize suspends an account by adding the `suspended` role, and the account
// keeps `bookseller`. The deny sits beside the allow it overrides, and `.id()`
// names each rule so a decision can report which one decided.
const = <, { : ; : }>()
.('question', () =>
.('delete', .('subject.roles', 'bookseller')),
)
.('report', () =>
.('read', .('subject.roles', 'bookseller'))
.('booksellers-read')
.('read', .('subject.roles', 'suspended'))
.('suspended-reads-nothing'),
)
.();
const = { : 'staff-3', : ['bookseller'] };
const = { : 'staff-5', : ['bookseller', 'suspended'] };
const = { : 'customer-41', : ['customer'] };
.(, 'report', 'read').; // -> 'allow'
.(, 'report', 'read').; // -> 'denied'
.(, 'report', 'read').; // -> 'suspended-reads-nothing'
.(, 'report', 'read').; // -> 'no-rule-matched'In this policy, the report.read permission is decided by two rules:
- An allow rule that grants access to anyone with the
booksellerrole. - A deny rule that refuses access to anyone with the
suspendedrole.
When can() is called, the engine checks deny rules first. If a user is both a bookseller and suspended, the deny rule wins, and the result is denied. If they are only a bookseller, they are allowed. If they are neither, the result is no-rule-matched (the default deny).
Identifying rules with .id
By default, @evanion/acl generates an internal hash of the rule’s conditions as the ID for every rule. The hash stays stable and names the rule in a log, but it tells a reader nothing when debugging.
You can use .id(name) to give a rule a human-readable name. For example, naming a rule suspended-reads-nothing allows your application to log exactly why a user was blocked, making it much easier to provide helpful feedback to the user or support staff.
The scope of subject-only rules
A subject-only rule is a powerful tool because it is completely decoupled from the resource. This leads to two major advantages:
- Instant Answers: Because the server resolves the subject (the user) once per request, subject-only rules can be evaluated immediately without triggering additional database queries.
- Pre-flight Checks: You can ask about these permissions before you’ve even fetched a specific object. For example, a navigation menu can use subject-only rules to decide whether to show the “Admin Dashboard” link without needing to know which specific admin resource is being targeted.
However, once you need to check a relationship—such as “Can this user edit this specific question?”—you have moved beyond the subject. You now need to read the object itself.
Rules about the thing being accessed explains how to write conditions that compare the user to the resource.