Schema-based access control in Neo4j 4.0

Neo4j had role-based access control for some time now. But this did not provide the fine-grained control some business need. Even if they only need it to put a checkmark on a security audit and perform the actual restriction in the application logic.

As Jim Webber pointed out so nicely, security is not the hottest topic for most (me included) developers. But, due to how you typically query graph databases, we need something more advanced and more fun.

Note
If you want to follow along, I suggest using 2 different sessions (esp. Neo4j Desktop browser and a separate session in a browser) to avoid constantly switching users. I will use a prompt in the code snippets to show which database/user is in use.

Before we start, lets create a database and a user with a role:

system$ CREATE OR REPLACE DATABASE blog;
system$ create or replace user user_a set password 'blog' set password change not required;
system$ create or replace role role_a as copy of reader;
system$ grant role role_a to user_a;

The new user will have the role_a which in turn is based on the build-in reader role. And yes, finding good names is hard.

As user with write permissions (neo4j) we create some data to work with:

:use blog

blog$ match(n) detach delete n;

blog$ create (a1:A {name:'a1'})
  create (a2:A {name:'a2'})
  create (b1:B {name:'b1', propB:'b'})
  create (b2:B {name:'b2'})
  create (b3:B {name:'b3'})
  create (c1:C:D {name:'c1', propA:'a', propB:'b'})
  create (c2:C {name:'c2'})
  create (a1)-[:REL_AB {propAB: 'ab'}]->(b1)
  create (a1)-[:REL_AC {propC: 'c'}]->(c1)
  create (b1)-[:REL_B]->(b2)
  create (b2)-[:REL_B]->(b3)
  create (a2)-[:REL_AB]->(b3)
  create (a2)-[:REL_AC]->(c2)
  create (c1)-[:REL_C {propC:'c'}]->(c2);
blog$ match (d:D) set d.propD = 'D'

This gives us the following graph:

Example graph
Figure 1. example graph

Note that c1 has the labels C and D. As you can see:

match (c1:C {name: 'c1'}) return c1, labels(c1) as labels
╒═════════════════════════════════════════════════╤═════════╕
│"c1"                                             │"labels" │
╞═════════════════════════════════════════════════╪═════════╡
│{"propB":"b","name":"c1","propA":"a","propD":"D"}│["C","D"]│
└─────────────────────────────────────────────────┴─────────┘

Querying by label D would return the same result, as expected. When we deny read access to a property for a role:

system$ deny read {propD} on graph blog nodes D to role_a

Then that property will not be included in the return:

blog$ match (d:D) return d
╒═════════════════════════════════════╕
│"d"                                  │
╞═════════════════════════════════════╡
│{"propB":"b","name":"c1","propA":"a"}│
└─────────────────────────────────────┘

Again matching on C would return the same set of properties; the properties belong to a node, not to a specific label of that node. Something to keep in mind. In the same way, access to all properties of a label, or a specific property across all labels can granted or denied. Access can either be read or write. A detailed description can be found in the cypher manual.

The fun starts with traversing the graph. With the new access restriction features, one can allow or deny to traverse over nodes and relationships, without reading any data from them. This includes the type or labels associated.

If we want to find the shortest path between a1 and a2 in our example graph above:

blog$ match p=shortestPath((:A {name: 'a2'})-[*]-(:A {name:'a1'})) return p

we get a path traversing the the C nodes:

shortest path without restrictions
Figure 2. shortest path

or in ASCII art:

[{"name":"a2"},{},{"name":"c2"},{"name":"c2"},{"propC":"c"},{"propB":"b","name":"c1","propA":"a","propD":"D"},{"propB":"b","name":"c1","propA":"a","propD":"D"},{"propC":"c"},{"name":"a1"}]

If we now deny access to the C nodes:

system$ deny read {*} on graph blog nodes C to role_a;
// or
system$ deny traverse on graph blog nodes C to role_a;

then we can’t traverse over the C nodes and the shortest path becomes:

shortest path with restrictions
Figure 3. shortest path with restrictions

or in ASCII:

╒══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╕
│"p"                                                                                                                                                               │
╞══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│[{"name":"a2"},{},{"name":"b3"},{"name":"b3"},{},{"name":"b2"},{"name":"b2"},{},{"propB":"b","name":"b1"},{"propB":"b","name":"b1"},{"propAB":"ab"},{"name":"a1"}]│
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Traversal restriction also works based on relationship types.

The following picture from the cypher manual gives a good overview of the syntax:

grant syntax
Figure 4. grant syntax

The link above also contains descriptions of how to revoke grants again.