Permission Management RESTHeart Cloud
🔧 Configuration
⚡ Setup Guide
To run the examples on this page, you need a RESTHeart instance.
Option 1: Use RESTHeart Cloud (Recommended)
The fastest way to get started is with RESTHeart Cloud. Create a free service in minutes:
-
Sign up at cloud.restheart.com
-
Create a free API service
-
Set up your root user following the Root User Setup guide
-
Use the configuration panel above to set your service URL and credentials
|
Tip
|
All code examples on this page will automatically use your configured RESTHeart Cloud credentials. |
Option 2: Run RESTHeart Locally
If you prefer local development, follow the Setup Guide to install RESTHeart on your machine.
|
Note
|
Local instances run at http://localhost:8080 with default credentials admin:secret
|
Introduction
This section provides comprehensive information about managing permissions in RESTHeart using the ACL (Access Control List) based authorization system.
RESTHeart’s pluggable security architecture supports different authorizers. This documentation focuses on mongoAclAuthorizer and fileAclAuthorizer, which share the same permission format:
-
mongoAclAuthorizer: Stores permissions as JSON documents in a MongoDB collection (default:
/acl) -
fileAclAuthorizer: Stores permissions in a YAML configuration file
Both authorizers provide a powerful, declarative ACL-based model for fine-grained access control.
|
Tip
|
For an overview of RESTHeart’s security architecture, see Security Overview and Authorization. |
Permission Format
A permission document defines who can access what resources under which conditions. The structure is the same whether stored in MongoDB (JSON) or in a configuration file (YAML).
Permission Properties
| Property | Type | Description |
|---|---|---|
predicate |
string |
A undertow predicate that defines when the permission applies. If the predicate resolves to |
roles |
array of strings |
The roles that this permission applies to. Use the special role |
priority |
number |
When multiple permissions match a request, higher priority permissions are evaluated first. Use this to create hierarchical permission rules. |
mongo |
object or null |
Optional. For requests to MongoDB REST API, specifies additional MongoDB-specific permissions. See MongoDB-Specific Permissions for details. |
_id |
string |
(Optional but recommended) A unique identifier for the permission. Useful for updates and debugging. |
description |
string or array |
(Optional) Human-readable description of what the permission does. |
Basic Permission Example (JSON)
{
"_id": "userCanReadInventory",
"description": "Users can read the inventory collection",
"roles": ["user"],
"predicate": "path('/inventory') and method(GET)",
"priority": 100
}
Basic Permission Example (YAML)
- _id: userCanReadInventory
description: Users can read the inventory collection
roles: [ user ]
predicate: path('/inventory') and method(GET)
priority: 100
Understanding Predicates
Predicates use the undertow predicate language to define conditions that must be met for a permission to apply.
Common Predicate Functions
| Function | Description | Example |
|---|---|---|
|
Matches exact path |
|
|
Matches path prefix |
|
|
Matches path with variables |
|
|
Matches HTTP method |
|
|
Compares two values |
|
|
Matches path with regex |
|
Combining Predicates
Use and, or, and not to create complex conditions:
method(GET) and path-prefix('/api')
(method(GET) or method(POST)) and path('/users')
path-prefix('/admin') and not method(DELETE)
RESTHeart Custom Predicates
RESTHeart extends the undertow predicate language with additional predicates specifically designed for API security:
Query Parameter Predicates
| Predicate | Description | Example |
|---|---|---|
|
Request must include specified query parameters |
|
|
Request must NOT include blacklisted parameters |
|
|
Request must only include whitelisted parameters |
|
|
Request must have exactly N query parameters |
|
Request Body Predicates (BSON/JSON)
| Predicate | Description | Example |
|---|---|---|
|
Request body must contain specified properties |
|
|
Request body can only contain whitelisted properties |
|
|
Request body must NOT contain blacklisted properties |
|
|
Property in request must equal a value |
|
|
Array property must contain all specified values |
|
|
Array property must be subset of allowed values |
|
|
Note
|
When using BSON predicates with values, use valid JSON/BSON syntax: "1" for strings, 1 for numbers, {"foo":"bar"} for objects.
|
Other Custom Predicates
| Predicate | Description | Example |
|---|---|---|
|
Check if value is in array |
|
Using Variables in Predicates
Predicates can reference dynamic values using special variables:
| Variable | Description |
|---|---|
|
Properties of authenticated user (e.g., |
|
Request properties (e.g., |
|
Request body content for fine-grained authorization (e.g., |
|
Query parameter value by key (e.g., |
|
Cryptographically secure random string (hexadecimal, e.g., |
|
Current MongoDB permissions object (e.g., |
|
Current date and time |
|
Value of the |
|
Path template variable (e.g., |
Example: User Can Only Access Their Own Data
{
"_id": "userCanAccessOwnProfile",
"roles": ["user"],
"predicate": "path-template('/{userid}') and equals(@user._id, ${userid})",
"priority": 100
}
This permission:
- Matches paths like /john123, /mary456
- Extracts the {userid} from the path
- Only authorizes if ${userid} equals the authenticated user’s _id
Example: Request Body Predicates
Control authorization based on the content of the request body:
{
"_id": "limitTransactionAmount",
"roles": ["user"],
"predicate": "path('/transactions') and method(POST) and less-than(@request.body.amount, 1000)",
"priority": 100
}
This permission:
- Allows users to create transactions via POST /transactions
- Only authorizes transactions where the amount field is less than 1000
- Requests with amount >= 1000 are rejected with 403 Forbidden
Nested Properties:
{
"_id": "requireCreditCardPayment",
"roles": ["customer"],
"predicate": "path('/orders') and method(POST) and equals(@request.body.payment.method, 'credit_card')",
"priority": 100
}
Array Elements:
{
"_id": "limitFirstItem",
"roles": ["user"],
"predicate": "path('/carts') and less-than(@request.body.items.0.quantity, 10)",
"priority": 100
}
|
Note
|
Missing properties and non-JSON/BSON requests resolve to null, implementing fail-safe denial semantics.
|
Example: Query Parameter Access with @qparams
Authorize requests based on query parameter values:
{
"_id": "userCanFilterOwnCategory",
"roles": ["user"],
"predicate": "path('/products') and method(GET) and equals(@qparams['category'], @user.category)",
"priority": 100
}
This permission:
- Allows users to query products via GET /products?category=electronics
- Only authorizes if the category query parameter matches the user’s assigned category
- Example: User with category: "electronics" can only query ?category=electronics
Example: Secure Random Strings with @rnd
Generate cryptographically secure random values for tokens, OTPs, and API keys:
User Registration with OTP:
{
"_id": "userSignup",
"roles": ["$unauthenticated"],
"predicate": "path('/users') and method(POST)",
"priority": 100,
"mongo": {
"mergeRequest": {
"otp": "@rnd(32)",
"verified": false,
"role": "pending"
}
}
}
This permission:
- Allows unauthenticated users to register via POST /users
- Automatically generates a 32-bit random OTP (8 hexadecimal characters)
- Sets initial user status as verified: false and role: "pending"
Account Verification:
{
"_id": "verifyAccount",
"roles": ["pending"],
"predicate": "path-template('/users/{userid}/verify') and method(PATCH) and equals(@user._id, ${userid}) and equals(@user.otp, @qparams['otp'])",
"priority": 100,
"mongo": {
"mergeRequest": {
"verified": true,
"role": "user"
}
}
}
This permission:
- Allows pending users to verify their account via PATCH /users/{userid}/verify?otp=abc123def
- Validates that the OTP in the query parameter matches the stored OTP
- Upon success, escalates user to role: "user" and sets verified: true
Complete OTP Workflow:
-
User registers:
POST /users→ System generates and stores OTP -
User receives OTP via email/SMS (handled by your application)
-
User verifies:
PATCH /users/{userid}/verify?otp={received-otp} -
System validates OTP from
@qparams['otp']against@user.otp -
On match: user is verified and granted full access
Other Use Cases for @rnd:
{
"mongo": {
"mergeRequest": {
"apiKey": "@rnd(128)", // 128-bit API key (32 hex chars)
"resetToken": "@rnd(64)", // Password reset token
"sessionId": "@rnd(256)" // Session identifier
}
}
}
|
Important
|
@rnd() uses java.security.SecureRandom for cryptographic security. The parameter specifies bits, resulting in bits/4 hexadecimal characters (e.g., @rnd(32) = 8 hex characters).
|
MongoDB-Specific Permissions
For requests handled by RESTHeart’s MongoDB REST API, you can specify additional fine-grained permissions using the mongo object.
MongoPermissions Structure
{
"mongo": {
"allowManagementRequests": false,
"allowBulkPatch": false,
"allowBulkDelete": false,
"allowWriteMode": false,
"readFilter": {"status": "public"},
"writeFilter": {"author": "@user._id"},
"mergeRequest": {"author": "@user._id", "createdAt": "@now"},
"projectResponse": {"password": 0, "secret": 0}
}
}
MongoPermissions Properties
| Property | Description |
|---|---|
|
Allows database/collection management operations (create/delete databases, collections, indexes, etc.). Default: |
|
Allows bulk PATCH operations (updating multiple documents). Default: |
|
Allows bulk DELETE operations (deleting multiple documents). Default: |
|
Allows using the |
|
Automatically adds filter conditions to all read operations |
|
Limits write operations to documents matching the filter |
|
Automatically merges properties into write request bodies |
|
Removes (or includes) specific fields from responses |
|
Important
|
Even if allowManagementRequests, allowBulkPatch, allowBulkDelete, or allowWriteMode is set to true, the permission’s predicate must also resolve to true for the request to be authorized.
|
readFilter
The readFilter adds a filter condition to all read operations (GET requests) authorized by this permission. This is useful for data partitioning by role.
{
"roles": ["user"],
"predicate": "method(GET) and path-prefix('/posts')",
"mongo": {
"readFilter": {
"$or": [
{"status": "public"},
{"author": "@user._id"}
]
}
}
}
Users with the user role can only see posts that are either public OR authored by themselves.
writeFilter
The writeFilter limits write operations (PATCH, PUT, DELETE) to documents matching the specified condition.
{
"roles": ["user"],
"predicate": "method(PATCH) and path-prefix('/posts')",
"mongo": {
"writeFilter": {"author": "@user._id"}
}
}
|
Warning
|
writeFilter only restricts updates to existing documents. It cannot prevent creating documents that don’t match the filter. Use mergeRequest to enforce properties on creation.
|
mergeRequest
The mergeRequest automatically merges specified properties into the request body. This ensures certain fields are set server-side and cannot be overridden by clients.
{
"roles": ["user"],
"predicate": "method(POST) and path('/posts')",
"mongo": {
"mergeRequest": {
"author": "@user._id",
"status": "draft",
"createdAt": "@now"
}
}
}
When a user creates a post, these properties are automatically added:
- author is set to the user’s ID
- status is set to "draft"
- createdAt is set to the current timestamp
|
Tip
|
Use mergeRequest together with writeFilter to ensure data integrity. Set required fields on creation, then restrict updates using the filter.
|
projectResponse
The projectResponse removes (or includes) specific fields from API responses. This is useful for hiding sensitive data.
Negative projection (hide specific fields):
{
"roles": ["user"],
"predicate": "path-prefix('/users')",
"mongo": {
"projectResponse": {"password": 0, "secret": 0, "internal.data": 0}
}
}
Positive projection (only show specific fields):
{
"roles": ["public"],
"predicate": "path-prefix('/users')",
"mongo": {
"projectResponse": {"name": 1, "email": 1, "avatar": 1}
}
}
|
Note
|
You can use dot notation to hide nested properties: "internal.secret": 0
|
Permission Priority
When multiple permissions match a request, RESTHeart evaluates them in order of priority (higher numbers first).
[
{
"_id": "adminFullAccess",
"roles": ["admin"],
"predicate": "path-prefix('/')",
"priority": 0,
"mongo": {
"allowManagementRequests": true,
"allowBulkPatch": true,
"allowBulkDelete": true,
"allowWriteMode": true
}
},
{
"_id": "userLimitedAccess",
"roles": ["user"],
"predicate": "path-prefix('/')",
"priority": 100,
"mongo": {
"readFilter": {"author": "@user._id"}
}
}
]
In this example:
- userLimitedAccess (priority 100) is evaluated before adminFullAccess (priority 0)
- If a user has both roles, the higher priority permission applies first
|
Tip
|
Use priority to create exception rules. For example, give high priority to specific restrictions and low priority to general access rules. |
Complete Permission Examples
Example 1: User Can Read Own Documents
{
"_id": "userCanGetOwnCollection",
"description": "Users can read their own collection with pagination",
"roles": ["user"],
"predicate": "method(GET) and path-template('/{userid}') and equals(@user._id, ${userid}) and qparams-contain(page) and qparams-blacklist(filter, sort)",
"priority": 100,
"mongo": {
"readFilter": {
"$or": [
{"status": "public"},
{"author": "@user._id"}
]
},
"projectResponse": {"log": 0}
}
}
This permission:
- Allows users to GET their own collection (e.g., /john123)
- Requires the page query parameter
- Blocks filter and sort query parameters
- Only returns documents that are public OR authored by the user
- Hides the log field from responses
Example 2: User Can Create Documents in Own Collection
{
"_id": "userCanCreateDocumentsInOwnCollection",
"description": "Users can create documents in their collection",
"roles": ["user"],
"predicate": "method(POST) and path-template('/{userid}') and equals(@user._id, ${userid})",
"priority": 100,
"mongo": {
"mergeRequest": {
"author": "@user._id",
"status": "draft",
"createdAt": "@now"
}
}
}
This permission:
- Allows users to POST to their own collection
- Automatically sets author, status, and createdAt fields
- Prevents users from setting these fields to arbitrary values
Example 3: User Can Update Own Documents
{
"_id": "userCanUpdateOwnDocuments",
"description": "Users can update their own documents",
"roles": ["user"],
"predicate": "method(PATCH) and path-template('/{userid}/*') and equals(@user._id, ${userid})",
"priority": 100,
"mongo": {
"writeFilter": {"author": "@user._id"},
"mergeRequest": {
"modifiedAt": "@now",
"modifiedBy": "@user._id"
}
}
}
This permission:
- Allows users to PATCH documents in their collection
- Only allows updating documents where author matches the user’s ID
- Automatically sets modifiedAt and modifiedBy on updates
Example 4: Admin Full Access
{
"_id": "adminCanDoEverything",
"description": ["Admin role can do everything"],
"roles": ["admin"],
"predicate": "path-prefix('/')",
"priority": 0,
"mongo": {
"allowManagementRequests": true,
"allowBulkPatch": true,
"allowBulkDelete": true,
"allowWriteMode": true
}
}
Example 5: Public Read-Only Access
{
"_id": "publicCanReadProducts",
"description": "Unauthenticated users can read public products",
"roles": ["$unauthenticated"],
"predicate": "method(GET) and path('/products')",
"priority": 100,
"mongo": {
"readFilter": {"published": true, "visibility": "public"},
"projectResponse": {"internalNotes": 0, "cost": 0}
}
}
Example 6: JWT-based Multi-tenant Access
- _id: jwtTenantAccess
roles: [ jwt-user ]
predicate: >
path-template('/{tenant}/data')
and in(value=${tenant}, array=@user.tenants)
priority: 100
mongo:
readFilter: >
{"tenantId": "${tenant}"}
mergeRequest: >
{"tenantId": "${tenant}", "userId": "@user.sub"}
This permission (YAML format):
- Extracts {tenant} from path like /acme/data
- Checks if user’s JWT contains tenant in their tenants array
- Filters data by tenantId
- Automatically adds tenantId and userId to write requests
Managing Permissions
With mongoAclAuthorizer (MongoDB Storage)
List All Permissions
cURL
curl -i -X GET [RESTHEART-URL]/acl \
-H "Authorization: Basic [BASIC-AUTH]"
HTTPie
http GET [RESTHEART-URL]/acl \
Authorization:"Basic [BASIC-AUTH]"
JavaScript
const credentials = btoa(`${username}:${password}`);
fetch('[RESTHEART-URL]/acl', {
method: 'GET',
headers: { 'Authorization': `Basic ${credentials}` }
})
.then(response => response.json())
.then(data => console.log('Permissions:', data));
Create a Permission
cURL
curl -i -X POST [RESTHEART-URL]/acl \
-H "Authorization: Basic [BASIC-AUTH]" \
-H "Content-Type: application/json" \
-d '{
"_id": "userCanReadInventory",
"roles": ["user"],
"predicate": "method(GET) and path-prefix(\"/inventory\")",
"priority": 100
}'
HTTPie
http POST [RESTHEART-URL]/acl \
Authorization:"Basic [BASIC-AUTH]" \
Content-Type:application/json \
_id="userCanReadInventory" \
roles:='["user"]' \
predicate="method(GET) and path-prefix(\"/inventory\")" \
priority:=100
JavaScript
const credentials = btoa(`${username}:${password}`);
fetch('[RESTHEART-URL]/acl', {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
"_id": "userCanReadInventory",
"roles": ["user"],
"predicate": "method(GET) and path-prefix(\"/inventory\")",
"priority": 100
})
})
.then(response => {
if (response.ok) console.log('Permission created');
});
Update a Permission
cURL
curl -i -X PATCH [RESTHEART-URL]/acl/userCanReadInventory \
-H "Authorization: Basic [BASIC-AUTH]" \
-H "Content-Type: application/json" \
-d '{
"priority": 200
}'
HTTPie
http PATCH [RESTHEART-URL]/acl/userCanReadInventory \
Authorization:"Basic [BASIC-AUTH]" \
Content-Type:application/json \
priority:=200
Delete a Permission
cURL
curl -i -X DELETE [RESTHEART-URL]/acl/userCanReadInventory \
-H "Authorization: Basic [BASIC-AUTH]"
HTTPie
http DELETE [RESTHEART-URL]/acl/userCanReadInventory \
Authorization:"Basic [BASIC-AUTH]"
With fileAclAuthorizer (Configuration File)
You can configure ACL permissions in two ways with fileAclAuthorizer:
Option 1: Inline Permissions (in main configuration file)
Define permissions directly in your main RESTHeart configuration file:
fileAclAuthorizer:
permissions:
- _id: adminFullAccess
roles: [ admin ]
predicate: path-prefix('/')
priority: 0
mongo:
allowManagementRequests: true
allowBulkPatch: true
allowBulkDelete: true
- _id: userCanReadInventory
roles: [ user ]
predicate: method(GET) and path-prefix('/inventory')
priority: 100
- _id: userCanCreateOwnData
roles: [ user ]
predicate: >
method(POST)
and path-template('/{userid}')
and equals(@user._id, ${userid})
priority: 100
mongo:
mergeRequest: >
{"author": "@user._id", "createdAt": "@now"}
Option 2: Separate ACL Configuration File
Define permissions in a separate configuration file:
fileAclAuthorizer:
conf-file: ./acl.yml
Then create acl.yml:
- _id: adminFullAccess
roles: [ admin ]
predicate: path-prefix('/')
priority: 0
mongo:
allowManagementRequests: true
allowBulkPatch: true
allowBulkDelete: true
- _id: userCanReadInventory
roles: [ user ]
predicate: method(GET) and path-prefix('/inventory')
priority: 100
- _id: userCanCreateOwnData
roles: [ user ]
predicate: >
method(POST)
and path-template('/{userid}')
and equals(@user._id, ${userid})
priority: 100
mongo:
mergeRequest: >
{"author": "@user._id", "createdAt": "@now"}
After editing the configuration, restart RESTHeart to apply changes.
Configuration
mongoAclAuthorizer Configuration
mongoAclAuthorizer:
acl-db: restheart
acl-collection: acl
# Users with root-role bypass all ACL checks
root-role: admin
cache-enabled: true
cache-size: 1000
cache-ttl: 5000
cache-expire-policy: AFTER_WRITE
fileAclAuthorizer Configuration
fileAclAuthorizer:
# Path to ACL configuration file (absolute or relative to restheart config)
conf-file: ./acl.yml
# Or define permissions inline:
permissions:
- role: admin
predicate: path-prefix('/')
priority: 0
Best Practices
1. Use Descriptive IDs and Descriptions
{
"_id": "userCanReadOwnPosts",
"description": "Users with 'user' role can read posts they authored or public posts",
...
}
2. Leverage Priority for Exception Handling
Use high priority for specific rules, low priority for general rules:
[
{
"_id": "blockSensitiveEndpoint",
"roles": ["user"],
"predicate": "path('/admin/secrets')",
"priority": 1000,
"mongo": null
},
{
"_id": "allowGeneralAccess",
"roles": ["user"],
"predicate": "path-prefix('/admin')",
"priority": 100
}
]
3. Combine writeFilter and mergeRequest
Always use both together to ensure data integrity:
{
"mongo": {
"writeFilter": {"author": "@user._id"},
"mergeRequest": {"author": "@user._id"}
}
}
4. Test Permissions Thoroughly
Start with restrictive permissions and gradually add more access. Test with different user roles and edge cases.
5. Use projectResponse to Hide Sensitive Data
{
"mongo": {
"projectResponse": {
"password": 0,
"ssn": 0,
"creditCard": 0
}
}
}
6. Whitelist Instead of Blacklist When Possible
Prefer bson-request-whitelist and qparams-whitelist over blacklists for better security:
bson-request-whitelist(name, email, age) // Only these fields allowed
7. Use Path Templates for User-Scoped Resources
path-template('/{userid}/documents') and equals(@user._id, ${userid})
8. Document Your Permission Strategy
Maintain a clear permission hierarchy and document the roles and their intended access levels.
Common Patterns
Pattern 1: User-Owned Resources
{
"_id": "userCRUDOwnResources",
"roles": ["user"],
"predicate": "path-template('/{userid}/*') and equals(@user._id, ${userid})",
"priority": 100,
"mongo": {
"writeFilter": {"author": "@user._id"},
"readFilter": {"author": "@user._id"},
"mergeRequest": {"author": "@user._id"}
}
}
Pattern 2: Public Read, Authenticated Write
[
{
"_id": "publicCanRead",
"roles": ["$unauthenticated"],
"predicate": "method(GET) and path-prefix('/posts')",
"priority": 100,
"mongo": {
"readFilter": {"status": "published"}
}
},
{
"_id": "userCanWrite",
"roles": ["user"],
"predicate": "(method(POST) or method(PATCH)) and path-prefix('/posts')",
"priority": 100,
"mongo": {
"mergeRequest": {"author": "@user._id"}
}
}
]
Pattern 3: Multi-Tenant with JWT
- _id: tenantDataAccess
roles: [ tenant-user ]
predicate: >
path-template('/{tenantId}/data')
and in(value=${tenantId}, array=@user.tenants)
priority: 100
mongo:
readFilter: >
{"tenantId": "${tenantId}"}
writeFilter: >
{"tenantId": "${tenantId}"}
mergeRequest: >
{"tenantId": "${tenantId}"}
Pattern 4: Time-Based Access
{
"_id": "timeLimitedAccess",
"roles": ["premium"],
"predicate": "path-prefix('/premium-content')",
"priority": 100,
"mongo": {
"readFilter": {
"$or": [
{"expiresAt": {"$gt": "@now"}},
{"expiresAt": {"$exists": false}}
]
}
}
}
Troubleshooting
Permission Not Applied
-
Check if the
predicatematches your request -
Verify the user has the specified
roles -
Check if a higher priority permission is overriding it
-
Ensure the collection/file is correctly configured in the authorizer settings
writeFilter Not Preventing Creation
writeFilter only restricts updates to existing documents. Use mergeRequest to enforce fields on creation and bson-request-whitelist to limit allowed fields.
Variables Not Resolving
Ensure you’re using the correct syntax:
- @user._id for user properties
- ${variable} for path template variables
- Quotes around strings: value='"draft"'