Neo4j 4.0 brings fine-grained access control and general RBAC capabilities to the table. MR2 has started showing how these features are going to work, though they’re only available in the Enterprise edition and MR2 is obviously pre-release so very much not production-ready.
Role support
A role is a collection of zero or more privileges assigned to zero or more users. Roles let you collect together the privileges required to accomplish some task. By allocating a role to a user, you in essence have them take on the privileges of the role in conjunction with any other roles they have. If you add a privilege to a role, all users with that role immediately have the new privilege.
Privilege support
As of MR2, Neo4j supports four privileges:
- READ – the ability to read properties of the object being secured
- TRAVERSE – the ability to include the object in a graph traversal – that is, the ability to find the node or relationship at all
- MATCH – a combination of the READ and TRAVERSE privileges
- WRITE – the ability to modify nodes, relationships and properties. In MR2 this isn’t as tightly defined as the other roles (though it will be eventually), and doesn’t really count as fine-grained but in its current form it does at least let you support read-only users by not including the WRITE privilege
WRITE is currently a graph-wide ‘insert/update/delete’ privilege, though support for finer-grained label-based control is coming. READ, TRAVERSE and MATCH are more interesting.
Privileges are granted to Roles, which are allocated to Users.
Cloudbooks – an imaginary SaaS provider
We need an example graph to explore this stuff, so: Cloudbooks is an imaginary provider of accounting software. Their software is multi-tenant, which means that all of their customers inhabit a single database in the cloud.
Neo4j is used amongst other things to track which features of the software users are interacting with. Our data model is roughly as follows:
A Tenant (in red) has multiple Users (brown) as members. Each User may have used zero or more Features (green).
Getting setup – Neo4j 4.0 MR2 Enterprise in Docker
RBAC functionality is only available in Neo4j Enterprise edition, so let’s spin out a Docker container with it in.
wget http://dist.neo4j.org/neo4j-enterprise-4.0.0-alpha09mr02-docker-complete.tar
docker load < neo4j-enterprise-4.0.0-alpha09mr02-docker-complete.tar
docker run --publish=7474:7474 --publish=7687:7687 --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes -t neo4j:4.0.0-alpha09mr02-enterprise
Things have changed in Neo 4.0 – there’s now support for multiple databases. When we first log in using Neo4j Browser we’ll be connected to the default ‘neo4j’ database, but we also have an option to use the ‘system’ database. The system database is where we can create new databases as well as define roles, create users and assign access to roles using the new RBAC functionality so let’s do that.
We’ll create a new database called ‘cloudbooks’, then add some sample data to it.
CREATE DATABASE cloudbooks
:use cloudbooks
MERGE (t1: Tenant { id: 1 })
MERGE (t2: Tenant { id: 2 })
MERGE (t1u1: User { name: 'Geoff Capes', id: 100 })
MERGE (t1u2: User { name: 'Bill Kazmaier', id: 101 })
MERGE (t2u1: User { name: 'Jón Páll Sigmarsson', id: 102 })
MERGE (f1: Feature { name: 'Create account' })
MERGE (f2: Feature { name: 'Print PDF' })
MERGE (f3: Feature { name: 'Merge accounts' })
MERGE (t1)<-[:MEMBER_OF]-(t1u1)
MERGE (t1)<-[:MEMBER_OF]-(t1u2)
MERGE (t2)<-[:MEMBER_OF]-(t2u1)
MERGE (t1u1)-[:USED]->(f1)
MERGE (t1u1)-[:USED]->(f2)
MERGE (t1u2)-[:USED]->(f3)
MERGE (t2u1)-[:USED]->(f2)
Defining roles
We need a few roles to support this application:
- Support – these members of the Cloudbooks team support staff can view all records in the system
- Analyst – this role represents data analysts who might analyse feature usage by tenant. These users aren’t allowed access to information about individual users of the Cloudbooks system
While we’re at it, we’re going to create two new users, one per role
- supportuser
- analystuser
Each user’s password is just their username again, since this is just a test.
:use system
CREATE USER supportuser SET PASSWORD 'supportuser' CHANGE NOT REQUIRED
CREATE USER analystuser SET PASSWORD 'analystuser' CHANGE NOT REQUIRED
CREATE ROLE Support
CREATE ROLE Analyst
GRANT ROLE Support TO supportuser
GRANT ROLE Analyst to analystuser
If we log in as any of those users, we’ll find that by default we have no privileges at all.
Let’s allocate some privileges to the roles.
Graph-wide privileges
Neo4j 4.0 supports graph-wide privileges – that is, we can specify a privilege be allocated to every node and relationship in the graph without needing to call out specific labels or relationship types.
Let’s give the Support role read access to the entire graph:
:use system
GRANT MATCH (*) ON GRAPH cloudbooks TO Support
If we log in as the support user we can check that we see the whole graph:
But we can’t make any modifications to the graph:
Good – that’s basic, graph-wide RBAC working.
The READ privilege and how DENY works
Let’s look at the following query, first:
MATCH (t: Tenant)<-[:MEMBER_OF]-(:User)-[:USED]->(f: Feature)
RETURN t.id as `Tenant ID`,
f.name AS `Feature Name`,
count(*) AS `Usage Count`
ORDER BY `Tenant ID`, `Feature Name`
We want to list, per tenant, the features they’re using and how often they’re getting used. Our result if we log in as the admin account (which can see and do everything) is what you’d expect:
Notice that we don’t actually care which User used the feature, nor do we return any information about them – we just traverse that node to get the insights we want.
We could use a Deny permission on User so prevent anyone in the Analysis group from reading user details – we’re going to drop and recreate the role here to properly clean up behind the scenes, as there seems to be a couple of issues at the minute in MR2 with REVOKE not quite cleaning house how it should:
:use system
DROP ROLE Analyst
CREATE ROLE Analyst
GRANT ROLE Analyst to analystuser
GRANT MATCH (*) ON GRAPH cloudbooks TO Analyst
DENY READ (*) ON GRAPH cloudbooks NODES User TO Analyst
If we log in as our analystuser account we’ll find that the same query still works:
But we can’t return any information about the user nodes themselves:
As with most such systems, DENY takes precedence over GRANT – even though we have MATCH on everything in the graph we still can’t read information from the privileged User nodes because of the DENY rule.
Property-specific privileges
Neo has nulled-out properties on the User object because we’ve got a DENY rule on our role. Let’s relax that a bit, and allow access to user IDs, but not user names.
DROP ROLE Analyst
CREATE ROLE Analyst
GRANT ROLE Analyst to analystuser
GRANT MATCH (*) ON GRAPH cloudbooks NODES User TO Analyst
DENY READ (name) ON GRAPH cloudbooks NODES User TO Analyst
READ and MATCH both support specifying one or more properties to allow or deny access to, with the (*) wildcard representing ‘all properties’. In the above, the DENY READ (name) privilege prevents us from reading the ‘name’ property of User nodes, but since no other DENY exists for other properties on User nodes we can cheerily read out the user’s ID.
The TRAVERSE privilege vs the MATCH privilege
We’ve been using the MATCH permission above, which is a combination of READ and TRAVERSE. Let’s see what happens if you just use one or the other.
We’ll grant MATCH on everything except User, and then try the two fine-grained controls on just User nodes.
The TRAVERSE privilege
DROP ROLE Analyst
CREATE ROLE Analyst
GRANT ROLE Analyst to analystuser
GRANT MATCH (*) ON GRAPH cloudbooks Nodes Tenant, Feature TO Analyst
GRANT TRAVERSE ON GRAPH cloudbooks Relationships * TO Analyst
With no further permissions, our query returns no results – because we aren’t permitted to traverse User nodes, we can’t make the jump from Tenant to Feature (even though we have a TRAVERSE privilege on every relationship in the graph):
MATCH (t: Tenant)<-[:MEMBER_OF]-(u:User)-[:USED]->(f: Feature)
RETURN t.id as `Tenant ID`,
f.name AS `Feature Name`,
u.name AS `User name`,
u.id AS `User ID`,
count(*) AS `Usage Count`
If we add the TRAVERSE privilege into the Analyst role on User nodes:
GRANT TRAVERSE ON GRAPH cloudbooks Nodes User TO Analyst
We start getting results again. We’ve had to include a TRAVERSE privilege on both the User node and the relationship types into and out of that node that we wanted to query on.
Because our privileges don’t extend to READ on the User node’s properties, we don’t get any data back about the user:
The MATCH privilege
MATCH is a combination of READ and TRAVERSE, so we would expect a MATCH privilege on User nodes to let us read out the properties of those nodes in a way TRAVERSE wouldn’t. Let’s try it:
GRANT MATCH (id) ON GRAPH cloudbooks NODES User TO Analyst
Wrap-up
Neo’s new RBAC functionality isn’t finished yet, and in MR2 things like the WRITE privilege not being fine-grained and REVOKE not cleaning things up properly are issues that will be fixed.
That said, it’s super promising and the splitting out of TRAVERSE from READ (and ability to specify privileges at the node and relationship level separately) will allow some interesting data protection scenarios to be cooked up.