Neo4jRole API Reference¶
The Neo4jRole Custom Resource Definition (CRD) provides declarative management of Neo4j roles and the privileges granted to them, for both Neo4jEnterpriseCluster and Neo4jEnterpriseStandalone deployments.
Overview¶
- API Version:
neo4j.neo4j.com/v1beta1 - Kind:
Neo4jRole - Scope: Namespaced
- Short names:
n4jrole,n4jroles - Categories:
neo4j - Supported Neo4j Versions: 5.26 LTS and any CalVer release (2025.x, 2026.x, and onward) — Enterprise edition only (RBAC roles are an Enterprise feature)
- Reconciliation: Role existence and privilege drift via
SHOW ROLE <r> PRIVILEGES AS COMMANDS
Design rules¶
- Privileges live on the role, not on individual users. Bind users to roles via
Neo4jUser.spec.roles. - Source of truth is the spec. On every reconcile the controller diffs
spec.privilegesagainst the live state fromSHOW ROLE PRIVILEGES AS COMMANDSand applies the difference. Manual changes are reverted unlessenforcePrivileges: false. - Built-in roles are protected by default.
PUBLIC,reader,editor,publisher,architect,admincannot be managed unlessadoptBuiltin: true. Adopted built-ins are never dropped on CR delete. - Immutable privileges are detected and skipped. Privileges created with
GRANT IMMUTABLEcannot be revoked while authentication is enabled; the controller filters them out of the revoke set and surfaces drift viastatus.privilegeDriftplus aPrivilegesDriftKeptevent.
Related Resources¶
Neo4jUser— Bind users to roles- User & Role Management Guide — End-to-end walkthrough
Spec¶
| Field | Type | Description |
|---|---|---|
clusterRef |
string |
Required. Name of the Neo4jEnterpriseCluster or Neo4jEnterpriseStandalone in the same namespace. In-database roles are not managed on Neo4j Aura — use Aura IAM / console-RBAC instead. |
name |
string |
Role name in Neo4j. Defaults to metadata.name. Pattern ^[a-zA-Z][a-zA-Z0-9_]*$. |
copyOf |
string |
Existing role to seed privileges from at creation time only (CREATE ROLE name AS COPY OF other). Ignored on subsequent reconciles. |
privileges |
[]string |
Desired set of GRANT and DENY statements. Each entry must be a complete Cypher statement starting with GRANT or DENY and ending with TO <spec.name>. |
privilegeRules |
[]PrivilegeRule |
Privileges in structured form, rendered to Cypher by the operator and then reconciled exactly like privileges. Combine freely with privileges; a rule that renders to a statement already in privileges is refused. |
enforcePrivileges |
boolean |
Reconcile drift back to spec. Default true. When false, the controller applies missing privileges but never revokes anything added out-of-band. |
adoptBuiltin |
boolean |
Allow name to be a built-in role (PUBLIC, reader, editor, publisher, architect, admin). Default false. Adopted roles are never dropped on CR delete. |
deletionPolicy |
string |
One of Delete (default) or Retain. With Retain, deleting the CR releases the finalizer without dropping the role from Neo4j. |
Status¶
| Field | Type | Description |
|---|---|---|
phase |
string |
One of Pending, Ready, Failed. |
message |
string |
Short human-readable summary of the current phase. |
observedGeneration |
int64 |
metadata.generation observed during the last reconcile. |
appliedPrivileges |
[]string |
Canonicalised privilege statements last observed via SHOW ROLE PRIVILEGES AS COMMANDS. |
privilegeDrift |
boolean |
True when one or more privileges could not be reconciled (e.g. immutable extras). |
privilegeRenderings |
[]{statement, rows, ambiguous} |
Learn mode (the default; not used with --privilege-normalisation=probe): how Neo4j stored each spec.privileges statement, learned from the operator's own grants. ambiguous: true means everything the statement grants was already on the role, so which rows are its own is unknown. |
privilegeRenderingScope |
string |
Learn mode: digest of the server, image and alias targets the renderings were learned against. A change re-learns them. |
unattributedPrivileges |
[]string |
Learn mode: rows learn mode cannot attribute to a spec statement, which it therefore never revokes. See Learn mode. |
conditions |
[]Condition |
See Conditions. |
Conditions¶
| Type | Reasons | Meaning |
|---|---|---|
Ready |
RoleReady, RoleSyncFailed, ClusterNotFound, ClusterNotReady, ConnectionFailed, ValidationFailed |
True when role exists and privileges are reconciled. |
PrivilegesSynced |
PrivilegesMatch, PrivilegesDrifted, UnattributedPrivileges |
True when live privileges match spec.privileges. Unknown/UnattributedPrivileges (learn mode, the default) means the role holds rows the operator cannot attribute to a spec statement and so will not revoke; the message lists them. |
PrivilegesResolve |
AllDatabasesResolve, NoDatabaseScopedPrivileges, DatabaseNotFound, GraphPrivilegeOnComposite |
True when every database named by a privilege exists on this cluster (or is an alias for one). False means those privileges are not on the role: Neo4j refuses a grant on a database that does not exist, and drops a role's privileges along with a dropped database, so the operator skips them — the rest of the role is applied. The usual cause on a DR cluster is the replica's name: a replica of foo is called foo-replica, and privileges attach to the database, not to an alias. Reported, never enforced: the database may simply not be created yet, and the condition clears on its own when it is. GraphPrivilegeOnComposite is the same class of silent failure on a composite database: a GRAPH privilege on a composite is accepted and shown back by SHOW ROLE PRIVILEGES, but graph privileges attach to the constituents' target databases, so it does nothing. (ACCESS ON DATABASE <composite> is correct and is not flagged.) |
ClusterNotReady |
ClusterNotReady, ClusterReady |
Mirrors the readiness of the referenced cluster. |
PrivilegeRule¶
Exactly one of grant or deny, and exactly one of onDatabase or onGraph.
| Field | Type | Description |
|---|---|---|
grant / deny |
string |
ACCESS, TRAVERSE, READ, MATCH or WRITE. |
onDatabase |
string |
For ACCESS: a database name, or "*". A name, not a reference to a Neo4jDatabase — the default neo4j database, replicas and shards have no CR. |
onGraph |
string |
For TRAVERSE, READ, MATCH, WRITE: a database name, or "*". Not a composite database — graph privileges on a composite are accepted by Neo4j and do nothing. |
properties |
[]string |
For READ and MATCH (required there, refused elsewhere): property names, or ["*"]. |
nodes / relationships / elements |
[]string |
For TRAVERSE, READ, MATCH: labels or types, or ["*"]. At most one of the three; none means nodes and relationships. |
privilegeRules:
- grant: ACCESS # GRANT ACCESS ON DATABASE `analytics` TO `analytics_reader`
onDatabase: analytics
- grant: MATCH # GRANT MATCH {*} ON GRAPH `analytics` NODES * TO `analytics_reader`
properties: ["*"]
onGraph: analytics
nodes: ["*"]
- deny: READ # DENY READ {`ssn`} ON GRAPH `analytics` NODES `Person` TO `analytics_reader`
properties: [ssn]
onGraph: analytics
nodes: [Person]
Everything else — DBMS privileges, FOR … WHERE property rules, IMMUTABLE,
HOME GRAPH — stays in privileges, which is not deprecated.
What the field buys over a string: the database is checked exactly (a string's
is extracted from Cypher text), a missing one is reported by field path in
PrivilegesResolve (e.g. spec.privilegeRules[1].onGraph), and the role
re-reconciles as soon as a Neo4jDatabase of that name appears. What it does
not buy: it does not follow a rename, and deleting the database does not
remove the rule — a name is a spelling, not ownership.
Validation rules¶
clusterRefmust resolve to aNeo4jEnterpriseClusterorNeo4jEnterpriseStandalonein the same namespace.name(ormetadata.namefallback) matches^[a-zA-Z][a-zA-Z0-9_]*$.- Built-in role names are rejected unless
adoptBuiltin: true. - Each
privileges[]entry: - Must start with
GRANTorDENY(case-insensitive).REVOKEis rejected — revokes are derived automatically when a privilege disappears from the spec. - Must end with
TO <name>matching the role's effective name. - Trailing semicolons produce a warning and are stripped during canonicalisation.
Privilege diff engine¶
The controller treats spec.privileges as the source of truth and reconciles drift on every loop:
- Read live:
SHOW ROLE <name> PRIVILEGES AS COMMANDS YIELD command, immutable. - Normalise the desired side to Neo4j's stored form. Neo4j expands lists, singularises plurals, fills in default segments, renames some verbs, lower-cases database names and resolves aliases, so spec text rarely equals the stored row. By default (learn mode) each statement's stored rows are learned from the operator's own grant to the role and kept in
status.privilegeRenderings; rows it cannot attribute are kept instatus.unattributedPrivilegesand never revoked. With--privilege-normalisation=probe, each statement is instead granted once to a short-livedoperator_privilege_probe_<random>role and read back, cached per server version and alias layout. Either way, a statement that names a missing database falls back to textual canonicalisation. Both sides are then canonicalised: - Whitespace runs collapsed to a single space (outside quoted strings).
- Reserved keywords upper-cased (
grant→GRANT,database→DATABASE, etc.). - Trailing semicolons stripped.
- Diff sets:
- Desired ∖ live → execute the original
GRANT/DENYstatement (once, even when it stands for several stored rows). - Live ∖ desired → derive a
REVOKEform by replacing the leading verb (GRANT/DENY) withREVOKE GRANT/REVOKE DENYandTO rolewithFROM role. Skip immutable rows. - Update status with
appliedPrivilegesset to the post-apply canonical list andprivilegeDriftset when any extras could not be revoked.
When enforcePrivileges: false, step 3's revoke pass is skipped — adds still happen.
Lifecycle¶
| CR Event | Cypher Issued |
|---|---|
| Create (custom role) | CREATE ROLE name IF NOT EXISTS [AS COPY OF other], then privilege apply |
Create (built-in, adoptBuiltin: true) |
Skip create; reconcile privileges |
| Update | Privilege diff + apply |
Delete (Delete policy, custom role) |
DROP ROLE name IF EXISTS, then remove finalizer |
Delete (built-in or Retain) |
Remove finalizer only |
| Cluster not Ready | No-op; requeue every 30s with ClusterNotReady condition |
| Drop refused (role still in use) | Reconcile fails with the Neo4j error; remove the role from each Neo4jUser.spec.roles first |
Examples¶
A read-only role for the analytics database¶
apiVersion: neo4j.neo4j.com/v1beta1
kind: Neo4jRole
metadata:
name: analytics-reader
namespace: prod
spec:
clusterRef: prod-cluster
name: analytics_reader # Neo4j role name; defaults to metadata.name, which must then be underscore-only (no hyphens)
privileges:
- "GRANT ACCESS ON DATABASE analytics TO analytics_reader"
- "GRANT MATCH {*} ON GRAPH analytics NODES * TO analytics_reader"
- "DENY WRITE ON GRAPH analytics TO analytics_reader"
A role copied from editor with extra restrictions¶
apiVersion: neo4j.neo4j.com/v1beta1
kind: Neo4jRole
metadata:
name: junior-editor
namespace: prod
spec:
clusterRef: prod-cluster
name: junior_editor
copyOf: editor # honoured at create time only
privileges:
- "DENY DELETE ON GRAPH * NODES Customer TO junior_editor"
Adopting the built-in editor role to lock it down¶
apiVersion: neo4j.neo4j.com/v1beta1
kind: Neo4jRole
metadata:
name: editor
namespace: prod
spec:
clusterRef: prod-cluster
name: editor
adoptBuiltin: true
privileges:
- "GRANT MATCH {*} ON GRAPH * TO editor"
- "GRANT WRITE ON GRAPH * TO editor"
- "DENY DELETE ON GRAPH * TO editor"
The built-in editor is not dropped when this CR is deleted; only its privilege drift reconciliation stops.
Disabling drift reconciliation¶
Useful when you intentionally layer manual privilege overrides on top of a baseline:
apiVersion: neo4j.neo4j.com/v1beta1
kind: Neo4jRole
metadata:
name: legacy-role
namespace: prod
spec:
clusterRef: prod-cluster
name: legacy_role
enforcePrivileges: false
privileges:
- "GRANT ACCESS ON DATABASE legacy TO legacy_role"
Property-Based Access Control (PBAC)¶
Neo4jRole.spec.privileges accepts the full Cypher privilege grammar, including the FOR pattern WHERE … clause used by property-based access control. PBAC refines the existing MATCH, READ, and TRAVERSE privileges with per-row conditions on node or relationship properties.
apiVersion: neo4j.neo4j.com/v1beta1
kind: Neo4jRole
metadata:
name: redacted-reader
spec:
clusterRef: production
name: redacted_reader
privileges:
# Only allow traversal of Email nodes whose classification has been set
- "GRANT TRAVERSE ON GRAPH * FOR (n:Email) WHERE n.classification IS NOT NULL TO redacted_reader"
# Hide anything not classified UNCLASSIFIED or PUBLIC
- "DENY READ {*} ON GRAPH * FOR (n) WHERE NOT n.classification IN ['UNCLASSIFIED', 'PUBLIC'] TO redacted_reader"
# Filter relationships by property
- "GRANT READ {since} ON GRAPH * FOR ()-[o:OWNS]-() WHERE o.classification = 'UNCLASSIFIED' TO redacted_reader"
PBAC privileges flow through the same diff-and-reconcile loop as standard privileges. The canonicaliser upper-cases WHERE, IS, NULL, IN, NOT, AND, OR, and FOR so spec strings round-trip equal against SHOW ROLE PRIVILEGES AS COMMANDS regardless of input casing.
Limitations (per upstream Neo4j docs):
- Single property per rule. Each
WHEREclause references one property only; chained comparisons across properties require multiple privileges. - Sharded property databases are unsupported. The role validator rejects PBAC privileges naming a
Neo4jShardedDatabaseand warns whenON GRAPH *is used in combination withFOR pattern WHERE …(silent ineffectiveness on any sharded DBs in scope). - Performance overhead. PBAC adds per-row evaluation, especially significant on
TRAVERSErules and on disk-backed storage. Profile before deploying widely; considerblockstorage format and label-based privileges where the row count is high. - Property immutability. If a user can write the property used in a PBAC rule, they can bypass the rule. Pair PBAC with a corresponding
DENY SET PROPERTYprivilege.
Troubleshooting¶
validation failed: ... privilege statement must end with TO <role>: every privilege must name the role being defined. Update the statement to end with TO <spec.name>.
cannot revoke immutable privilege ...: the controller cannot revoke GRANT IMMUTABLE privileges while auth is enabled. Either include them in spec.privileges (so they're no longer drift) or set enforcePrivileges: false.
drop role failed: role X is assigned to user Y: Neo4j refuses to drop a role still in use. Remove the role from each Neo4jUser.spec.roles first; once the operator has revoked the role from all users, deletion succeeds.
Pending phase, ClusterNotReady=True: the referenced Neo4jEnterpriseCluster/Neo4jEnterpriseStandalone is not yet Ready. The role controller will reconcile automatically when the cluster transitions.