Saga Documentation 0.9.609-2

Access Control

Table of Contents

Permission Model

Access control is facilitated through roles. Each role contains multiple permissions. The permission model has two clean namespaces: route permissions (endpoint access) and model permissions (field-level data access with optional document filters).

For detailed documentation on how complex permissions interact — deny rules with filters, wildcard + deny combinations, evaluation order, and enforcement mechanics — see Permission Evaluation.

Each permission has:

  • path — the resource path
  • action — the permitted action
  • allow — whether to allow or deny
  • filter — (optional) a MongoDB query that scopes access to specific documents

Permission Namespaces

Route Permissions

Route permissions gate access to HTTP endpoints and socket events. Paths are prefixed with /routes/.

Actions: get, post, put, delete, *

{"path": "/routes/bots", "action": "get", "allow": true}

This grants access to /routes/bots only. To also cover subpaths like /routes/bots/123, use a trailing wildcard:

{"path": "/routes/bots/*", "action": "get", "allow": true}

Multiple permissions accumulate:

[
  {"path": "/routes/bots/*", "action": "get", "allow": true},
  {"path": "/routes/bots/*", "action": "post", "allow": true}
]

Deny permissions override allows for matching paths:

[
  {"path": "/routes/bots/*", "action": "*", "allow": true},
  {"path": "/routes/bots/21312", "action": "*", "allow": false}
]

A * in a non-trailing position matches any single path segment:

{"path": "/routes/users/*/properties", "action": "get", "allow": true}

A trailing * matches the parent path and all deeper subpaths:

{"path": "/routes/bots/*", "action": "get", "allow": true}

Model Permissions

Model permissions gate data access at the field level. Paths follow the format /models/<model>/<field|*>.

Actions: read, write, delete, *

Grant read access to all fields on users:

{"path": "/models/users/*", "action": "read", "allow": true}

Grant write access to specific fields only:

[
  {"path": "/models/users/username", "action": "write", "allow": true},
  {"path": "/models/users/email", "action": "write", "allow": true}
]

Grant delete access on a model (field segment is * for delete):

{"path": "/models/users/*", "action": "delete", "allow": true}

The delete action means "can delete documents from this model." Methods like find, create, etc. are implementation details — permissions gate the data, not the mechanism.

Capability Permissions

Capability permissions control sandbox features for runnables (scripts/jobs).

{"path": "/capabilities/network", "action": "read", "allow": true}
{"path": "/capabilities/require/lodash", "action": "read", "allow": true}

Role Assignment Permissions

Controls which roles a user or runnable can assign to others:

{"path": "/roles/ROLE_ID/assign", "action": "write", "allow": true}

Filters

The optional filter field on a permission is a MongoDB query object that scopes which documents the permission applies to.

Filter Examples

User can only read/write their own user document:

{"path": "/models/users/*", "action": "*", "allow": true, "filter": {"_id": "auth_id"}}

User can only read/write their own properties:

{"path": "/models/user_properties/*", "action": "*", "allow": true, "filter": {"parent_id": "auth_id"}}

Runnable can only read bots tagged 'npc':

{"path": "/models/bots/*", "action": "read", "allow": true, "filter": {"tags": "npc"}}

Filter Enforcement

  • Reads: Filters are ANDed into the query. Documents outside the filter are invisible.
  • Writes: For updates, the filter is ANDed into the query so only matching documents are modified.
  • Deletes: The filter is ANDed into the delete query so only matching documents can be deleted.
  • Multiple permissions with different filters: Union semantics — filters are ORed together.
  • Permissions without filters: Apply to all documents (no restriction).

auth_id Substitution

The special token auth_id in permission paths and filter values is replaced at evaluation time. For HTTP requests, it is replaced with the authenticated user's ID. For runnables (scripts/jobs), it is replaced with the triggering entity's ID — the user, bot, or parent that caused the script to fire.

This allows a single role definition to scope access to the relevant entity's own resources:

{
  "path": "/routes/users/auth_id/*",
  "action": "*",
  "allow": true
}

When user abc123 authenticates, this becomes /routes/users/abc123/*, granting access to all subpaths under the user's own resource.

The same substitution works recursively in filter objects:

{"filter": {"_id": "auth_id"}}

Becomes {"_id": "abc123"} for user abc123.

Runnable entity mapping

Script type auth_id resolves to
Property change parent._id (the user, bot, or global)
Message event subject._id (the user or bot receiving/sending)
Signal event parent._id
User lifecycle user._id
Bot lifecycle bot._id
Socket connection user._id
Socket event user._id
HTTP request user._id (only present when requires_user)
Job Not substituted — jobs have no entity context

Enforcement Tiers

CRUD Routes

Routes that go through the standard CRUD handler get full field-level enforcement:

  • Route permission gates endpoint access
  • Reads filter to permitted fields and inject document filters
  • Writes validate all fields have write permission
  • Deletes check delete permission and inject document filters

Custom Route Handlers

Handlers like login, register, verify use the raw model directly for unrestricted internal access. Field permissions only filter the final response sent to the client via getSafe.

Runnables (Scripts/Jobs)

  • No route permissions — runnables bypass HTTP routes
  • Full field-level + filter enforcement on all model operations
  • Scripts see undefined for restricted fields and don't see documents outside their filters
  • auth_id substitution uses the triggering entity's ID (see auth_id Substitution)

Roles

A role groups permissions together. Changes to a role apply to all role holders.

Role Scopes

  • anonymous — applied to all non-authenticated requests.
  • user-default — resolved dynamically for all authenticated users. An authenticated user's effective permissions are the union of: anonymous permissions + user-default permissions + any roles explicitly assigned to the user. Because user-default is resolved at evaluation time (not copied at registration), changes to the user-default role take immediate effect for all authenticated users.
  • runnable-default — base permissions for scripts and jobs. Grants read access to all model fields by default.
  • normal — a role that can be manually assigned to users or runnables for various purposes.

Default Roles

admin (scope: normal):

{"permissions": [{"path": "/*", "action": "*", "allow": true}]}

anonymous (scope: anonymous):

{
  "permissions": [
    {"path": "/routes/mcp/*", "action": "*", "allow": true},
    {"path": "/routes/users/oauth/*", "action": "*", "allow": true},
    {"path": "/routes/users/register", "action": "post", "allow": true},
    {"path": "/routes/users/login", "action": "post", "allow": true},
    {"path": "/routes/users/trigger_verify_notification", "action": "post", "allow": true},
    {"path": "/routes/users/verify", "action": "post", "allow": true},
    {"path": "/routes/users/change_password_request", "action": "post", "allow": true},
    {"path": "/routes/users/change_password_verify", "action": "post", "allow": true},
    {"path": "/routes/users/*/change_email_verify", "action": "post", "allow": true},
    {"path": "/routes/users/*/refresh_token", "action": "post", "allow": true},
    {"path": "/routes/users/generate", "action": "post", "allow": true},
    {"path": "/routes/requests/*", "action": "*", "allow": true},
    {"path": "/routes/requests_raw/*", "action": "*", "allow": true},
    {"path": "/models/users/*", "action": "*", "allow": true}
  ]
}

user (scope: user-default):

{
  "permissions": [
    {"path": "/routes/users/auth_id/*", "action": "*", "allow": true},
    {"path": "/routes/users/whoami", "action": "*", "allow": true},
    {"path": "/models/users/*", "action": "*", "allow": true, "filter": {"_id": "auth_id"}}
  ]
}

runnable-default (scope: runnable-default):

{
  "permissions": [
    {"path": "/models/users/*", "action": "read", "allow": true},
    {"path": "/models/bots/*", "action": "read", "allow": true},
    {"path": "/models/globals/*", "action": "read", "allow": true},
    {"path": "/models/conversations/*", "action": "read", "allow": true},
    {"path": "/models/messages/*", "action": "read", "allow": true},
    {"path": "/models/storages/*", "action": "read", "allow": true},
    {"path": "/models/user_properties/*", "action": "read", "allow": true},
    {"path": "/models/bot_properties/*", "action": "read", "allow": true},
    {"path": "/models/global_properties/*", "action": "read", "allow": true},
    {"path": "/models/scripts/*", "action": "read", "allow": true},
    {"path": "/models/jobs/*", "action": "read", "allow": true}
  ]
}

HTTP Resources

GET /roles

Paginated list of roles:

[
  {
    "_id": "txy3831ff9h",
    "title": "user",
    "scope": "user-default",
    "permissions": [
      {"path": "/routes/users/auth_id/*", "action": "*", "allow": true},
      {"path": "/routes/users/whoami", "action": "*", "allow": true},
      {"path": "/models/users/*", "action": "*", "allow": true, "filter": {"_id": "auth_id"}}
    ]
  },
  {
    "_id": "hxy1231ff9g",
    "title": "admin",
    "scope": "normal",
    "permissions": [
      {"path": "/*", "action": "*", "allow": true}
    ]
  }
]

POST /roles

Create a role:

{
  "title": "custom-reader",
  "scope": "normal",
  "permissions": [
    {"path": "/routes/bots/*", "action": "get", "allow": true},
    {"path": "/models/bots/*", "action": "read", "allow": true}
  ]
}

GET /roles/:id

Retrieve a single role by ID.

{
  "_id": "txy3831ff9h",
  "title": "user",
  "scope": "user-default",
  "permissions": [
    {"path": "/routes/users/auth_id/*", "action": "*", "allow": true},
    {"path": "/routes/users/whoami", "action": "*", "allow": true},
    {"path": "/models/users/*", "action": "*", "allow": true, "filter": {"_id": "auth_id"}}
  ]
}

PATCH /roles/:id

Partially update a role. Accepts the same body format as PUT but only the provided fields are updated.

{
  "title": "renamed-role"
}

PUT /roles/:id

Update the role with the given id.

{
  "title": "updated-role",
  "permissions": [
    {"path": "/routes/users/auth_id/*", "action": "*", "allow": true},
    {"path": "/models/users/*", "action": "*", "allow": true, "filter": {"_id": "auth_id"}}
  ]
}

DELETE /roles/:id

Delete the role with the given id.

User Role Management

Manage the explicit roles assigned to a user. These endpoints are on the /users route but are documented here because they are part of the access control system.

Modifying a user's roles requires two permissions:

  1. Model write on users — /models/users/* with write action
  2. Role assignment for each role being added or removed — /roles/{roleId}/assign with write action

Both checks apply to POST and DELETE operations. See Role Assignment Permissions for details.

GET /users/:id/roles

Returns the list of role IDs explicitly assigned to the user.

["txy3831ff9h", "abc1234def5"]

POST /users/:id/roles

Set the user's roles. Replaces the entire roles array.

REQUEST

{
  "roles": ["txy3831ff9h", "abc1234def5"]
}

RESPONSE

["txy3831ff9h", "abc1234def5"]

DELETE /users/:id/roles/:role_id

Remove a single role from the user.

RESPONSE

["txy3831ff9h"]