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:
- Roles — Built-in (admin, editor, viewer) and user-defined custom roles
- Global entitlements — Permissions that apply server-wide (read, write, admin, schema, etc.)
- Per-database access — Control which roles can see and access each database
- Per-database privileges — Fine-grained read/write control per (role, database) pair
- JWT authentication — Stateless token-based auth with password policies and account lockout
Roles
Built-in Roles
| Role | Entitlements | Description |
|---|---|---|
admin | read, write, create, delete, admin, schema, user_manage | Full access and system administration |
editor | read, write, create, delete | Read and write data |
viewer | read | Read-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:
curl -X GET http://localhost:7474/auth/roles \
-H "Authorization: Bearer $ADMIN_TOKEN"Response:
["admin", "editor", "viewer", "analyst", "auditor"]Create a custom role:
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:
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:
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:
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).
| ID | Name | What It Controls |
|---|---|---|
read | Read | Cypher MATCH, search, metrics, status, Bifrost/GraphQL reads, MCP reads |
write | Write | Cypher CREATE/DELETE/SET/MERGE, registered MODE WRITE procedures, embed triggers, search rebuild, GraphQL mutations |
create | Create | Resource creation operations |
delete | Delete | Delete operations (e.g. GDPR delete) |
admin | Admin | Role 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) |
schema | Schema | CREATE/DROP INDEX and CONSTRAINT via Bolt, registered MODE SCHEMA procedures |
user_manage | User Management | Create, update, delete user accounts (distinct from admin) |
Per-Database Entitlements
These apply per database and are controlled through the allowlist and privileges APIs.
| ID | Name | What It Controls |
|---|---|---|
database_see | Database: See | Database appears in SHOW DATABASES and the catalogue |
database_access | Database: Access | Role can run queries against this database |
database_read | Database: Read | Read operations (MATCH, property reads) on this database |
database_write | Database: Write | Write 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:
curl -X GET http://localhost:7474/auth/access/databases \
-H "Authorization: Bearer $ADMIN_TOKEN"Response:
[
{"role": "analyst", "databases": ["analytics", "reporting"]},
{"role": "viewer", "databases": ["public_data"]}
]Set allowlist for a single role:
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:
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:
curl -X GET http://localhost:7474/auth/access/privileges \
-H "Authorization: Bearer $ADMIN_TOKEN"Response:
[
{"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:
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:
// 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. BOOSTEDlets the role run the matching procedures even when they need administrator rights. A plainEXECUTE PROCEDUREgrant 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
readentitlement and read access to each database the procedure reads; give it read access only on that database to keep it to one. GRANTneeds the role to exist (built-in, or created withPOST /auth/roles);REVOKEfrom a role that doesn't exist changes nothing. Only administrators may runGRANT,REVOKEandSHOW … 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,
GRANTandREVOKEtake 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 asapoc.cypher.runruns) it fails withNeo.ClientError.Transaction.ForbiddenDueToTransactionType. - Grants come only from
GRANTandREVOKE: 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
SHOWcommands, and calls of system procedures (worksOnSysteminSHOW PROCEDURES). Any other procedure, and any other statement (RETURN 1,RETURN COUNT { … },EXISTS { … },UNWIND …,CALL { … }), fails there withNeo.ClientError.Statement.SemanticError, and so does a procedure call whoseYIELD,WHEREorRETURNreads the graph (aCOUNT,EXISTSorCOLLECTsubquery, a pattern comprehension or a pattern expression). ASHOWcommand's tail filters, orders, pages and projects the command's columns only: another clause afterYIELD(WITH,UNWIND,CALL { … },MATCH, a write) and an expression that reads the graph fail with Neo4j'sNeo.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, afterUSE system, in a statementapoc.cypher.runor aCALL { }runs, and through the embedded executor:DB.GetCypherExecutor,DB.ExecuteCypher,DB.Cypher). On a user database aSHOWtail may read the graph, as in Neo4j. A system procedure that reads the graph, such asdb.labels(), sees none of the records the system database keeps.EXPLAINof an administration command runs nothing, andPROFILEruns 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 theadminentitlement, as APOC 5 marksinstallProcedureanddropProcedureadmin-only;SHOW PROCEDURESlists them withadmintrue. 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.runruns, with the caller's permissions: an administration command in it fails withNeo.ClientError.Transaction.ForbiddenDueToTransactionType, whoever calls the procedure. - A role's name has one form everywhere:
ROLE_ops,Opsandopsname the roleops. - "Admin" means the
adminentitlement, whatever the role holding it is called:dbms.listConnectionslists 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, inCALL { }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.AccessModeotherwise), and on the system database the rules above apply. DROP ROLEis Neo4j Community'sNeo.ClientError.Statement.UnsupportedAdministrationCommand; roles are deleted withDELETE /auth/roles/{name}.GRANT IMMUTABLE/REVOKE IMMUTABLEfollow Neo4j: immutable privileges can be granted and revoked only while authentication is disabled; with it enabled,GRANT IMMUTABLEand anyREVOKEof an immutable privilege fail withNeo.ClientError.Security.Forbidden.REVOKE IMMUTABLEremoves only an immutable privilege; a plainREVOKEremoves either.SHOW PRIVILEGESreports it in theimmutablecolumn.- A procedure name or glob may be backticked, whole or in parts (
`db.cdc.*`,db.`cdc`.*); it is stored unquoted. SHOW PRIVILEGESlists the procedure privileges in Neo4j's columns (access,action,resource,graph,segment,role,immutable) and takesYIELDandWHERE. Other privilege statements (GRANT ROLE,DENY, graph privileges) aren't supported.
A change-feed reader, set up once:
- Create the role (
POST /auth/roles), give it thereadentitlement (PUT /auth/role-entitlements) and read access on its database only (PUT /auth/access/privileges), and assign it to the worker login. - Run
GRANT EXECUTE BOOSTED PROCEDURE db.cdc.* ON DBMS TO cdc_readeron the system database as an administrator.
How Access Is Resolved
When a user queries a database, NornicDB resolves access in this order:
- Allowlist check — Can this user's role(s) see and access this database?
- Global entitlements — The roles' global entitlements (stored overrides, else the built-in defaults) gate the rest: without
readthe user can use no database; withoutwriteit writes none. - Privileges check — Is there a (role, database) privilege entry? If yes, it decides read and write within the entitlements.
- 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
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:
{
"username": "alice",
"email": "alice@localhost",
"roles": ["analyst"],
"created_at": "2026-04-01T10:30:00Z",
"disabled": false
}Update User Roles
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
curl -X GET http://localhost:7474/auth/users \
-H "Authorization: Bearer $ADMIN_TOKEN"Disable a User
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
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:
# 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)
curl -X POST http://localhost:7474/auth/token \
-d "grant_type=password&username=alice&password=SecurePass123!"Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 86400
}Using Tokens
# 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):
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.
auth:
min_password_length: 12Passwords 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
auth:
enabled: true
username: admin
password: "${ADMIN_PASSWORD}"
jwt_secret: "${NORNICDB_AUTH_JWT_SECRET}" # Min 32 characters
token_expiry: 24h
min_password_length: 12# 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=noneThe 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
| Endpoint | Required Permission |
|---|---|
GET /health | None (public) |
GET /status | read |
GET /metrics | read |
POST /db/{db}/tx/commit | read or write (per-database) |
POST /nornicdb/search | read |
POST /auth/users | user_manage |
GET/POST /auth/roles | admin |
GET/PUT /auth/access/databases | admin |
GET/PUT /auth/access/privileges | admin |
GET /auth/entitlements | read |
DELETE /nornicdb/gdpr/* | admin |
GET /admin/* | admin |
Audit Integration
All authentication and access events are logged:
{
"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
| Requirement | NornicDB Feature |
|---|---|
| GDPR Art. 32 | Access controls, authentication |
| HIPAA §164.312(a)(1) | Unique user identification |
| HIPAA §164.312(d) | Person or entity authentication |
| FISMA AC-2 | Account management |
| SOC2 CC6.1 | Logical access controls |
See Also
- Entitlements Reference — Canonical list of all assignable entitlements
- Per-Database RBAC & Lockout Recovery — Allowlist details and admin lockout recovery
- Encryption — Data protection at rest
- Audit Logging — Access and event audit trails
- HIPAA Compliance — Healthcare compliance mapping
- Multi-Database Guide — System database and user storage