Skip to Content
AuthorizationChecking permissions in your UI

Unreleased changes

These pages document main, which has changes 0.1.2 does not. npm install @evanion/acl gives you 0.1.2, documented here. Values on these pages are produced by running the current source on every build.

Checking permissions in your UI

An action bar calls access.can once per button it might draw and draws the buttons whose decision is allowed. The rules stay in the policy, so the component holds a list of actions and tests no role itself.

The listing policy behind Baize’s action bar

Baize’s page for the Brass: Birmingham listing draws an action bar with three buttons: Review, Edit and Publish. Baize’s stock service owns the listing rules, and there are four:

  • Everyone signed in reviews a listing.
  • A bookseller or the owner edits a listing.
  • The owner publishes a listing.
  • Nobody edits a published listing.

stock builds them with policy() and serves stock.matrix as JSON. The storefront adopts that document with parseMatrix, which builds an evaluator from a document another service served and answers unknown-action for an action the document does not carry. The action bar asks that evaluator:

import { , } from '@evanion/acl'; /** Who is signed in. `role` is what the policy reads. */ interface Shopper { : 'customer' | 'bookseller' | 'owner'; } /** A game listing in the shop. A status, because that is all a rule reads. */ interface Listing { : 'draft' | 'published'; } // `stock` writes the listing rules and serves `stock.matrix` as JSON. const = < Shopper, { : Listing }, { : 'review' | 'edit' | 'publish' } >() .('listing', () => .('review', .) .('edit', .('subject.role', ['bookseller', 'owner'])) .('publish', .('subject.role', ['owner'])) .('edit', .('object.status', 'published')), ) .(); // The storefront adopts the document it fetched, and the action bar asks it. const = <Shopper, { : Listing }>(.); const : Shopper = { : 'bookseller' }; const : Listing = { : 'draft' }; .(, 'listing', 'review', ).; // -> true .(, 'listing', 'edit', ).; // -> true .(, 'listing', 'publish', ).; // -> 'no-rule-matched' .(, 'listing', 'edit', { : 'published' }).; // -> 'denied' .(, 'listing', 'edit', {}).; // -> 'unevaluable' .(, 'listing', 'archive', ).; // -> 'unknown-action'

The third type argument to policy() names the actions each object kind may declare. A kind it leaves out may declare only create, read, update and delete, so review, edit and publish are listed there, and a misspelled p.allow('publsh', …) fails to compile.

Two condition helpers are new in that block:

  • p.always holds for every subject, so review is granted to anybody the storefront asks about.
  • p.in(path, values) holds when the value at path is one of values. Shopper carries role as one string, so p.in tests it against the roles written in the rule.

.deny('edit', …) reads object.status off the listing in hand. A deny that matches outranks every allow that matches with it, so nobody edits a published listing, the owner included.

What Baize’s action bar draws for each shopper

The bar loops over review, edit and publish, calls access.can(shopper, 'listing', action, listing) for each, and draws a button where allowed is true. A granted decision carries the reason allow. Every reason the four rules produce, per shopper and per listing status:

Signed in asListing statusrevieweditpublish
customerdraftallowno-rule-matchedno-rule-matched
booksellerdraftallowallowno-rule-matched
ownerdraftallowallowallow
customerpublishedallowdeniedno-rule-matched
booksellerpublishedallowdeniedno-rule-matched
ownerpublishedallowdeniedallow

Publishing the listing moves the bar with no change to the policy. The status becomes published, the deny matches, and edit answers denied for every shopper. The customer gets denied too, where no allow would have matched, because the engine checks the deny rules before the allow rules.

A fourth button takes a rule in stock’s policy, with its action added to the third type argument, and one more action in the bar’s list. A role test in the component is a rule the policy cannot see, and the handler the button posts to never reads it.

Try Baize’s action bar against the listing policy

The demo draws the listing’s action bar from the four rules. Sign in as each of the three shoppers and the bar rebuilds itself from access.can. The panel shows the roles each p.in lists: press a role to strike it out of that list, and the button that rule produced disappears. Press Publish and the deny matches, so neither the bookseller nor the owner is offered Edit.

Mika Persson

Brass: Birminghamdraft

Network building on the canals and railways of the Midlands, two to four players, about two hours. Complexity 3.9 of 5. A copy is on the shelf and another sits on table three most evenings.

  • Jo VainioPlayed it twice at the back tables before buying. Worth the shelf space.
Signed in as Mika Persson, bookseller. Review, Edit.

access

const access = policy<Shopper, ShopObjects, ShopVerbs>()
  .for('listing', (p) =>
    p.allow('review', p.always)
      .allow('edit',
        p.in('subject.role', [, , ]))
      .allow('publish',
        p.in('subject.role', [, , ]))
      .deny('edit', p.eq('object.status', 'published'))
      .id('published-is-locked'),
  )
  .build();

The demo builds its policy with the same policy() call stock runs and asks it directly, and each button is one decision’s allowed.

Only the server’s acl decision guards a listing write

The decisions the action bar draws are convenience. The stock service runs the same can in its own runtime, over a subject it resolved itself, before any listing is edited or published. A hidden button leaves the handler it posts to reachable, so that handler decides for itself.

Wording a refused button from the acl decision

A bar that greys a button out, where Baize’s action bar removes it, has to say why. allowed still decides whether the button works, and reason only picks the words. Baize’s action bar can get four refusals:

reasonWhat happenedWhat the bar does
deniedA deny matched, and rule names it.Says the listing is published.
no-rule-matchedNo allow matched.Says which role the action needs.
unevaluableThe listing arrived without status, so the deny went unread.Fetches what missing names and asks again. The shopper may hold the action.
unknown-actionThe document stock served carries no such action.Hides the button. stock dropped the action, or the storefront’s copy predates it.

Where to go next

Last updated on