Ulabase

Your users

Permissions

Who may do what in your service, by role: rules with a path, a method and a priority, plus filters that limit a user to their own documents.

A permission says what a role may do: which paths, which methods, and optionally which documents. Every request to your service is checked against them on the server, where the client cannot bypass them. Permissions are documents in the acl collection, managed under Permissions.

The Permissions page: each rule with its roles

How a request is checked

Every request is checked against the permissions in the acl collection:

  1. Only the permissions for the user’s roles count.

  2. They are tried in descending priority order: higher numbers are evaluated first.

  3. The first one whose predicate matches the request (path, method, query string and so on) allows it, and its mongo options apply.

  4. If none matches, the request is refused.

The path on Enterprise

Plan ACL collection path

Free, Starter and Pro

/acl

Enterprise

/restheart/acl

In the console the page is always Permissions, whatever the plan.

A permission document

{
  "_id": "usersCanReadOrders",
  "roles": ["user"],
  "predicate": "path-prefix('/orders') and method('GET')",
  "priority": 100,
  "mongo": {
    "readFilter": { "owner": "@user._id" },
    "writeFilter": null,
    "mergeRequest": null,
    "projectResponse": null
  }
}
Field Required Description

_id

Yes

A unique name for the permission. Make it say what it does, such as readersCanGetPosts.

roles

Yes

The role names the permission applies to. A user who holds at least one of them is covered. Roles are set on each user, on the Users page.

predicate

Yes

The condition a request must meet for the permission to match. See the predicate language below.

priority

Yes

The evaluation order. Higher values are evaluated first, so give the more specific permissions the higher numbers.

mongo

No

Filters and changes applied to the data the request reads or writes. See below.

apiKeys

No

Only on a permission for /keys: which roles an API key issued under it may carry, and its limits. See API Keys below.

The list

The page lists the permissions with their roles, predicate and priority, a page at a time.

Filter and sort

Type a MongoDB query in the Filter field, for example:

{ "roles": "user" }

or

{ "priority": { "$lt": 50 } }

The Sort field takes a sort document, such as {"priority": -1}.

Add a permission

  1. Click Add permission.

  2. Fill in the form fields:

    • Permission ID (_id): a unique name.

    • Roles: one or more role names, separated by commas.

    • Predicate: which requests it is about (see below). The console checks it as you type.

    • Priority: higher numbers are evaluated first.

    • MongoDB permissions: optionally, readFilter, writeFilter, mergeRequest and projectResponse, as JSON.

    • API key issuance: optionally, the apiKeys block of a permission on /keys. See API keys.

  3. Click Create permission. The console sends POST /acl with the document.

Edit a permission

  1. Click Edit on a permission. The form opens in place.

  2. Change any field except the ID.

  3. Click Save. The console sends PUT /acl/<id> with the whole document.

Delete a permission

Click Delete on a permission and confirm. The console sends DELETE /acl/<id>.

Warning
A deleted permission stops working within 20 seconds, the time any change to a permission takes to apply. Users whose access relied on it alone are refused from then on.

The predicate language

A predicate is a condition on the request, written in the Undertow predicate language. These are the ones you will use most:

Paths

Predicate Description Example

path('/exact/path')

Matches the exact request path.

path('/status')

path-prefix('/prefix')

Matches any path that starts with the given prefix.

path-prefix('/orders')

path-template('/users/{id}')

Matches a path with variable segments. Refer to a captured segment later in the predicate as ${id}.

path-template('/users/{id}/profile')

path-suffix('.json')

Matches paths ending with the given suffix.

path-suffix('.csv')

regex(pattern: '…​', value: %R)

Matches the path against a regular expression.

regex(pattern: '^/data/[0-9]+$', value: %R)

Methods

Predicate Example

method('GET')

method('POST')

method('PUT')

method('DELETE')

method('PATCH')

Combining them

Combine predicates with and, or and not:

path-prefix('/orders') and method('GET')
path-prefix('/reports') and (method('GET') or method('POST'))
path-prefix('/admin') and not method('GET')

Common predicates

Goal Predicate

Allow GET on any path

method('GET')

Allow all methods on /orders and sub-paths

path-prefix('/orders')

Allow POST only to /users

path('/users') and method('POST')

Allow GET and PATCH on /products/{id}

path-template('/products/{id}') and (method('GET') or method('PATCH'))

Allow everything under /public

path-prefix('/public')

Allow every method except DELETE, on every path

not method('DELETE')

Allow a user to edit only their own document

path-template('/users/{userId}') and method(PATCH) and (equals(@user._id, ${userId}) or equals(@user.sub, ${userId}))

…​ and only one field of it

…​ and bson-request-whitelist(consents)

The @user.sub branch above covers users authenticating with a JWT, who carry the account id in the sub claim. bson-request-whitelist takes one or more comma-separated keys, dotted for nested ones (bson-request-whitelist(profile.name, profile.surname)), and checks the request body as the client sent it, before any mergeRequest adds to it.

Tip
The Predicate reference panel at the bottom of the Permissions page lists the predicates, and the help icon beside the Predicate field opens this section.

Limit what a user sees and writes

The optional mongo object acts on the data. It applies to every request the permission allows, and the client does not see it.

readFilter

A MongoDB filter added to every read (GET). The client sees only the documents that match it, whatever filter it sends.

Each user sees only their own documents:

{
  "mongo": {
    "readFilter": { "owner": "@user._id" }
  }
}

@user._id is replaced with the _id of the user making the request.

writeFilter

A MongoDB filter applied to every write (PUT, PATCH, DELETE). A write reaches only the documents that match it, and nothing tells the client.

Users cannot change other users' documents:

{
  "mongo": {
    "writeFilter": { "owner": "@user._id" }
  }
}

mergeRequest

A document merged into the body of every write before it is stored. Use it for fields the server sets and the client cannot forge.

A new document is stamped with its owner:

{
  "mongo": {
    "mergeRequest": { "owner": "@user._id" }
  }
}

Write sub-documents as nested objects, without dotted keys. MongoDB rejects a merged request that touches both a field and a path inside it, with ConflictingUpdateOperators.

Update operators in mergeRequest

Plain fields are merged into $set. A MongoDB update operator can be merged too, written with a leading underscore because a key starting with $ cannot be stored in a document. _$push becomes $push when the request is merged.

Append to a history the server controls, which the client cannot overwrite:

{
  "mongo": {
    "mergeRequest": {
      "lastSeenAt": "@now",
      "_$push": { "auditTrail": { "at": "@now", "by": "@user._id" } }
    }
  }
}

The client sends the field only to satisfy whatever bson-request-whitelist the predicate carries; the value it sends for a key that an array operator manages is discarded, so the two cannot conflict.

projectResponse

A MongoDB projection applied to every document in the response. Use it to hide sensitive fields.

Hide sensitive fields:

{
  "mongo": {
    "projectResponse": { "passwordHash": 0, "internalNotes": 0 }
  }
}

Variables you can use

Variable Value at runtime

@user._id

The _id of the authenticated user.

@user.roles

The roles array of the authenticated user.

@now

The current instant, as a BSON date. In a JSON Schema, declare it as {"_$date": {"type": "number"}}.

API keys

A request that authenticates with an API key goes through the same permissions as a password sign-in, with one difference: it carries the key’s roles, which can differ from the roles of the user who issued it. A key’s roles are usually narrower than its owner’s, and each one is an ordinary role that you grant permissions to like any other. readFilter, projectResponse and the rest apply as usual.

Which roles a key may be given is decided here too. A permission that lets a role reach /keys carries an apiKeys block:

{
  "_id": "usersIssueKeys",
  "roles": ["user"],
  "predicate": "path-prefix('/keys')",
  "priority": 100,
  "apiKeys": {
    "roles": ["cli-ro"],
    "max-expires-in-days": 90,
    "max-keys-per-user": 5
  }
}
Field Meaning

roles

The roles a key issued under this permission may carry. Required: a non-empty array of distinct role names.

max-expires-in-days

The longest life such a key may have. Optional; the service default applies otherwise, and a user may always ask for less.

max-keys-per-user

How many keys one user may hold at a time. Optional; the service default applies otherwise.

Three things to know:

  • A permission on /keys without the block issues nothing. Its roles reach the endpoint and get a 403 that says so.

  • A malformed block is refused when the permission is written, with a 400 that names the field, whether it comes from the console, ulabase or curl.

  • The key’s roles need not be roles the user holds. With the example above a user may issue a cli-ro key, and what that key can do is whatever other permissions grant to cli-ro.

The API Keys page lists the permissions on /keys with the roles each one grants and adds new ones from three fields. They are the same documents you see on the Permissions page.

Priority

Permissions with higher priority numbers are evaluated first. For each of the user’s roles, the first permission that matches decides, together with its mongo options.

Priority value Typical use

200 and up

Specific exceptions that must win over a standard permission: an exact path, a single method.

100

Standard application permissions.

1 to 99

Catch-all or fallback permissions.

0

rootCanDoEverything, the only permission of the root role.

Tip
When two permissions of the same role match a request, the one with the higher number decides. Give the most specific ones the highest numbers.

A worked example: orders

Three permissions for a typical order system:

Allow admins to do everything on /orders
{
  "_id": "adminsFullOrders",
  "roles": ["admin"],
  "predicate": "path-prefix('/orders')",
  "priority": 10
}
Allow users to create orders (with auto-stamped owner)
{
  "_id": "usersCreateOrders",
  "roles": ["user"],
  "predicate": "path('/orders') and method('POST')",
  "priority": 100,
  "mongo": {
    "mergeRequest": { "owner": "@user._id" }
  }
}
Allow users to read only their own orders
{
  "_id": "usersReadOwnOrders",
  "roles": ["user"],
  "predicate": "path-prefix('/orders') and method('GET')",
  "priority": 100,
  "mongo": {
    "readFilter": { "owner": "@user._id" }
  }
}

From a script

Operation Endpoint

List permissions

GET /acl?page=…​&pagesize=…​&filter=…​&sort=…​

Create permission

POST /acl

Update permission

PUT /acl/<id> with the whole document, or PATCH /acl/<id> with the fields to change

Delete permission

DELETE /acl/<id>

On Enterprise plans replace /acl with /restheart/acl in all paths above.

  • Users: the users and the roles permissions refer to.

  • Guards: rules checked after permissions, on the user’s state.

  • Root user: where the rootCanDoEverything permission comes from.

  • Users and permissions through the API: the requests behind this page.

  • API keys: the apiKeys block, and what a request made with a key may do.

  • Plans: the collection paths that change on Enterprise.