From 1b7d511695c83e0ef93a4e359d10c24a202a0aaa Mon Sep 17 00:00:00 2001 From: Cameron Taylor <50385537+ct3685@users.noreply.github.com> Date: Fri, 15 May 2026 16:20:14 -0400 Subject: [PATCH] fix: admin workspace access bypass in getChatflowById + migrate template chatflow to Default Workspace (#1075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Root Cause `getChatflowById` gates access by checking that `chatflow.workspaceId` is in `req.user.assignedWorkspaces` (populated from explicit `workspace_user` membership rows). This is correct for non-admins. For admins, however, the template source chatflow (the row referenced by `INITIAL_CHATFLOW_IDS`) was created with whatever `activeWorkspaceId` the admin had at that moment — often a Personal Workspace or ad-hoc workspace. That workspace isn't in another admin's `assignedWorkspaces`, so they hit a misleading "chatflow not found" despite the row existing in the DB. ## Changes **`packages/server/src/controllers/chatflows/index.ts`** Adds an admin bypass using the same `roles?.includes('Admin') || permissions?.includes('org:manage')` pattern used throughout the codebase (documentstore, billing, organizations controllers). Admins check org membership instead of workspace membership: ```typescript const isAdmin = req.user?.roles?.includes('Admin') || req.user?.permissions?.includes('org:manage') const hasWorkspaceAccess = isAdmin ? apiResponse.organizationId === req.user?.activeOrganizationId : apiResponse.workspaceId ? assignedWorkspaces.some((ws) => ws.id === apiResponse.workspaceId) : false ``` Non-admin workspace gate is unchanged. **`packages/server/src/database/migrations/postgres/aai/1770000000003-FixTemplateSourceChatflowWorkspace.ts`** Idempotent migration that moves each chatflow in `INITIAL_CHATFLOW_IDS` / `INITIAL_CHATFLOW_ID` to the Default Workspace of its organization, so the template lives in a predictable shared location going forward. Only moves chatflows that are NOT already in a Default Workspace. ## Test Plan - [ ] As an admin, access a chatflow whose `workspaceId` is a Personal Workspace the admin is not a direct member of — should now return the chatflow - [ ] As a non-admin, access a chatflow in a workspace they're not a member of — should still return NOT_FOUND - [ ] Run migration on staging DB — template chatflow moves to Default Workspace - [ ] Template chatflow already in Default Workspace — migration is a no-op - [ ] No `INITIAL_CHATFLOW_IDS` configured — migration logs skip and exits cleanly --- .../server/src/controllers/chatflows/index.ts | 9 ++- ...0003-FixTemplateSourceChatflowWorkspace.ts | 75 +++++++++++++++++++ .../src/database/migrations/postgres/index.ts | 5 +- 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 packages/server/src/database/migrations/postgres/aai/1770000000003-FixTemplateSourceChatflowWorkspace.ts diff --git a/packages/server/src/controllers/chatflows/index.ts b/packages/server/src/controllers/chatflows/index.ts index 66e7609d78a..acc67fe2c0a 100644 --- a/packages/server/src/controllers/chatflows/index.ts +++ b/packages/server/src/controllers/chatflows/index.ts @@ -129,10 +129,17 @@ const getChatflowById = async (req: Request, res: Response, next: NextFunction) throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: chatflowsController.getChatflowById - id not provided!`) } const apiResponse = await chatflowsService.getChatflowById(req.params.id) + const isAdmin = req.user?.roles?.includes('Admin') || req.user?.permissions?.includes('org:manage') const assignedWorkspaces = (req.user as { assignedWorkspaces?: Array<{ id: string }> } | undefined)?.assignedWorkspaces || [] - const hasWorkspaceAccess = apiResponse.workspaceId + + // Admins can access any chatflow within their organization regardless of workspace membership. + // Non-admins must have explicit workspace_user membership for the chatflow's workspace. + const hasWorkspaceAccess = isAdmin + ? apiResponse.organizationId === req.user?.activeOrganizationId + : apiResponse.workspaceId ? assignedWorkspaces.some((ws: { id: string }) => ws.id === apiResponse.workspaceId) : false + if (!hasWorkspaceAccess) { throw new InternalFlowiseError( StatusCodes.NOT_FOUND, diff --git a/packages/server/src/database/migrations/postgres/aai/1770000000003-FixTemplateSourceChatflowWorkspace.ts b/packages/server/src/database/migrations/postgres/aai/1770000000003-FixTemplateSourceChatflowWorkspace.ts new file mode 100644 index 00000000000..5974b9daa3f --- /dev/null +++ b/packages/server/src/database/migrations/postgres/aai/1770000000003-FixTemplateSourceChatflowWorkspace.ts @@ -0,0 +1,75 @@ +/* eslint-disable no-console */ +import { MigrationInterface, QueryRunner } from 'typeorm' + +/** + * Ensure that the source template chatflows identified by INITIAL_CHATFLOW_IDS / + * INITIAL_CHATFLOW_ID are assigned to the Default Workspace of their organization. + * + * Root cause: when the template chatflow is created via the normal POST /chatflows + * endpoint the workspaceId is set to whatever the admin's activeWorkspaceId was at + * that moment (often a Personal Workspace or an ad-hoc workspace). The + * getChatflowById controller then gates access by workspace_user membership, so + * any admin whose assignedWorkspaces does not include that workspace gets a + * misleading "not found" error even though the row exists. + * + * The controller is also fixed in this PR (admin bypass via org check), but this + * migration repairs the data so the template lives in a predictable, shared location. + * + * Idempotent: only moves chatflows that are NOT already in a Default Workspace. + */ +export class FixTemplateSourceChatflowWorkspace1770000000003 implements MigrationInterface { + name = 'FixTemplateSourceChatflowWorkspace1770000000003' + + public async up(queryRunner: QueryRunner): Promise { + console.log('[FixTemplateSourceChatflowWorkspace] Starting') + + // Read template IDs from environment (same logic as the server at runtime) + const rawIds = process.env.INITIAL_CHATFLOW_IDS ?? process.env.INITIAL_CHATFLOW_ID ?? '' + const templateIds = rawIds + .split(',') + .map((id) => id.trim()) + .filter(Boolean) + + if (!templateIds.length) { + console.log('[FixTemplateSourceChatflowWorkspace] No INITIAL_CHATFLOW_IDS configured — skipping') + return + } + + console.log(`[FixTemplateSourceChatflowWorkspace] Template IDs: ${templateIds.join(', ')}`) + + for (const templateId of templateIds) { + const result = await queryRunner.query( + ` + UPDATE chat_flow cf + SET "workspaceId" = dw.id, + "updatedDate" = NOW() + FROM workspace dw + WHERE dw."organizationId" = cf."organizationId" + AND dw.name = 'Default Workspace' + AND cf.id = $1 + AND cf."deletedDate" IS NULL + AND cf."workspaceId" NOT IN ( + SELECT id FROM workspace WHERE name = 'Default Workspace' + ) + RETURNING cf.id, cf.name, cf."workspaceId" AS new_workspace_id + `, + [templateId] + ) + + const moved = Array.isArray(result) ? result[0]?.length ?? 0 : 0 + if (moved > 0) { + console.log(`[FixTemplateSourceChatflowWorkspace] Moved template ${templateId} to Default Workspace`) + } else { + console.log( + `[FixTemplateSourceChatflowWorkspace] Template ${templateId} already in correct workspace or not found — no change` + ) + } + } + + console.log('[FixTemplateSourceChatflowWorkspace] Done') + } + + public async down(): Promise { + console.log('[FixTemplateSourceChatflowWorkspace] Down migration is a no-op') + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 5b505f543bd..573adfc2614 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -98,6 +98,7 @@ import { UpdateFiddlerCredentialsVisibility1768413137117 } from './aai/176841313 import { MoveDefaultChatflowsToPersonalWorkspace1770000000000 } from './aai/1770000000000-MoveDefaultChatflowsToPersonalWorkspace' import { NormalizeLegacyCredentialNames1770000000001 } from './aai/1770000000001-NormalizeLegacyCredentialNames' import { FixBulkUpdateChatflowWorkspace1770000000002 } from './aai/1770000000002-FixBulkUpdateChatflowWorkspace' +import { FixTemplateSourceChatflowWorkspace1770000000003 } from './aai/1770000000003-FixTemplateSourceChatflowWorkspace' export const postgresMigrations = [ Init1693891895163, @@ -199,5 +200,7 @@ export const postgresMigrations = [ // AAI: Normalize legacy credential names in chat_flow.flowData and credential rows (e.g. JiraApi -> jiraApi) NormalizeLegacyCredentialNames1770000000001, // AAI: Fix chatflows re-assigned to Default Workspace by bulkUpdateChatflows workspace leak - FixBulkUpdateChatflowWorkspace1770000000002 + FixBulkUpdateChatflowWorkspace1770000000002, + // AAI: Move INITIAL_CHATFLOW_IDS source templates to Default Workspace for consistent admin access + FixTemplateSourceChatflowWorkspace1770000000003 ]