Neo4j’s security model is designed to be granular, but most users end up with a binary "can do anything" or "can do nothing" situation.

Let’s see it in action. Imagine a simple movie graph:

{
  "nodes": [
    {"id": 0, "labels": ["Movie"], "properties": {"title": "The Matrix", "released": 1999}},
    {"id": 1, "labels": ["Person"], "properties": {"name": "Keanu Reeves", "born": 1964}},
    {"id": 2, "labels": ["Movie"], "properties": {"title": "John Wick", "released": 2014}},
    {"id": 3, "labels": ["Person"], "properties": {"name": "Chad Stahelski", "born": 1968}}
  ],
  "relationships": [
    {"start": 1, "end": 0, "type": "ACTED_IN"},
    {"start": 1, "end": 2, "type": "ACTED_IN"},
    {"start": 3, "end": 2, "type": "DIRECTED"}
  ]
}

This data can be loaded using the Neo4j Browser’s "Import/Export" feature or programmatically. Once loaded, we can query it:

MATCH (m:Movie {title: "The Matrix"}) RETURN m.title, m.released

And this would return:

[
  {
    "m.title": "The Matrix",
    "m.released": 1999
  }
]

Now, let’s talk about managing who can do what. Neo4j uses roles and privileges. By default, there’s a neo4j role with all privileges. New users are typically added to this role, giving them carte blanche.

The core problem Neo4j solves with its security model is enabling different users or applications to interact with the same graph database with varying levels of access. This is crucial for multi-tenant applications, data segregation in enterprises, or even just separating read-only reporting access from write-heavy application access.

Internally, Neo4j maps privileges to specific operations on graph elements. These operations can be applied to the entire database, specific labels, specific relationship types, or even specific property keys. This fine-grained control is where the real power lies, though it’s often overlooked.

Here’s how the system works:

  1. Users: Individual accounts that authenticate with the database.
  2. Roles: Collections of privileges. Users are assigned to one or more roles.
  3. Privileges: Define permissions for specific actions (e.g., READ, WRITE, CREATE, DELETE, ALL) on specific database elements (e.g., NODE, RELATIONSHIP, PROPERTY, INDEX, CONSTRAINT, PROCEDURE, FUNCTION).

You can manage these via Cypher:

Creating a User:

CREATE USER alice
SET password = 'supersecretpassword'
FOR ROLE neo4j; -- Assigning to the default admin role

Creating a Role for Read-Only Access:

CREATE ROLE reader;
GRANT READ ON DATABASE * TO reader; -- Grant read on all databases
GRANT READ ON ALL NODES TO reader; -- Grant read on all node labels
GRANT READ ON ALL RELATIONSHIPS TO reader; -- Grant read on all relationship types

Assigning the Role to a User:

GRANT reader TO alice;

Now, if Alice tries to execute a write operation:

CREATE (:Person {name: "Neo"})-[:IS]->(:Concept {name: "The One"});

She’ll get an error like: Neo.ClientError.Security.Forbidden: User alice is forbidden to perform this action. It requires the WRITE privilege on nodes.

The real power comes when you restrict privileges to specific labels or relationship types. For instance, if you have sensitive Person data but want to allow broad access to Movie data:

CREATE ROLE movie_viewer;
GRANT READ ON DATABASE * TO movie_viewer;
GRANT READ ON NODE LABEL Movie TO movie_viewer;
GRANT READ ON RELATIONSHIP TYPE ACTED_IN TO movie_viewer;
GRANT READ ON RELATIONSHIP TYPE DIRECTED TO movie_viewer;

CREATE USER bob
SET password = 'bobspassword'
FOR ROLE movie_viewer;

Now, Bob can query movies and actors involved in them, but he cannot see or modify Person nodes directly, nor can he see properties like born on Person nodes. He also can’t create new node labels or relationship types.

The most counterintuitive aspect of Neo4j’s security system is how it handles property-level access. While you can grant READ or WRITE on ALL NODES or ALL RELATIONSHIPS, specifying which properties are accessible is done indirectly through the element type. If you grant READ on NODE LABEL Movie, you implicitly grant read access to all properties of Movie nodes. There isn’t a direct GRANT READ ON PROPERTY Movie.title TO role; command. To achieve property-level security, you typically need to:

  1. Use different node labels for data with varying sensitivity.
  2. Use procedures (like APOC) to dynamically filter properties based on user context, although this bypasses the native privilege system for actual data access and relies on application logic within the procedure.
  3. Grant privileges at the most specific label/relationship level possible and then ensure your Cypher queries only select the allowed properties.

This means that if you grant READ on NODE LABEL Person, you grant read access to all properties of Person nodes, including sensitive ones like ssn or salary. To secure those, you’d typically segregate them into a different label (e.g., SensitivePersonData) and grant access only to specific roles.

The next challenge you’ll encounter is managing access to stored procedures and user-defined functions, which have their own distinct privilege sets.

Want structured learning?

Take the full Neo4j course →