Tenant hierarchy and information resolution for Cyber Ware's multi-tenancy layer.
The tenant model that Cyber Ware operates on is described in TENANT_MODEL.md.
The tenant_resolver module provides a hierarchical tenant model with:
- Tenant information — Retrieve tenant metadata (name, status, parent, type)
- Hierarchy traversal — Navigate parent chains (ancestors) and children subtrees (descendants)
- Ancestry verification — Check if one tenant is an ancestor of another
The module supports barrier semantics where self-managed tenants act as boundaries that block hierarchy traversal from parent tenants (unless explicitly ignored).
The module registers TenantResolverClient in ClientHub:
get_tenant(ctx, id)— Retrieve single tenant by IDget_root_tenant(ctx)— Retrieve the root tenant (the unique tenant with no parent)get_tenants(ctx, ids, options)— Retrieve multiple tenants by IDs (batch)get_ancestors(ctx, id, options)— Get parent chain from tenant to rootget_descendants(ctx, id, options)— Get children subtree;response.tenantcontains the starting tenant,response.descendantscontains the subtreeis_ancestor(ctx, ancestor_id, descendant_id, options)— Check ancestry relationship
The SecurityContext is passed to the plugin for use in access control decisions. Each plugin
implements its own logic for using (or ignoring) the context.
T1 (root)
├── T2 (self_managed=true) ← BARRIER
│ └── T3
└── T4
parent_id: Links a tenant to its parent (Nonefor the root tenant — exactly one tenant in a single-root tree)self_managed: When true, this tenant is a barrier that blocks parent traversal into its subtree
Filter tenants by status via options structs:
// No filter (all tenants)
let tenants = resolver.get_tenants(&ctx, &ids, &GetTenantsOptions::default()).await?;
// Only active tenants
let opts = GetTenantsOptions {
status: vec![TenantStatus::Active],
};
let tenants = resolver.get_tenants(&ctx, &ids, &opts).await?;An empty status vector means "no constraint" (include all statuses).
GetDescendantsOptions has the same status field for filtering descendants.
Control barrier behavior during hierarchy traversal via request structs:
// Default: respect barriers
let ancestors = resolver.get_ancestors(&ctx, id, &GetAncestorsOptions::default()).await?;
// Ignore barriers: traverse through self_managed tenants
let opts = GetAncestorsOptions {
barrier_mode: BarrierMode::Ignore,
};
let ancestors = resolver.get_ancestors(&ctx, id, &opts).await?;When self_managed = true (barrier exists):
- Parent cannot traverse into this subtree (with
BarrierMode::Respect, the default) - Using
BarrierMode::Ignoreignores barriers and traverses through - Barriers never produce errors — traversal stops silently and returns truncated results
get_ancestors — if the starting tenant is a barrier, ancestors are empty; if a barrier is encountered in the chain, it is included but traversal stops after it:
get_ancestors(T2, barrier_mode=Respect)→ tenant=T2, ancestors=[] (T2 is a barrier, cannot see parent chain)get_ancestors(T3, barrier_mode=Respect)→ tenant=T3, ancestors=[T2] (T2 is the barrier, included; T1 is not reached)get_ancestors(T3, barrier_mode=Ignore)→ tenant=T3, ancestors=[T2, T1]
get_descendants — barrier tenant is excluded along with its subtree; but a barrier can see its own subtree:
get_descendants(T1, barrier_mode=Respect)→ tenant=T1, descendants=[T4] (T2 is a barrier, excluded along with T3)get_descendants(T2, barrier_mode=Respect)→ tenant=T2, descendants=[T3] (T2 can see its own subtree)get_descendants(T1, barrier_mode=Ignore)→ tenant=T1, descendants=[T2, T3, T4]
is_ancestor — returns false if a barrier blocks the path:
is_ancestor(T1, T3, barrier_mode=Respect)→ false (blocked by T2)is_ancestor(T1, T3, barrier_mode=Ignore)→ true
Filter is supported only for methods where it provides significant value:
| Method | TenantNotFound error |
Filter support |
|---|---|---|
get_tenant(id) |
id doesn't exist |
— (no filter) |
get_tenants(ids, options) |
— (skip missing) | filters results |
get_ancestors(id) |
id doesn't exist |
— (no filter) |
get_descendants(id, options) |
id doesn't exist |
filters descendants |
is_ancestor(a, d) |
a or d doesn't exist |
— (no filter) |
Design rationale:
get_ancestorsreturns the full chain — caller can filter the typically small resultis_ancestoranswers "is A ancestor of D?" — filtering the path is a rare use caseget_descendantssupports filter because descendants can be many and "all active descendants" is a common access control pattern
Principles:
TenantNotFound— Only raised when tenant physically doesn't exist- Filter does NOT affect existence check of the starting tenant
- Filter applies only to results (descendants list), not to the starting tenant
get_descendants uses pre-order traversal: each node is visited before its children.
Filter as traversal barrier: If a node doesn't pass the filter, it is excluded along with its entire subtree. This is intentional — if a parent tenant is suspended, its children should not be reachable.
Example:
A (active) → B (suspended) → C (active)
→ D (active)
// Without filter: returns [B, C, D] (pre-order)
resolver.get_descendants(&ctx, A, &GetDescendantsOptions::default()).await?;
// With filter={status: Active}: returns [D] only
// B is excluded (suspended), so C is unreachable
let opts = GetDescendantsOptions {
status: vec![TenantStatus::Active],
..Default::default()
};
resolver.get_descendants(&ctx, A, &opts).await?;Note: Sibling order within the same parent is not guaranteed.
See models.rs: TenantId, TenantInfo, TenantRef, TenantStatus, BarrierMode, GetTenantsOptions, GetAncestorsOptions, GetAncestorsResponse, GetDescendantsOptions, GetDescendantsResponse, IsAncestorOptions
TenantInfo — Full tenant information (for get_tenant, get_tenants):
id— Unique tenant identifiername— Human-readable tenant namestatus— Lifecycle status (Active,Suspended,Deleted)tenant_type— Optional classification string (e.g.,"enterprise","trial")parent_id— Parent tenant ID (Nonefor the root tenant — exactly one tenant in a single-root tree)self_managed— True if this tenant is a barrier
TenantRef — Tenant reference without name (for get_ancestors, get_descendants):
- All fields except
name - Use
get_tenants(ids)if display names are needed
See error.rs: TenantNotFound, Unauthorized, NoPluginAvailable, ServiceUnavailable, Internal
Plugins implement TenantResolverPluginClient and register via GTS.
Cyber Ware includes two plugins out of the box:
static_tr_plugin— Config-based plugin with hierarchical tenant supportsingle_tenant_tr_plugin— Zero-config plugin for single-tenant deployments
See config.rs
modules:
tenant_resolver:
vendor: "cyberfabric" # Selects plugin by matching vendorSee config.rs
modules:
static_tr_plugin:
vendor: "cyberfabric"
priority: 100 # Lower = higher priority
tenants:
- id: "550e8400-e29b-41d4-a716-446655440001"
name: "Root Tenant"
status: active
type: enterprise
- id: "550e8400-e29b-41d4-a716-446655440002"
name: "Child Tenant"
status: active
parent_id: "550e8400-e29b-41d4-a716-446655440001"
self_managed: falselet resolver = hub.get::<dyn TenantResolverClient>()?;
// Get tenant info
let tenant = resolver.get_tenant(&ctx, tenant_id).await?;
// Get multiple tenants (batch)
let tenants = resolver.get_tenants(&ctx, &[id1, id2], &GetTenantsOptions::default()).await?;
// Get ancestor chain
let response = resolver.get_ancestors(&ctx, tenant_id, &GetAncestorsOptions::default()).await?;
println!("Tenant: {:?}, Ancestors: {:?}", response.tenant, response.ancestors);
// Get descendants (max_depth=None means unlimited)
let response = resolver.get_descendants(&ctx, tenant_id, &GetDescendantsOptions::default()).await?;
println!("Tenant: {:?}, Descendants: {:?}", response.tenant, response.descendants);
// Get only active descendants
let opts = GetDescendantsOptions {
status: vec![TenantStatus::Active],
..Default::default()
};
let response = resolver.get_descendants(&ctx, tenant_id, &opts).await?;
// Check ancestry
let is_parent = resolver.is_ancestor(&ctx, parent_id, child_id, &IsAncestorOptions::default()).await?;Multiple backends are planned (config-based, DB-driven, external API). The Tenant Resolver module handles cross-cutting concerns consistently while plugins can be developed independently.
The self_managed field on tenants creates traversal barriers. This enables:
- Delegated administration: self-managed tenants control their own subtrees
- Privacy boundaries: parent organizations cannot see into self-managed subsidiaries
- Flexible traversal: callers can opt to ignore barriers when needed (e.g., system operations)
The get_tenants method returns only found tenants — missing IDs are silently skipped.
This simplifies callers who want to fetch multiple tenants without handling per-item errors.
- Order: Output order is not guaranteed (may differ from input order)
- Duplicates: Duplicate IDs in the input are deduplicated
- Empty input: Returns an empty list when
idsis empty
get_tenant,get_tenantsAPIsget_ancestors,get_descendants,is_ancestorfor hierarchy traversal- Status filtering via
GetTenantsOptionsandGetDescendantsOptions BarrierModefor traversal control via options structs- Static plugin with config-driven hierarchy
- Single-tenant plugin for simple deployments
- ClientHub registration for in-process consumption
- gRPC API for out-of-process consumers