Rules

A rule is a filter that names the caller. It is compiled into the SQL that reads or writes the rows, so a row somebody may not see never leaves SQLite.

Where rules go

Each collection has five, one per action. Leave one out and only superusers may do it; "" lets anybody.

migrations/V1__init.json
"rules": {
  "list":   "@request.auth.id != null",
  "view":   "@request.auth.id != null",
  "create": "author = @request.auth.id",
  "update": "author = @request.auth.id",
  "delete": "author = @request.auth.id || @request.auth.staff = true"
}

A field can have two of its own: visible, who may read it, and writable, who may set it. Both default to whoever the collection already let in.

migrations/V1__init.json
{ "name": "mark", "type": "number",
  "visible":  "@request.auth.id = pupil || @request.auth.staff = true",
  "writable": "@request.auth.roles ~ \"examiner\"" }

The language

The same one filters use:

Compare= != > >= < <=
Contains, or doesn’t~ !~
Combine&& || and parentheses
Values"text", numbers, true, false, null
The recordits field names: author, org, status
The caller@request.auth.…
Moments@now, @days_ago.30, @years_ago.13

What a rule can say about the caller

  • @request.auth.id, collection, email and superuser come from the token and cost nothing.
  • Any other field of the caller’s own record works too: @request.auth.staff, @request.auth.org. Sluurp reads that record only when a rule asks, by its id, in the same transaction.
  • @request.auth.roles is a set. @request.auth.roles ~ "admin" asks whether “admin” is one of them, so it never matches “superadmin”.
  • For somebody who isn’t signed in, every one of these is null. A rule written for members is simply false for them, never an error.
@request.auth.staff = true && org = @request.auth.org && id != @request.auth.id

Staff may delete people in their own organisation, but not themselves.

How they are enforced

  • Lists filter. A caller gets the rows they may see, and the counts are right.
  • View, update and delete answer 404 when the rule says no, so a rule can’t be used to find out that a row exists.
  • Writes are checked twice. The row is checked before the change and again inside the transaction after it. A change that would put a row out of the writer’s own reach is rolled back.
  • A rule about the caller alone is settled before the query. @request.auth.roles ~ "admin" becomes true or false and the query never sees it. A rule about the record becomes part of the query’s WHERE.
  • Field rules too. A field that nobody in the caller’s position may see is not selected. One that depends on the row is blanked row by row, in SQL.

Asking before trying

GET /api/acl answers, for the signed-in caller, what each collection lets them do: allow, deny, or conditional (they may, for some rows, such as their own). A screen uses it to show only the buttons that can work. It is the same rules asked in advance, not a second set, so the two can’t disagree.