Why deny, then allow, then deny?
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.
The concept. decideResolved settles one permission in seven steps that
run in a fixed order.
What you get. The seven steps written out, and which of the four outcomes each one produces.
Why you want it. No option reorders them, so a rule you read in one service decides the same way in every other.
How the library gets you there. decideResolved reads
permission.denyRules and permission.rules, the deny side first.
The order acl resolves a permission in
The deny side is permission.denyRules and the allow side is
permission.rules. Rules within a side are OR-ed, so one match settles that
side, and any number of rules can match at once. Four outcomes cover most calls:
| Deny side | Allow side | Decision |
|---|---|---|
| matches | anything | denied |
| fails | matches | allow |
| fails | fails | no-rule-matched |
| unevaluable | not failing | unevaluable, naming both sides |
libs/acl/src/evaluate.ts defines the full order, and
Decisions has the shape each step hands back:
- a deny rule matches →
denied - the allow side definitely fails →
no-rule-matched - the deny side reads an unusable clock →
unusable-clock, naming the deny rule - the deny side is unevaluable →
unevaluable, naming the deny rule - an allow rule matches →
allow - the allow side reads an unusable clock →
unusable-clock, naming the allow rule - the allow side is unevaluable →
unevaluable
Why acl reads the deny side twice
Step 2 answers no-rule-matched ahead of the deny side’s undecided cases,
because no fetch repairs a definite “no allow rule matched”.
Steps 3 and 4 sit above step 5, so a deny side held up by an unreadable path or
by a now that does not parse reports itself and hands back no allow. An
undecided deny turns an allow into an unevaluable and leaves a definite
no-allow alone. Every branch below step 1 answers allowed: false, so no
undecided path leaks a grant.
Why acl declines last-declaration-wins
CASL resolves the other way. _indexAndAnalyzeRules walks the raw rules
backwards so the last declaration is scanned first, and relevantRuleFor
returns the first conditional match, inverted or not. CASL’s guide names the
hazard: “the order of rules matters: cannot declarations should follow after
can, otherwise they will be overridden by can”.
A matrix is a frozen JSON document one service emits and another
adopts with parseMatrix. Three things break there under order-dependence:
- Identical rules in a different order decide differently, so a producer that reorders its own output changes what a consumer grants.
federatedPolicieswould resolve two origins by the order their documents were concatenated in.- A reviewer could not say what a matrix grants without reading every rule in sequence, and matrix review is the only control over an over-broad grant.
Under step 1 a deny wins wherever it sits in the array, so array order carries no meaning.
Deny-overrides, and the names XACML gave it
acl runs deny-overrides and exposes no setting. Four other engines put that choice where a caller can change it:
| Engine | How it combines | Where that choice lives | What it answers |
|---|---|---|---|
| CASL | the last matching declaration, can or cannot | the code that built the ability | a boolean |
| XACML 3.0 | one of the named algorithms, deny-overrides among them | the policy, per policy set | Permit, Deny, NotApplicable, Indeterminate |
| AWS IAM | deny-overrides, over an implicit deny | fixed by the service | a boolean |
| Casbin | whatever [policy_effect] names | the model file, per deployment | a boolean |
| acl | deny-overrides, and nothing configures it | nowhere: the seven steps above are the engine | allow, a refusal, or unevaluable |
A consumer adopting a foreign matrix inherits whichever algorithm the producer picked. Three of those engines write the algorithm down:
- XACML 3.0. OASIS named the combining algorithms in Appendix C of the
eXtensible Access Control Markup Language (XACML) 3.0: deny-overrides,
permit-overrides, first-applicable and only-one-applicable, alongside ordered
and
deny-unless-permitvariants. XACML states deny-overrides as “if a single<Rule>or<Policy>element is encountered that evaluates to ‘Deny’, then, regardless of the evaluation result of the other … elements in the applicable policy, the combined result is ‘Deny’”. - AWS IAM. “By default, all requests are implicitly denied”, a policy must explicitly allow them, and “An explicit deny overrides an explicit allow.” Those are steps 1, 2 and 5 with the undecided steps taken out.
- Casbin. The
[policy_effect]section takes allow-override, deny-override, the conjunction of the two, orpriority(p.eft) || deny, which is first-applicable.
The third state, and what XACML calls Indeterminate
unevaluable is acl’s third answer: the engine could not reach a decision with
the data it was handed, and missing names the paths to fetch. Steps 3 and 4
separate that case from a refusal, and CASL, AWS IAM and Casbin all answer a
boolean.
XACML is the exception. Its decision values are Permit, Deny, NotApplicable and
Indeterminate, and its extended Indeterminate{D}, {P} and {DP} variants
travel through the combining algorithms. § 7.3.5 says that where a required
attribute is not found “the result is Indeterminate, and a MissingAttributeDetail
element SHOULD be included in the Status”. Nobody checked for this page whether a
deployed implementation returns those details.
Why a fixed order is what lets every layer re-decide
acl in a browser toggles what a user sees and enforces nothing. A trusted environment makes the decision that counts, and every app in the chain evaluates for itself and trusts no earlier layer, which the security contract argues in full.
Every step above reads the permission and the context, and no array order, construction option or per-process setting. A browser, the server behind it and a downstream service holding the same document and subject reach the same decision, so a layer that re-decides confirms the answer it was handed.
Where to go next
- Decisions — the decision object,
unevaluableas a third state, and the clock - Why was this refused? — which of the seven reasons you got
- Security contract — the trust boundary every layer re-decides across