Skip to content

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

  1. Privileges live on the role, not on individual users. Bind users to roles via Neo4jUser.spec.roles.
  2. Source of truth is the spec. On every reconcile the controller diffs spec.privileges against the live state from SHOW ROLE PRIVILEGES AS COMMANDS and applies the difference. Manual changes are reverted unless enforcePrivileges: false.
  3. Built-in roles are protected by default. PUBLIC, reader, editor, publisher, architect, admin cannot be managed unless adoptBuiltin: true. Adopted built-ins are never dropped on CR delete.
  4. Immutable privileges are detected and skipped. Privileges created with GRANT IMMUTABLE cannot be revoked while authentication is enabled; the controller filters them out of the revoke set and surfaces drift via status.privilegeDrift plus a PrivilegesDriftKept event.

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

  • clusterRef must resolve to a Neo4jEnterpriseCluster or Neo4jEnterpriseStandalone in the same namespace.
  • name (or metadata.name fallback) matches ^[a-zA-Z][a-zA-Z0-9_]*$.
  • Built-in role names are rejected unless adoptBuiltin: true.
  • Each privileges[] entry:
  • Must start with GRANT or DENY (case-insensitive). REVOKE is 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:

  1. Read live: SHOW ROLE <name> PRIVILEGES AS COMMANDS YIELD command, immutable.
  2. 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 in status.unattributedPrivileges and never revoked. With --privilege-normalisation=probe, each statement is instead granted once to a short-lived operator_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:
  3. Whitespace runs collapsed to a single space (outside quoted strings).
  4. Reserved keywords upper-cased (grant → GRANT, database → DATABASE, etc.).
  5. Trailing semicolons stripped.
  6. Diff sets:
  7. Desired ∖ live → execute the original GRANT/DENY statement (once, even when it stands for several stored rows).
  8. Live ∖ desired → derive a REVOKE form by replacing the leading verb (GRANT/DENY) with REVOKE GRANT/REVOKE DENY and TO role with FROM role. Skip immutable rows.
  9. Update status with appliedPrivileges set to the post-apply canonical list and privilegeDrift set 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 WHERE clause references one property only; chained comparisons across properties require multiple privileges.
  • Sharded property databases are unsupported. The role validator rejects PBAC privileges naming a Neo4jShardedDatabase and warns when ON GRAPH * is used in combination with FOR pattern WHERE … (silent ineffectiveness on any sharded DBs in scope).
  • Performance overhead. PBAC adds per-row evaluation, especially significant on TRAVERSE rules and on disk-backed storage. Profile before deploying widely; consider block storage 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 PROPERTY privilege.

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.

See also