Permission Mongo provides built-in support for organizational hierarchies, allowing managers to access their subordinates’ data and enforcing reporting structures.
Overview
Hierarchical features:
Transitive closure table - Efficient hierarchy queries
Manager-subordinate relationships - Track reporting chains
Special variables - user.$subordinates, user.$directReports, user.$ancestors
Automatic hierarchy sync - Keep hierarchy up-to-date
Redis caching - Fast hierarchy lookups
Source: /home/daytona/workspace/source/pkg/hierarchy/hierarchy.go:1-17
Configuration
Step 1: Define User Collection
Configure the users collection with hierarchy fields:
collections :
users :
fields :
name :
type : string
email :
type : string
manager_id :
type : objectId
ref : users # Self-reference
hierarchy :
user_id_field : _id
manager_field : manager_id
Source: /home/daytona/workspace/source/pkg/config/schema.go:147-151
Step 2: Initialize Hierarchy
Sync the hierarchy on startup or when users change:
POST /api/hierarchy/sync
Content-Type: application/json
{
"tenant_id" : "acme-corp",
"user_collection" : "users"
}
This builds the transitive closure table in _pm_hierarchy collection.
Source: /home/daytona/workspace/source/pkg/hierarchy/resolver.go:320-429
Use hierarchy variables in RBAC policies:
policies :
expense_reports :
manager :
actions : [ read , update ]
when : |
doc.submitted_by in user.$subordinates
Hierarchy Variables
user.$subordinates
All users reporting to the current user (direct and indirect):
policies :
performance_reviews :
manager :
actions : [ read , update ]
when : |
doc.employee_id in user.$subordinates
Example hierarchy:
CEO (user-1)
├── VP Sales (user-2)
│ ├── Sales Manager (user-3)
│ │ ├── Sales Rep 1 (user-4)
│ │ └── Sales Rep 2 (user-5)
│ └── Sales Manager 2 (user-6)
└── VP Engineering (user-7)
└── Tech Lead (user-8)
VP Sales’ subordinates: [user-3, user-4, user-5, user-6]
Sales Manager’s subordinates: [user-4, user-5]
Source: /home/daytona/workspace/source/pkg/hierarchy/resolver.go:60-109
user.$directReports
Only immediate subordinates (depth = 1):
policies :
timesheets :
manager :
actions : [ read , update ]
when : |
doc.employee_id in user.$directReports
VP Sales’ direct reports: [user-3, user-6]
Sales Manager’s direct reports: [user-4, user-5]
Source: /home/daytona/workspace/source/pkg/hierarchy/resolver.go:111-159
user.$ancestors
All managers above the current user:
policies :
approvals :
employee :
actions : [ create ]
when : |
doc.approver_id in user.$ancestors
Sales Rep 1’s ancestors: [user-3, user-2, user-1] (Manager, VP, CEO)
Sales Manager’s ancestors: [user-2, user-1] (VP, CEO)
Source: /home/daytona/workspace/source/pkg/hierarchy/resolver.go:161-213
Common Patterns
Pattern 1: Manager Access to Subordinates
Managers can view and update their team’s records:
policies :
performance_reviews :
manager :
actions : [ read , update ]
when : |
doc.employee_id in user.$subordinates
employee :
actions : [ read ]
when : |
doc.employee_id == user.id
Example request:
# Manager user-3 can access reviews for user-4, user-5
GET /performance_reviews?employee_id=user-4
Generated MongoDB query:
{
employee_id : { $in : [ "user-4" , "user-5" ] } // user-3's subordinates
}
Pattern 2: Skip-Level Manager Access
VP can access all team members, including indirect reports:
policies :
expense_reports :
vp :
actions : [ read , update ]
when : |
doc.submitted_by in user.$subordinates
manager :
actions : [ read , update ]
when : |
doc.submitted_by in user.$directReports
VP Sales (user-2) subordinates: [user-3, user-4, user-5, user-6]
Sales Manager (user-3) direct reports: [user-4, user-5]
Pattern 3: Approval Workflows
Employees can submit requests to their manager or any ancestor:
policies :
leave_requests :
employee :
actions : [ create , read ]
when : |
doc.requestor_id == user.id ||
doc.approver_id in user.$ancestors
manager :
actions : [ read , update ]
when : |
doc.requestor_id in user.$directReports &&
doc.status == "pending"
Create request:
POST /leave_requests
Content-Type: application/json
{
"requestor_id" : "user-4",
"approver_id" : "user-3", // Direct manager
"start_date" : "2024-02-01",
"end_date" : "2024-02-05",
"status" : "pending"
}
Pattern 4: Self + Team Access
Users can access their own data plus their subordinates’:
policies :
timesheets :
manager :
actions : [ read , update ]
when : |
doc.employee_id == user.id ||
doc.employee_id in user.$subordinates
Source: /home/daytona/workspace/source/pkg/rbac/engine.go:607-659
Hierarchy Sync
Sync Single User
Update hierarchy when a user’s manager changes:
POST /api/hierarchy/sync-user
Content-Type: application/json
{
"tenant_id" : "acme-corp",
"user_id" : "user-4",
"manager_id" : "user-6" // Moved to new manager
}
Internal process:
Deletes existing hierarchy records for user-4
Walks up manager chain from user-6
Creates new hierarchy records at each depth
Invalidates affected caches
Source: /home/daytona/workspace/source/pkg/hierarchy/resolver.go:215-318
Sync All Users
Rebuild entire hierarchy (use sparingly):
POST /api/hierarchy/sync-all
Content-Type: application/json
{
"tenant_id" : "acme-corp",
"user_collection" : "users"
}
Performance note: This is an expensive operation. Run during maintenance windows or off-peak hours.
Source: /home/daytona/workspace/source/pkg/hierarchy/resolver.go:320-429
Hierarchy Storage
Hierarchy data is stored in _pm_hierarchy collection:
{
"tenant_id" : "acme-corp" ,
"user_id" : "user-4" , // Subordinate
"ancestor_id" : "user-2" , // Manager
"depth" : 2 // 2 levels up
}
Records for user-4:
[
{ user_id: "user-4" , ancestor_id: "user-3" , depth: 1 }, // Direct manager
{ user_id: "user-4" , ancestor_id: "user-2" , depth: 2 }, // VP
{ user_id: "user-4" , ancestor_id: "user-1" , depth: 3 } // CEO
]
Source: /home/daytona/workspace/source/pkg/hierarchy/hierarchy.go:28-35
Caching
Hierarchy lookups are cached in Redis:
server :
redis :
host : localhost
port : 6379
hierarchy_ttl : 5m # Cache hierarchy for 5 minutes
Cache keys:
subordinates:acme-corp:user-2 → ["user-3", "user-4", "user-5", "user-6"]
directReports:acme-corp:user-2 → ["user-3", "user-6"]
ancestors:acme-corp:user-4 → ["user-3", "user-2", "user-1"]
Cache invalidation:
Automatic on hierarchy sync
Manual via /api/hierarchy/invalidate
Source: /home/daytona/workspace/source/pkg/hierarchy/resolver.go:43-57
Complex Hierarchies
Multi-Level Management
Handle deep organizational structures:
policies :
budget_approvals :
team_lead :
actions : [ read , update ]
when : |
doc.amount <= 5000 &&
doc.submitted_by in user.$directReports
department_manager :
actions : [ read , update ]
when : |
doc.amount <= 25000 &&
doc.submitted_by in user.$subordinates
vp :
actions : [ read , update ]
when : |
doc.amount <= 100000 &&
doc.submitted_by in user.$subordinates
cfo :
actions : [ read , update ]
# No amount limit, no subordinate check
Matrix Organizations
Handle multiple reporting lines:
collections :
users :
fields :
functional_manager_id :
type : objectId
project_manager_id :
type : objectId
hierarchy :
user_id_field : _id
manager_field : functional_manager_id
Policies:
policies :
timesheets :
manager :
actions : [ read , update ]
when : |
doc.employee_id in user.$subordinates ||
doc.project_manager_id == user.id
Testing Hierarchies
func TestSubordinateAccess ( t * testing . T ) {
// Setup hierarchy
// CEO → VP → Manager → Rep
ceoID := createUser ( "CEO" , nil )
vpID := createUser ( "VP" , ceoID )
managerID := createUser ( "Manager" , vpID )
repID := createUser ( "Rep" , managerID )
// Sync hierarchy
syncHierarchy ( "test-tenant" , "users" )
// Create expense report as rep
repToken := createToken ( repID , "test-tenant" , [] string { "employee" })
reportID := createExpenseReport ( repToken , map [ string ] interface {}{
"amount" : 500 ,
"submitted_by" : repID ,
})
// Manager can access (direct report)
managerToken := createToken ( managerID , "test-tenant" , [] string { "manager" })
resp := getExpenseReport ( managerToken , reportID )
assert . Equal ( t , 200 , resp . StatusCode )
// VP can access (indirect report)
vpToken := createToken ( vpID , "test-tenant" , [] string { "manager" })
resp = getExpenseReport ( vpToken , reportID )
assert . Equal ( t , 200 , resp . StatusCode )
// Unrelated manager cannot access
otherManagerToken := createToken ( "other-manager" , "test-tenant" , [] string { "manager" })
resp = getExpenseReport ( otherManagerToken , reportID )
assert . Equal ( t , 404 , resp . StatusCode ) // Filtered by subordinate check
}
Index Creation
Hierarchy queries use these indexes:
db . _pm_hierarchy . createIndex ({ tenant_id: 1 , ancestor_id: 1 })
db . _pm_hierarchy . createIndex ({ tenant_id: 1 , user_id: 1 })
db . _pm_hierarchy . createIndex ({ tenant_id: 1 , ancestor_id: 1 , depth: 1 })
Source: /home/daytona/workspace/source/pkg/hierarchy/resolver.go:526-560
Batch Sync
For large organizations, sync in batches:
// Sync 1000 users at a time
batchSize := 1000
for i := 0 ; i < totalUsers ; i += batchSize {
userBatch := users [ i : min ( i + batchSize , totalUsers )]
syncUserBatch ( tenantID , userBatch )
}
Cache Warming
Pre-load hierarchy cache for active managers:
POST /api/hierarchy/warm-cache
Content-Type: application/json
{
"tenant_id" : "acme-corp",
"user_ids" : [ "manager-1" , "manager-2", "vp-1"]
}
Circular Reference Detection
The system prevents circular hierarchies:
POST /api/hierarchy/sync-user
{
"user_id" : "user-3",
"manager_id" : "user-5" // user-5 reports to user-3!
}
Error response:
{
"error" : {
"code" : "circular_reference" ,
"message" : "circular reference detected in hierarchy"
}
}
Source: /home/daytona/workspace/source/pkg/hierarchy/resolver.go:256-260
Best Practices
Sync hierarchy when users change managers
Use Redis caching for performance
Index _pm_hierarchy collection properly
Test edge cases (circular refs, deep hierarchies)
Monitor hierarchy cache hit rates
Use
d i r e c t R e p o r t s f o r d i r e c t m a n a g e r c h e c k s < / C h e c k > < C h e c k > U s e directReports for direct manager checks</Check> <Check>Use d i rec tR e p or t s f or d i rec t mana g erc h ec k s < / C h ec k >< C h ec k > U se subordinates for skip-level access
Document reporting structures in your API
Troubleshooting
Subordinates Not Found
Cause: Hierarchy not synced
Solution: Run sync-user or sync-all
POST /api/hierarchy/sync-user
Stale Hierarchy Data
Cause: Cache not invalidated after changes
Solution: Invalidate cache manually
POST /api/hierarchy/invalidate
Content-Type: application/json
{
"tenant_id" : "acme-corp",
"user_id" : "user-3"
}
Cause: Missing indexes or cache
Solution: Ensure indexes and Redis are configured
Next Steps
Computed Fields Dynamic field calculations
Batch Operations Bulk create, update, and delete