Charter · access

What you are not granted does not exist.

Most access control puts a check in front of each thing, and every check is a place to forget one. In Nisc a screen is a whole unit of data: what you see, what you can press, what it calls. So Charter decides which screens your app is built from. Anything the charter does not mention was never put in, and there is nothing left to guard.

pnpm add @niscorp/charter · 0.1.4 · Apache-2.0

One charter, and the app each person is given

Who signs in

The charter: every role there is

{
  guest: {
    actions: ['welcome', 'programme'],
    data: ['shows.read'],
  },
  member: {
    actions: ['programme', 'seats', 'me.*'],
    data: [
      'shows.read',
      'bookings.*',
      'passes.read',
      'members.read',
    ],
    scoping: 'personal',
  },
  usher: {
    actions: ['programme', 'door.*'],
    data: ['shows.read', 'bookings.read', 'members.read'],
  },
  manager: {
    extends: ['usher'],
    actions: { allow: ['*'], deny: ['welcome', 'me.*'] },
    data: ['*.read'],
  },
}
Their appmember
Screens in it
programme
seats
me.tickets
me.pass

5 of the app’s screens are not in it

What the database will answer them
shows.read
bookings.read
bookings.write
passes.read
members.read
How far their reads go
personal

A member. me.* is every screen whose id starts with me., and bookings.* is every grant on that table. scoping says how far her reads go: her own rows.

Asked to open takings, which they were not given, their app answers:

UnknownActionError: Unknown action: takings

There is no check in front of that screen that could be skipped, because the screen is not there. This page builds each app the way Moss does on a server: a shell that holds the granted screens and nothing else. The sentence in orange is that shell’s own.

First steps →
The idea

Decided once, where the app is put together.

When you sign in, your roles are resolved to a list of screens, and the server builds your app from that list. Nothing checks you again on the way to a screen, because a screen you were not given is not there to reach. The default is no: what no role mentions, nobody gets.

No check to forgetNo screen has a line in it that asks who you are. A screen you were not given is not in your app, so there is nothing to get past.
The default is noA new screen reaches nobody until a role names it. Forgetting to grant something shows a person less, never more.
One place to readWhat a role can reach is a list. It can be printed, reviewed and compared with the last release.
Data

The same document decides what the database answers.

A role also lists what it may do with each table. Vex is handed that list as its policy for you, and a table your roles do not mention has no rule to be read by. Here one stored read, who is coming on Saturday, is asked as each of four people, against Postgres in this page.

Postgres is starting in this page and loading the theatre’s bookings. It is the real database, compiled to run in a browser, so the first time takes a few seconds. Nothing is sent to a server.

Before it starts

A wrong charter does not start.

Before the server starts, a verifier reads the charter against the screens and tables that really exist. A deny that matches nothing is an error. A typo there leaves something unprotected, and nothing else would ever say so. Moss refuses to start on any error.

The charter

The role that was changed

{
  member: {
    actions: {
      allow: ['*'],
      deny: ['welcome', 'door.*', 'members', 'takings', 'settngs'],
    },
    data: [
      'shows.read',
      'bookings.*',
      'passes.read',
      'members.read',
    ],
    scoping: 'personal',
  },
}
Does not start
dead-deny: role "member" denies "settngs" (actions) which matches nothing — silent means unprotected

Somebody rewrote the member role as every screen minus the staff ones, and mistyped settings. Without the verifier every member would get the settings screen, and nothing would fail.

In Nisc

Charter decides. Moss and Vex enforce.

Charter resolves names and checks the document. It does not run while a request is served.

MossBuilds each person’s app on the server when they sign in, from their granted screens only. It refuses to start on a charter the verifier rejects.
VexIs handed your table grants as its policy. Which rows a granted read returns is Vex’s too, set by how far the role reaches.
NovaA shell opens only the screens it was given. Nova has no charter of its own: an app that runs wholly in the browser holds whatever the page ships.
The assistantRuns as the user it works for, so it gets the same screens and the same data. Give it roles of its own to narrow that.
Layout variantsA role can be granted a richer version of a screen. Moss puts it in when it builds that person’s app.
Your sideThe charter and the table of who has which role are yours. Charter stores nothing and knows nothing about sign-in.
How it is built

It only knows names.

Charter never learns what a screen or a table is. It is handed a list of names and a role, and answers with the names that role selects. That is why screens, table grants, layout variants and the functions a screen calls all go through one function. It is also how it grows: to govern something new, whatever enforces it lists its names and takes the answer back. The grammar of a role does not change. What can be governed this way is anything that is set up as data and treats a missing entry as no.

Screens
handed in
  welcome
  programme
  seats
  me.tickets
  me.pass
  door.list
  members
  takings
  settings

the role
  member: ['programme', 'seats', 'me.*']

comes back
  programme
  seats
  me.tickets
  me.pass
Table grants
handed in
  shows.read
  bookings.read
  bookings.write
  passes.read
  members.read

the role
  manager: ['*.read']

comes back
  shows.read
  bookings.read
  passes.read
  members.read
Tools, a list of your own
handed in
  bookings.find
  bookings.cancel
  shows.find
  refunds.issue

the role
  desk: ['bookings.*', 'shows.find']

comes back
  bookings.find
  bookings.cancel
  shows.find
One wildcardA star matches anything, dots included. There is no other pattern. When a rule cannot be written, the fix is a better id.
Deny winsInside a role a deny beats an allow. A role that extends another gets everything it has.
No dependenciesThe package imports nothing. It does not store, serve or enforce.