Skip to content

Role-Based Access Control (RBAC) ​

Define roles, assign entitlements, and control per-database access.

Last Updated: April 2026


Overview ​

NornicDB implements a layered RBAC system:

  1. Roles — Built-in (admin, editor, viewer) and user-defined custom roles
  2. Global entitlements — Permissions that apply server-wide (read, write, admin, schema, etc.)
  3. Per-database access — Control which roles can see and access each database
  4. Per-database privileges — Fine-grained read/write control per (role, database) pair
  5. JWT authentication — Stateless token-based auth with password policies and account lockout

Roles ​

Built-in Roles ​

RoleEntitlementsDescription
adminread, write, create, delete, admin, schema, user_manageFull access and system administration
editorread, write, create, deleteRead and write data
viewerreadRead-only access
none—No access (disabled account)

Built-in roles cannot be renamed or deleted.

Custom Roles ​

Create custom roles to model your organization's access patterns. Custom roles start with no global entitlements — assign per-database privileges to control what they can do.

List all roles:

bash
curl -X GET http://localhost:7474/auth/roles \
  -H "Authorization: Bearer $ADMIN_TOKEN"

Response:

json
["admin", "editor", "viewer", "analyst", "auditor"]

Create a custom role:

bash
curl -X POST http://localhost:7474/auth/roles \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "analyst"}'

Rename a custom role:

bash
curl -X PATCH http://localhost:7474/auth/roles/analyst \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "data_analyst"}'

Renaming a role automatically updates the allowlist and all user assignments.

Delete a custom role:

bash
curl -X DELETE http://localhost:7474/auth/roles/data_analyst \
  -H "Authorization: Bearer $ADMIN_TOKEN"

A role cannot be deleted while any user is assigned to it.


Entitlements ​

Entitlements are the individual permissions that can be assigned to roles. Query the full canonical list from the API:

bash
curl -X GET http://localhost:7474/auth/entitlements \
  -H "Authorization: Bearer $TOKEN"

Global Entitlements ​

These apply server-wide and are assigned to roles by default (built-in roles) or via configuration (custom roles).

IDNameWhat It Controls
readReadCypher MATCH, search, metrics, status, Bifrost/GraphQL reads, MCP reads
writeWriteCypher CREATE/DELETE/SET/MERGE, registered MODE WRITE procedures, embed triggers, search rebuild, GraphQL mutations
createCreateResource creation operations
deleteDeleteDelete operations (e.g. GDPR delete)
adminAdminRole management, database access/privileges, backup, GPU config, system administration, and the procedures SHOW PROCEDURES lists as admin (for example db.stats.*, dbms.listConfig, db.clearQueryCaches, db.cdc.query, db.txlog.*; a registered MODE ADMIN or MODE DBMS procedure is one of them)
schemaSchemaCREATE/DROP INDEX and CONSTRAINT via Bolt, registered MODE SCHEMA procedures
user_manageUser ManagementCreate, update, delete user accounts (distinct from admin)

Per-Database Entitlements ​

These apply per database and are controlled through the allowlist and privileges APIs.

IDNameWhat It Controls
database_seeDatabase: SeeDatabase appears in SHOW DATABASES and the catalogue
database_accessDatabase: AccessRole can run queries against this database
database_readDatabase: ReadRead operations (MATCH, property reads) on this database
database_writeDatabase: WriteWrite operations (CREATE, DELETE, SET, MERGE) on this database

Per-Database Access Control ​

Database Allowlist ​

The allowlist controls which roles can see and access which databases. If no allowlist is configured for a role, that role can access all databases (empty list = all).

View current allowlist:

bash
curl -X GET http://localhost:7474/auth/access/databases \
  -H "Authorization: Bearer $ADMIN_TOKEN"

Response:

json
[
  {"role": "analyst", "databases": ["analytics", "reporting"]},
  {"role": "viewer", "databases": ["public_data"]}
]

Set allowlist for a single role:

bash
curl -X PUT http://localhost:7474/auth/access/databases \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "role": "analyst",
    "databases": ["analytics", "reporting"]
  }'

Set allowlist for multiple roles at once:

bash
curl -X PUT http://localhost:7474/auth/access/databases \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "mappings": [
      {"role": "analyst", "databases": ["analytics", "reporting"]},
      {"role": "auditor", "databases": ["audit_logs"]}
    ]
  }'

Per-Database Privileges ​

The privileges matrix provides fine-grained read and write control per (role, database) pair, within the role's global entitlements: reading a database needs the global read entitlement, and writing it the write entitlement too. When none of a principal's roles has a privilege entry for a database, its global entitlements decide alone (the ones stored with PUT /auth/role-entitlements, else the built-in defaults: editor = read+write, viewer = read-only).

View current privileges:

bash
curl -X GET http://localhost:7474/auth/access/privileges \
  -H "Authorization: Bearer $ADMIN_TOKEN"

Response:

json
[
  {"role": "analyst", "database": "analytics", "read": true, "write": false},
  {"role": "analyst", "database": "reporting", "read": true, "write": true},
  {"role": "auditor", "database": "audit_logs", "read": true, "write": false}
]

Set privileges:

bash
curl -X PUT http://localhost:7474/auth/access/privileges \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[
    {"role": "analyst", "database": "analytics", "read": true, "write": false},
    {"role": "analyst", "database": "reporting", "read": true, "write": true}
  ]'

Procedure Privileges ​

A procedure that needs administrator rights (the change-feed procedure db.cdc.query, for example) can be granted to a role with Neo4j's statement, so that a non-admin login can run it without other administrator rights:

cypher
// On the system database, as an administrator
GRANT EXECUTE BOOSTED PROCEDURE db.cdc.* ON DBMS TO cdc_reader
REVOKE EXECUTE BOOSTED PROCEDURE db.cdc.* ON DBMS FROM cdc_reader
SHOW PRIVILEGES
SHOW ROLE cdc_reader PRIVILEGES
  • A procedure is named exactly or with a glob: * stands for any characters, dots included, and ? for one. Several names and several roles can be listed, separated by commas.
  • BOOSTED lets the role run the matching procedures even when they need administrator rights. A plain EXECUTE PROCEDURE grant is recorded and listed, but adds nothing to what the role may already run.
  • The grant doesn't widen database access. The role still needs the global read entitlement and read access to each database the procedure reads; give it read access only on that database to keep it to one.
  • GRANT needs the role to exist (built-in, or created with POST /auth/roles); REVOKE from a role that doesn't exist changes nothing. Only administrators may run GRANT, REVOKE and SHOW … PRIVILEGES.
  • Grants are read from the system database on every check, so a change applies at once to every server over the same storage. In an explicit transaction, GRANT and REVOKE take effect when it commits and are undone by a rollback. They run on the system database whichever database the transaction was opened on, as in Neo4j: an administration command as a transaction's first statement makes it an administration transaction, and after a data statement (or in a statement a procedure such as apoc.cypher.run runs) it fails with Neo.ClientError.Transaction.ForbiddenDueToTransactionType.
  • Grants come only from GRANT and REVOKE: the system database runs no statement that writes graph data (CREATE, MERGE, SET, DELETE, REMOVE, write procedures), as in Neo4j. Deleting a role removes its grants.
  • The system database runs what Neo4j 5.26.30's runs: administration and SHOW commands, and calls of system procedures (worksOnSystem in SHOW PROCEDURES). Any other procedure, and any other statement (RETURN 1, RETURN COUNT { … }, EXISTS { … }, UNWIND …, CALL { … }), fails there with Neo.ClientError.Statement.SemanticError, and so does a procedure call whose YIELD, WHERE or RETURN reads the graph (a COUNT, EXISTS or COLLECT subquery, a pattern comprehension or a pattern expression). A SHOW command's tail filters, orders, pages and projects the command's columns only: another clause after YIELD (WITH, UNWIND, CALL { … }, MATCH, a write) and an expression that reads the graph fail with Neo4j's Neo.ClientError.Statement.SyntaxError ("The COUNT expression is not valid on SHOW commands.", "Pattern expressions are not valid on SHOW commands.", "Invalid input 'WITH' …"). All of this holds on every route (directly, after USE system, in a statement apoc.cypher.run or a CALL { } runs, and through the embedded executor: DB.GetCypherExecutor, DB.ExecuteCypher, DB.Cypher). On a user database a SHOW tail may read the graph, as in Neo4j. A system procedure that reads the graph, such as db.labels(), sees none of the records the system database keeps. EXPLAIN of an administration command runs nothing, and PROFILE runs it, on both parsers.
  • A custom procedure (apoc.custom.installProcedure / declareProcedure, CREATE PROCEDURE) is one for the whole server, whichever database it was installed from. Installing, replacing or dropping one (apoc.custom.dropProcedure, DROP PROCEDURE) needs the admin entitlement, as APOC 5 marks installProcedure and dropProcedure admin-only; SHOW PROCEDURES lists them with admin true. A call of a custom procedure is never answered from the result cache, so a call after a replacement runs the new body.
  • A custom procedure's body runs like a statement apoc.cypher.run runs, with the caller's permissions: an administration command in it fails with Neo.ClientError.Transaction.ForbiddenDueToTransactionType, whoever calls the procedure.
  • A role's name has one form everywhere: ROLE_ops, Ops and ops name the role ops.
  • "Admin" means the admin entitlement, whatever the role holding it is called: dbms.listConnections lists every connection only to a principal with it (any other sees its own, as in Neo4j). Personal API tokens are available to every authenticated user and inherit only that user’s identity and permissions.
  • A statement that selects a database with USE, at the top level, in CALL { } or in a statement a procedure runs, goes through one router: in a transaction it must stay on the transaction's database (Neo.ClientError.Statement.AccessMode otherwise), and on the system database the rules above apply.
  • DROP ROLE is Neo4j Community's Neo.ClientError.Statement.UnsupportedAdministrationCommand; roles are deleted with DELETE /auth/roles/{name}.
  • GRANT IMMUTABLE / REVOKE IMMUTABLE follow Neo4j: immutable privileges can be granted and revoked only while authentication is disabled; with it enabled, GRANT IMMUTABLE and any REVOKE of an immutable privilege fail with Neo.ClientError.Security.Forbidden. REVOKE IMMUTABLE removes only an immutable privilege; a plain REVOKE removes either. SHOW PRIVILEGES reports it in the immutable column.
  • A procedure name or glob may be backticked, whole or in parts (`db.cdc.*`, db.`cdc`.*); it is stored unquoted.
  • SHOW PRIVILEGES lists the procedure privileges in Neo4j's columns (access, action, resource, graph, segment, role, immutable) and takes YIELD and WHERE. Other privilege statements (GRANT ROLE, DENY, graph privileges) aren't supported.

A change-feed reader, set up once:

  1. Create the role (POST /auth/roles), give it the read entitlement (PUT /auth/role-entitlements) and read access on its database only (PUT /auth/access/privileges), and assign it to the worker login.
  2. Run GRANT EXECUTE BOOSTED PROCEDURE db.cdc.* ON DBMS TO cdc_reader on the system database as an administrator.

How Access Is Resolved ​

When a user queries a database, NornicDB resolves access in this order:

  1. Allowlist check — Can this user's role(s) see and access this database?
  2. Global entitlements — The roles' global entitlements (stored overrides, else the built-in defaults) gate the rest: without read the user can use no database; without write it writes none.
  3. Privileges check — Is there a (role, database) privilege entry? If yes, it decides read and write within the entitlements.
  4. Global fallback — If no privilege entry, the global entitlements decide (admin/editor = read+write, viewer = read-only).

HTTP, Bolt, gRPC, GraphQL, MCP and plugins resolve access through this one resolver, and decide a statement's permissions with one decision, so a change to a role's entitlements or privileges applies at once on every transport. A statement that only writes (CREATE, and SET or DELETE of what it created) needs write but not read on the database, as in Neo4j, where a principal with WRITE but no READ can run CREATE; a statement that reads the graph (MATCH, MERGE, CALL, a WHERE, a COUNT { } or EXISTS { } subquery, a pattern comprehension) needs read, since NornicDB doesn't hide individual records from a read.

When a new database is created, the admin role and the creating user's roles are automatically granted full access.


User Management ​

Create Users ​

bash
curl -X POST http://localhost:7474/auth/users \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "password": "SecurePass123!",
    "roles": ["analyst"]
  }'

Response:

json
{
  "username": "alice",
  "email": "alice@localhost",
  "roles": ["analyst"],
  "created_at": "2026-04-01T10:30:00Z",
  "disabled": false
}

Update User Roles ​

bash
curl -X PUT http://localhost:7474/auth/users/alice \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"roles": ["analyst", "viewer"]}'

List Users ​

bash
curl -X GET http://localhost:7474/auth/users \
  -H "Authorization: Bearer $ADMIN_TOKEN"

Disable a User ​

bash
curl -X PUT http://localhost:7474/auth/users/alice \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"disabled": true}'

Delete a User ​

bash
curl -X DELETE http://localhost:7474/auth/users/alice \
  -H "Authorization: Bearer $ADMIN_TOKEN"

User Profile (Self-Service) ​

Users can update their own password and profile without admin privileges:

bash
# Change password
curl -X POST http://localhost:7474/auth/password \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "old_password": "OldPass123!",
    "new_password": "NewSecurePass456!"
  }'

# Update profile metadata
curl -X PUT http://localhost:7474/auth/profile \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alice@example.com",
    "metadata": {"department": "Engineering", "team": "Data"}
  }'

User Storage ​

User accounts are stored as nodes in the system database:

  • Persisted immediately on create/update/delete
  • Loaded automatically on server startup
  • Included in database backups
  • Internal database IDs and password hashes are never exposed in API responses

Authentication ​

Login (Get Token) ​

bash
curl -X POST http://localhost:7474/auth/token \
  -d "grant_type=password&username=alice&password=SecurePass123!"

Response:

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 86400
}

Using Tokens ​

bash
# Authorization header
curl http://localhost:7474/db/nornic/tx/commit \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

# Or API key header
curl http://localhost:7474/db/nornic/tx/commit \
  -H "X-API-Key: ndb_sk_abc123..."

API Keys ​

For service-to-service communication, generate an API token via the admin endpoint (requires the admin permission):

bash
curl -X POST http://localhost:7474/auth/api-token \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "backend-service", "role": "editor"}'

Security Features ​

Account Lockout ​

After repeated failed login attempts, accounts are locked automatically. The lockout threshold and duration use built-in defaults (pkg/auth/auth.go); they are not exposed through configuration.

Password Policy ​

The minimum password length is configurable via auth.min_password_length (default 8). Complexity rules (uppercase / number / special-character requirements) are not exposed through configuration today — passwords are validated only against the minimum length.

yaml
auth:
  min_password_length: 12

Passwords are hashed using bcrypt with automatic salt generation. Plain-text passwords are never stored.

Session Management ​

Tokens can be revoked (logout) by adding them to a server-side blacklist. Revoked tokens are rejected on subsequent requests.

A principal's roles are read from the user store on every request, by one owner: for a Bearer token and for Basic credentials (whose password check the server caches for 30 seconds, but not the roles; a changed password stops working at once, on HTTP, Bolt and gRPC) on HTTP and gRPC, and for a Bolt session before every statement and transaction. A role removed from a user, or deleted (which removes it from every user), stops applying at once, as Neo4j applies role changes at once. A deleted or disabled account, or another account created later under the same name, authorizes nothing: its token is refused (HTTP 401) and its open Bolt session runs no statement. A cluster node's token (from /api/v1/auth/cluster-token) is not a user account and keeps its own role.

Role Records ​

A role's entitlement overrides, database allowlist and database privileges are written only for a role that exists: a write for a missing role fails (HTTP 400, Neo.ClientError.General.SchemaViolation) and changes nothing. Each request is one transaction, so a batch (mappings, the privilege matrix) applies all of its entries or none, and a write racing the role's deletion or rename can't commit alongside it.


Configuration ​

yaml
auth:
  enabled: true
  username: admin
  password: "${ADMIN_PASSWORD}"
  jwt_secret: "${NORNICDB_AUTH_JWT_SECRET}"  # Min 32 characters
  token_expiry: 24h
  min_password_length: 12
bash
# Required: JWT signing secret (min 32 characters)
export NORNICDB_AUTH_JWT_SECRET="your-super-secret-jwt-key-min-32-chars"

# Optional: Disable auth for development
export NORNICDB_AUTH=none

The configurable surface is intentionally small: the auth block above (enabled, username, password, min_password_length, token_expiry, jwt_secret) is the full set. Bcrypt cost, password complexity rules, and lockout thresholds are not exposed through configuration today; they default to the values in pkg/auth/auth.go.


Endpoint Protection ​

EndpointRequired Permission
GET /healthNone (public)
GET /statusread
GET /metricsread
POST /db/{db}/tx/commitread or write (per-database)
POST /nornicdb/searchread
POST /auth/usersuser_manage
GET/POST /auth/rolesadmin
GET/PUT /auth/access/databasesadmin
GET/PUT /auth/access/privilegesadmin
GET /auth/entitlementsread
DELETE /nornicdb/gdpr/*admin
GET /admin/*admin

Audit Integration ​

All authentication and access events are logged:

json
{
  "timestamp": "2026-04-01T10:30:00Z",
  "event_type": "LOGIN",
  "user_id": "usr_abc123",
  "username": "alice",
  "ip_address": "192.168.1.100",
  "user_agent": "Mozilla/5.0...",
  "success": true
}

See Audit Logging for full details.


Compliance Mapping ​

RequirementNornicDB Feature
GDPR Art. 32Access controls, authentication
HIPAA §164.312(a)(1)Unique user identification
HIPAA §164.312(d)Person or entity authentication
FISMA AC-2Account management
SOC2 CC6.1Logical access controls

See Also ​

Analytics privacy