Skip to content

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:

  1. tenant_id is ALWAYS None - no tenant isolation!
  2. agent_id is ALWAYS None - no agent isolation!
  3. is_admin comes from init, not context - can't change per-operation!
  4. OperationContext is 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 context is passed
  • Problem: Bypassed when context is 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:

  1. Add tenant_id to OperationContext
  2. Use ctx.tenant_id in router.route() calls
  3. Set _default_context.is_system = False
  4. 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.