Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@mikesname
Last active March 22, 2022 15:40
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save mikesname/6008933 to your computer and use it in GitHub Desktop.
Save mikesname/6008933 to your computer and use it in GitHub Desktop.
Very simplistic way of doing role-based access control (RBAC) with Neo4j.

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.

First, lets create our basic example data:

CREATE
    (admins { type: 'role', name: 'admins' }),

    (role1 { type: 'role', name: 'role1' }),

	role1-[r1:BELONGS_TO]->admins,

    (role2 { type: 'role', name: 'role2' }),

    (user1 { type: 'user', name: 'user1' }),

	user1-[r2:BELONGS_TO]->role1,

    (user2 { type: 'user', name: 'user2' }),

	user2-[r3:BELONGS_TO]->role2,

    (item1 { type: 'item', name: 'item1' }),

	item1-[r4:ACCESSIBLE_TO]->admins,

    (item2 { type: 'item', name: 'item2' }),

	item2-[r5:ACCESSIBLE_TO]->role2,

    (item3 { type: 'item', name: 'item3' })

RETURN admins, role1, role2, user1, user2, item1, item2, item3

First, check what items are accessible to everyone, because they have no constraints. This should return just item3.

START items = node(*)
MATCH access = items-[r?:ACCESSIBLE_TO]->accessors
WHERE items.type! = 'item' AND access IS NULL
RETURN DISTINCT items

Now lets list all items accessible to 'user1'. The result should include 'item1' (because it is ACCESSIBLE_TO 'admins', and 'user1' belongs to 'role1', which in turn belongs to 'admins') and 'item3' which has no access constraints at all.

START items = node(*)
MATCH access = items-[r1?:ACCESSIBLE_TO]->accessors, users = user-[r2?:BELONGS_TO*]->accessors
WHERE items.type! = 'item' AND (access IS NULL OR user.name! = 'user1')
RETURN DISTINCT items

Okay, that seems to work. Likewise, if we try the same thing with 'user2' we should be 'item2' and 'item3':

START items = node(*)
MATCH access = items-[r1?:ACCESSIBLE_TO]->accessors, users = user-[r2?:BELONGS_TO*]->accessors
WHERE items.type! = 'item' AND (access IS NULL OR user.name! = 'user2')
RETURN DISTINCT items

Check if item is 'item1' is accessible to 'user1':

START item = node(*)
MATCH access = item-[r1?:ACCESSIBLE_TO]->accessor, users = user-[r2?:BELONGS_TO*]->accessor
WHERE item.type! = 'item' AND item.name! = 'item1' AND (access IS NULL OR user.name! = 'user1')
RETURN DISTINCT item, access

Right, now let’s create a new user and grant them exclusive access to 'item3':

MATCH item
WHERE item.name! = 'item3'
CREATE (user3 { type: 'user', name: 'user3' }), item-[r:ACCESSIBLE_TO]->user3
RETURN user3

Now we’ve added a constraint to 'item3', 'user1' should only have access to 'item1':

START items = node(*)
MATCH access = items-[r1?:ACCESSIBLE_TO]->accessors, users = user-[r2?:BELONGS_TO*]->accessors
WHERE items.type! = 'item' AND (access IS NULL OR user.name! = 'user1')
RETURN DISTINCT items

The above queries should now change so that 'user1' only has access to 'item1', 'user2' to 'item2', and 'user3' to 'item3'. Note that the method of access is different:

  • 'user1' belongs to 'role1', which belongs to 'admin', which has access to 'item1'

  • 'user2' belongs to 'role2', which has direct access to 'item2'

  • 'user3' has direct access to 'item3'

@mrts
Copy link

mrts commented Dec 10, 2021

Hi @mikesname! This looks nice! Did you use it in a real project, did you run into any issues (performance, manageability or whatnot)?

@mikesname
Copy link
Author

@mrts wow, this is a blast from the past! Yes, this approach (slightly simplified here from IRL) is taken in the EHRI project database of archival collections and its associated admin system. It has worked well for the past 6 years or so. We have not had any issues with performance but there is definitely an overhead to adding an access control system.

Note also that the Cypher here is a bit outdated.

@mrts
Copy link

mrts commented Dec 13, 2021

Good to hear, thanks! Another thing - were Neo4j, Tinkerpop and GraphQL the right choice or would you choose different building blocks today?

@mikesname
Copy link
Author

It's hard to say TBH. Today I'd probably just opt for PostgreSQL because it's such a dependable piece of infrastructure. It would make our dataset much more difficult to work with in a variety of ways but dependability means more to me now. Neo4j itself has actually been very stable and enjoyable to work with over the years but because it's one company's product you can never be sure they won't change direction or deprecate something you rely on in the next release.

We mainly used Tinkerpop at the outset in case we had to switch away from Neo4j, but in the event they went in a different direction at version 3 and now version 2 remains a bit of legacy in our codebase, while we still use Neo4j several versions on.

Overall, the graph stuff has been a win for us because it's great for tree structures and hierarchies, which we deal with a lot in our dataset. I find Cypher a great language for analytics and the lack of a strict SQL-style schema has been more of a blessing than a curse, though obviously there are trade-offs with that.

@mrts
Copy link

mrts commented Dec 16, 2021

Ah, great points, heartfelt thanks for the advice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment