feat: Multi-tenant per-user data isolation#549
feat: Multi-tenant per-user
data isolation#549khalidnass wants to merge 2 commits intolfnovo:mainfrom
Conversation
Each user gets an isolated SurrealDB database (user_alice, user_bob) via contextvar + dynamic db_connection(). Controlled by MULTI_TENANT_MODE=true. - ProxyAuthMiddleware reads X-Forwarded-User header from auth proxy - Per-user DB migrations run lazily on first request - Frontend config returns relative URLs when behind auth proxy - Zero changes to domain models, routers, services, or search - Fully backward-compatible (disabled by default) Includes 27 unit tests, 12 E2E tests, 4 frontend tests, and mock auth proxy.
There was a problem hiding this comment.
6 issues found across 11 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="mock_auth_proxy.py">
<violation number="1" location="mock_auth_proxy.py:23">
P2: HTTPS upstreams will fail because the proxy always uses HTTPConnection and ignores the upstream URL scheme.</violation>
</file>
<file name="api/auth.py">
<violation number="1" location="api/auth.py:19">
P2: Race condition: concurrent requests for the same user can both pass the `_migrated_users` check and run migrations in parallel because the guard uses a shared set without locking.</violation>
</file>
<file name="tests/test_multi_tenant_e2e.sh">
<violation number="1" location="tests/test_multi_tenant_e2e.sh:73">
P2: List endpoint tests can falsely pass because assert_json_count only checks len(json) without validating a successful status or ensuring the response is a list; a 1-key error object would satisfy expected count=1.</violation>
</file>
<file name="frontend/src/lib/hooks/use-auth.ts">
<violation number="1" location="frontend/src/lib/hooks/use-auth.ts:63">
P2: authRequired can remain null on network errors, causing isLoading to stay true and the UI to remain stuck in a loading state.</violation>
</file>
<file name="open_notebook/database/repository.py">
<violation number="1" location="open_notebook/database/repository.py:36">
P1: Lossy sanitization of user identifiers can map different users to the same database name, breaking per-user isolation.</violation>
</file>
<file name="api/main.py">
<violation number="1" location="api/main.py:115">
P2: ProxyAuthMiddleware trusts X-Forwarded-User without verifying the request came from a trusted proxy, allowing direct clients to spoof the header and impersonate users in multi-tenant mode.</violation>
</file>
Since this is your first cubic review, here's how it works:
- cubic automatically reviews your code and comments on bugs and improvements
- Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
- Ask questions if you need clarification on any suggestion
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| return { | ||
| isAuthenticated, | ||
| isLoading: isLoading || !hasHydrated, // Treat lack of hydration as loading | ||
| isLoading: isLoading || !hasHydrated || authRequired === null, // Wait for auth check to complete |
There was a problem hiding this comment.
P2: authRequired can remain null on network errors, causing isLoading to stay true and the UI to remain stuck in a loading state.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/lib/hooks/use-auth.ts, line 63:
<comment>authRequired can remain null on network errors, causing isLoading to stay true and the UI to remain stuck in a loading state.</comment>
<file context>
@@ -60,7 +60,7 @@ export function useAuth() {
return {
isAuthenticated,
- isLoading: isLoading || !hasHydrated, // Treat lack of hydration as loading
+ isLoading: isLoading || !hasHydrated || authRequired === null, // Wait for auth check to complete
error,
login: handleLogin,
</file context>
There was a problem hiding this comment.
Fixed in auth-store.ts — network errors now set authRequired: false instead of null
| ] | ||
|
|
||
| if MULTI_TENANT: | ||
| app.add_middleware(ProxyAuthMiddleware, excluded_paths=_auth_excluded_paths) |
There was a problem hiding this comment.
P2: ProxyAuthMiddleware trusts X-Forwarded-User without verifying the request came from a trusted proxy, allowing direct clients to spoof the header and impersonate users in multi-tenant mode.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api/main.py, line 115:
<comment>ProxyAuthMiddleware trusts X-Forwarded-User without verifying the request came from a trusted proxy, allowing direct clients to spoof the header and impersonate users in multi-tenant mode.</comment>
<file context>
@@ -88,20 +98,23 @@ async def lifespan(app: FastAPI):
+]
+
+if MULTI_TENANT:
+ app.add_middleware(ProxyAuthMiddleware, excluded_paths=_auth_excluded_paths)
+else:
+ app.add_middleware(PasswordAuthMiddleware, excluded_paths=_auth_excluded_paths)
</file context>
There was a problem hiding this comment.
Added optional PROXY_AUTH_SECRET env var in auth.py — when set, middleware verifies X-Proxy-Auth-Secret header from the proxy
…xy secret, HTTPS support, UI loading fix, E2E validation
|
@cubic-dev-ai re-review Fixed remaining issues:
|
@khalidnass I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
1 issue found across 12 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="mock_auth_proxy.py">
<violation number="1" location="mock_auth_proxy.py:38">
P2: Host header includes ":None" when upstream URL omits a port, producing an invalid Host header that can break upstream routing.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| if key.lower() != "host": | ||
| headers[key] = val | ||
| headers["X-Forwarded-User"] = self.server.user | ||
| headers["Host"] = f"{upstream.hostname}:{upstream.port}" |
There was a problem hiding this comment.
P2: Host header includes ":None" when upstream URL omits a port, producing an invalid Host header that can break upstream routing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mock_auth_proxy.py, line 38:
<comment>Host header includes ":None" when upstream URL omits a port, producing an invalid Host header that can break upstream routing.</comment>
<file context>
@@ -0,0 +1,107 @@
+ if key.lower() != "host":
+ headers[key] = val
+ headers["X-Forwarded-User"] = self.server.user
+ headers["Host"] = f"{upstream.hostname}:{upstream.port}"
+ # Force non-chunked response so read() doesn't hang
+ headers["Accept-Encoding"] = "identity"
</file context>
Summary
Files Changed (11 files)
Backend (4): user_context.py (new), auth.py, main.py, repository.py
Frontend (2): config/route.ts, use-auth.ts
Tests (3): test_multi_tenant.py (27 tests), test_multi_tenant_e2e.sh (12 tests), config-multitenant.test.ts (4
tests)
Tools (1): mock_auth_proxy.py
Config (1): .gitignore
How it works
Test plan