10.3. Fine-grained access control

This section contains a worked example that illustrates various aspects of security and fine-grained access control.

The topics described in this section are:

10.3.1. The data model

Consider a healthcare database, as could be relevant in a medical clinic or hospital. A simple version of this might contain only three labels, representing three entity types:

(:Patient)

Nodes of this type represent patients that visit the clinic because they have some symptoms. Information specific to the patient can be captured in properties:

  • Name
  • SSN
  • Address
  • Date of birth
(:Symptom)

A medical database contains a catalog of known illnesses and associated symptoms, which can be described using properties:

  • Name
  • Description
(:Disease)

A medical database contains a catalog of known illnesses and associated symptoms, which can be described using properties:

  • Name
  • Description

These entities will be modelled as nodes, and connected using relationships of the following types:

(:Patient)-[:HAS]→(:Symptom)

When a patient reports to the clinic, they will describe their symptoms to the nurse or the doctor. The nurse or doctor will then enter this information into the database in the form of connections between the patient node and a graph of known symptoms. Possible properties of interest on this relationship could be:

  • Date - date when symptom was reported
(:Symptom)-[:OF]→(:Disease)

The graph of known symptoms is part of a graph of diseases and their symptoms. The relationship between a symptom and a disease can include a probability factor for how likely or common it is for people with that disease to express that symptom. This will make it easier for the doctor to make a diagnosis using statistical queries.

  • Probability - probability of symptom matching disease
(:Patient)-[:DIAGNOSIS]→(:Disease)

The doctor can use the graph of diseases and their symptoms to perform an initial investigation into the most likely diseases to match the patient. Based on this, and their own assessment of the patient, they may make a diagnosis which they would persist to the graph through the addition of this relationship with appropriate properties:

  • By: doctor’s name
  • Date: date of diagnosis
  • Description: additional doctors' notes
Figure 10.1. Healthcare use case
security example

The database would be used by a number of different user types, with different needs for access.

  • Doctors who need to perform diagnosis on patients.
  • Nurses who need to treat patients.
  • Receptionists who need to identify and record patient information.
  • Researchers who need to perform statistical analysis of medical data.
  • IT administrators who need to administer the database, creating and assigning users.

10.3.2. Security

When building an application for a specific domain, it usual to model the different users within the application itself. However, when working with a database that provides rich user management with roles and privileges, it is possible to model these entirely within the database security model. This results in separation of concerns for the access control to the data and the data itself. We will show two approaches to using Neo4j security features to support the healthcare database application. First, a simple approach using built-in roles, and then a more advanced approach using fine-grained privileges for sub-graph access control.

Our healthcare example will involve five users of the database:

  • Alice the doctor
  • Daniel the nurse
  • Bob the receptionist
  • Charlie the researcher
  • Tina the IT administrator

These users can be created using the CREATE USER command:

Example 10.1. Creating users
CREATE USER charlie SET PASSWORD $secret1 CHANGE NOT REQUIRED;
CREATE USER alice SET PASSWORD $secret2 CHANGE NOT REQUIRED;
CREATE USER daniel SET PASSWORD $secret3 CHANGE NOT REQUIRED;
CREATE USER bob SET PASSWORD $secret4 CHANGE NOT REQUIRED;
CREATE USER tina SET PASSWORD $secret5 CHANGE NOT REQUIRED;

At this point the users have no ability to interact with the database, so we need to grant those capabilities using roles. There are two different ways of doing this, either by using the built-in roles or a more fine-grained access control using privileges and custom roles.

10.3.3. Access control using built-in roles

Neo4j 4.0 comes with a number of built-in roles that cover a number of common needs:

  • reader - Can only read data from the database.
  • editor - Can read and update the database, but not expand the schema with new labels, relationship types or property names.
  • publisher - Can read and edit, as well as add new labels, relationship types and property names.
  • architect - Has all the capabilities of the publisher as well as the ability to manage indexes and constraints.
  • admin - Can perform architect actions as well as manage database, users, roles and privileges.

Charlie is a researcher and will not need write access to the database, and so he is assigned the reader role. Alice the doctor, Daniel the nurse and Bob the receptionist all need to update the database with new patient information, but do not need to expand the schema with new labels, relationship types, property names or index. We assign them all the editor role. Tina is the IT administrator that installs and manages the database. In order to create all other users, Tina is assigned the admin role.

Example 10.2. Granting roles
GRANT ROLE reader TO charlie;
GRANT ROLE editor TO alice;
GRANT ROLE editor TO daniel;
GRANT ROLE editor TO bob;
GRANT ROLE admin TO tina;

A limitation of this approach is that it does allow all users to see all data in the database, and in many real-world scenarios we would prefer to restrict the users’ access. For example, we would want to restrict the researcher from being able to read any personal information about the patients, and the receptionist should only be able to see the patient records and no more.

These, and more restrictions, could be coded into the application layer. However, it is possible and more secure to enforce these kinds of fine-grained restrictions directly within the Neo4j security model, by creating custom roles and assigning specific privileges to those roles. 2 Since we will be creating new custom roles, the first thing to do is revoke the current roles from the users:

Example 10.3. Revoking roles
REVOKE ROLE reader FROM charlie;
REVOKE ROLE editor FROM alice;
REVOKE ROLE editor FROM daniel;
REVOKE ROLE editor FROM bob;
REVOKE ROLE admin FROM tina;

Now the users are unable to do anything, and so we can start building the set of new privileges based on a complete understanding of what we want each user to be able to do.

10.3.4. Sub-graph access control using privileges

With privileges, we can take much more control over what each user is capable of doing. We start by identifying each type of user:

Doctor
Should be able to read and write most of the graph. We would, however, like to prevent the doctor from reading the patient’s address.
Receptionist
Should be able to read and write all patient data, but not be able to see the symptoms, diseases or diagnoses.
Researcher
Should be able to perform statistical analysis on all data, except patients’ personal information, and as such should not be able to read most patient properties.
Nurse
The nurse should be able to perform all tasks that both the doctor and the receptionist can do. For this reason, we do not need to create a dedicated role, but can assign nurses to both doctor and receptionist roles.
IT administrator
This role is very similar to the built-in admin role, except that we wish to restrict access to the patients SSN. To achieve this, we can create this role by copying the built-in admin role and modifying the privileges of the copy.
Example 10.4. Creating custom roles
CREATE ROLE doctor;
CREATE ROLE receptionist;
CREATE ROLE researcher;
CREATE ROLE itadmin AS COPY OF admin;

Before we assign the new roles to Alice, Bob, Daniel, Charlie and Tina, we should define the privileges of each role.

10.3.4.1. Privileges of itadmin

This role was created as a copy of the built-in admin role, and so all we need to do is restrict access to the patient’s SSN:

DENY READ {ssn} ON GRAPH healthcare NODES Patient TO itadmin;

The complete set of privileges available to users assigned the itadmin role can be viewed using the following command:

SHOW ROLE itadmin PRIVILEGES
+------------------------------------------------------------------------------------------+
| access    | action     | resource         | graph        | segment           | role      |
+------------------------------------------------------------------------------------------+
| "GRANTED" | "read"     | "all_properties" | "*"          | "NODE(*)"         | "itadmin" |
| "GRANTED" | "write"    | "all_properties" | "*"          | "NODE(*)"         | "itadmin" |
| "GRANTED" | "traverse" | "graph"          | "*"          | "NODE(*)"         | "itadmin" |
| "GRANTED" | "read"     | "all_properties" | "*"          | "RELATIONSHIP(*)" | "itadmin" |
| "GRANTED" | "write"    | "all_properties" | "*"          | "RELATIONSHIP(*)" | "itadmin" |
| "GRANTED" | "traverse" | "graph"          | "*"          | "RELATIONSHIP(*)" | "itadmin" |
| "GRANTED" | "access"   | "database"       | "*"          | "database"        | "itadmin" |
| "GRANTED" | "admin"    | "database"       | "*"          | "database"        | "itadmin" |
| "GRANTED" | "schema"   | "database"       | "*"          | "database"        | "itadmin" |
| "GRANTED" | "token"    | "database"       | "*"          | "database"        | "itadmin" |
| "DENIED"  | "read"     | "property(ssn)"  | "healthcare" | "NODE(Patient)"   | "itadmin" |
+------------------------------------------------------------------------------------------+

In order for the IT-Admin tina to be provided these privileges, she must be assigned the new role itadmin.

GRANT ROLE itadmin TO tina;

To demonstrate that Tina is not able to see the patients SSN, we can login to healthcare as tina and run the query:

MATCH (n:Patient)
 WHERE n.dateOfBirth < date('1972-06-12')
RETURN n.name, n.ssn, n.address, n.dateOfBirth;
╒═══════════════╤═══════╤════════════════════════╤═══════════════╕
│"n.name"       │"n.ssn"│"n.address"             │"n.dateOfBirth"│
╞═══════════════╪═══════╪════════════════════════╪═══════════════╡
│"Mark Smith"   │null   │"1 secret way, downtown"│"1970-01-21"   │
├───────────────┼───────┼────────────────────────┼───────────────┤
│"Mary Smith"   │null   │"1 secret way, downtown"│"1970-12-30"   │
├───────────────┼───────┼────────────────────────┼───────────────┤
│"Sally Stone"  │null   │"1 secret way, downtown"│"1971-06-17"   │
├───────────────┼───────┼────────────────────────┼───────────────┤
│"Jane Anderson"│null   │"1 secret way, downtown"│"1971-10-22"   │
└───────────────┴───────┴────────────────────────┴───────────────┘

10.3.4.2. Privileges of researcher

Charlie the researcher was previously our only read-only user. We could do something similar to what we did with the itadmin role, by copying and modifying the reader role. However, we would like to explicitly illustrate how to build a role from scratch. There are various possibilities for building this role using the concepts of whitelisting and blacklisting:

  • Blacklisting:

    We could grant the role the ability to find all nodes and read all properties (much like the reader role) and then deny read access to the Patient properties we want to restrict the researcher from seeing, such as name, SSN and address. This approach is simple but suffers from one problem. If Patient nodes are assigned additional properties, after we have restricted access, these new properties will automatically be visible to the researcher, which may not be desirable.

    Example 10.5. Blacklisting
    GRANT ACCESS ON DATABASE healthcare TO researcherB;
    // First grant access to everything
    GRANT MATCH {*}
        ON GRAPH healthcare
        TO researcherB;
    // Then deny read on specific node properties
    DENY READ {name, address, ssn}
        ON GRAPH healthcare
        NODES Patient
        TO researcherB;
    // And deny traversal of the doctors diagnosis
    DENY TRAVERSE
        ON GRAPH healthcare
        RELATIONSHIPS DIAGNOSIS
        TO researcherB;
  • Whitelisting:

    An alternative is to only provide specific access to the properties we wish the researcher to see. Then, the addition of new properties will not automatically make them visible to the researcher. In this case, adding new properties to a Patient will not mean that the researcher can see them by default. If we wish to have them visible, we need to explicitly grant read access.

    Example 10.6. Whitelisting
    GRANT ACCESS ON DATABASE healthcare TO researcherW;
    // We allow the researcher to find all nodes
    GRANT TRAVERSE
        ON GRAPH healthcare
        NODES *
        TO researcherW;
    // Now only allow the researcher to traverse specific relationships
    GRANT TRAVERSE
        ON GRAPH healthcare
        RELATIONSHIPS HAS, OF
        TO researcherW;
    // Allow reading of all properties of medical metadata
    GRANT READ {*}
        ON GRAPH healthcare
        NODES Symptom, Disease
        TO researcherW;
    // Only allow reading dateOfBirth for research purposes
    GRANT READ {dateofbirth}
        ON GRAPH healthcare
        NODES Patient
        TO researcherW;

In order to test that Charlie now has the privileges we have specified, we assign him to the researcherB role with blacklisting:

GRANT ROLE researcherB TO charlie;

We can use a version of the SHOW PRIVILEGES command to see Charlies access rights:

neo4j@system> SHOW USER charlie PRIVILEGES;
+---------------------------------------------------------------------------------------------------------------------+
| access    | action     | resource            | graph        | segment                   | role          | user      |
+---------------------------------------------------------------------------------------------------------------------+
| "GRANTED" | "read"     | "all_properties"    | "healthcare" | "NODE(*)"                 | "researcherB" | "charlie" |
| "GRANTED" | "traverse" | "graph"             | "healthcare" | "NODE(*)"                 | "researcherB" | "charlie" |
| "DENIED"  | "read"     | "property(address)" | "healthcare" | "NODE(Patient)"           | "researcherB" | "charlie" |
| "DENIED"  | "read"     | "property(name)"    | "healthcare" | "NODE(Patient)"           | "researcherB" | "charlie" |
| "DENIED"  | "read"     | "property(ssn)"     | "healthcare" | "NODE(Patient)"           | "researcherB" | "charlie" |
| "GRANTED" | "read"     | "all_properties"    | "healthcare" | "RELATIONSHIP(*)"         | "researcherB" | "charlie" |
| "GRANTED" | "traverse" | "graph"             | "healthcare" | "RELATIONSHIP(*)"         | "researcherB" | "charlie" |
| "DENIED"  | "traverse" | "graph"             | "healthcare" | "RELATIONSHIP(DIAGNOSIS)" | "researcherB" | "charlie" |
| "GRANTED" | "access"   | "database"          | "healthcare" | "database"                | "researcherB" | "charlie" |
+---------------------------------------------------------------------------------------------------------------------+

Now when Charlie logs into the healthcare database and tries to run a command similar to the one used by the itadmin above, we will see different results:

MATCH (n:Patient)
 WHERE n.dateOfBirth < date('1972-06-12')
RETURN n.name, n.ssn, n.address, n.dateOfBirth;
╒════════╤═══════╤═══════════╤═══════════════╕
│"n.name"│"n.ssn"│"n.address"│"n.dateOfBirth"│
╞════════╪═══════╪═══════════╪═══════════════╡
│null    │null   │null       │"1970-01-21"   │
├────────┼───────┼───────────┼───────────────┤
│null    │null   │null       │"1970-12-30"   │
├────────┼───────┼───────────┼───────────────┤
│null    │null   │null       │"1971-06-17"   │
├────────┼───────┼───────────┼───────────────┤
│null    │null   │null       │"1971-10-22"   │
└────────┴───────┴───────────┴───────────────┘

Only the date of birth is available, so Charlie the researcher may perform statistical analysis, for example. Another query Charlie could try is to find the ten diseases a patient younger than 25 is most likely to be diagnosed with, listed by probability:

WITH datetime() - duration({years:25}) AS timeLimit
MATCH (n:Patient)
WHERE n.dateOfBirth > date(timeLimit)
MATCH (n)-[h:HAS]->(s:Symptom)-[o:OF]->(d:Disease)
WITH d.name AS disease, o.probability AS prob
RETURN disease, sum(prob) AS score ORDER BY score DESC LIMIT 10;
╒═════════════════════╤══════════════════╕
│"disease"            │"score"           │
╞═════════════════════╪══════════════════╡
│"Chronic Whatitis"   │111.30050876448621│
├─────────────────────┼──────────────────┤
│"Chronic Someitis"   │110.56964390091147│
├─────────────────────┼──────────────────┤
│"Acute Yellowitis"   │98.82266316401365 │
├─────────────────────┼──────────────────┤
│"Chronic Otheritis"  │80.41346486864003 │
├─────────────────────┼──────────────────┤
│"Acute Otheritis"    │79.82679831362869 │
├─────────────────────┼──────────────────┤
│"Acute Placeboitis"  │78.86090865510758 │
├─────────────────────┼──────────────────┤
│"Chronic Yellowitis" │77.53519713418886 │
├─────────────────────┼──────────────────┤
│"Chronic Argitis"    │70.04150610048167 │
├─────────────────────┼──────────────────┤
│"Acute Someitis"     │69.45011166554933 │
├─────────────────────┼──────────────────┤
│"Chronic Placeboitis"│64.36353437805441 │
└─────────────────────┴──────────────────┘

10.3.4.3. Privileges of doctor

Doctors should be given the ability to read and write almost everything. We would, however, like to remove the ability to read the patients' address property. This role can be built from scratch by assigning full read and write access, and then specifically denying access to the address property:

GRANT ACCESS ON DATABASE healthcare TO doctor;
GRANT TRAVERSE ON GRAPH healthcare TO doctor;
GRANT READ {*} ON GRAPH healthcare TO doctor;
GRANT WRITE ON GRAPH healthcare TO doctor;
DENY READ {address} ON GRAPH healthcare NODES Patient TO doctor;

To allow Alice to have these privileges, we grant her this new role:

GRANT ROLE doctor TO alice;

To demonstrate that Alice is not able to see patient addresses, we can run the query:

MATCH (n:Patient)
 WHERE n.dateOfBirth < date('1972-06-12')
RETURN n.name, n.ssn, n.address, n.dateOfBirth;
╒═══════════════╤═══════╤═══════════╤═══════════════╕
│"n.name"       │"n.ssn"│"n.address"│"n.dateOfBirth"│
╞═══════════════╪═══════╪═══════════╪═══════════════╡
│"Mark Smith"   │1234610│null       │"1970-01-21"   │
├───────────────┼───────┼───────────┼───────────────┤
│"Mary Smith"   │1234640│null       │"1970-12-30"   │
├───────────────┼───────┼───────────┼───────────────┤
│"Sally Stone"  │1234641│null       │"1971-06-17"   │
├───────────────┼───────┼───────────┼───────────────┤
│"Jane Anderson"│1234652│null       │"1971-10-22"   │
└───────────────┴───────┴───────────┴───────────────┘

As we can see, the doctor has the expected privileges, including being able to see the SSN, but not the address of each patient.

10.3.4.4. Privileges of receptionist

Receptionists should only be able to manage patient information. They are not allowed to find or read any other parts of the graph:

GRANT ACCESS ON DATABASE healthcare TO receptionist;
GRANT MATCH {*} ON GRAPH healthcare NODES Patient TO receptionist;
GRANT WRITE ON GRAPH healthcare TO receptionist;

It is currently not possible to be specific on WRITE access, and therefore a user that is granted write access is able to write to all nodes and relationships. For example, the receptionist could create a new Symptom node, even if they are then not able to find that in the database due to the restricted read access.

GRANT ROLE receptionist TO bob;

With these privileges, if Bob tries to read the entire database, he will still only see the patients:

MATCH (n) WITH labels(n) AS labels
RETURN labels, count(*);
╒═══════════╤══════════╕
│"labels"   │"count(*)"│
╞═══════════╪══════════╡
│["Patient"]│101       │
└───────────┴──────────┘

However, Bob is able to see all fields of the Patient records:

MATCH (n:Patient)
 WHERE n.dateOfBirth < date('1972-06-12')
RETURN n.name, n.ssn, n.address, n.dateOfBirth;
╒═══════════════╤═══════╤════════════════════════╤═══════════════╕
│"n.name"       │"n.ssn"│"n.address"             │"n.dateOfBirth"│
╞═══════════════╪═══════╪════════════════════════╪═══════════════╡
│"Mark Smith"   │1234610│"1 secret way, downtown"│"1970-01-21"   │
├───────────────┼───────┼────────────────────────┼───────────────┤
│"Mary Smith"   │1234640│"1 secret way, downtown"│"1970-12-30"   │
├───────────────┼───────┼────────────────────────┼───────────────┤
│"Sally Stone"  │1234641│"1 secret way, downtown"│"1971-06-17"   │
├───────────────┼───────┼────────────────────────┼───────────────┤
│"Jane Anderson"│1234652│"1 secret way, downtown"│"1971-10-22"   │
└───────────────┴───────┴────────────────────────┴───────────────┘

Let say that Bob the receptionist wants to remove a patient from the database:

MATCH (n:Patient)
 WHERE n.SSN = 1234610
DETACH DELETE n;
org.neo4j.graphdb.ConstraintViolationException: Cannot delete node<42>, because it still has relationships. To delete this node, you must first delete its relationships.

The reason this fails is that Bob can find the (:Patient) node, but does not have sufficient traverse rights to find the outgoing relationships from it. Either he needs to ask Tina the itadmin for help for this task, or we can add more privileges to the receptionist role:

GRANT TRAVERSE ON GRAPH healhcare NODES Symptom, Disease TO receptionist;
GRANT TRAVERSE ON GRAPH healthcare RELATIONSHIPS HAS, DIAGNOSIS TO receptionist;

10.3.4.5. Privileges of nurses

The nurse is not defined as a separate role because, as we have established, nurses have the capabilities of both doctors and receptionists. Therefore we can assign both those roles to Daniel the nurse and achieve desired behaviour for a nurse.

GRANT ROLE doctor, receptionist TO daniel;

Now we can see that the user 'Daniel' has a combined set of privileges:

neo4j@system> SHOW USER daniel PRIVILEGES;
+-------------------------------------------------------------------------------------------------------------+
| access    | action     | resource            | graph        | segment           | role           | user     |
+-------------------------------------------------------------------------------------------------------------+
| "GRANTED" | "read"     | "all_properties"    | "healthcare" | "NODE(*)"         | "doctor"       | "daniel" |
| "GRANTED" | "write"    | "all_properties"    | "healthcare" | "NODE(*)"         | "doctor"       | "daniel" |
| "GRANTED" | "traverse" | "graph"             | "healthcare" | "NODE(*)"         | "doctor"       | "daniel" |
| "DENIED"  | "read"     | "property(address)" | "healthcare" | "NODE(Patient)"   | "doctor"       | "daniel" |
| "GRANTED" | "read"     | "all_properties"    | "healthcare" | "RELATIONSHIP(*)" | "doctor"       | "daniel" |
| "GRANTED" | "write"    | "all_properties"    | "healthcare" | "RELATIONSHIP(*)" | "doctor"       | "daniel" |
| "GRANTED" | "traverse" | "graph"             | "healthcare" | "RELATIONSHIP(*)" | "doctor"       | "daniel" |
| "GRANTED" | "access"   | "database"          | "healthcare" | "database"        | "doctor"       | "daniel" |
| "GRANTED" | "write"    | "all_properties"    | "healthcare" | "NODE(*)"         | "receptionist" | "daniel" |
| "GRANTED" | "read"     | "all_properties"    | "healthcare" | "NODE(Patient)"   | "receptionist" | "daniel" |
| "GRANTED" | "traverse" | "graph"             | "healthcare" | "NODE(Patient)"   | "receptionist" | "daniel" |
| "GRANTED" | "write"    | "all_properties"    | "healthcare" | "RELATIONSHIP(*)" | "receptionist" | "daniel" |
| "GRANTED" | "access"   | "database"          | "healthcare" | "database"        | "receptionist" | "daniel" |
+-------------------------------------------------------------------------------------------------------------+