Files
base/docs/rules.md
T

5.8 KiB

Access rules and identity

A collection has five rules: listRule, viewRule, createRule, updateRule and deleteRule. They are all of Base's authorization. Base checks them in Go on every /v1 request. A list rule becomes part of the query, so a row the caller may not see is neither returned nor counted.

Rule values

Value Who passes A caller who fails gets
null, the default superusers 403 Only superusers can perform this action.
"" everyone, signed in or not
an expression callers for whom it is true list: a page with no items; view, update, delete: 404; create: 400 Failed to create record.

An expression is a filter over the record and the request:

@request.auth.id != ""
owner = @request.auth.id
status = "published" || owner = @request.auth.id
@request.body.role:isset = false
Part Forms
Compare = != > >= < <= ~ (contains) !~
Compare any of several values ?= ?!= ?> ?>= ?< ?<= ?~ ?!~
Combine && || and parentheses
The request @request.auth.* @request.body.* @request.query.* @request.headers.* @request.method @request.context
Another collection @collection.<name>.<field>
Modifiers :isset :changed :length :each :lower

?filter= on list requests uses the same syntax.

Rules also decide what ?expand= returns and what the /v1/rest/{table} reads return. A route you add in a hook applies no rule on its own. Call $app.canAccessRecord(record, e.requestInfo(), collection.listRule) for each record, as the hybrid search example does.

Where identity comes from

Base signs nobody in. It has no passwords, no sign-up and no sessions of its own. A caller is whoever a Hanzo IAM access token says, sent as Authorization: Bearer <token>.

Point Base at IAM to turn this on:

IAM_ENDPOINT=https://hanzo.id ./base serve --http 127.0.0.1:8090

With IAM_ENDPOINT set, Base:

  • verifies every token against IAM's keys at $IAM_ENDPOINT/v1/iam/.well-known/jwks, and treats a token that fails as no token;
  • ignores tokens signed any other way, including those an older Base issued;
  • passes /v1/iam/* through to IAM;
  • lists IAM as the one sign-in provider at GET /v1/collections/users/auth-methods, with IAM's authorize URL and a PKCE challenge;
  • deletes X-Org-Id, X-User-Id and X-User-Email from every request as it arrives. X-Org-Id is read first, as the org the caller asks for.

For the length of one request, the token becomes an auth record that is never saved:

Field From the token
id sub (else preferred_username): kept when it is 15 characters of a-z, 0-9 and _, padded with _ when shorter, otherwise the first 24 hex characters of its SHA-256
email email
name name, else displayName
org_id the org the request acts in, when the auth collection has an org_id field
admin whether the caller administers that org (its IAM role there is owner or admin)
role the caller's role in that org: owner, admin or member

Rules read these as @request.auth.id, @request.auth.email and so on, so "only the org's admins delete" is @request.auth.admin = true.

Superusers are the members of IAM's admin org, and nobody else. Their record is mirrored into _superusers; everyone else's into users. A superuser passes every rule, manages collections and settings, and can act in any org. An admin role inside an ordinary org is a different thing and grants none of this.

An org's admins change its schema. In an org's Base, an org admin creates, changes and deletes the org's ordinary base collections through /v1/collections (never auth, view or system collections, nor Base's own _ tables) and installs modules (POST /v1/modules/crm). Every member reads the schema.

Each org has its own database. Base takes the org from the token's memberships and opens <data dir>/orgs/<org>/data.db for the request. Two orgs never share a file, so a query in one cannot return another's rows. A token that belongs to several orgs picks one with X-Org-Id: <org>. Naming an org the token does not carry gets 403, and so does a token with no org.

Keys stand in for tokens when no person is signing in. IAM issues them. sk- keys are for servers. pk- keys are for web pages and reach only routes marked public, such as record lists and views.

Without IAM

Leave IAM_ENDPOINT unset and the base binary serves one Base with no orgs. There is no sign-in route, so a caller without a token cannot get one: every such caller is anonymous and none is a superuser. That is enough to build against collections whose rules admit anonymous callers. Define the collections in migrations, because /v1/collections needs a superuser.

A users token Base signed before sign-in moved to IAM still counts. A superuser token does not; versions.md says why.

Such a Base prints this when it starts:

(!) No superuser exists yet. Create the first one with:
    ./base superuser upsert EMAIL PASS

That command was removed. Superusers come from IAM.

Who can add users rows

The users collection's createRule is null, so only superusers add rows. Before v1.5.98 a new Base set it to "", which let anyone add them. The upgrade sets null where all five users rules are still the ones Base created, and leaves any other rule set alone. To close one that kept "", run a migration:

migrate((app) => {
  const users = app.findCollectionByNameOrId("users")
  users.createRule = null
  app.save(users)
})

After it runs, an anonymous POST /v1/collections/users/records gets 403.

Rate limits and CORS have defaults to change too. See threat-model.md.