Skip to main content
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

1
Step 1: Define User Collection
2
Configure the users collection with hierarchy fields:
3
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
4
Source: /home/daytona/workspace/source/pkg/config/schema.go:147-151
5
Step 2: Initialize Hierarchy
6
Sync the hierarchy on startup or when users change:
7
POST /api/hierarchy/sync
Content-Type: application/json

{
  "tenant_id": "acme-corp",
  "user_collection": "users"
}
8
This builds the transitive closure table in _pm_hierarchy collection.
9
Source: /home/daytona/workspace/source/pkg/hierarchy/resolver.go:320-429
10
Step 3: Configure Policies
11
Use hierarchy variables in RBAC policies:
12
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:
  1. Deletes existing hierarchy records for user-4
  2. Walks up manager chain from user-6
  3. Creates new hierarchy records at each depth
  4. 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
}

Performance Optimization

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 directReportsfordirectmanagerchecks</Check><Check>UsedirectReports for direct manager checks</Check> <Check>Use 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"
}

Performance Issues

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

Build docs developers (and LLMs) love