Neo4j access governance — every Cypher statement reviewed before it runs
Last updated
AccessFlow governs Neo4j through the same submit, analyse, approve, execute pipeline as a SQL database. Cypher is the one language where row-level security needed a genuinely new shape: there is no single WHERE clause to splice into, so predicates are attached to each MATCH that binds a node of the policied label — and anything that cannot be attached provably fails closed.
- Family
- Graph
- Query language
- Cypher
- Runs as
- Engine plugin (SHA-256 pinned)
- Default port
7687- Default SSL mode
REQUIRE- Install
- One-click from the connector catalog
How AccessFlow connects
It is an engine plugin rather than an in-process JDBC datasource: a standalone shaded JAR, pinned in the connector catalog by URL and SHA-256, downloaded on first use, hash-verified, and loaded into an isolated classloader. If the hash does not match, it does not load. In an air-gapped install the JAR can be pre-seeded into the driver cache and the runtime told never to reach the network.
Neo4j connects over Bolt, and the SSL mode is encoded in the URI scheme: plaintext when disabled, encrypted trusting any certificate at the default, and encrypted with trust-store verification for the verifying modes. A full URI can be supplied verbatim instead, which is how a managed Aura instance or a clustered routing URI is configured.
The database name is required and selects the Neo4j database for the session. One driver is cached per datasource; the driver pools and routes Bolt connections internally.
What AccessFlow understands
Cypher is clause-based rather than verb-led, so the query type is the strongest write clause present: DELETE, DETACH DELETE or REMOVE make it a delete; CREATE or MERGE an insert; SET an update; and a pure MATCH … RETURN a read. Schema and admin commands are DDL, told apart from a data CREATE (n:Label) by the token following the verb.
Grants target every node label and relationship type the statement touches, so the allow-list and routing globs work on graph shape the way they work on table names elsewhere.
What it refuses
LOAD CSV is refused with HTTP 422 — it reads from a URL inside the database, which is the Cypher analogue of the server-side-execution ban every other engine has.
A CALL to a stored procedure is refused unless the procedure is on a small read-only allow-list covering schema introspection — labels, relationship types, property keys and the schema visualisations. A CALL { … } subquery, which is ordinary Cypher rather than a procedure invocation, is allowed.
Row-level security is spliced per MATCH
There is no WHERE … FROM to attach a predicate to, so a policy on a node label becomes a property predicate ANDed onto the WHERE scoped to each MATCH or OPTIONAL MATCH that binds a variable of that label — extending an existing WHERE or inserting one before the next clause. Values are bound as Cypher named parameters, never concatenated. The same splice governs reads, SET updates and deletes, since all of them select through a MATCH.
It fails closed on any shape it cannot provably filter: a policied label that appears only anonymously as (:Label) with no variable to constrain, or only inside a predicate or pattern comprehension rather than a clause-level match. A statement that creates or merges a policied label is rejected — a write cannot be filtered into existence.
Masking is label-aware: a rule on Label.property redacts that property of any returned node or relationship whose labels include it, however it is aliased, while a bare property rule redacts that property anywhere it appears, including nested maps and lists. It errs toward masking.
What the rewrite actually does
Cypher has no single WHERE to splice into, so the predicate is attached to each MATCH that binds a node of the policied label:
// submitted MATCH (o:Order) WHERE o.status = 'OPEN' RETURN o.id, o.email // executed (row-security policy: Order.region = the caller's region) MATCH (o:Order) WHERE (o.status = 'OPEN') AND o.region = $af_rls_1 RETURN o.id, o.email // but a policied label with no variable to constrain MATCH (:Order)-[:PLACED_BY]->(c:Customer) RETURN c HTTP 422 — row-level security cannot be expressed for this statement
An anonymous (:Order) has nothing to attach a property predicate to, so there is no way to narrow it — and a filter that cannot be applied must fail closed rather than be quietly skipped.
Introspection and dry-run
The connection test verifies connectivity and runs a trivial return against the target database. Introspection calls the server's own schema procedures — the ones on the read-only allow-list — so each node label becomes a table, its sampled property keys the columns, and a synthetic element id the primary key; relationship types are surfaced as additional tables so the allow-list and ER diagram see the whole graph shape.
A dry-run plans the governed statement with Cypher's EXPLAIN. On submission, an UPDATE or DELETE additionally gets an exact affected-node count from a MATCH … RETURN count(*) carrying the same row-security predicate.