# 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..` | | 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](../examples/hooks-hybrid-search/hooks/search.js) 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 `. Point Base at IAM to turn this on: ```sh 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`, or `visitor` (below) | 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 `/orgs//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: `. Naming an org the token does not carry gets `403`, and so does a token with no org. **Visitors.** A Base its host opened for one org (a site's) refuses a token that acts in another org, with `403`, everywhere except where a collection admits visitors. A base collection does with `"visitors": true`, which a superuser or the org's admin sets through `/v1/collections`. On such a collection a visitor may list, view, create, update and delete records and read their files, as its rules allow. Its auth record has `role` `visitor`, `admin` false, `org_id` its own org, and `id` and `name` as above, so a comment that is its writer's alone reads: ``` createRule: @request.auth.id != "" && author = @request.auth.id updateRule: author = @request.auth.id deleteRule: author = @request.auth.id ``` A visitor reaches nothing else: no other collection, not the schema, settings, batch, realtime, aggregates, history or a file token, and naming the Base's org in `X-Org-Id` is still `403`. Where a visitors' collection relates to another, that collection's rules read the visitor as a guest, so an expand or a filter through the relation shows only what a guest would see. Visitors' writes are held, in each Base, to ten a minute from one account, fifteen from one client address, and 120 from all visitors together. A Base's GraphQL schema is the caller's too: a member sees every collection, a visitor only those that admit visitors, and anyone else only those whose list rule is `""`. A superuser is never a visitor; it names the 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](hooks.md#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](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: ```js 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](threat-model.md).