= Permissions :nav-title: Permissions :description: 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. :keywords: access control, permissions, ACL, role-based access, row-level security, authorization backend, readFilter :group: Your users :order: 20 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, so the client cannot talk its way past. Permissions are documents in the `acl` collection, managed under **Permissions**. image::/assets/docs-images/permissions.png[The Permissions page: each rule with its roles, predicate and priority] == How a request is checked RESTHeart's `mongoAclAuthorizer` evaluates every incoming HTTP request against the ACL documents stored in the `acl` collection. The authorizer: 1. Finds all ACL documents whose `roles` array intersects with the authenticated user's roles. 2. Among those, finds documents whose `predicate` matches the request (path, method, query string, etc.). 3. Evaluates them in ascending `priority` order — **lower numbers are evaluated first**. 4. If any matching rule permits the request, it is allowed; otherwise it is denied. === The path on Dedicated [cols="1,2"] |=== | Plan | ACL collection path | Free / Shared | `/acl` | Dedicated | `/restheart/acl` |=== The UI abstracts this difference — you always navigate to *Service → Permissions* regardless of plan. == A permission document [source,json] ---- { "_id": "usersCanReadOrders", "roles": ["user"], "predicate": "path-prefix('/orders') and method('GET')", "priority": 100, "mongo": { "readFilter": { "owner": "@user._id" }, "writeFilter": null, "mergeRequest": null, "projectResponse": null } } ---- [cols="1,1,3"] |=== | Field | Required | Description | `_id` | Yes | Unique identifier for this permission rule. Choose a descriptive name (e.g. `readersCanGetPosts`). | `roles` | Yes | Array of role name strings. The rule applies to any authenticated user who holds at least one of these roles. Roles are defined on user documents in the xref:managing-users.adoc[Users] section. | `predicate` | Yes | An Undertow predicate expression that must evaluate to `true` for the rule to match the incoming request. See <> below. | `priority` | Yes | Integer evaluation order. Lower values are evaluated first. Use lower numbers for more specific (higher-priority) rules. | `mongo` | No | Optional object with MongoDB-level security modifiers. See <> below. | `apiKeys` | No | Only on a permission for `/keys`: which roles an API key issued under it may carry, and its limits. See <> below. |=== == The list The ACL list is fetched via `GET /acl?page=...&pagesize=...` and displayed in a paginated, searchable table. Each row shows the `_id`, the `roles`, the `predicate`, the `priority`, and action buttons. === Search and filter Use the **Search** box to apply a MongoDB filter expression, for example: [source,json] ---- { "roles": "user" } ---- or [source,json] ---- { "priority": { "$lt": 50 } } ---- A **Sort** expression (JSON object) can also be applied, mapping to the `sort` query parameter. == Add a permission . Click **New Permission**. . Fill in the form fields: ** **ID** (`_id`) — unique name for the rule. ** **Roles** — one or more role names this rule applies to. ** **Predicate** — the Undertow predicate expression (see below). ** **Priority** — integer; lower evaluates first. ** **MongoDB Options** — optionally configure `readFilter`, `writeFilter`, `mergeRequest`, and `projectResponse`. . Click **Save**. The UI submits `POST /acl` with the document body. == Edit a permission . Click **Edit** on a permission row to expand the inline accordion form. . All fields except `_id` are editable. . Click **Save**. The UI issues `PATCH /acl/` with the updated fields. == Delete a permission Click **Delete** on a permission row. A confirmation dialog appears before the `DELETE /acl/` request is issued. WARNING: Deleting a permission rule takes effect immediately. Any users whose access relied solely on that rule will be denied on the next request. == The predicate language [[predicate-language]] The `predicate` field uses the **Undertow predicate expression language**. A predicate is a boolean expression that is evaluated against the incoming HTTP request. The following predicates are available: === Paths [cols="1,2,2"] |=== | 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 parameterised paths; a captured segment is referenced 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 [cols="1,2"] |=== | Predicate | Example | `method('GET')` | `method('POST')` | `method('PUT')` | `method('DELETE')` | `method('PATCH')` | |=== === Combining them Use `and`, `or`, and `not` to compose complex rules: [source] ---- path-prefix('/orders') and method('GET') ---- [source] ---- path-prefix('/reports') and (method('GET') or method('POST')) ---- [source] ---- path-prefix('/admin') and not method('GET') ---- === Common predicates [cols="2,3"] |=== | 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')` | Deny DELETE everywhere (use `not`) | `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, where the account id is carried in the `sub` claim rather than in `_id`. `bson-request-whitelist` takes one or more comma-separated keys, dotted for nested ones (`bson-request-whitelist(profile.name, profile.surname)`), and matches the request body as the client sent it — before any `mergeRequest` enriches it. TIP: Use the **Predicate helper** tooltip inside the permission form — it shows common predicate patterns inline without leaving the page. == Limit what a user sees and writes [[mongo-options]] The optional `mongo` object adds MongoDB-level security on top of the predicate match. These modifiers are applied transparently to every request that matches the rule. === `readFilter` A MongoDB filter document appended to every read (`GET`) query. Clients can only see documents that match the filter, regardless of any `filter` parameter they provide. *Use case — row-level security:* [source,json] ---- { "mongo": { "readFilter": { "owner": "@user._id" } } } ---- The placeholder `@user._id` is replaced at runtime with the authenticated user's `_id`. This ensures each user sees only documents they own. === `writeFilter` A MongoDB filter document applied to every write operation (`PUT`, `PATCH`, `DELETE`). Write operations are silently restricted to documents matching the filter. *Use case — prevent writing to other users' documents:* [source,json] ---- { "mongo": { "writeFilter": { "owner": "@user._id" } } } ---- === `mergeRequest` A document merged into every write request body before it is persisted. Use this to automatically inject server-enforced fields that clients should not be able to forge. *Use case — auto-stamp ownership on document creation:* [source,json] ---- { "mongo": { "mergeRequest": { "owner": "@user._id" } } } ---- Write sub-documents as nested objects rather than with dotted keys. A merged request that touches both a field and a path inside it is rejected by MongoDB with `ConflictingUpdateOperators`. ==== Update operators in `mergeRequest` Plain fields are merged into `$set`. A MongoDB update operator can be merged as well — written with a **leading underscore**, since the key would otherwise be an illegal one in a stored document. `_$push` becomes `$push` when the request is merged. *Use case — append to a server-controlled history, without letting the client overwrite it:* [source,json] ---- { "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 response document before it is sent to the client. Use this to mask sensitive fields. *Use case — hide sensitive fields:* [source,json] ---- { "mongo": { "projectResponse": { "passwordHash": 0, "internalNotes": 0 } } } ---- === Variables you can use [cols="1,3"] |=== | 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. Declare it in a JSON Schema as `{"_$date": {"type": "number"}}`, not as a string. |=== == API keys [[api-keys]] A request that authenticates with an xref:api-keys.adoc[API key] goes through the same ACL as a password sign-in, with one difference: it carries the **key's roles**, not 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: [source,json] ---- { "_id": "usersIssueKeys", "roles": ["user"], "predicate": "path-prefix('/keys')", "priority": 100, "apiKeys": { "roles": ["cli-ro"], "max-expires-in-days": 90, "max-keys-per-user": 5 } } ---- [cols="1,3"] |=== | 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, `rhc` 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 Rules with **lower** priority numbers are evaluated **first**. This is the opposite of what "priority" means in everyday language — think of it as _evaluation order_ rather than _importance_. [cols="1,3"] |=== | Priority value | Typical use | `0` | Root / super-admin rules that must always win (e.g. `rootCanDoEverything`). | `1–99` | Highly specific rules (exact paths, narrow method sets). | `100` | Standard application rules. | `1000+` | Catch-all or fallback rules. |=== TIP: When two rules both match a request, the first one evaluated (lowest priority number) takes precedence. Structure your rules so the most specific rules have the lowest numbers. == A worked example: orders The following three ACL documents implement a typical order-management permission model: .Allow admins to do everything on `/orders` [source,json] ---- { "_id": "adminsFullOrders", "roles": ["admin"], "predicate": "path-prefix('/orders')", "priority": 10 } ---- .Allow users to create orders (with auto-stamped owner) [source,json] ---- { "_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 [source,json] ---- { "_id": "usersReadOwnOrders", "roles": ["user"], "predicate": "path-prefix('/orders') and method('GET')", "priority": 100, "mongo": { "readFilter": { "owner": "@user._id" } } } ---- == From a script [cols="2,3"] |=== | Operation | Endpoint | List permissions | `GET /acl?page=...&pagesize=...&filter=...&sort=...` | Create permission | `POST /acl` | Update permission | `PATCH /acl/` | Delete permission | `DELETE /acl/` |=== On **Dedicated** plans replace `/acl` with `/restheart/acl` in all paths above. == Related pages * xref:managing-users.adoc[Managing Users] — create the users and roles referenced in ACL rules. * xref:guards.adoc[Guards] — rules evaluated *after* authorization, for conditions that depend on the user's state rather than their role. * xref:root-user-setup.adoc[Root User Setup] — the initial `rootCanDoEverything` permission is created here. * xref:users-and-permissions.adoc[Users and Permissions (API reference)] — API-level permission examples. * xref:api-keys.adoc[API Keys] — the `apiKeys` block, and what a request made with a key may do. * xref:plans.adoc[Dedicated vs. Free/Shared Plans] — understand collection path differences between plans. * link:{restheart-docs}/security/permissions[Permission Management (full reference)] — deep-dive into the `mongoAclAuthorizer`, predicate language, and advanced patterns.