diff --git a/apps/api/src/common/interceptors/rls-context.integration.spec.ts b/apps/api/src/common/interceptors/rls-context.integration.spec.ts index 6d52614..27b9531 100644 --- a/apps/api/src/common/interceptors/rls-context.integration.spec.ts +++ b/apps/api/src/common/interceptors/rls-context.integration.spec.ts @@ -137,13 +137,13 @@ describe("RLS Context Integration", () => { queries: ["findMany"], }); - // Verify SET LOCAL was called + // Verify transaction-local set_config calls were made expect(mockTransactionClient.$executeRaw).toHaveBeenCalledWith( - expect.arrayContaining(["SET LOCAL app.current_user_id = ", ""]), + expect.arrayContaining(["SELECT set_config('app.current_user_id', ", ", true)"]), userId ); expect(mockTransactionClient.$executeRaw).toHaveBeenCalledWith( - expect.arrayContaining(["SET LOCAL app.current_workspace_id = ", ""]), + expect.arrayContaining(["SELECT set_config('app.current_workspace_id', ", ", true)"]), workspaceId ); }); diff --git a/apps/api/src/common/interceptors/rls-context.interceptor.spec.ts b/apps/api/src/common/interceptors/rls-context.interceptor.spec.ts index c21be1f..1eda38b 100644 --- a/apps/api/src/common/interceptors/rls-context.interceptor.spec.ts +++ b/apps/api/src/common/interceptors/rls-context.interceptor.spec.ts @@ -80,7 +80,7 @@ describe("RlsContextInterceptor", () => { expect(result).toEqual({ data: "test response" }); expect(mockTransactionClient.$executeRaw).toHaveBeenCalledWith( - expect.arrayContaining(["SET LOCAL app.current_user_id = ", ""]), + expect.arrayContaining(["SELECT set_config('app.current_user_id', ", ", true)"]), userId ); }); @@ -111,13 +111,13 @@ describe("RlsContextInterceptor", () => { // Check that user context was set expect(mockTransactionClient.$executeRaw).toHaveBeenNthCalledWith( 1, - expect.arrayContaining(["SET LOCAL app.current_user_id = ", ""]), + expect.arrayContaining(["SELECT set_config('app.current_user_id', ", ", true)"]), userId ); // Check that workspace context was set expect(mockTransactionClient.$executeRaw).toHaveBeenNthCalledWith( 2, - expect.arrayContaining(["SET LOCAL app.current_workspace_id = ", ""]), + expect.arrayContaining(["SELECT set_config('app.current_workspace_id', ", ", true)"]), workspaceId ); }); diff --git a/apps/api/src/common/interceptors/rls-context.interceptor.ts b/apps/api/src/common/interceptors/rls-context.interceptor.ts index b6921c9..0b3886d 100644 --- a/apps/api/src/common/interceptors/rls-context.interceptor.ts +++ b/apps/api/src/common/interceptors/rls-context.interceptor.ts @@ -100,12 +100,12 @@ export class RlsContextInterceptor implements NestInterceptor { this.prisma .$transaction( async (tx) => { - // Set user context (always present for authenticated requests) - await tx.$executeRaw`SET LOCAL app.current_user_id = ${userId}`; + // Use set_config(..., true) so values are transaction-local and parameterized safely. + // Direct SET LOCAL with bind parameters produces invalid SQL on PostgreSQL. + await tx.$executeRaw`SELECT set_config('app.current_user_id', ${userId}, true)`; - // Set workspace context (if present) if (workspaceId) { - await tx.$executeRaw`SET LOCAL app.current_workspace_id = ${workspaceId}`; + await tx.$executeRaw`SELECT set_config('app.current_workspace_id', ${workspaceId}, true)`; } // Propagate the transaction client via AsyncLocalStorage diff --git a/docs/scratchpads/362-auth-session-chain-debug.md b/docs/scratchpads/362-auth-session-chain-debug.md index abf2168..40d957a 100644 --- a/docs/scratchpads/362-auth-session-chain-debug.md +++ b/docs/scratchpads/362-auth-session-chain-debug.md @@ -151,3 +151,56 @@ Regression test added: - File: `apps/api/src/auth/auth.service.spec.ts` - `should preserve raw cookie token value without URL re-encoding` + +## Deploy + Live Repro (after auth cookie fix deploy) + +Deployment actions executed: + +1. Pushed auth cookie fix commit to `develop`. +2. Waited for Woodpecker pipeline success (`mosaic/stack`, build `#514`). +3. On `10.1.1.90`: + - Ran `/home/localadmin/mosaic/pull_all.sh`. + - Updated swarm services to `:dev` images: + - `stack_api` + - `stack_web` + - `stack_coordinator` + - `stack_orchestrator` + - Verified service convergence. + +Post-deploy behavior: + +- Initial `/auth/session` without cookies now returns `401` (expected). +- OAuth callback succeeds and sets BetterAuth session cookie. +- `/auth/session` still fails after callback, now due to a new backend `500`. + +## New Root Cause Discovered (RLS interceptor SQL) + +Live `stack_api` logs showed: + +- Auth guard successfully finds session cookie: + - `Session cookie found: __Secure-better-auth.session_token` +- Then failure inside RLS setup: + - PostgreSQL `42601` syntax error at or near `$1` + - Source: `RlsContextInterceptor` raw SQL while setting context vars + - Request ends as `500 Request processing failed` on `/auth/session` + +Cause: + +- `SET LOCAL app.current_user_id = ${userId}` became `SET LOCAL ... = $1` under parameterization. +- PostgreSQL does not accept bind placeholders in `SET` assignment syntax. + +## RLS Fix Applied Locally (pending commit/deploy) + +Files updated: + +- `apps/api/src/common/interceptors/rls-context.interceptor.ts` + - Replaced `SET LOCAL` statements with parameter-safe, transaction-local calls: + - `SELECT set_config('app.current_user_id', ${userId}, true)` + - `SELECT set_config('app.current_workspace_id', ${workspaceId}, true)` + - Keeps transaction scoping (`true` => local to transaction). + +- `apps/api/src/common/interceptors/rls-context.interceptor.spec.ts` + - Updated expected SQL template fragments to `set_config(...)`. + +- `apps/api/src/common/interceptors/rls-context.integration.spec.ts` + - Updated integration expectations to `set_config(...)`.