This is a very simple approach to doing role-based access control with Neo4j. It is optimistic, in the sense that all items are assumed to be world-readable unless they have specific constraints. Item visibility can be constrained to either individual users or all users who belong to a role. Roles are also hierarchical, so can inherit privileges from other roles.
This file is updated based on CYPHER guideline of 3.0
Setup:
Using docker is the easiest way to get started quickly
docker pull neo4j
docker run \
-p 7474:7474 -p 7687:7687 --env=NEO4J_AUTH=none \
--volume=$HOME/neo4j/data:/data \
neo4j
Now the UI can be accessed at http://<dmip>:7474/browser/
We are Ready !
First, lets create our basic example data:
session.write_transaction(add_role, "operator")
session.write_transaction(add_role, "role1")
session.write_transaction(add_role, "role2")
# Inherit the operator role to role1
# this will avail all the items to role1 that belongs to operator
# In entity relationship a user with role1 is a operator
session.write_transaction(inherit_role, "operator", "role1")
session.write_transaction(add_user, "user1", "role1")
session.write_transaction(add_user, "user2", "role2")
# we will add 5 distinct items
session.write_transaction(create_item, "item1")
session.write_transaction(create_item, "item2")
session.write_transaction(create_item, "item3")
session.write_transaction(create_item, "item4")
session.write_transaction(create_item, "item5")
session.write_transaction(create_item, "item6")
# Item1 belongs to only operator role
session.write_transaction(add_item_to_role, "item1", "operator")
# Item2 belongs to the role1
session.write_transaction(add_item_to_role, "item2", "role1")
# Item 2, Item3 and item4 belongs to the role2
session.write_transaction(add_item_to_role, "item2", "role2")
session.write_transaction(add_item_to_role, "item3", "role2")
session.write_transaction(add_item_to_role, "item4", "role2")
# Item 5 only belongs to user1
session.write_transaction(add_item_to_user, "item5", "user1")
First, check what items are accessible to everyone, because they have no constraints. This should return just item6.
MATCH (items:item)
WHERE not ((items)-[:ACCESSIBLE_TO]->(:role)) AND not ((items)-[:ACCESSIBLE_TO]->(:user))
RETURN DISTINCT items
// Result:
╒════════════════╕
│"items" │
╞════════════════╡
│{"name":"item6"}│
└────────────────┘
Now lets list all items accessible to 'user1'. The result should include 'item1' (because it is ACCESSIBLE_TO 'operator', and 'user1' belongs to 'role1', which in turn belongs to 'operator') and 'item6' which has no access constraints at all.
MATCH (items:item), (roles:role), (users:user {name: 'user1'})
MATCH (users)-[:BELONGS_TO]->(roles)
WHERE (items)-[:ACCESSIBLE_TO]->(roles) OR ((items)-[:ACCESSIBLE_TO]->(users)) or NOT (items)-[:ACCESSIBLE_TO]->(:role)
RETURN DISTINCT items
// Result:
╒════════════════╕
│"items" │
╞════════════════╡
│{"name":"item2"}│
├────────────────┤
│{"name":"item5"}│
├────────────────┤
│{"name":"item6"}│
├────────────────┤
│{"name":"item1"}│
└────────────────┘
Okay, that seems to work. Likewise, if we try the same thing with 'user2' we should be 'item2', 'item3', 'item4' and 'item6'.
If we only want to list the item that has a relation to the user:
MATCH (items:item), (roles:role), (users:user {name: 'user1'})
MATCH (users)-[:BELONGS_TO]->(roles)
WHERE (items)-[:ACCESSIBLE_TO]->(roles) OR ((items)-[:ACCESSIBLE_TO]->(users))
RETURN DISTINCT items
// Result (This will not consider 'item6'):
╒════════════════╕
│"items" │
╞════════════════╡
│{"name":"item2"}│
├────────────────┤
│{"name":"item5"}│
├────────────────┤
│{"name":"item1"}│
└────────────────┘
Check if item is 'item1' is accessible to 'user1':
MATCH (items:item {name: 'item1'}), (roles:role), (users:user {name: 'user1'})
MATCH (users)-[:BELONGS_TO]->(roles)
WHERE (items)-[:ACCESSIBLE_TO]->(roles) OR (items)-[:ACCESSIBLE_TO]->(users) OR NOT (items)-[:ACCESSIBLE_TO]->(:role)
RETURN DISTINCT items
// Result:
╒════════════════╕
│"items" │
╞════════════════╡
│{"name":"item1"}│
└────────────────┘
// Result (item6):
╒════════════════╕
│"items" │
╞════════════════╡
│{"name":"item6"}│
└────────────────┘
// Result (item5):
╒════════════════╕
│"items" │
╞════════════════╡
│{"name":"item5"}│
└────────────────┘
// Result (item3):
null
// Result (item3, user2):
╒════════════════╕
│"items" │
╞════════════════╡
│{"name":"item3"}│
└────────────────┘
WORKING WITH RESTRICT ITEMS:
In a practical use case a derived role can be defined by the collection of items the SuperRoles defines The role can also restrict access to one or more item. It gives a hierarchy of roles where each roles defines the super roles and the item that it restricts. It allows to define the roles scope on the runtime, making it extreamly flexible for the addition of removal of the items.
Example:
In Cypher we could create a new relation [:RETRICTED_TO] from the item to the node. That denotes the Item is restricted to the Role
CREATE (i)-[:RESTRICTED_TO]->(r)
When evaluating the item list against a Role, it is derived from the hierarchy:
U -> Union
^ -> intersect
! -> Not
R1 -> [I1, I2, I3, I4]
R2 -> [I1, I5, I6, I7, I8]
R3 -> R1 + R2 - [I2, I8] // Restricted Item I2, I8
-> [[I1, I2, I3, I4] U [I1, I5, I6, I7, I8]] and NOT [I2, I8]
-> [I1, I2, I3, I4, I5, I6, I7, I8] and NOT [I2, I8]
-> [I1, I3, I4, I5, I6, I7]
R4 -> R3 + R2 - [I1]
-> [[[[I1, I2, I3, I4] U [I1, I5, I6, I7, I8]] and NOT [I2, I8]] U [I1, I5, I6, I7, I8]] and NOT [I1]
-> [[I1, I3, I4, I5, I6, I7] U [I1, I5, I6, I7, I8]] and NOT [I1]
-> [I1, I3, I4, I5, I6, I7, I8] and NOT [I1]
-> [I3, I4, I5, I6, I7, I8]
R5 -> R4 + R3 + R1 - [I1]
^^ This Logic Sucks !
The basic code snippet in python that allows to do the above operations:
Install the oficial new4j bolt library:
pip install neo4j-driver
To Initialize (using bolt protocol):
from neo4j.v1 import GraphDatabase
driver = GraphDatabase.driver("bolt://localhost:7687",
auth=("neo4j", "password"))
with driver.session() as session():
session.write_transaction(<operation>, [args ...])
Below is the list of write operations
Create Item:
def create_item(tx, item_name):
tx.run("CREATE ( %s:item { name: '%s'})" % (item_name, item_name))
Delete Item:
def delete_item(tx, item_name):
tx.run("MATCH (i:item {name:'%s'}) DETACH DELETE i" % item_name)
Add role:
def add_role(tx, role_name):
tx.run("CREATE ( %s:role { name: '%s'})" % (role_name, role_name))
Delete role:
def delete_role(tx, role_name):
tx.run("MATCH (i:role {name:'%s'}) DETACH DELETE i" % role_name)
Add Items to Role:
def add_item_to_role(tx, item_name, role_name):
tx.run("MATCH (in:item {name:'%s'}), (rn:role {name:'%s'}) "
"CREATE (in)-[:ACCESSIBLE_TO]->(rn)" % (item_name, role_name))
tx.run("MATCH (r:role {name: '%s'}) MATCH (i:item {name: '%s'}) "
"MATCH (roles:role)-[:BELONGS_TO*]->(r) "
"WITH collect(roles) AS roles_set "
"UNWIND roles_set AS x_role "
"WITH DISTINCT x_role "
"MATCH (i:item {name: '%s'}) "
"WHERE NOT (i)-[:RESTRICTED_TO]->(x_role) "
"AND NOT (i)-[:ACCESSIBLE_TO]->(x_role) "
"CREATE (i)-[:ACCESSIBLE_TO]->(x_role)"
% (role_name, item_name, item_name))
Delete Items from Role:
def delete_item_from_role(tx, item_name, role_name):
tx.run("MATCH (i:item {name:'%s'})-"
"[access:ACCESSIBLE_TO]->(r:role {name:'%s'}) "
"WHERE access IS NOT NULL "
"DETACH DELETE access" % (item_name, role_name))
# TODO: remove items from dependent nodes
Inherit Role:
def inherit_role(tx, parent_role, child_role):
tx.run("MATCH (rc:role {name:'%s'}), (rp:role {name:'%s'}) "
"CREATE (rc)-[:BELONGS_TO]->(rp)" % (child_role, parent_role))
tx.run("MATCH (pr:role {name: '%s'}) "
"MATCH (items:item)-[:ACCESSIBLE_TO]->(pr) "
"WITH collect(items) AS items_set "
"UNWIND items_set AS x_item "
"WITH DISTINCT x_item MATCH (cr:role {name: '%s'}) "
"WHERE NOT (x_item)-[:RESTRICTED_TO]->(cr) AND NOT "
"(x_item)-[:ACCESSIBLE_TO]->(cr) "
"CREATE (x_item)-[:ACCESSIBLE_TO]->(cr)"
% (child_role, parent_role))
Remove inheritance:
def remove_inheritance(tx, parent_role, child_role):
tx.run("MATCH (rc:role {name:'%s'})-"
"[inherit:BELONGS_TO]->(rp:role {name:'%s'}) "
"WHERE inherit IS NOT NULL "
"DETACH DELETE inherit" % (child_role, parent_role))
# TODO: remove items from nodes
Restrict Items to Role (Only to inherited items):
def restrict_item_to_role(tx, role_name, item_name):
tx.run("MATCH (i:item {name:'%s'}), (r:role {name:'%s'}) "
"CREATE (i)-[:RESTRICTED_TO]->(r)" % (item_name, role_name))
tx.run("MATCH (:item {name:'%s'})"
"-[access:ACCESSIBLE_TO]->(:role {name:'%s'}) "
"WHERE access IS NOT NULL "
"DETACH DELETE access" % (item_name, role_name))
Remove Item restriction from Role:
def remove_restriction_from_role(tx, role_name, item_name):
tx.run("MATCH (i:item {name:'%s'})-"
"[restriction:RESTRICTED_TO]->(r:role {name:'%s'}) "
"WHERE restriction IS NOT NULL "
"DETACH DELETE restriction" % (item_name, role_name))
tx.run("MATCH (i:item {name:'%s'}), (r:role {name:'%s'}) "
"CREATE (i)-[:ACCESSIBLE_TO]->(r)" % (item_name, role_name))
tx.run("MATCH (r:role {name: '%s'}) MATCH (i:item {name: '%s'}) "
"MATCH (roles:role)-[:BELONGS_TO*]->(r) "
"WITH collect(roles) AS roles_set "
"UNWIND roles_set AS x_role "
"WITH DISTINCT x_role "
"MATCH (i:item {name: '%s'}) "
"WHERE NOT (i)-[:RESTRICTED_TO]->(x_role) "
"AND NOT (i)-[:ACCESSIBLE_TO]->(x_role) "
"CREATE (i)-[:ACCESSIBLE_TO]->(x_role)"
% (role_name, item_name, item_name))
Add User:
def add_user(tx, user_name, role_name):
tx.run("CREATE ( %s:user { name: '%s' })" % (user_name, user_name))
tx.run("MATCH (un:user {name:'%s'}), (rn:role {name:'%s'}) "
"CREATE (un)-[:BELONGS_TO]->(rn)" % (user_name, role_name))
Delete User:
def delete_user(tx, user_name):
tx.run("MATCH (i:user {name:'%s'}) DETACH DELETE i" % user_name)
Add Role to User:
def add_role_to_user(tx, user_name, role_name):
tx.run("MATCH (un:user {name:'%s'}), (rn:role {name:'%s'}) "
"CREATE (un)-[:BELONGS_TO]->(rn)" % (user_name, role_name))
Remove Role from User:
def delete_role_from_user(tx, user_name, role_name):
tx.run("MATCH (un:user {name:'%s'})-"
"[relation:BELONGS_TO]->(rn:role {name:'%s'}) "
"WHERE relation IS NOT NULL "
"DETACH DELETE relation" % (user_name, role_name))
Add item directly for a user:
def add_item_to_user(tx, item_name, user_name):
tx.run("MATCH (i:item {name:'%s'}), (u:user {name:'%s'}) "
"CREATE (i)-[:ACCESSIBLE_TO]->(u)" % (item_name, user_name))
Remove direct item from a user:
def delete_item_from_user(tx, item_name, user_name):
tx.run("MATCH (i:item {name:'%s'})-"
"[access:ACCESSIBLE_TO]->(u:user {name:'%s'}) "
"WHERE access IS NOT NULL "
"DETACH DELETE access" % (item_name, user_name))
Add Item Restriction to User [only to items accessible via roles]:
# todo:
Remove Item Restriction from User:
# todo:
Test data Setup Code:
with driver.session() as session:
session.write_transaction(add_role, "operator")
session.write_transaction(add_role, "role1")
session.write_transaction(add_role, "role2")
# Inherit the operator role to role1
# this will avail all the items to role1 that belongs to operator
# In entity relationship a user with role1 is a operator
session.write_transaction(inherit_role, "operator", "role1")
session.write_transaction(add_user, "user1", "role1")
session.write_transaction(add_user, "user2", "role2")
# we will add 5 distinct items
session.write_transaction(create_item, "item1")
session.write_transaction(create_item, "item2")
session.write_transaction(create_item, "item3")
session.write_transaction(create_item, "item4")
session.write_transaction(create_item, "item5")
session.write_transaction(create_item, "item6")
# Item1 belongs to only operator role
session.write_transaction(add_item_to_role, "item1", "operator")
# Item2 belongs to the role1
session.write_transaction(add_item_to_role, "item2", "role1")
# Item 2, Item3 and item4 belongs to the role2
session.write_transaction(add_item_to_role, "item2", "role2")
session.write_transaction(add_item_to_role, "item3", "role2")
session.write_transaction(add_item_to_role, "item4", "role2")
# Item 5 only belongs to user1
session.write_transaction(add_item_to_user, "item5", "user1")