Fine-grained access control

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

  • dateOfBirth

(: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

security example
Figure 1. Healthcare use case

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.

Security

When building an application for a specific domain, it is common 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 (for more information, see Cypher Manual → Administration → Security). 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 involves 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 (from the system database):

Example 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 through more fine-grained access control using privileges and custom roles.

Access control using built-in roles

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

  • PUBLIC - All users have this role, can by default only access the default database.

  • reader - Can read data from all databases.

  • editor - Can read and update all databases, 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 databases, 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 indexes. 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 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 it would be preferable to restrict the users’ access. In this example, we would want to restrict the researcher from being able to read any of the patients' personal information, and the receptionist should only be able to see the patient records and nothing 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.

Since we will be creating new custom roles, the first thing to do is revoke the current roles from the users:

Example 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 once again unable to do anything, and so we need to start over by building the set of new privileges based on a complete understanding of what we want each user to be able to do.

Sub-graph access control using privileges

With the concept of 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. We would also like to make sure the doctor can save diagnoses to the database, but not expand the schema of the database with new concepts.

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. To illustrate two different ways of setting up the same effective privileges, we will create two roles and compare them.

Nurse

The nurse should be able to perform all tasks that both the doctor and the receptionist can do. At first one might be tempted to simply grant both those roles, but this does not work as expected. We will demonstrate why below, and instead create a dedicated nurse role.

Junior nurse

The senior nurse above is able to save diagnoses just as a doctor can. However, we might wish to have nurses that are not allowed to make that update to the graph. While we could build another role from scratch, this could more easily be achieved by combining the nurse role with a new disableDiagnoses role that specifically restricts that activity.

IT administrator

This role is very similar to the built-in admin role, except that we wish to restrict access to the patients SSN, as well as prevent the administrator from performing the very critical action of saving a diagnosis, something specific to medical professionals. To achieve this, we can create this role by copying the built-in admin role and modifying the privileges of that copy.

User manager

It is possible that we would like the IT administrator to be less powerful than described above. We can create a new role from scratch, granting only the specific administrative capabilities we actually desire.

Before we create the new roles and assign them to Alice, Bob, Daniel, Charlie and Tina, we should define the privileges of each role. Since all users need ACCESS privilege to the healthcare database, we can add this to the PUBLIC role instead of all the individual roles:

GRANT ACCESS ON DATABASE healthcare TO PUBLIC;

Privileges of itadmin

This role can be created as a copy of the built-in admin role:

CREATE ROLE itadmin AS COPY OF admin;

Then all we need to do is deny the two specific actions this role is not supposed to do:

  • Should not be able to read any patients social security number.

  • Should not be able to perform medical diagnosis.

DENY READ {ssn} ON GRAPH healthcare NODES Patient TO itadmin;
DENY CREATE ON GRAPH healthcare RELATIONSHIPS DIAGNOSIS 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" | "match"          | "all_properties" | "*"          | "NODE(*)"                 | "itadmin" |
| "GRANTED" | "write"          | "graph"          | "*"          | "NODE(*)"                 | "itadmin" |
| "GRANTED" | "match"          | "all_properties" | "*"          | "RELATIONSHIP(*)"         | "itadmin" |
| "GRANTED" | "write"          | "graph"          | "*"          | "RELATIONSHIP(*)"         | "itadmin" |
| "GRANTED" | "access"         | "database"       | "*"          | "database"                | "itadmin" |
| "GRANTED" | "admin"          | "database"       | "*"          | "database"                | "itadmin" |
| "GRANTED" | "constraint"     | "database"       | "*"          | "database"                | "itadmin" |
| "GRANTED" | "index"          | "database"       | "*"          | "database"                | "itadmin" |
| "GRANTED" | "token"          | "database"       | "*"          | "database"                | "itadmin" |
| "DENIED"  | "read"           | "property(ssn)"  | "healthcare" | "NODE(Patient)"           | "itadmin" |
| "DENIED"  | "create_element" | "graph"          | "healthcare" | "RELATIONSHIP(DIAGNOSIS)" | "itadmin" |
+--------------------------------------------------------------------------------------------------------+
Privileges that were granted or denied earlier can be revoked using the REVOKE command. See the Cypher Manual → The REVOKE command.

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

neo4j@system> 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 |
+--------------------------------------------------------------------+
| "Mary Stone"    | NULL  | "1 secret way, downtown" | 1970-01-15    |
| "Ally Anderson" | NULL  | "1 secret way, downtown" | 1970-08-20    |
| "Sally Stone"   | NULL  | "1 secret way, downtown" | 1970-03-12    |
| "Jane Stone"    | NULL  | "1 secret way, downtown" | 1970-07-21    |
| "Ally Svensson" | NULL  | "1 secret way, downtown" | 1971-08-15    |
| "Jane Svensson" | NULL  | "1 secret way, downtown" | 1972-05-12    |
| "Ally Svensson" | NULL  | "1 secret way, downtown" | 1971-07-30    |
+--------------------------------------------------------------------+

The results make it seem as if these nodes do not even have an ssn field. This is a key feature of the security model, that users cannot tell the difference between data that is not there, and data that is hidden using fine-grained read privileges.

Now remember that we also denied the administrator from saving diagnoses, because that is a critical medical function reserved for only doctors and senior medical staff. We can test this by trying to create DIAGNOSIS relationships:

MATCH (n:Patient), (d:Disease)
CREATE (n)-[:DIAGNOSIS]->(d);
Create relationship with type 'DIAGNOSIS' is not allowed for user 'tina' with roles [PUBLIC, itadmin].
While restrictions on reading data do not result in errors and only make it appear as if the data is not there, restrictions on updating, i.e. writing to the graph will produce an appropriate error when the user attempts to perform an update they are not permitted to do.

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 related concepts of blacklisting and whitelisting:

  • 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 4. Blacklisting
    // First create the role
    CREATE ROLE researcherB;
    // Then grant access to everything
    GRANT MATCH {*}
        ON GRAPH healthcare
        TO researcherB;
    // And deny read on specific node properties
    DENY READ {name, address, ssn}
        ON GRAPH healthcare
        NODES Patient
        TO researcherB;
    // And finally 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 5. Whitelisting
    // Create the role first
    CREATE ROLE 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;
    // Allow reading of all properties of the disease-symptom relationship
    GRANT READ {*}
        ON GRAPH healthcare
        RELATIONSHIPS OF
        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" | "access"   | "database"          | "DEFAULT"    | "database"                | "PUBLIC"      | "charlie" |
| "GRANTED" | "access"   | "database"          | "healthcare" | "database"                | "PUBLIC"      | "charlie" |
| "GRANTED" | "match"    | "all_properties"    | "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" | "match"    | "all_properties"    | "healthcare" | "RELATIONSHIP(*)"         | "researcherB" | "charlie" |
| "DENIED"  | "traverse" | "graph"             | "healthcare" | "RELATIONSHIP(DIAGNOSIS)" | "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      | 1971-05-31    |
| NULL   | NULL  | NULL      | 1971-04-17    |
| NULL   | NULL  | NULL      | 1971-12-27    |
| NULL   | NULL  | NULL      | 1970-02-13    |
| NULL   | NULL  | NULL      | 1971-02-04    |
| NULL   | NULL  | NULL      | 1971-05-10    |
| NULL   | NULL  | NULL      | 1971-02-21    |
+--------------------------------------------+

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             |
+-------------------------------------------+
| "Acute Argitis"       | 95.05395287286318 |
| "Chronic Someitis"    | 88.7220337139605  |
| "Chronic Placeboitis" | 88.43609533058974 |
| "Acute Whatitis"      | 83.23493746472457 |
| "Acute Otheritis"     | 82.46129768949129 |
| "Chronic Otheritis"   | 82.03650063794025 |
| "Acute Placeboitis"   | 77.34207326583929 |
| "Acute Yellowitis"    | 76.34519967465832 |
| "Chronic Whatitis"    | 73.73968070128234 |
| "Chronic Yellowitis"  | 71.58791287376775 |
+-------------------------------------------+

Now if we revoke the researcherB and instead grant the researcherW role to Charlie, and re-run these queries, we will see the same results.

Privileges that were granted or denied earlier can be revoked using the REVOKE command. See the Cypher Manual → The REVOKE command.

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:

CREATE ROLE 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;
DENY SET PROPERTY {address} ON GRAPH healthcare NODES Patient TO doctor;

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

neo4j@system> GRANT ROLE doctor TO alice;

To demonstrate that Alice is not able to see patient addresses, we log in as alice to healthcare 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 |
+-------------------------------------------------------+
| "Jack Anderson" | 1234647 | NULL      | 1970-07-23    |
| "Joe Svensson"  | 1234659 | NULL      | 1972-06-07    |
| "Mary Jackson"  | 1234568 | NULL      | 1971-10-19    |
| "Jack Jackson"  | 1234583 | NULL      | 1971-05-04    |
| "Ally Smith"    | 1234590 | NULL      | 1971-12-07    |
| "Ally Stone"    | 1234606 | NULL      | 1970-03-29    |
| "Mark Smith"    | 1234610 | NULL      | 1971-03-30    |
+-------------------------------------------------------+

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

The doctor is also able to see all other node types:

MATCH (n) WITH labels(n) AS labels
RETURN labels, count(*);
+------------------------+
| labels      | count(*) |
+------------------------+
| ["Patient"] | 101      |
| ["Symptom"] | 10       |
| ["Disease"] | 12       |
+------------------------+

In addition, the doctor can traverse the graph, finding symptoms and diseases connected to patients:

MATCH (n:Patient)-[:HAS]->(s:Symptom)-[:OF]->(d:Disease)
  WHERE n.ssn = 1234657
RETURN n.name, d.name, count(s) AS score ORDER BY score DESC;

The resulting table shows which are the most likely diagnoses based on symptoms. The doctor can use this table to facilitate further questioning and testing of the patient in order to decide on the final diagnosis.

+--------------------------------------------------+
| n.name           | d.name                | score |
+--------------------------------------------------+
| "Sally Anderson" | "Chronic Otheritis"   | 4     |
| "Sally Anderson" | "Chronic Yellowitis"  | 3     |
| "Sally Anderson" | "Chronic Placeboitis" | 3     |
| "Sally Anderson" | "Acute Whatitis"      | 2     |
| "Sally Anderson" | "Acute Yellowitis"    | 2     |
| "Sally Anderson" | "Chronic Someitis"    | 2     |
| "Sally Anderson" | "Chronic Argitis"     | 2     |
| "Sally Anderson" | "Chronic Whatitis"    | 2     |
| "Sally Anderson" | "Acute Someitis"      | 1     |
| "Sally Anderson" | "Acute Argitis"       | 1     |
| "Sally Anderson" | "Acute Otheritis"     | 1     |
+--------------------------------------------------+

Once the doctor has investigated further, they would be able to decide on the diagnosis and save that result to the database:

WITH datetime({epochmillis:timestamp()}) AS now
WITH now, date(now) as today
MATCH (p:Patient)
  WHERE p.ssn = 1234657
MATCH (d:Disease)
  WHERE d.name = "Chronic Placeboitis"
MERGE (p)-[i:DIAGNOSIS {by: 'Alice'}]->(d)
  ON CREATE SET i.created_at = now, i.updated_at = now, i.date = today
  ON MATCH SET i.updated_at = now
RETURN p.name, d.name, i.by, i.date, duration.between(i.created_at, i.updated_at) AS updated;

This allows this doctor to record their diagnosis as well as take note of previous diagnoses:

+----------------------------------------------------------------------------------------+
| p.name           | d.name                | i.by    | i.date     | updated              |
+----------------------------------------------------------------------------------------+
| "Sally Anderson" | "Chronic Placeboitis" | "Alice" | 2020-05-29 | P0M0DT213.076000000S |
+----------------------------------------------------------------------------------------+
In order to create the DIAGNOSIS relationship for the first time, it is required to have the privilege to create new types. This is also true of the property names doctor, created_at and updated_at. This can be fixed by either granting the doctor NAME MANAGEMENT privileges or by pre-creating the missing types. The latter would be more precise and can be achieved by running, as an administrator, the procedures db.createRelationshipType and db.createProperty with appropriate arguments.

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. In addition, they should be able to create and delete patients, but not any other nodes:

CREATE ROLE receptionist;
GRANT MATCH {*} ON GRAPH healthcare NODES Patient TO receptionist;
GRANT CREATE ON GRAPH healthcare NODES Patient TO receptionist;
GRANT DELETE ON GRAPH healthcare NODES Patient TO receptionist;
GRANT SET PROPERTY {*} ON GRAPH healthcare NODES Patient TO receptionist;

It would have been simpler to grant global WRITE privileges. However, this would have the unfortunate side effect of allowing the receptionist the ability to create other nodes, like new Symptom nodes, even though they would subsequently be unable to find or read those same nodes. While there are use cases for being able to create data you cannot read, that is not desired for this model.

neo4j@system> 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 Stone"    | 1234666 | "1 secret way, downtown" | 1970-08-04    |
| "Sally Jackson" | 1234633 | "1 secret way, downtown" | 1970-10-21    |
| "Bob Stone"     | 1234581 | "1 secret way, downtown" | 1972-02-16    |
| "Ally Anderson" | 1234582 | "1 secret way, downtown" | 1970-05-13    |
| "Mark Svensson" | 1234594 | "1 secret way, downtown" | 1970-01-16    |
| "Bob Anderson"  | 1234597 | "1 secret way, downtown" | 1970-09-23    |
| "Jack Svensson" | 1234599 | "1 secret way, downtown" | 1971-02-13    |
| "Mark Jackson"  | 1234618 | "1 secret way, downtown" | 1970-03-28    |
| "Jack Jackson"  | 1234623 | "1 secret way, downtown" | 1971-04-02    |
+----------------------------------------------------------------------+

We have granted Bob the receptionist the ability to delete patient nodes. This will allow him to delete any new patients he has just created, but will not allow him the ability to delete patients that have already received diagnoses, because those are connected to parts of the graph the receptionist cannot see. Let’s demonstrate both these scenarios:

CREATE (n:Patient {
  ssn:87654321,
  name: 'Another Patient',
  email: 'another@example.com',
  address: '1 secret way, downtown',
  dateOfBirth: date('2001-01-20')
})
RETURN n.name, n.dateOfBirth;
+-----------------------------------+
| n.name            | n.dateOfBirth |
+-----------------------------------+
| "Another Patient" | 2001-01-20    |
+-----------------------------------+

The receptionist is able to modify any patient record:

MATCH (n:Patient)
WHERE n.ssn = 87654321
SET n.address = '2 streets down, uptown'
RETURN n.name, n.dateOfBirth, n.address;
+--------------------------------------------------------------+
| n.name            | n.dateOfBirth | n.address                |
+--------------------------------------------------------------+
| "Another Patient" | 2001-01-20    | "2 streets down, uptown" |
+--------------------------------------------------------------+

The receptionist is also able to delete this recently created patient because it is not connected to any other records:

MATCH (n:Patient)
 WHERE n.ssn = 87654321
DETACH DELETE n;

However, if the receptionist attempts to delete a patient that has existing diagnoses, this will fail:

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 nor delete 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 healthcare NODES Symptom, Disease TO receptionist;
GRANT TRAVERSE ON GRAPH healthcare RELATIONSHIPS HAS, DIAGNOSIS TO receptionist;
GRANT DELETE ON GRAPH healthcare RELATIONSHIPS HAS, DIAGNOSIS TO receptionist;
Privileges that were granted or denied earlier can be revoked using the REVOKE command. See the Cypher Manual → The REVOKE command.

Privileges of nurses

As previously described, nurses have the capabilities of both doctors and receptionists. As such it would be tempting to assign them both the doctor and the receptionist roles. However, this might not have the effect you would expect. If those two roles were created with GRANT privileges only, combining them would be simply cumulative. But it turns out the doctor contains some DENY privileges, and these always overrule GRANT. This means that the nurse will still have the same restrictions as a doctor, which is not what we wanted.

To demonstrate this, let’s give it a try:

neo4j@system> GRANT ROLE doctor, receptionist TO daniel;

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

SHOW USER daniel PRIVILEGES;
+-------------------------------------------------------------------------------------------------------------------+
| access    | action           | resource            | graph        | segment           | role           | user     |
+-------------------------------------------------------------------------------------------------------------------+
| "GRANTED" | "access"         | "database"          | "DEFAULT"    | "database"        | "PUBLIC"       | "daniel" |
| "GRANTED" | "access"         | "database"          | "healthcare" | "database"        | "PUBLIC"       | "daniel" |
| "GRANTED" | "read"           | "all_properties"    | "healthcare" | "NODE(*)"         | "doctor"       | "daniel" |
| "GRANTED" | "traverse"       | "graph"             | "healthcare" | "NODE(*)"         | "doctor"       | "daniel" |
| "GRANTED" | "write"          | "graph"             | "healthcare" | "NODE(*)"         | "doctor"       | "daniel" |
| "DENIED"  | "read"           | "property(address)" | "healthcare" | "NODE(Patient)"   | "doctor"       | "daniel" |
| "DENIED"  | "set_property"   | "property(address)" | "healthcare" | "NODE(Patient)"   | "doctor"       | "daniel" |
| "GRANTED" | "read"           | "all_properties"    | "healthcare" | "RELATIONSHIP(*)" | "doctor"       | "daniel" |
| "GRANTED" | "traverse"       | "graph"             | "healthcare" | "RELATIONSHIP(*)" | "doctor"       | "daniel" |
| "GRANTED" | "write"          | "graph"             | "healthcare" | "RELATIONSHIP(*)" | "doctor"       | "daniel" |
| "GRANTED" | "match"          | "all_properties"    | "healthcare" | "NODE(Patient)"   | "receptionist" | "daniel" |
| "GRANTED" | "set_property"   | "all_properties"    | "healthcare" | "NODE(Patient)"   | "receptionist" | "daniel" |
| "GRANTED" | "create_element" | "graph"             | "healthcare" | "NODE(Patient)"   | "receptionist" | "daniel" |
| "GRANTED" | "delete_element" | "graph"             | "healthcare" | "NODE(Patient)"   | "receptionist" | "daniel" |
+-------------------------------------------------------------------------------------------------------------------+
Privileges that were granted or denied earlier can be revoked using the REVOKE command. See the Cypher Manual → The REVOKE command.

Now the intention is that a nurse can perform the actions of a receptionist. This would mean they should be able to read and write the address field of the Patient nodes.

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 |
+-------------------------------------------------------+
| "Jane Anderson" | 1234572 | NULL      | 1971-05-26    |
| "Mark Stone"    | 1234586 | NULL      | 1972-06-07    |
| "Joe Smith"     | 1234595 | NULL      | 1970-12-28    |
| "Joe Jackson"   | 1234603 | NULL      | 1970-08-31    |
| "Jane Jackson"  | 1234628 | NULL      | 1972-01-31    |
| "Mary Anderson" | 1234632 | NULL      | 1971-01-07    |
| "Jack Svensson" | 1234639 | NULL      | 1970-01-06    |
+-------------------------------------------------------+

Clearly the address field is invisible. This is due to the DENIED privileges we could see in the table earlier. If we tried to write to the address field we would receive an error. This is not the intended behavior. We have two choices to correct otherwise:

  • We could redefine the doctor role with only whitelisting, requiring that we define each Patient property we wish the doctor to be able to read.

  • We can redefine the nurse role with the actual intended behavior.

It turns out that the latter choice is by far the simplest. The nurse is essentially the doctor without the address restrictions:

CREATE ROLE nurse
GRANT TRAVERSE ON GRAPH healthcare TO nurse;
GRANT READ {*} ON GRAPH healthcare TO nurse;
GRANT WRITE ON GRAPH healthcare TO nurse;

Now let’s assign this role to Daniel and test the new behavior:

REVOKE ROLE doctor FROM daniel;
REVOKE ROLE receptionist FROM daniel;
GRANT ROLE nurse TO daniel;

When the improved nurse Daniel takes another look at the patient records, he will see the address fields:

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 |
+----------------------------------------------------------------------+
| "Jane Anderson" | 1234572 | "1 secret way, downtown" | 1971-05-26    |
| "Mark Stone"    | 1234586 | "1 secret way, downtown" | 1972-06-07    |
| "Joe Smith"     | 1234595 | "1 secret way, downtown" | 1970-12-28    |
| "Joe Jackson"   | 1234603 | "1 secret way, downtown" | 1970-08-31    |
| "Jane Jackson"  | 1234628 | "1 secret way, downtown" | 1972-01-31    |
| "Mary Anderson" | 1234632 | "1 secret way, downtown" | 1971-01-07    |
| "Jack Svensson" | 1234639 | "1 secret way, downtown" | 1970-01-06    |
+----------------------------------------------------------------------+

Now Daniel can see the previously hidden address field. The other main action we want the nurse to be able to perform, is the primary doctor action of saving a diagnosis to the database:

WITH date(datetime({epochmillis:timestamp()})) AS today
MATCH (p:Patient)
  WHERE p.ssn = 1234657
MATCH (d:Disease)
  WHERE d.name = "Chronic Placeboitis"
MERGE (p)-[i:DIAGNOSIS {by: 'Daniel'}]->(d)
  ON CREATE SET i.date = today
RETURN p.name, d.name, i.by, i.date;
+------------------------------------------------------------------+
| p.name           | d.name                | i.by     | i.date     |
+------------------------------------------------------------------+
| "Sally Anderson" | "Chronic Placeboitis" | "Daniel" | 2020-05-29 |
+------------------------------------------------------------------+

Performing an action otherwise reserved for the doctor role involves more responsibility for the nurse. Perhaps it is not desirable to entrust all nurses with this option, which is why we can divide the nurses into senior and junior nurses. Daniel is currently a senior nurse.

Privileges of junior nurses

When we tried to create the senior nurse by combining the doctor and receptionist roles, that did not work out. As previously mentioned, it would work to combine two roles if the intention is to increase capabilities and the roles were created with GRANT privileges only. It is also possible to combine two roles if the intention is to reduce capabilities and the combination brings in DENY privileges.

Consider this case, we would like a junior nurse to be able to perform the same actions as a senior nurse, but not be able to save diagnoses. We could create a special role that contains specifically only the additional restrictions:

CREATE ROLE disableDiagnoses;
DENY CREATE ON GRAPH healthcare RELATIONSHIPS DIAGNOSIS TO disableDiagnoses;

Now let’s assign this role to Daniel and test the new behaviour:

GRANT ROLE disableDiagnoses TO daniel;

If we look at what privileges Daniel now has, it will be the combination of the two roles nurse and disableDiagnoses:

neo4j@system> SHOW USER daniel PRIVILEGES;
+----------------------------------------------------------------------------------------------------------------------------+
| access    | action           | resource         | graph        | segment                   | role               | user     |
+----------------------------------------------------------------------------------------------------------------------------+
| "GRANTED" | "access"         | "database"       | "DEFAULT"    | "database"                | "PUBLIC"           | "daniel" |
| "GRANTED" | "access"         | "database"       | "healthcare" | "database"                | "PUBLIC"           | "daniel" |
| "DENIED"  | "create_element" | "graph"          | "healthcare" | "RELATIONSHIP(DIAGNOSIS)" | "disableDiagnoses" | "daniel" |
| "GRANTED" | "read"           | "all_properties" | "healthcare" | "NODE(*)"                 | "nurse"            | "daniel" |
| "GRANTED" | "traverse"       | "graph"          | "healthcare" | "NODE(*)"                 | "nurse"            | "daniel" |
| "GRANTED" | "write"          | "graph"          | "healthcare" | "NODE(*)"                 | "nurse"            | "daniel" |
| "GRANTED" | "read"           | "all_properties" | "healthcare" | "RELATIONSHIP(*)"         | "nurse"            | "daniel" |
| "GRANTED" | "traverse"       | "graph"          | "healthcare" | "RELATIONSHIP(*)"         | "nurse"            | "daniel" |
| "GRANTED" | "write"          | "graph"          | "healthcare" | "RELATIONSHIP(*)"         | "nurse"            | "daniel" |
+----------------------------------------------------------------------------------------------------------------------------+

Daniel can still see address fields, and can even perform the diagnosis investigation that the doctor can perform:

MATCH (n:Patient)-[:HAS]->(s:Symptom)-[:OF]->(d:Disease)
WHERE n.ssn = 1234650
RETURN n.ssn, n.name, d.name, count(s) AS score ORDER BY score DESC;
+--------------------------------------------------------+
| n.ssn   | n.name       | d.name                | score |
+--------------------------------------------------------+
| 1234650 | "Mark Smith" | "Chronic Whatitis"    | 3     |
| 1234650 | "Mark Smith" | "Chronic Someitis"    | 3     |
| 1234650 | "Mark Smith" | "Acute Someitis"      | 2     |
| 1234650 | "Mark Smith" | "Chronic Otheritis"   | 2     |
| 1234650 | "Mark Smith" | "Chronic Yellowitis"  | 2     |
| 1234650 | "Mark Smith" | "Chronic Placeboitis" | 2     |
| 1234650 | "Mark Smith" | "Acute Otheritis"     | 2     |
| 1234650 | "Mark Smith" | "Chronic Argitis"     | 2     |
| 1234650 | "Mark Smith" | "Acute Placeboitis"   | 2     |
| 1234650 | "Mark Smith" | "Acute Yellowitis"    | 1     |
| 1234650 | "Mark Smith" | "Acute Argitis"       | 1     |
| 1234650 | "Mark Smith" | "Acute Whatitis"      | 1     |
+--------------------------------------------------------+

But when he tries to save a diagnosis to the database, he will be denied:

WITH date(datetime({epochmillis:timestamp()})) AS today
MATCH (p:Patient)
  WHERE p.ssn = 1234650
MATCH (d:Disease)
  WHERE d.name = "Chronic Placeboitis"
MERGE (p)-[i:DIAGNOSIS {by: 'Daniel'}]->(d)
  ON CREATE SET i.date = today
RETURN p.name, d.name, i.by, i.date;
Create relationship with type 'DIAGNOSIS' is not allowed for user 'daniel' with roles [PUBLIC, disableDiagnoses, nurse].

Promoting Daniel back to senior nurse will be as simple as revoking the role that introduced the restriction:

REVOKE ROLE disableDiagnoses FROM daniel;

Building a custom administrator role

Originally we created the itadmin role by copying the built-in admin role and adding restrictions. However, we have also shown cases where using blacklisting can be less convenient than whitelisting. So can we instead build the administrator role from the ground up?

Let’s review the purpose of this role. The intention is that Tina, the administrator, can create new users and assign them to the product roles. We can create a new role called userManager and grant it the appropriate privileges:

CREATE ROLE userManager;
GRANT USER MANAGEMENT ON DBMS TO userManager;
GRANT ROLE MANAGEMENT ON DBMS TO userManager;
GRANT SHOW PRIVILEGE ON DBMS TO userManager;

We need to revoke the itadmin role from Tina and grant her the userManager role instead:

REVOKE ROLE itadmin FROM tina
GRANT ROLE userManager TO tina

The three privileges we’ve granted will allow:

  • USER MANAGEMENT allows creating, updating and dropping users

  • ROLE MANAGEMENT allows assigning roles to users

  • SHOW PRIVILEGE allows listing the users privileges

Listing Tina’s new privileges should show a much shorter list than when she was a more powerful administrator:

neo4j@system> SHOW USER tina PRIVILEGES;
+-------------------------------------------------------------------------------------------------+
| access    | action            | resource   | graph        | segment    | role          | user   |
+-------------------------------------------------------------------------------------------------+
| "GRANTED" | "access"          | "database" | "DEFAULT"    | "database" | "PUBLIC"      | "tina" |
| "GRANTED" | "access"          | "database" | "healthcare" | "database" | "PUBLIC"      | "tina" |
| "GRANTED" | "role_management" | "database" | "*"          | "database" | "userManager" | "tina" |
| "GRANTED" | "show_privilege"  | "database" | "*"          | "database" | "userManager" | "tina" |
| "GRANTED" | "user_management" | "database" | "*"          | "database" | "userManager" | "tina" |
+-------------------------------------------------------------------------------------------------+
We have not granted any other privilege management privileges. How much power this role should have would depend on the requirements of the system. Refer to the section Cypher Manual → Security of Administration for a complete list of privileges to consider.

Now Tina should be able to create new users and assign them to roles:

CREATE USER sally SET PASSWORD 'secret' CHANGE REQUIRED;
GRANT ROLE receptionist TO sally;
SHOW USER sally PRIVILEGES;
+-------------------------------------------------------------------------------------------------------------+
| access    | action           | resource         | graph        | segment         | role           | user    |
+-------------------------------------------------------------------------------------------------------------+
| "GRANTED" | "access"         | "database"       | "DEFAULT"    | "database"      | "PUBLIC"       | "sally" |
| "GRANTED" | "access"         | "database"       | "healthcare" | "database"      | "PUBLIC"       | "sally" |
| "GRANTED" | "match"          | "all_properties" | "healthcare" | "NODE(Patient)" | "receptionist" | "sally" |
| "GRANTED" | "set_property"   | "all_properties" | "healthcare" | "NODE(Patient)" | "receptionist" | "sally" |
| "GRANTED" | "create_element" | "graph"          | "healthcare" | "NODE(Patient)" | "receptionist" | "sally" |
| "GRANTED" | "delete_element" | "graph"          | "healthcare" | "NODE(Patient)" | "receptionist" | "sally" |
+-------------------------------------------------------------------------------------------------------------+