Tenant isolation
🚨 THE ACTUAL IMPLEMENTATION (v0.5.0 is Incomplete!)¶
Looking at the actual code, tenant isolation is STILL using the OLD v0.4.x instance-level API!
Found in src/nexus/core/nexus_fs_core.py:¶
route = self.router.route(
path,
tenant_id=self.tenant_id, # ← Instance variable (v0.4.x style)
agent_id=self.agent_id, # ← Instance variable (v0.4.x style)
is_admin=self.is_admin, # ← Instance variable (v0.4.x style)
check_write=False,
)
Where do these come from?¶
From src/nexus/core/nexus_fs.py:133-137:
# v0.5.0: No longer accept tenant_id/agent_id/user_id in __init__
# These are set to None - operations must pass subject parameter instead
self.tenant_id: str | None = None
self.agent_id: str | None = None
self.user_id: str | None = None
They're ALL None!
🚨 THE BROKEN ARCHITECTURE¶
What v0.5.0 CLAIMED to do:¶
# "New way" (from docstring)
nx = NexusFS(backend)
nx.read("/file.txt", context=OperationContext(user="alice", ...))
What v0.5.0 ACTUALLY does:¶
# Instance variables are still used!
nx.tenant_id # → None (never set!)
nx.agent_id # → None (never set!)
nx.is_admin # → False (from init param)
# File operations use these None values:
def read(self, path, context=None):
route = self.router.route(
path,
tenant_id=self.tenant_id, # ← ALWAYS None!
agent_id=self.agent_id, # ← ALWAYS None!
is_admin=self.is_admin # ← From init, not context!
)
What this means:¶
- ❌
tenant_idis ALWAYS None - no tenant isolation! - ❌
agent_idis ALWAYS None - no agent isolation! - ❌
is_admincomes from init, not context - can't change per-operation! - ❌
OperationContextis NOT used for routing - only for ReBAC permissions!
The Complete Picture¶
Two Parallel Permission Systems:¶
1. Path Router (tenant isolation)¶
- Uses:
self.tenant_id,self.agent_id,self.is_admin - Problem: Always None in v0.5.0
- Result: NO tenant isolation enforcement
2. Permission Enforcer (ReBAC/ACL/UNIX)¶
- Uses:
OperationContext - Works: ✅ When
contextis passed - Problem: Bypassed when
contextis None (uses_default_context)
The Migration Never Finished:¶
# v0.4.x (WORKED):
nx = NexusFS(backend, tenant_id="org_acme", user_id="alice")
nx.write("/workspace/org_acme/alice/file.txt", data)
# ✅ self.tenant_id = "org_acme"
# ✅ Router checks: path tenant matches self.tenant_id
# ✅ Tenant isolation enforced
# v0.5.0 (BROKEN):
nx = NexusFS(backend) # tenant_id=None
ctx = OperationContext(user="alice", ...)
nx.write("/workspace/org_acme/alice/file.txt", data, context=ctx)
# ❌ self.tenant_id = None
# ❌ Router can't enforce tenant isolation!
# ❌ OperationContext not used for routing!
How Tenant Isolation SHOULD Work in v0.5.0¶
Option A: Extract tenant_id from OperationContext¶
# Add tenant_id to OperationContext
@dataclass
class OperationContext:
user: str
groups: list[str]
tenant_id: str | None = None # ← ADD THIS
is_admin: bool = False
is_system: bool = False
# Use it in routing
def write(self, path, content, context=None):
ctx = context or self._default_context
route = self.router.route(
path,
tenant_id=ctx.tenant_id, # ← From context!
agent_id=None, # Or extract from user string
is_admin=ctx.is_admin,
check_write=True
)
Option B: Extract tenant_id from JWT/AuthResult¶
# AuthResult already has tenant_id!
@dataclass
class AuthResult:
authenticated: bool
subject_id: str
tenant_id: str | None # ← Already exists!
is_admin: bool = False
# RPC server should create context WITH tenant_id
def handle_request(jwt_token):
auth_result = auth.authenticate(jwt_token)
ctx = OperationContext(
user=auth_result.subject_id,
tenant_id=auth_result.tenant_id, # ← Pass it through!
groups=[],
is_admin=auth_result.is_admin
)
nx.write(path, data, context=ctx)
Option C: Extract from path (current broken approach)¶
# This is what it TRIES to do now but fails because:
# 1. self.tenant_id is always None
# 2. Can't check if path tenant matches user tenant
# 3. No enforcement
Current State Summary¶
| Component | Claims to Support | Actually Works | Why |
|---|---|---|---|
| Path routing | Tenant isolation | ❌ NO | self.tenant_id always None |
| OperationContext | Per-operation identity | ⚠️ Partial | Only for ReBAC, not routing |
| _default_context | Backward compat | ❌ NO | is_system=True bypasses everything |
| Multi-user RPC | Yes | ❌ NO | No context passed to operations |
| Tenant isolation | Yes | ❌ NO | Router never checks tenant |
What Needs to Happen¶
Immediate Fix:¶
- Add
tenant_idtoOperationContext - Use
ctx.tenant_idin router.route() calls - Set
_default_context.is_system = False - RPC server: create proper context from JWT
Code Changes Required:¶
# 1. Update OperationContext
@dataclass
class OperationContext:
user: str
groups: list[str]
tenant_id: str | None = None # ← ADD
is_admin: bool = False
is_system: bool = False
# 2. Update all router.route() calls
def write(self, path, content, context=None):
ctx = context or self._default_context
route = self.router.route(
path,
tenant_id=ctx.tenant_id, # ← Use context
agent_id=None, # ← Or ctx.agent_id if added
is_admin=ctx.is_admin, # ← Use context
check_write=True
)
# 3. RPC server: create context from JWT
auth_result = await auth.authenticate(token)
ctx = OperationContext(
user=auth_result.subject_id,
tenant_id=auth_result.tenant_id, # ← From JWT!
groups=[],
is_admin=auth_result.is_admin
)
Final Answer to Your Question¶
Q: How do we decide tenant in ctx = OperationContext(user="alice", groups=[], is_admin=False)?
A: WE DON'T! That's the bug!
OperationContext doesn't have tenant_id, so: 1. ❌ No way to pass tenant from auth to router 2. ❌ Router uses self.tenant_id which is always None 3. ❌ No tenant isolation enforcement 4. ❌ Multi-user deployments are completely broken
The v0.5.0 migration is incomplete. It changed the API but didn't wire up the new OperationContext to the router.