RBAC effective permissions through group inheritance
Objective
In any large system, users inherit permissions through nested groups and roles. "What can Sarah actually do?" becomes a recursive walk across user → group → group → role → permission. Cypher flattens this into a single MATCH that returns the deduplicated list of effective permissions.
Step 1: Create the graph
// Users
MERGE (sarah:User {name: "Sarah Chen", email: "sarah@acme.com"})
MERGE (raj:User {name: "Raj Patel", email: "raj@acme.com"})
MERGE (mia:User {name: "Mia Rossi", email: "mia@acme.com"})
// Groups (can be nested)
MERGE (eng:Group {name: "Engineering"})
MERGE (platform:Group {name: "Platform Team"})
MERGE (mobile:Group {name: "Mobile Team"})
MERGE (ops:Group {name: "Ops"})
MERGE (platform)-[:MEMBER_OF]->(eng)
MERGE (mobile)-[:MEMBER_OF]->(eng)
// Direct user memberships
MERGE (sarah)-[:MEMBER_OF]->(platform)
MERGE (raj)-[:MEMBER_OF]->(mobile)
MERGE (mia)-[:MEMBER_OF]->(ops)
// Roles
MERGE (admin:Role {name: "Admin"})
MERGE (deploy:Role {name: "Deployer"})
MERGE (viewer:Role {name: "Viewer"})
MERGE (oncall:Role {name: "OnCall"})
MERGE (eng)-[:HAS_ROLE]->(viewer)
MERGE (platform)-[:HAS_ROLE]->(deploy)
MERGE (platform)-[:HAS_ROLE]->(admin)
MERGE (mobile)-[:HAS_ROLE]->(deploy)
MERGE (ops)-[:HAS_ROLE]->(oncall)
MERGE (ops)-[:HAS_ROLE]->(viewer)
// Permissions
MERGE (read:Permission {action: "read:repo"})
MERGE (write:Permission {action: "write:repo"})
MERGE (deployPerm:Permission {action: "deploy:prod"})
MERGE (manage:Permission {action: "manage:users"})
MERGE (page:Permission {action: "page:oncall"})
MERGE (viewer)-[:GRANTS]->(read)
MERGE (deploy)-[:GRANTS]->(read)
MERGE (deploy)-[:GRANTS]->(write)
MERGE (deploy)-[:GRANTS]->(deployPerm)
MERGE (admin)-[:GRANTS]->(manage)
MERGE (admin)-[:GRANTS]->(write)
MERGE (oncall)-[:GRANTS]->(page);
Step 2: What can Sarah actually do?
// User -> (zero or more group hops via MEMBER_OF) -> Role -> Permission
MATCH (u:User {email: "sarah@acme.com"})-[:MEMBER_OF*1..5]->(g:Group)
-[:HAS_ROLE]->(r:Role)-[:GRANTS]->(perm:Permission)
RETURN DISTINCT perm.action AS effective_permission,
collect(DISTINCT r.name) AS via_roles
ORDER BY effective_permission;
What's happening
[:MEMBER_OF*1..5]traverses nested group membership transitively — Sarah is in Platform Team, which is in Engineering, so she inherits both groups' roles in one walk.DISTINCTdeduplicates permissions granted via multiple paths (e.g.read:repovia Viewer and Deployer).collect()shows which roles granted each permission — useful for auditors asking "why does this user have access?".- In SQL this is the textbook recursive-CTE problem; in Cypher it is one expression. Adding deny
rules or time-bound grants (
WHERE r.expires > date()) is a property change, not a redesign. - The same query works for any user — flip the email and you have a real audit endpoint.
Try this next
MATCH (u:User)-[:MEMBER_OF*]->(:Group)-[:HAS_ROLE]->(:Role)-[:GRANTS]->(p:Permission {action: "deploy:prod"})
RETURN DISTINCT u.email AS who_can_deploy;
MATCH path = (u:User {email: "sarah@acme.com"})-[:MEMBER_OF*]->(g:Group)
RETURN [n IN nodes(path) | coalesce(n.name, n.email)] AS group_chain;
MATCH (r:Role)-[:GRANTS]->(p:Permission)
RETURN r.name AS role, collect(p.action) AS permissions
ORDER BY r.name;