From 0e0b95b255e8606ed6f5f51c2ce6aea17a887fc7 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Thu, 11 Dec 2025 16:56:34 +0800 Subject: [PATCH 01/29] feat(database.util.ts): add function to check if column exists in table --- packages/server/src/utils/database.util.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/server/src/utils/database.util.ts diff --git a/packages/server/src/utils/database.util.ts b/packages/server/src/utils/database.util.ts new file mode 100644 index 00000000000..34217130590 --- /dev/null +++ b/packages/server/src/utils/database.util.ts @@ -0,0 +1,9 @@ +import { QueryRunner } from 'typeorm' + +export async function hasColumn(queryRunner: QueryRunner, tableName: string, columnName: string): Promise { + const table = await queryRunner.getTable(tableName) + + const hasColumn = table!.columns.some((column) => column.name === columnName) + + return hasColumn +} From 7aa57393a9646830fa8834ed538b382a194c77ea Mon Sep 17 00:00:00 2001 From: yau-wd Date: Thu, 11 Dec 2025 22:03:08 +0800 Subject: [PATCH 02/29] feat(migrations): add permission column to apikey table --- .../1765360298674-AddApiKeyPermission.ts | 21 +++++++++++++++++++ .../src/database/migrations/mariadb/index.ts | 4 +++- .../1765360298674-AddApiKeyPermission.ts | 21 +++++++++++++++++++ .../src/database/migrations/mysql/index.ts | 4 +++- .../1765360298674-AddApiKeyPermission.ts | 21 +++++++++++++++++++ .../src/database/migrations/postgres/index.ts | 4 +++- .../1765360298674-AddApiKeyPermission.ts | 21 +++++++++++++++++++ .../src/database/migrations/sqlite/index.ts | 4 +++- 8 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts create mode 100644 packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts create mode 100644 packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts create mode 100644 packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts diff --git a/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts new file mode 100644 index 00000000000..eea70779560 --- /dev/null +++ b/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' +import { hasColumn } from '../../../utils/database.util' + +export class AddApiKeyPermission1765360298674 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const tableName = 'apikey' + const columnName = 'permission' + + const columnExists = await hasColumn(queryRunner, tableName, columnName) + if (!columnExists) { + await queryRunner.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${columnName}\` TEXT NOT NULL DEFAULT ('[]');`) + + const permission = + '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","credentials:share","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","datasets:view","datasets:create","datasets:update","datasets:delete","executions:view","executions:delete","evaluators:view","evaluators:create","evaluators:update","evaluators:delete","evaluations:view","evaluations:create","evaluations:update","evaluations:delete","evaluations:run","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport","templates:custom-share","workspace:view","workspace:create","workspace:update","workspace:add-user","workspace:unlink-user","workspace:delete","workspace:export","workspace:import","users:manage","roles:manage",null,"admin:view"]' + + await queryRunner.query(`UPDATE \`${tableName}\` SET \`${columnName}\` = '${permission}';`) + } + } + + public async down(): Promise {} +} diff --git a/packages/server/src/database/migrations/mariadb/index.ts b/packages/server/src/database/migrations/mariadb/index.ts index 07ddb6ed006..b0ec9e5d5af 100644 --- a/packages/server/src/database/migrations/mariadb/index.ts +++ b/packages/server/src/database/migrations/mariadb/index.ts @@ -41,6 +41,7 @@ import { ModifyChatflowType1755066758601 } from './1755066758601-ModifyChatflowT import { AddTextToSpeechToChatFlow1759419231100 } from './1759419231100-AddTextToSpeechToChatFlow' import { AddChatFlowNameIndex1759424809984 } from './1759424809984-AddChatFlowNameIndex' import { FixDocumentStoreFileChunkLongText1765000000000 } from './1765000000000-FixDocumentStoreFileChunkLongText' +import { AddApiKeyPermission1765360298674 } from './1765360298674-AddApiKeyPermission' import { AddAuthTables1720230151482 } from '../../../enterprise/database/migrations/mariadb/1720230151482-AddAuthTables' import { AddWorkspace1725437498242 } from '../../../enterprise/database/migrations/mariadb/1725437498242-AddWorkspace' @@ -108,5 +109,6 @@ export const mariadbMigrations = [ ModifyChatflowType1755066758601, AddTextToSpeechToChatFlow1759419231100, AddChatFlowNameIndex1759424809984, - FixDocumentStoreFileChunkLongText1765000000000 + FixDocumentStoreFileChunkLongText1765000000000, + AddApiKeyPermission1765360298674 ] diff --git a/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts new file mode 100644 index 00000000000..eea70779560 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' +import { hasColumn } from '../../../utils/database.util' + +export class AddApiKeyPermission1765360298674 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const tableName = 'apikey' + const columnName = 'permission' + + const columnExists = await hasColumn(queryRunner, tableName, columnName) + if (!columnExists) { + await queryRunner.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${columnName}\` TEXT NOT NULL DEFAULT ('[]');`) + + const permission = + '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","credentials:share","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","datasets:view","datasets:create","datasets:update","datasets:delete","executions:view","executions:delete","evaluators:view","evaluators:create","evaluators:update","evaluators:delete","evaluations:view","evaluations:create","evaluations:update","evaluations:delete","evaluations:run","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport","templates:custom-share","workspace:view","workspace:create","workspace:update","workspace:add-user","workspace:unlink-user","workspace:delete","workspace:export","workspace:import","users:manage","roles:manage",null,"admin:view"]' + + await queryRunner.query(`UPDATE \`${tableName}\` SET \`${columnName}\` = '${permission}';`) + } + } + + public async down(): Promise {} +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index c7f5d2ebacc..1f34da60bfc 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -42,6 +42,7 @@ import { ModifyChatflowType1755066758601 } from './1755066758601-ModifyChatflowT import { AddTextToSpeechToChatFlow1759419216034 } from './1759419216034-AddTextToSpeechToChatFlow' import { AddChatFlowNameIndex1759424828558 } from './1759424828558-AddChatFlowNameIndex' import { FixDocumentStoreFileChunkLongText1765000000000 } from './1765000000000-FixDocumentStoreFileChunkLongText' +import { AddApiKeyPermission1765360298674 } from './1765360298674-AddApiKeyPermission' import { AddAuthTables1720230151482 } from '../../../enterprise/database/migrations/mysql/1720230151482-AddAuthTables' import { AddWorkspace1720230151484 } from '../../../enterprise/database/migrations/mysql/1720230151484-AddWorkspace' @@ -110,5 +111,6 @@ export const mysqlMigrations = [ ModifyChatflowType1755066758601, AddTextToSpeechToChatFlow1759419216034, AddChatFlowNameIndex1759424828558, - FixDocumentStoreFileChunkLongText1765000000000 + FixDocumentStoreFileChunkLongText1765000000000, + AddApiKeyPermission1765360298674 ] diff --git a/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts new file mode 100644 index 00000000000..39bc21d933e --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' +import { hasColumn } from '../../../utils/database.util' + +export class AddApiKeyPermission1765360298674 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const tableName = 'apikey' + const columnName = 'permission' + + const columnExists = await hasColumn(queryRunner, tableName, columnName) + if (!columnExists) { + await queryRunner.query(`ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" TEXT NOT NULL DEFAULT '[]';`) + + const permission = + '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","credentials:share","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","datasets:view","datasets:create","datasets:update","datasets:delete","executions:view","executions:delete","evaluators:view","evaluators:create","evaluators:update","evaluators:delete","evaluations:view","evaluations:create","evaluations:update","evaluations:delete","evaluations:run","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport","templates:custom-share","workspace:view","workspace:create","workspace:update","workspace:add-user","workspace:unlink-user","workspace:delete","workspace:export","workspace:import","users:manage","roles:manage",null,"admin:view"]' + + await queryRunner.query(`UPDATE "${tableName}" SET "${columnName}" = '${permission}';`) + } + } + + public async down(): Promise {} +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 3dbca614764..cd25296cc38 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -40,6 +40,7 @@ import { AddTextToSpeechToChatFlow1754986480347 } from './1754986480347-AddTextT import { ModifyChatflowType1755066758601 } from './1755066758601-ModifyChatflowType' import { AddTextToSpeechToChatFlow1759419194331 } from './1759419194331-AddTextToSpeechToChatFlow' import { AddChatFlowNameIndex1759424903973 } from './1759424903973-AddChatFlowNameIndex' +import { AddApiKeyPermission1765360298674 } from './1765360298674-AddApiKeyPermission' import { AddAuthTables1720230151482 } from '../../../enterprise/database/migrations/postgres/1720230151482-AddAuthTables' import { AddWorkspace1720230151484 } from '../../../enterprise/database/migrations/postgres/1720230151484-AddWorkspace' @@ -106,5 +107,6 @@ export const postgresMigrations = [ AddTextToSpeechToChatFlow1754986480347, ModifyChatflowType1755066758601, AddTextToSpeechToChatFlow1759419194331, - AddChatFlowNameIndex1759424903973 + AddChatFlowNameIndex1759424903973, + AddApiKeyPermission1765360298674 ] diff --git a/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts new file mode 100644 index 00000000000..39bc21d933e --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' +import { hasColumn } from '../../../utils/database.util' + +export class AddApiKeyPermission1765360298674 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const tableName = 'apikey' + const columnName = 'permission' + + const columnExists = await hasColumn(queryRunner, tableName, columnName) + if (!columnExists) { + await queryRunner.query(`ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" TEXT NOT NULL DEFAULT '[]';`) + + const permission = + '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","credentials:share","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","datasets:view","datasets:create","datasets:update","datasets:delete","executions:view","executions:delete","evaluators:view","evaluators:create","evaluators:update","evaluators:delete","evaluations:view","evaluations:create","evaluations:update","evaluations:delete","evaluations:run","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport","templates:custom-share","workspace:view","workspace:create","workspace:update","workspace:add-user","workspace:unlink-user","workspace:delete","workspace:export","workspace:import","users:manage","roles:manage",null,"admin:view"]' + + await queryRunner.query(`UPDATE "${tableName}" SET "${columnName}" = '${permission}';`) + } + } + + public async down(): Promise {} +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index cbed0760c04..b793abab7d0 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -38,6 +38,7 @@ import { AddTextToSpeechToChatFlow1754986486669 } from './1754986486669-AddTextT import { ModifyChatflowType1755066758601 } from './1755066758601-ModifyChatflowType' import { AddTextToSpeechToChatFlow1759419136055 } from './1759419136055-AddTextToSpeechToChatFlow' import { AddChatFlowNameIndex1759424923093 } from './1759424923093-AddChatFlowNameIndex' +import { AddApiKeyPermission1765360298674 } from './1765360298674-AddApiKeyPermission' import { AddAuthTables1720230151482 } from '../../../enterprise/database/migrations/sqlite/1720230151482-AddAuthTables' import { AddWorkspace1720230151484 } from '../../../enterprise/database/migrations/sqlite/1720230151484-AddWorkspace' @@ -102,5 +103,6 @@ export const sqliteMigrations = [ AddTextToSpeechToChatFlow1754986486669, ModifyChatflowType1755066758601, AddTextToSpeechToChatFlow1759419136055, - AddChatFlowNameIndex1759424923093 + AddChatFlowNameIndex1759424923093, + AddApiKeyPermission1765360298674 ] From cde6400df6c8e7a39521b89b3d8d7ff741da23ce Mon Sep 17 00:00:00 2001 From: yau-wd Date: Fri, 12 Dec 2025 14:53:56 +0800 Subject: [PATCH 03/29] chore(services/apikey): remove auto-create default API key --- packages/server/src/services/apikey/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/server/src/services/apikey/index.ts b/packages/server/src/services/apikey/index.ts index 5e009c92774..063457bba3e 100644 --- a/packages/server/src/services/apikey/index.ts +++ b/packages/server/src/services/apikey/index.ts @@ -30,11 +30,6 @@ const getAllApiKeysFromDB = async (workspaceId: string, page: number = -1, limit const getAllApiKeys = async (workspaceId: string, autoCreateNewKey?: boolean, page: number = -1, limit: number = -1) => { try { let keys = await getAllApiKeysFromDB(workspaceId, page, limit) - const isEmpty = keys?.total === 0 || (Array.isArray(keys) && keys?.length === 0) - if (isEmpty && autoCreateNewKey) { - await createApiKey('DefaultKey', workspaceId) - keys = await getAllApiKeysFromDB(workspaceId, page, limit) - } return keys } catch (error) { throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.getAllApiKeys - ${getErrorMessage(error)}`) From 40fabc6057d10df5b462bbbac3f19f79674522a2 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Fri, 12 Dec 2025 15:12:18 +0800 Subject: [PATCH 04/29] chore: remove auto-create default API key --- packages/server/src/controllers/apikey/index.ts | 3 +-- packages/server/src/services/apikey/index.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/server/src/controllers/apikey/index.ts b/packages/server/src/controllers/apikey/index.ts index 677d689311c..c96337bf06d 100644 --- a/packages/server/src/controllers/apikey/index.ts +++ b/packages/server/src/controllers/apikey/index.ts @@ -7,12 +7,11 @@ import { getPageAndLimitParams } from '../../utils/pagination' // Get api keys const getAllApiKeys = async (req: Request, res: Response, next: NextFunction) => { try { - const autoCreateNewKey = true const { page, limit } = getPageAndLimitParams(req) if (!req.user?.activeWorkspaceId) { throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Workspace ID is required`) } - const apiResponse = await apikeyService.getAllApiKeys(req.user?.activeWorkspaceId, autoCreateNewKey, page, limit) + const apiResponse = await apikeyService.getAllApiKeys(req.user?.activeWorkspaceId, page, limit) return res.json(apiResponse) } catch (error) { next(error) diff --git a/packages/server/src/services/apikey/index.ts b/packages/server/src/services/apikey/index.ts index 063457bba3e..f41f739c737 100644 --- a/packages/server/src/services/apikey/index.ts +++ b/packages/server/src/services/apikey/index.ts @@ -27,7 +27,7 @@ const getAllApiKeysFromDB = async (workspaceId: string, page: number = -1, limit } } -const getAllApiKeys = async (workspaceId: string, autoCreateNewKey?: boolean, page: number = -1, limit: number = -1) => { +const getAllApiKeys = async (workspaceId: string, page: number = -1, limit: number = -1) => { try { let keys = await getAllApiKeysFromDB(workspaceId, page, limit) return keys From 63e67848bec910ad1f24cfb4e3cc8ec731b89cea Mon Sep 17 00:00:00 2001 From: yau-wd Date: Fri, 12 Dec 2025 16:52:19 +0800 Subject: [PATCH 05/29] fix(migrations): rename permission to permissions in apikey table --- .../migrations/mariadb/1765360298674-AddApiKeyPermission.ts | 2 +- .../migrations/mysql/1765360298674-AddApiKeyPermission.ts | 2 +- .../migrations/postgres/1765360298674-AddApiKeyPermission.ts | 2 +- .../migrations/sqlite/1765360298674-AddApiKeyPermission.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts index eea70779560..6e8271bd272 100644 --- a/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts @@ -4,7 +4,7 @@ import { hasColumn } from '../../../utils/database.util' export class AddApiKeyPermission1765360298674 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const tableName = 'apikey' - const columnName = 'permission' + const columnName = 'permissions' const columnExists = await hasColumn(queryRunner, tableName, columnName) if (!columnExists) { diff --git a/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts index eea70779560..6e8271bd272 100644 --- a/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts @@ -4,7 +4,7 @@ import { hasColumn } from '../../../utils/database.util' export class AddApiKeyPermission1765360298674 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const tableName = 'apikey' - const columnName = 'permission' + const columnName = 'permissions' const columnExists = await hasColumn(queryRunner, tableName, columnName) if (!columnExists) { diff --git a/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts index 39bc21d933e..2f8c27d5793 100644 --- a/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts @@ -4,7 +4,7 @@ import { hasColumn } from '../../../utils/database.util' export class AddApiKeyPermission1765360298674 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const tableName = 'apikey' - const columnName = 'permission' + const columnName = 'permissions' const columnExists = await hasColumn(queryRunner, tableName, columnName) if (!columnExists) { diff --git a/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts index 39bc21d933e..2f8c27d5793 100644 --- a/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts @@ -4,7 +4,7 @@ import { hasColumn } from '../../../utils/database.util' export class AddApiKeyPermission1765360298674 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const tableName = 'apikey' - const columnName = 'permission' + const columnName = 'permissions' const columnExists = await hasColumn(queryRunner, tableName, columnName) if (!columnExists) { From 28963b64bcb9cc2d67ce6a65eb61c7cab4785ebb Mon Sep 17 00:00:00 2001 From: yau-wd Date: Fri, 12 Dec 2025 18:01:01 +0800 Subject: [PATCH 06/29] feat(services/apikey): add permissions to create and update --- packages/server/src/Interface.ts | 9 -------- .../server/src/controllers/apikey/index.ts | 21 +++++++++++++++++-- .../server/src/database/entities/ApiKey.ts | 6 ++++-- packages/server/src/services/apikey/index.ts | 19 +++++++++++++++-- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index f1c603043cb..cff078a25a9 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -346,15 +346,6 @@ export interface IUploadFileSizeAndTypes { maxUploadSize: number } -export interface IApiKey { - id: string - keyName: string - apiKey: string - apiSecret: string - updatedDate: Date - workspaceId: string -} - export interface ICustomTemplate { id: string name: string diff --git a/packages/server/src/controllers/apikey/index.ts b/packages/server/src/controllers/apikey/index.ts index c96337bf06d..c13385b68cb 100644 --- a/packages/server/src/controllers/apikey/index.ts +++ b/packages/server/src/controllers/apikey/index.ts @@ -23,10 +23,16 @@ const createApiKey = async (req: Request, res: Response, next: NextFunction) => if (typeof req.body === 'undefined' || !req.body.keyName) { throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.createApiKey - keyName not provided!`) } + if (!req.body.permissions || typeof req.body.permissions !== 'string') { + throw new InternalFlowiseError( + StatusCodes.PRECONDITION_FAILED, + `Error: apikeyController.createApiKey - permissions not provided!` + ) + } if (!req.user?.activeWorkspaceId) { throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Workspace ID is required`) } - const apiResponse = await apikeyService.createApiKey(req.body.keyName, req.user?.activeWorkspaceId) + const apiResponse = await apikeyService.createApiKey(req.body.keyName, req.body.permissions, req.user?.activeWorkspaceId) return res.json(apiResponse) } catch (error) { next(error) @@ -42,10 +48,21 @@ const updateApiKey = async (req: Request, res: Response, next: NextFunction) => if (typeof req.body === 'undefined' || !req.body.keyName) { throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.updateApiKey - keyName not provided!`) } + if (!req.body.permissions || typeof req.body.permissions !== 'string') { + throw new InternalFlowiseError( + StatusCodes.PRECONDITION_FAILED, + `Error: apikeyController.updateApiKey - permissions not provided!` + ) + } if (!req.user?.activeWorkspaceId) { throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Workspace ID is required`) } - const apiResponse = await apikeyService.updateApiKey(req.params.id, req.body.keyName, req.user?.activeWorkspaceId) + const apiResponse = await apikeyService.updateApiKey( + req.params.id, + req.body.keyName, + req.body.permissions, + req.user?.activeWorkspaceId + ) return res.json(apiResponse) } catch (error) { next(error) diff --git a/packages/server/src/database/entities/ApiKey.ts b/packages/server/src/database/entities/ApiKey.ts index 4778962a14f..8087fc5dfa0 100644 --- a/packages/server/src/database/entities/ApiKey.ts +++ b/packages/server/src/database/entities/ApiKey.ts @@ -1,8 +1,7 @@ import { Column, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm' -import { IApiKey } from '../../Interface' @Entity('apikey') -export class ApiKey implements IApiKey { +export class ApiKey { @PrimaryColumn({ type: 'varchar', length: 20 }) id: string @@ -15,6 +14,9 @@ export class ApiKey implements IApiKey { @Column({ type: 'text' }) keyName: string + @Column({ nullable: false, type: 'text', default: '[]' }) + permissions: string + @Column({ type: 'timestamp' }) @UpdateDateColumn() updatedDate: Date diff --git a/packages/server/src/services/apikey/index.ts b/packages/server/src/services/apikey/index.ts index f41f739c737..d3a5f62311a 100644 --- a/packages/server/src/services/apikey/index.ts +++ b/packages/server/src/services/apikey/index.ts @@ -66,7 +66,7 @@ const getApiKeyById = async (apiKeyId: string) => { } } -const createApiKey = async (keyName: string, workspaceId: string) => { +const createApiKey = async (keyName: string, permissions: string, workspaceId: string) => { try { const apiKey = generateAPIKey() const apiSecret = generateSecretHash(apiKey) @@ -76,6 +76,7 @@ const createApiKey = async (keyName: string, workspaceId: string) => { newKey.apiKey = apiKey newKey.apiSecret = apiSecret newKey.keyName = keyName + newKey.permissions = permissions newKey.workspaceId = workspaceId const key = appServer.AppDataSource.getRepository(ApiKey).create(newKey) await appServer.AppDataSource.getRepository(ApiKey).save(key) @@ -86,7 +87,7 @@ const createApiKey = async (keyName: string, workspaceId: string) => { } // Update api key -const updateApiKey = async (id: string, keyName: string, workspaceId: string) => { +const updateApiKey = async (id: string, keyName: string, permissions: string, workspaceId: string) => { try { const appServer = getRunningExpressApp() const currentKey = await appServer.AppDataSource.getRepository(ApiKey).findOneBy({ @@ -97,6 +98,7 @@ const updateApiKey = async (id: string, keyName: string, workspaceId: string) => throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `ApiKey ${currentKey} not found`) } currentKey.keyName = keyName + currentKey.permissions = permissions await appServer.AppDataSource.getRepository(ApiKey).save(currentKey) return await getAllApiKeysFromDB(workspaceId) } catch (error) { @@ -135,6 +137,7 @@ const importKeys = async (body: any) => { } const requiredFields = ['keyName', 'apiKey', 'apiSecret', 'createdAt', 'id'] + const optionalFields = ['permissions'] for (let i = 0; i < keys.length; i++) { const key = keys[i] if (typeof key !== 'object' || key === null) { @@ -161,6 +164,16 @@ const importKeys = async (body: any) => { ) } } + + // Validate optional fields if present + for (const field of optionalFields) { + if (field in key && typeof key[field] !== 'string') { + throw new InternalFlowiseError( + StatusCodes.BAD_REQUEST, + `Invalid format: Key at index ${i} field '${field}' must be a string` + ) + } + } } const appServer = getRunningExpressApp() @@ -192,6 +205,7 @@ const importKeys = async (body: any) => { currentKey.id = uuidv4() currentKey.apiKey = key.apiKey currentKey.apiSecret = key.apiSecret + currentKey.permissions = key.permissions || '[]' currentKey.workspaceId = workspaceId await appServer.AppDataSource.getRepository(ApiKey).save(currentKey) break @@ -214,6 +228,7 @@ const importKeys = async (body: any) => { newKey.apiKey = key.apiKey newKey.apiSecret = key.apiSecret newKey.keyName = key.keyName + newKey.permissions = key.permissions || '[]' newKey.workspaceId = workspaceId const newKeyEntity = appServer.AppDataSource.getRepository(ApiKey).create(newKey) await appServer.AppDataSource.getRepository(ApiKey).save(newKeyEntity) From ab252a2dd16bdf813bafd94209b2e44ec162bb5c Mon Sep 17 00:00:00 2001 From: yau-wd Date: Fri, 12 Dec 2025 20:18:46 +0800 Subject: [PATCH 07/29] feat(views/apikey): add permissions to create and update --- packages/ui/src/views/apikey/APIKeyDialog.css | 98 +++++++ packages/ui/src/views/apikey/APIKeyDialog.jsx | 272 ++++++++++++++++-- packages/ui/src/views/apikey/index.jsx | 35 ++- 3 files changed, 383 insertions(+), 22 deletions(-) create mode 100644 packages/ui/src/views/apikey/APIKeyDialog.css diff --git a/packages/ui/src/views/apikey/APIKeyDialog.css b/packages/ui/src/views/apikey/APIKeyDialog.css new file mode 100644 index 00000000000..c8a30c2fb07 --- /dev/null +++ b/packages/ui/src/views/apikey/APIKeyDialog.css @@ -0,0 +1,98 @@ +.apikey-editor { + padding: 20px 0px; + border-radius: 10px; + width: 100%; + font-family: Arial, sans-serif; + display: flex; + flex-direction: column; + gap: 20px; + height: 75vh; +} + +.key-name { + position: sticky; + top: 0; + z-index: 1; +} + +.permissions-container > p, +.key-name label { + display: block; + font-weight: bold; + margin: 0; + margin-bottom: 5px; +} + +.key-name input { + width: 100%; + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; + font-size: 14px; + display: block; +} + +.permissions-container { + overflow-y: hidden; + max-height: calc(100vh - 120px); +} + +.permissions-list-wrapper { + overflow-y: auto; + max-height: 100%; + padding-right: 10px; + padding-bottom: 10px; +} + +.permission-category { + margin-bottom: 20px; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 15px; +} + +.category-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #e0e0e0; + padding-bottom: 10px; + margin-bottom: 10px; +} + +.category-header h3 { + margin: 0; + font-size: 16px; +} + +.category-header button { + background-color: #007bff; + color: white; + border: none; + border-radius: 5px; + padding: 5px 10px; + cursor: pointer; + font-size: 14px; +} + +.permissions-list { + display: flex; + flex-wrap: wrap; + margin-top: 10px; +} + +.permission-item { + width: 50%; + box-sizing: border-box; +} + +.permission-item label { + font-size: 14px; + display: flex; + align-items: center; + padding: 5px 0; +} + +.permission-item input { + margin-right: 10px; +} diff --git a/packages/ui/src/views/apikey/APIKeyDialog.jsx b/packages/ui/src/views/apikey/APIKeyDialog.jsx index 4b81102a79b..fda42ee1c01 100644 --- a/packages/ui/src/views/apikey/APIKeyDialog.jsx +++ b/packages/ui/src/views/apikey/APIKeyDialog.jsx @@ -19,16 +19,26 @@ import { } from '@mui/material' import { useTheme } from '@mui/material/styles' import { StyledButton } from '@/ui-component/button/StyledButton' +import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' // Icons -import { IconX, IconCopy } from '@tabler/icons-react' +import { IconX, IconCopy, IconKey } from '@tabler/icons-react' // API import apikeyApi from '@/api/apikey' +import authApi from '@/api/auth' + +// Hooks +import useApi from '@/hooks/useApi' // utils import useNotifier from '@/utils/useNotifier' +// const +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions' + +import './APIKeyDialog.css' + const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { const portalElement = document.getElementById('portal') @@ -45,6 +55,10 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { const [keyName, setKeyName] = useState('') const [anchorEl, setAnchorEl] = useState(null) const openPopOver = Boolean(anchorEl) + const [selectedPermissions, setSelectedPermissions] = useState({}) + const [permissions, setPermissions] = useState({}) + + const getAllPermissionsApi = useApi(authApi.getAllPermissions) useEffect(() => { if (dialogProps.type === 'EDIT' && dialogProps.key) { @@ -52,15 +66,143 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { } else if (dialogProps.type === 'ADD') { setKeyName('') } + getAllPermissionsApi.request() + return () => { + setSelectedPermissions({}) + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [dialogProps]) + useEffect(() => { + if (getAllPermissionsApi.error) { + if (setError) setError(getAllPermissionsApi.error) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getAllPermissionsApi.error]) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + useEffect(() => { + if (getAllPermissionsApi.data) { + const permissionsData = getAllPermissionsApi.data + setPermissions(permissionsData) + + if (dialogProps.type === 'EDIT' && dialogProps.key) { + const keyPermissions = JSON.parse(dialogProps.key.permissions || '[]') + if (keyPermissions && keyPermissions.length > 0) { + const tempSelectedPermissions = {} + Object.keys(permissionsData).forEach((category) => { + permissionsData[category].forEach((permission) => { + keyPermissions.forEach((perm) => { + if (perm === permission.key) { + if (!tempSelectedPermissions[category]) { + tempSelectedPermissions[category] = {} + } + tempSelectedPermissions[category][perm] = true + } + }) + }) + }) + setSelectedPermissions(tempSelectedPermissions) + } + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getAllPermissionsApi.data]) + const handleClosePopOver = () => { setAnchorEl(null) } + const handlePermissionChange = (category, key) => { + setSelectedPermissions((prevPermissions) => { + const updatedCategoryPermissions = { + ...prevPermissions[category], + [key]: !prevPermissions[category]?.[key] + } + + if (category === 'templates') { + if (key !== 'templates:marketplace' && key !== 'templates:custom') { + updatedCategoryPermissions['templates:marketplace'] = true + updatedCategoryPermissions['templates:custom'] = true + } + } else { + const viewPermissionKey = `${category}:view` + if (key !== viewPermissionKey) { + const hasEnabledPermissions = Object.keys(updatedCategoryPermissions).some( + ([permissionKey, isEnabled]) => permissionKey !== viewPermissionKey && isEnabled + ) + if (hasEnabledPermissions) { + updatedCategoryPermissions[viewPermissionKey] = true + } + } else { + const hasEnabledPermissions = Object.keys(updatedCategoryPermissions).some( + ([permissionKey, isEnabled]) => permissionKey === viewPermissionKey && isEnabled + ) + if (hasEnabledPermissions) { + updatedCategoryPermissions[key] = true + } + } + } + + return { + ...prevPermissions, + [category]: updatedCategoryPermissions + } + }) + } + + const isCheckboxDisabled = (permissions, category, key) => { + if (category === 'templates') { + // For templates, disable marketplace and custom view if any other permission is enabled + if (key === 'templates:marketplace' || key === 'templates:custom') { + return Object.entries(permissions[category] || {}).some( + ([permKey, isEnabled]) => permKey !== 'templates:marketplace' && permKey !== 'templates:custom' && isEnabled + ) + } + } else { + const viewPermissionKey = `${category}:view` + if (key === viewPermissionKey) { + // Disable the view permission if any other permission is enabled + return Object.entries(permissions[category] || {}).some( + ([permKey, isEnabled]) => permKey !== viewPermissionKey && isEnabled + ) + } + } + + // Non-view permissions are never disabled + return false + } + + const handleSelectAll = (category) => { + const allSelected = permissions[category].every((permission) => selectedPermissions[category]?.[permission.key]) + setSelectedPermissions((prevPermissions) => ({ + ...prevPermissions, + [category]: Object.fromEntries(permissions[category].map((permission) => [permission.key, !allSelected])) + })) + } + const addNewKey = async () => { try { - const createResp = await apikeyApi.createNewAPI({ keyName }) + const tempPermissions = Object.keys(selectedPermissions) + .map((category) => { + return Object.keys(selectedPermissions[category]).map((key) => { + if (selectedPermissions[category][key]) { + return key + } + }) + }) + .flat() + .filter(Boolean) + + const createResp = await apikeyApi.createNewAPI({ + keyName, + permissions: JSON.stringify(tempPermissions) + }) if (createResp.data) { enqueueSnackbar({ message: 'New API key added', @@ -99,7 +241,21 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { const saveKey = async () => { try { - const saveResp = await apikeyApi.updateAPI(dialogProps.key.id, { keyName }) + const tempPermissions = Object.keys(selectedPermissions) + .map((category) => { + return Object.keys(selectedPermissions[category]).map((key) => { + if (selectedPermissions[category][key]) { + return key + } + }) + }) + .flat() + .filter(Boolean) + + const saveResp = await apikeyApi.updateAPI(dialogProps.key.id, { + keyName, + permissions: JSON.stringify(tempPermissions) + }) if (saveResp.data) { enqueueSnackbar({ message: 'API Key saved', @@ -136,19 +292,46 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { } } + const checkDisabled = () => { + if (!keyName || keyName === '') { + return true + } + if (!Object.keys(selectedPermissions).length || !ifPermissionContainsTrue(selectedPermissions)) { + return true + } + return false + } + + const ifPermissionContainsTrue = (obj) => { + for (const key in obj) { + if (typeof obj[key] === 'object' && obj[key] !== null) { + // Recursively check nested objects + if (ifPermissionContainsTrue(obj[key])) { + return true + } + } else if (obj[key] === true) { + return true + } + } + return false + } + const component = show ? ( - {dialogProps.title} +
+ + {dialogProps.title} +
- + {dialogProps.type === 'EDIT' && ( API Key @@ -199,23 +382,71 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { )} - - - Key Name - - setKeyName(e.target.value)} - /> - +
+ + + *  Key Name + + setKeyName(e.target.value)} + /> + +
+

+ *  Permissions +

+
+ {permissions && + Object.keys(permissions).map((category) => ( +
+
+

+ {category + .replace(/([A-Z])/g, ' $1') + .trim() + .toUpperCase()} +

+ +
+
+ {permissions[category].map((permission, index) => ( +
+ +
+ ))} +
+
+ ))} +
+
+
+ (dialogProps.type === 'ADD' ? addNewKey() : saveKey())} id={dialogProps.customBtnId} @@ -223,6 +454,7 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { {dialogProps.confirmButtonName} +
) : null diff --git a/packages/ui/src/views/apikey/index.jsx b/packages/ui/src/views/apikey/index.jsx index e6f8881aded..1e13be92fce 100644 --- a/packages/ui/src/views/apikey/index.jsx +++ b/packages/ui/src/views/apikey/index.jsx @@ -3,6 +3,7 @@ import moment from 'moment/moment' import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' +import React from 'react' // material-ui import { @@ -94,7 +95,7 @@ function APIKeyRow(props) { {props.apiKey.keyName} - + {props.showApiKeys.includes(props.apiKey.apiKey) ? props.apiKey.apiKey : `${props.apiKey.apiKey.substring(0, 2)}${'•'.repeat(18)}${props.apiKey.apiKey.substring( @@ -124,6 +125,29 @@ function APIKeyRow(props) { + + + + {JSON.parse(props.apiKey.permissions || '[]').map((d, key) => ( + + {d} + {', '} + + ))} + + + {props.apiKey.chatFlows.length}{' '} {props.apiKey.chatFlows.length > 0 && ( @@ -150,7 +174,7 @@ function APIKeyRow(props) { {open && ( - + @@ -445,6 +469,7 @@ const APIKey = () => { Key Name API Key + Permissions Usage Updated @@ -471,6 +496,9 @@ const APIKey = () => { + + + @@ -491,6 +519,9 @@ const APIKey = () => { + + + From 5e5f730f1064de8d9ce01b17e03eeb1d1ebd3a0a Mon Sep 17 00:00:00 2001 From: yau-wd Date: Mon, 15 Dec 2025 17:51:33 +0800 Subject: [PATCH 08/29] feat(api): use API key permissions instead of role permissions --- packages/server/src/index.ts | 23 +++++++---------------- packages/server/src/utils/validateKey.ts | 8 ++++---- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 258be4cbd53..d956b4bf2b4 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -3,7 +3,7 @@ import path from 'path' import cors from 'cors' import http from 'http' import cookieParser from 'cookie-parser' -import { DataSource, IsNull } from 'typeorm' +import { DataSource } from 'typeorm' import { MODE, Platform } from './Interface' import { getNodeModulesPackagePath, getEncryptionKey } from './utils' import logger, { expressRequestLogger } from './utils/logger' @@ -32,7 +32,6 @@ import 'global-agent/bootstrap' import { UsageCacheManager } from './UsageCacheManager' import { Workspace } from './enterprise/database/entities/workspace.entity' import { Organization } from './enterprise/database/entities/organization.entity' -import { GeneralRole, Role } from './enterprise/database/entities/role.entity' import { migrateApiKeysFromJsonToDb } from './utils/apiKey' import { ExpressAdapter } from '@bull-board/express' @@ -236,27 +235,19 @@ export class App { } } - const { isValid, workspaceId: apiKeyWorkSpaceId } = await validateAPIKey(req) - if (!isValid) { + const { isValid, apiKey } = await validateAPIKey(req) + if (!isValid || !apiKey) { return res.status(401).json({ error: 'Unauthorized Access' }) } // Find workspace const workspace = await this.AppDataSource.getRepository(Workspace).findOne({ - where: { id: apiKeyWorkSpaceId } + where: { id: apiKey.workspaceId } }) if (!workspace) { return res.status(401).json({ error: 'Unauthorized Access' }) } - // Find owner role - const ownerRole = await this.AppDataSource.getRepository(Role).findOne({ - where: { name: GeneralRole.OWNER, organizationId: IsNull() } - }) - if (!ownerRole) { - return res.status(401).json({ error: 'Unauthorized Access' }) - } - // Find organization const activeOrganizationId = workspace.organizationId as string const org = await this.AppDataSource.getRepository(Organization).findOne({ @@ -272,14 +263,14 @@ export class App { // @ts-ignore req.user = { - permissions: [...JSON.parse(ownerRole.permissions)], + permissions: [...JSON.parse(apiKey.permissions)], features, activeOrganizationId: activeOrganizationId, activeOrganizationSubscriptionId: subscriptionId, activeOrganizationCustomerId: customerId, activeOrganizationProductId: productId, - isOrganizationAdmin: true, - activeWorkspaceId: apiKeyWorkSpaceId!, + isOrganizationAdmin: false, + activeWorkspaceId: workspace.id, activeWorkspace: workspace.name } next() diff --git a/packages/server/src/utils/validateKey.ts b/packages/server/src/utils/validateKey.ts index 840735895ff..12fca65b0be 100644 --- a/packages/server/src/utils/validateKey.ts +++ b/packages/server/src/utils/validateKey.ts @@ -40,9 +40,9 @@ export const validateFlowAPIKey = async (req: Request, chatflow: ChatFlow): Prom /** * Validate and Get API Key Information * @param {Request} req - * @returns {Promise<{isValid: boolean, apiKey?: ApiKey, workspaceId?: string}>} + * @returns {Promise<{isValid: boolean, apiKey?: ApiKey}>} */ -export const validateAPIKey = async (req: Request): Promise<{ isValid: boolean; apiKey?: ApiKey; workspaceId?: string }> => { +export const validateAPIKey = async (req: Request): Promise<{ isValid: boolean; apiKey?: ApiKey }> => { const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? '' if (!authorizationHeader) return { isValid: false } @@ -58,10 +58,10 @@ export const validateAPIKey = async (req: Request): Promise<{ isValid: boolean; const apiSecret = apiKey.apiSecret if (!apiSecret || !compareKeys(apiSecret, suppliedKey)) { - return { isValid: false, apiKey, workspaceId: apiKey.workspaceId } + return { isValid: false, apiKey } } - return { isValid: true, apiKey, workspaceId: apiKey.workspaceId } + return { isValid: true, apiKey } } catch (error) { return { isValid: false } } From cc545bab395d003b0daeedbaf2d1bd92242ac8d3 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Mon, 15 Dec 2025 22:58:20 +0800 Subject: [PATCH 09/29] feat(permissions): filter by user.permissions except for ROLE type --- .../src/enterprise/controllers/auth/index.ts | 26 ++++++++++++++++++- .../src/enterprise/routes/auth/index.ts | 2 +- packages/ui/src/api/auth.js | 2 +- packages/ui/src/views/apikey/APIKeyDialog.jsx | 2 +- .../src/views/roles/CreateEditRoleDialog.jsx | 2 +- packages/ui/src/views/roles/index.jsx | 2 +- 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/server/src/enterprise/controllers/auth/index.ts b/packages/server/src/enterprise/controllers/auth/index.ts index 304b7eee6e5..48a21cc0d25 100644 --- a/packages/server/src/enterprise/controllers/auth/index.ts +++ b/packages/server/src/enterprise/controllers/auth/index.ts @@ -1,10 +1,34 @@ import { NextFunction, Request, Response } from 'express' import { getRunningExpressApp } from '../../../utils/getRunningExpressApp' +import { LoggedInUser } from '../../Interface.Enterprise' const getAllPermissions = async (req: Request, res: Response, next: NextFunction) => { try { const appServer = getRunningExpressApp() - return res.json(appServer.identityManager.getPermissions()) + const type = req.params.type as string + const allPermissions = appServer.identityManager.getPermissions().toJSON() + const user = req.user as LoggedInUser + + let permissions: { [key: string]: { key: string; value: string }[] } = allPermissions + + if (type !== 'ROLE' && user.isOrganizationAdmin === false) { + const userPermissions = user.permissions as string[] + const filteredPermissions: { [key: string]: { key: string; value: string }[] } = {} + + for (const [category, categoryPermissions] of Object.entries(allPermissions)) { + const filteredCategoryPermissions = (categoryPermissions as any[]).filter((permission) => + userPermissions?.includes(permission.key) + ) + + if (filteredCategoryPermissions.length > 0) { + filteredPermissions[category] = filteredCategoryPermissions + } + } + + permissions = filteredPermissions + } + + return res.json(permissions) } catch (error) { next(error) } diff --git a/packages/server/src/enterprise/routes/auth/index.ts b/packages/server/src/enterprise/routes/auth/index.ts index 494b30ccb0a..a2bf391ad05 100644 --- a/packages/server/src/enterprise/routes/auth/index.ts +++ b/packages/server/src/enterprise/routes/auth/index.ts @@ -3,7 +3,7 @@ import authController from '../../controllers/auth' const router = express.Router() // RBAC -router.get(['/', '/permissions'], authController.getAllPermissions) +router.get(['/:type', '/permissions/:type'], authController.getAllPermissions) router.get(['/sso-success'], authController.ssoSuccess) diff --git a/packages/ui/src/api/auth.js b/packages/ui/src/api/auth.js index 50cac09a7a4..cf929db53a3 100644 --- a/packages/ui/src/api/auth.js +++ b/packages/ui/src/api/auth.js @@ -5,7 +5,7 @@ const resolveLogin = (body) => client.post(`/auth/resolve`, body) const login = (body) => client.post(`/auth/login`, body) // permissions -const getAllPermissions = () => client.get(`/auth/permissions`) +const getAllPermissions = (type) => client.get(`/auth/permissions/${type}`) const ssoSuccess = (token) => client.get(`/auth/sso-success?token=${token}`) export default { diff --git a/packages/ui/src/views/apikey/APIKeyDialog.jsx b/packages/ui/src/views/apikey/APIKeyDialog.jsx index fda42ee1c01..f649db14938 100644 --- a/packages/ui/src/views/apikey/APIKeyDialog.jsx +++ b/packages/ui/src/views/apikey/APIKeyDialog.jsx @@ -66,7 +66,7 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { } else if (dialogProps.type === 'ADD') { setKeyName('') } - getAllPermissionsApi.request() + getAllPermissionsApi.request('API_KEY') return () => { setSelectedPermissions({}) } diff --git a/packages/ui/src/views/roles/CreateEditRoleDialog.jsx b/packages/ui/src/views/roles/CreateEditRoleDialog.jsx index 9592966c72d..9dcb900009a 100644 --- a/packages/ui/src/views/roles/CreateEditRoleDialog.jsx +++ b/packages/ui/src/views/roles/CreateEditRoleDialog.jsx @@ -131,7 +131,7 @@ const CreateEditRoleDialog = ({ show, dialogProps, onCancel, onConfirm, setError if ((dialogProps.type === 'EDIT' || dialogProps.type === 'VIEW') && dialogProps.data) { setDialogData(dialogProps.data) } - getAllPermissionsApi.request() + getAllPermissionsApi.request('ROLE') return () => { setRoleName('') setRoleDescription('') diff --git a/packages/ui/src/views/roles/index.jsx b/packages/ui/src/views/roles/index.jsx index d090c4acb59..b81866735b7 100644 --- a/packages/ui/src/views/roles/index.jsx +++ b/packages/ui/src/views/roles/index.jsx @@ -81,7 +81,7 @@ function ViewPermissionsDrawer(props) { useEffect(() => { if (props.open) { - getAllPermissionsApi.request() + getAllPermissionsApi.request('ROLE') } return () => { setSelectedPermissions({}) From 2e46bb622f8802f1ac02a7a6675a03090e3828e5 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Tue, 16 Dec 2025 14:46:43 +0800 Subject: [PATCH 10/29] feat(services/apikey): filter keys by user permissions in getAllApiKeys --- .../server/src/controllers/apikey/index.ts | 42 +++++--- .../src/enterprise/Interface.Enterprise.ts | 2 +- packages/server/src/services/apikey/index.ts | 101 ++++++++++++------ 3 files changed, 96 insertions(+), 49 deletions(-) diff --git a/packages/server/src/controllers/apikey/index.ts b/packages/server/src/controllers/apikey/index.ts index c13385b68cb..8f499e2b6e1 100644 --- a/packages/server/src/controllers/apikey/index.ts +++ b/packages/server/src/controllers/apikey/index.ts @@ -1,5 +1,6 @@ -import { Request, Response, NextFunction } from 'express' +import { NextFunction, Request, Response } from 'express' import { StatusCodes } from 'http-status-codes' +import { LoggedInUser } from '../../enterprise/Interface.Enterprise' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import apikeyService from '../../services/apikey' import { getPageAndLimitParams } from '../../utils/pagination' @@ -7,11 +8,16 @@ import { getPageAndLimitParams } from '../../utils/pagination' // Get api keys const getAllApiKeys = async (req: Request, res: Response, next: NextFunction) => { try { + const user = req.user as LoggedInUser const { page, limit } = getPageAndLimitParams(req) - if (!req.user?.activeWorkspaceId) { - throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Workspace ID is required`) - } - const apiResponse = await apikeyService.getAllApiKeys(req.user?.activeWorkspaceId, page, limit) + + const apiResponse = await apikeyService.getAllApiKeys( + user.permissions, + user.isOrganizationAdmin, + user.activeWorkspaceId, + page, + limit + ) return res.json(apiResponse) } catch (error) { next(error) @@ -29,10 +35,14 @@ const createApiKey = async (req: Request, res: Response, next: NextFunction) => `Error: apikeyController.createApiKey - permissions not provided!` ) } - if (!req.user?.activeWorkspaceId) { - throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Workspace ID is required`) - } - const apiResponse = await apikeyService.createApiKey(req.body.keyName, req.body.permissions, req.user?.activeWorkspaceId) + const user = req.user as LoggedInUser + const apiResponse = await apikeyService.createApiKey( + user.permissions, + user.isOrganizationAdmin, + user.activeWorkspaceId, + req.body.keyName, + req.body.permissions + ) return res.json(apiResponse) } catch (error) { next(error) @@ -54,14 +64,14 @@ const updateApiKey = async (req: Request, res: Response, next: NextFunction) => `Error: apikeyController.updateApiKey - permissions not provided!` ) } - if (!req.user?.activeWorkspaceId) { - throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Workspace ID is required`) - } + const user = req.user as LoggedInUser const apiResponse = await apikeyService.updateApiKey( + user.permissions, + user.isOrganizationAdmin, + user.activeWorkspaceId, req.params.id, req.body.keyName, - req.body.permissions, - req.user?.activeWorkspaceId + req.body.permissions ) return res.json(apiResponse) } catch (error) { @@ -78,8 +88,8 @@ const importKeys = async (req: Request, res: Response, next: NextFunction) => { if (!req.user?.activeWorkspaceId) { throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Workspace ID is required`) } - req.body.workspaceId = req.user?.activeWorkspaceId - const apiResponse = await apikeyService.importKeys(req.body) + const user = req.user as LoggedInUser + const apiResponse = await apikeyService.importKeys(user.permissions, user.isOrganizationAdmin, req.body) return res.json(apiResponse) } catch (error) { next(error) diff --git a/packages/server/src/enterprise/Interface.Enterprise.ts b/packages/server/src/enterprise/Interface.Enterprise.ts index 5dd4384e043..e52f9341e58 100644 --- a/packages/server/src/enterprise/Interface.Enterprise.ts +++ b/packages/server/src/enterprise/Interface.Enterprise.ts @@ -72,7 +72,7 @@ export type LoggedInUser = { activeWorkspaceId: string activeWorkspace: string assignedWorkspaces: IAssignedWorkspace[] - permissions?: string[] + permissions: string[] features?: Record ssoRefreshToken?: string ssoToken?: string diff --git a/packages/server/src/services/apikey/index.ts b/packages/server/src/services/apikey/index.ts index d3a5f62311a..10506022f4c 100644 --- a/packages/server/src/services/apikey/index.ts +++ b/packages/server/src/services/apikey/index.ts @@ -1,36 +1,60 @@ import { StatusCodes } from 'http-status-codes' -import { generateAPIKey, generateSecretHash } from '../../utils/apiKey' -import { addChatflowsCount } from '../../utils/addChatflowsCount' +import { IsNull, Not } from 'typeorm' +import { v4 as uuidv4 } from 'uuid' +import { ApiKey } from '../../database/entities/ApiKey' +import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getErrorMessage } from '../../errors/utils' +import { addChatflowsCount } from '../../utils/addChatflowsCount' +import { generateAPIKey, generateSecretHash } from '../../utils/apiKey' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' -import { ApiKey } from '../../database/entities/ApiKey' -import { Not, IsNull } from 'typeorm' -import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils' -import { v4 as uuidv4 } from 'uuid' -const getAllApiKeysFromDB = async (workspaceId: string, page: number = -1, limit: number = -1) => { - const appServer = getRunningExpressApp() - const queryBuilder = appServer.AppDataSource.getRepository(ApiKey).createQueryBuilder('api_key').orderBy('api_key.updatedDate', 'DESC') - if (page > 0 && limit > 0) { - queryBuilder.skip((page - 1) * limit) - queryBuilder.take(limit) - } - queryBuilder.andWhere('api_key.workspaceId = :workspaceId', { workspaceId }) - const [data, total] = await queryBuilder.getManyAndCount() - const keysWithChatflows = await addChatflowsCount(data) +/** + * Get all API keys for a workspace + * Non-admin users can only view API keys whose permissions are a subset of their own permissions + */ +const getAllApiKeys = async ( + userPermissions: string[], + isOrganizationAdmin: boolean, + workspaceId: string, + page: number = -1, + limit: number = -1 +) => { + try { + const appServer = getRunningExpressApp() + const queryBuilder = appServer.AppDataSource.getRepository(ApiKey) + .createQueryBuilder('api_key') + .orderBy('api_key.updatedDate', 'DESC') + if (page > 0 && limit > 0) { + queryBuilder.skip((page - 1) * limit) + queryBuilder.take(limit) + } + queryBuilder.andWhere('api_key.workspaceId = :workspaceId', { workspaceId }) + const allKeys = await queryBuilder.getMany() - if (page > 0 && limit > 0) { - return { total, data: keysWithChatflows } - } else { - return keysWithChatflows - } -} + // Filter keys based on user permissions + let filteredKeys = allKeys + if (!isOrganizationAdmin) { + // Non-admin users can only see API keys whose permissions are a subset of their own + filteredKeys = allKeys.filter((key) => { + try { + const keyPermissions = JSON.parse(key.permissions) + // Check if all key permissions are included in user permissions + return keyPermissions.every((permission: string) => permission === null || userPermissions.includes(permission)) + } catch (error) { + // If parsing fails, exclude this key + return false + } + }) + } -const getAllApiKeys = async (workspaceId: string, page: number = -1, limit: number = -1) => { - try { - let keys = await getAllApiKeysFromDB(workspaceId, page, limit) - return keys + const keysWithChatflows = await addChatflowsCount(filteredKeys) + + if (page > 0 && limit > 0) { + return { total: filteredKeys.length, data: keysWithChatflows } + } else { + return keysWithChatflows + } } catch (error) { throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.getAllApiKeys - ${getErrorMessage(error)}`) } @@ -66,7 +90,13 @@ const getApiKeyById = async (apiKeyId: string) => { } } -const createApiKey = async (keyName: string, permissions: string, workspaceId: string) => { +const createApiKey = async ( + userPermissions: string[], + isOrganizationAdmin: boolean, + workspaceId: string, + keyName: string, + permissions: string +) => { try { const apiKey = generateAPIKey() const apiSecret = generateSecretHash(apiKey) @@ -80,14 +110,21 @@ const createApiKey = async (keyName: string, permissions: string, workspaceId: s newKey.workspaceId = workspaceId const key = appServer.AppDataSource.getRepository(ApiKey).create(newKey) await appServer.AppDataSource.getRepository(ApiKey).save(key) - return await getAllApiKeysFromDB(workspaceId) + return await getAllApiKeys(userPermissions, isOrganizationAdmin, workspaceId) } catch (error) { throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.createApiKey - ${getErrorMessage(error)}`) } } // Update api key -const updateApiKey = async (id: string, keyName: string, permissions: string, workspaceId: string) => { +const updateApiKey = async ( + userPermissions: string[], + isOrganizationAdmin: boolean, + workspaceId: string, + id: string, + keyName: string, + permissions: string +) => { try { const appServer = getRunningExpressApp() const currentKey = await appServer.AppDataSource.getRepository(ApiKey).findOneBy({ @@ -100,7 +137,7 @@ const updateApiKey = async (id: string, keyName: string, permissions: string, wo currentKey.keyName = keyName currentKey.permissions = permissions await appServer.AppDataSource.getRepository(ApiKey).save(currentKey) - return await getAllApiKeysFromDB(workspaceId) + return await getAllApiKeys(userPermissions, isOrganizationAdmin, workspaceId) } catch (error) { throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.updateApiKey - ${getErrorMessage(error)}`) } @@ -119,7 +156,7 @@ const deleteApiKey = async (id: string, workspaceId: string) => { } } -const importKeys = async (body: any) => { +const importKeys = async (userPermissions: string[], isOrganizationAdmin: boolean, body: any) => { try { const jsonFile = body.jsonFile const workspaceId = body.workspaceId @@ -234,7 +271,7 @@ const importKeys = async (body: any) => { await appServer.AppDataSource.getRepository(ApiKey).save(newKeyEntity) } } - return await getAllApiKeysFromDB(workspaceId) + return await getAllApiKeys(userPermissions, isOrganizationAdmin, workspaceId) } catch (error) { throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.importKeys - ${getErrorMessage(error)}`) } From 952807b0d9f0bfef9724e0e7ae6782973fa4bf14 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Tue, 16 Dec 2025 15:42:55 +0800 Subject: [PATCH 11/29] feat(services/apikey): limit API key permissions to user permissions during creation/update --- packages/server/src/services/apikey/index.ts | 76 ++++++++++++-------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/packages/server/src/services/apikey/index.ts b/packages/server/src/services/apikey/index.ts index 10506022f4c..ed77a0c410e 100644 --- a/packages/server/src/services/apikey/index.ts +++ b/packages/server/src/services/apikey/index.ts @@ -97,23 +97,30 @@ const createApiKey = async ( keyName: string, permissions: string ) => { - try { - const apiKey = generateAPIKey() - const apiSecret = generateSecretHash(apiKey) - const appServer = getRunningExpressApp() - const newKey = new ApiKey() - newKey.id = uuidv4() - newKey.apiKey = apiKey - newKey.apiSecret = apiSecret - newKey.keyName = keyName - newKey.permissions = permissions - newKey.workspaceId = workspaceId - const key = appServer.AppDataSource.getRepository(ApiKey).create(newKey) - await appServer.AppDataSource.getRepository(ApiKey).save(key) - return await getAllApiKeys(userPermissions, isOrganizationAdmin, workspaceId) - } catch (error) { - throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.createApiKey - ${getErrorMessage(error)}`) + // Validate permissions before creating the key + if (!isOrganizationAdmin) { + const requestedPermissions = JSON.parse(permissions) + // Check if all requested permissions are included in user permissions + const hasInvalidPermissions = requestedPermissions.some( + (permission: string) => permission !== null && !userPermissions.includes(permission) + ) + if (hasInvalidPermissions) + throw new InternalFlowiseError(StatusCodes.FORBIDDEN, 'Cannot create API key with permissions that exceed your own permissions') } + + const apiKey = generateAPIKey() + const apiSecret = generateSecretHash(apiKey) + const appServer = getRunningExpressApp() + const newKey = new ApiKey() + newKey.id = uuidv4() + newKey.apiKey = apiKey + newKey.apiSecret = apiSecret + newKey.keyName = keyName + newKey.permissions = permissions + newKey.workspaceId = workspaceId + const key = appServer.AppDataSource.getRepository(ApiKey).create(newKey) + await appServer.AppDataSource.getRepository(ApiKey).save(key) + return await getAllApiKeys(userPermissions, isOrganizationAdmin, workspaceId) } // Update api key @@ -125,22 +132,29 @@ const updateApiKey = async ( keyName: string, permissions: string ) => { - try { - const appServer = getRunningExpressApp() - const currentKey = await appServer.AppDataSource.getRepository(ApiKey).findOneBy({ - id: id, - workspaceId: workspaceId - }) - if (!currentKey) { - throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `ApiKey ${currentKey} not found`) - } - currentKey.keyName = keyName - currentKey.permissions = permissions - await appServer.AppDataSource.getRepository(ApiKey).save(currentKey) - return await getAllApiKeys(userPermissions, isOrganizationAdmin, workspaceId) - } catch (error) { - throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.updateApiKey - ${getErrorMessage(error)}`) + // Validate permissions before updating the key + if (!isOrganizationAdmin) { + const requestedPermissions = JSON.parse(permissions) + // Check if all requested permissions are included in user permissions + const hasInvalidPermissions = requestedPermissions.some( + (permission: string) => permission !== null && !userPermissions.includes(permission) + ) + if (hasInvalidPermissions) + throw new InternalFlowiseError(StatusCodes.FORBIDDEN, 'Cannot update API key with permissions that exceed your own permissions') + } + + const appServer = getRunningExpressApp() + const currentKey = await appServer.AppDataSource.getRepository(ApiKey).findOneBy({ + id: id, + workspaceId: workspaceId + }) + if (!currentKey) { + throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `ApiKey ${currentKey} not found`) } + currentKey.keyName = keyName + currentKey.permissions = permissions + await appServer.AppDataSource.getRepository(ApiKey).save(currentKey) + return await getAllApiKeys(userPermissions, isOrganizationAdmin, workspaceId) } const deleteApiKey = async (id: string, workspaceId: string) => { From aacfad5c437a37afbfc3ee827196408b053970be Mon Sep 17 00:00:00 2001 From: yau-wd Date: Tue, 16 Dec 2025 16:33:34 +0800 Subject: [PATCH 12/29] fix(database.util.ts): replace non-null assertion with explicit error handling --- packages/server/src/utils/database.util.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/database.util.ts b/packages/server/src/utils/database.util.ts index 34217130590..7ae71b1798e 100644 --- a/packages/server/src/utils/database.util.ts +++ b/packages/server/src/utils/database.util.ts @@ -3,7 +3,11 @@ import { QueryRunner } from 'typeorm' export async function hasColumn(queryRunner: QueryRunner, tableName: string, columnName: string): Promise { const table = await queryRunner.getTable(tableName) - const hasColumn = table!.columns.some((column) => column.name === columnName) + if (!table) { + throw new Error(`Table ${tableName} not found`) + } + + const hasColumn = table.columns.some((column) => column.name === columnName) return hasColumn } From 9d333185b4f8766996dbd0353d01f646eacd635c Mon Sep 17 00:00:00 2001 From: yau-wd Date: Tue, 16 Dec 2025 16:48:06 +0800 Subject: [PATCH 13/29] feat(services/apikey): log errors for malformed API key permissions --- packages/server/src/services/apikey/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/server/src/services/apikey/index.ts b/packages/server/src/services/apikey/index.ts index ed77a0c410e..de953bff9b5 100644 --- a/packages/server/src/services/apikey/index.ts +++ b/packages/server/src/services/apikey/index.ts @@ -8,6 +8,7 @@ import { getErrorMessage } from '../../errors/utils' import { addChatflowsCount } from '../../utils/addChatflowsCount' import { generateAPIKey, generateSecretHash } from '../../utils/apiKey' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import logger from '../../utils/logger' /** * Get all API keys for a workspace @@ -42,6 +43,11 @@ const getAllApiKeys = async ( // Check if all key permissions are included in user permissions return keyPermissions.every((permission: string) => permission === null || userPermissions.includes(permission)) } catch (error) { + // Log parsing errors to help with debugging malformed permissions + logger.error( + `[server]: Failed to parse permissions for API key ${key.id} (${key.keyName}). Raw value: ${key.permissions}`, + error + ) // If parsing fails, exclude this key return false } From 9e24e04b671da7010d937c8164b309b59a6597d0 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Tue, 16 Dec 2025 17:05:06 +0800 Subject: [PATCH 14/29] refactor(services/apikey): extract permission validation --- packages/server/src/services/apikey/index.ts | 44 ++++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/packages/server/src/services/apikey/index.ts b/packages/server/src/services/apikey/index.ts index de953bff9b5..7a9db44d497 100644 --- a/packages/server/src/services/apikey/index.ts +++ b/packages/server/src/services/apikey/index.ts @@ -10,6 +10,30 @@ import { generateAPIKey, generateSecretHash } from '../../utils/apiKey' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import logger from '../../utils/logger' +/** + * Validates that requested permissions do not exceed user's own permissions + * @param userPermissions - Array of permissions the user has + * @param isOrganizationAdmin - Whether the user is an organization admin + * @param permissions - JSON string of requested permissions + * @param operation - The operation being performed (for error message) + * @throws InternalFlowiseError if validation fails + */ +function validatePermissions(userPermissions: string[], isOrganizationAdmin: boolean, permissions: string, operation: string) { + if (!isOrganizationAdmin) { + const requestedPermissions = JSON.parse(permissions) + // Check if all requested permissions are included in user permissions + const hasInvalidPermissions = requestedPermissions.some( + (permission: string) => permission !== null && !userPermissions.includes(permission) + ) + if (hasInvalidPermissions) { + throw new InternalFlowiseError( + StatusCodes.FORBIDDEN, + `Cannot ${operation} API key with permissions that exceed your own permissions` + ) + } + } +} + /** * Get all API keys for a workspace * Non-admin users can only view API keys whose permissions are a subset of their own permissions @@ -104,15 +128,7 @@ const createApiKey = async ( permissions: string ) => { // Validate permissions before creating the key - if (!isOrganizationAdmin) { - const requestedPermissions = JSON.parse(permissions) - // Check if all requested permissions are included in user permissions - const hasInvalidPermissions = requestedPermissions.some( - (permission: string) => permission !== null && !userPermissions.includes(permission) - ) - if (hasInvalidPermissions) - throw new InternalFlowiseError(StatusCodes.FORBIDDEN, 'Cannot create API key with permissions that exceed your own permissions') - } + validatePermissions(userPermissions, isOrganizationAdmin, permissions, 'create') const apiKey = generateAPIKey() const apiSecret = generateSecretHash(apiKey) @@ -139,15 +155,7 @@ const updateApiKey = async ( permissions: string ) => { // Validate permissions before updating the key - if (!isOrganizationAdmin) { - const requestedPermissions = JSON.parse(permissions) - // Check if all requested permissions are included in user permissions - const hasInvalidPermissions = requestedPermissions.some( - (permission: string) => permission !== null && !userPermissions.includes(permission) - ) - if (hasInvalidPermissions) - throw new InternalFlowiseError(StatusCodes.FORBIDDEN, 'Cannot update API key with permissions that exceed your own permissions') - } + validatePermissions(userPermissions, isOrganizationAdmin, permissions, 'update') const appServer = getRunningExpressApp() const currentKey = await appServer.AppDataSource.getRepository(ApiKey).findOneBy({ From 15d67b284404b8e47ace8bcdf72ce7541903c695 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Wed, 17 Dec 2025 17:45:29 +0800 Subject: [PATCH 15/29] feat(permission): enhance permission filtering based on user features and platform type --- .../src/enterprise/controllers/auth/index.ts | 48 ++++- .../server/src/enterprise/rbac/Permissions.ts | 182 +++++++++--------- packages/ui/src/views/apikey/APIKeyDialog.jsx | 38 +++- .../src/views/roles/CreateEditRoleDialog.jsx | 36 ++-- 4 files changed, 191 insertions(+), 113 deletions(-) diff --git a/packages/server/src/enterprise/controllers/auth/index.ts b/packages/server/src/enterprise/controllers/auth/index.ts index 48a21cc0d25..4f0478d64ff 100644 --- a/packages/server/src/enterprise/controllers/auth/index.ts +++ b/packages/server/src/enterprise/controllers/auth/index.ts @@ -1,4 +1,5 @@ import { NextFunction, Request, Response } from 'express' +import { Platform } from '../../../Interface' import { getRunningExpressApp } from '../../../utils/getRunningExpressApp' import { LoggedInUser } from '../../Interface.Enterprise' @@ -11,11 +12,56 @@ const getAllPermissions = async (req: Request, res: Response, next: NextFunction let permissions: { [key: string]: { key: string; value: string }[] } = allPermissions + // Mapping of feature flags to permission prefixes + const featureToPermissionMap: { [key: string]: string[] } = { + 'feat:login-activity': ['loginActivity:'], + 'feat:logs': ['logs:'], + 'feat:roles': ['roles:'], + 'feat:share': ['credentials:share', 'templates:custom-share'], + 'feat:sso-config': ['sso:'], + 'feat:users': ['users:'], + 'feat:workspaces': ['workspace:'] + } + + if (type !== 'ROLE' && appServer.identityManager.getPlatformType() === Platform.CLOUD) { + const userFeatures = user.features + if (userFeatures) { + const disabledFeatures = Object.entries(userFeatures).filter(([, value]) => value === 'false') + + // Get list of disabled permission prefixes + const disabledPermissionPrefixes: string[] = [] + disabledFeatures.forEach(([featureKey]) => { + const prefixes = featureToPermissionMap[featureKey] + if (prefixes) { + disabledPermissionPrefixes.push(...prefixes) + } + }) + + // Filter out permissions based on disabled features + const filteredPermissions: { [key: string]: { key: string; value: string }[] } = {} + + for (const [category, categoryPermissions] of Object.entries(allPermissions)) { + const filteredCategoryPermissions = (categoryPermissions as any[]).filter((permission) => { + // Check if this permission starts with any disabled prefix + const isDisabled = disabledPermissionPrefixes.some((prefix) => permission.key.startsWith(prefix)) + return !isDisabled + }) + + // Only include category if it has remaining permissions + if (filteredCategoryPermissions.length > 0) { + filteredPermissions[category] = filteredCategoryPermissions + } + } + + permissions = filteredPermissions + } + } + if (type !== 'ROLE' && user.isOrganizationAdmin === false) { const userPermissions = user.permissions as string[] const filteredPermissions: { [key: string]: { key: string; value: string }[] } = {} - for (const [category, categoryPermissions] of Object.entries(allPermissions)) { + for (const [category, categoryPermissions] of Object.entries(permissions)) { const filteredCategoryPermissions = (categoryPermissions as any[]).filter((permission) => userPermissions?.includes(permission.key) ) diff --git a/packages/server/src/enterprise/rbac/Permissions.ts b/packages/server/src/enterprise/rbac/Permissions.ts index e44f541a820..abcc4742adc 100644 --- a/packages/server/src/enterprise/rbac/Permissions.ts +++ b/packages/server/src/enterprise/rbac/Permissions.ts @@ -6,138 +6,140 @@ export class Permissions { // this.categories.push(auditCategory) const chatflowsCategory = new PermissionCategory('chatflows') - chatflowsCategory.addPermission(new Permission('chatflows:view', 'View')) - chatflowsCategory.addPermission(new Permission('chatflows:create', 'Create')) - chatflowsCategory.addPermission(new Permission('chatflows:update', 'Update')) - chatflowsCategory.addPermission(new Permission('chatflows:duplicate', 'Duplicate')) - chatflowsCategory.addPermission(new Permission('chatflows:delete', 'Delete')) - chatflowsCategory.addPermission(new Permission('chatflows:export', 'Export')) - chatflowsCategory.addPermission(new Permission('chatflows:import', 'Import')) - chatflowsCategory.addPermission(new Permission('chatflows:config', 'Edit Configuration')) - chatflowsCategory.addPermission(new Permission('chatflows:domains', 'Allowed Domains')) + chatflowsCategory.addPermission(new Permission('chatflows:view', 'View', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:create', 'Create', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:update', 'Update', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:duplicate', 'Duplicate', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:delete', 'Delete', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:export', 'Export', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:import', 'Import', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:config', 'Edit Configuration', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:domains', 'Allowed Domains', true, true, true)) this.categories.push(chatflowsCategory) const agentflowsCategory = new PermissionCategory('agentflows') - agentflowsCategory.addPermission(new Permission('agentflows:view', 'View')) - agentflowsCategory.addPermission(new Permission('agentflows:create', 'Create')) - agentflowsCategory.addPermission(new Permission('agentflows:update', 'Update')) - agentflowsCategory.addPermission(new Permission('agentflows:duplicate', 'Duplicate')) - agentflowsCategory.addPermission(new Permission('agentflows:delete', 'Delete')) - agentflowsCategory.addPermission(new Permission('agentflows:export', 'Export')) - agentflowsCategory.addPermission(new Permission('agentflows:import', 'Import')) - agentflowsCategory.addPermission(new Permission('agentflows:config', 'Edit Configuration')) - agentflowsCategory.addPermission(new Permission('agentflows:domains', 'Allowed Domains')) + agentflowsCategory.addPermission(new Permission('agentflows:view', 'View', true, true, true)) + agentflowsCategory.addPermission(new Permission('agentflows:create', 'Create', true, true, true)) + agentflowsCategory.addPermission(new Permission('agentflows:update', 'Update', true, true, true)) + agentflowsCategory.addPermission(new Permission('agentflows:duplicate', 'Duplicate', true, true, true)) + agentflowsCategory.addPermission(new Permission('agentflows:delete', 'Delete', true, true, true)) + agentflowsCategory.addPermission(new Permission('agentflows:export', 'Export', true, true, true)) + agentflowsCategory.addPermission(new Permission('agentflows:import', 'Import', true, true, true)) + agentflowsCategory.addPermission(new Permission('agentflows:config', 'Edit Configuration', true, true, true)) + agentflowsCategory.addPermission(new Permission('agentflows:domains', 'Allowed Domains', true, true, true)) this.categories.push(agentflowsCategory) const toolsCategory = new PermissionCategory('tools') - toolsCategory.addPermission(new Permission('tools:view', 'View')) - toolsCategory.addPermission(new Permission('tools:create', 'Create')) - toolsCategory.addPermission(new Permission('tools:update', 'Update')) - toolsCategory.addPermission(new Permission('tools:delete', 'Delete')) - toolsCategory.addPermission(new Permission('tools:export', 'Export')) + toolsCategory.addPermission(new Permission('tools:view', 'View', true, true, true)) + toolsCategory.addPermission(new Permission('tools:create', 'Create', true, true, true)) + toolsCategory.addPermission(new Permission('tools:update', 'Update', true, true, true)) + toolsCategory.addPermission(new Permission('tools:delete', 'Delete', true, true, true)) + toolsCategory.addPermission(new Permission('tools:export', 'Export', true, true, true)) this.categories.push(toolsCategory) const assistantsCategory = new PermissionCategory('assistants') - assistantsCategory.addPermission(new Permission('assistants:view', 'View')) - assistantsCategory.addPermission(new Permission('assistants:create', 'Create')) - assistantsCategory.addPermission(new Permission('assistants:update', 'Update')) - assistantsCategory.addPermission(new Permission('assistants:delete', 'Delete')) + assistantsCategory.addPermission(new Permission('assistants:view', 'View', true, true, true)) + assistantsCategory.addPermission(new Permission('assistants:create', 'Create', true, true, true)) + assistantsCategory.addPermission(new Permission('assistants:update', 'Update', true, true, true)) + assistantsCategory.addPermission(new Permission('assistants:delete', 'Delete', true, true, true)) this.categories.push(assistantsCategory) const credentialsCategory = new PermissionCategory('credentials') - credentialsCategory.addPermission(new Permission('credentials:view', 'View')) - credentialsCategory.addPermission(new Permission('credentials:create', 'Create')) - credentialsCategory.addPermission(new Permission('credentials:update', 'Update')) - credentialsCategory.addPermission(new Permission('credentials:delete', 'Delete')) - credentialsCategory.addPermission(new Permission('credentials:share', 'Share')) + credentialsCategory.addPermission(new Permission('credentials:view', 'View', true, true, true)) + credentialsCategory.addPermission(new Permission('credentials:create', 'Create', true, true, true)) + credentialsCategory.addPermission(new Permission('credentials:update', 'Update', true, true, true)) + credentialsCategory.addPermission(new Permission('credentials:delete', 'Delete', true, true, true)) + credentialsCategory.addPermission(new Permission('credentials:share', 'Share', false, true, true)) this.categories.push(credentialsCategory) const variablesCategory = new PermissionCategory('variables') - variablesCategory.addPermission(new Permission('variables:view', 'View')) - variablesCategory.addPermission(new Permission('variables:create', 'Create')) - variablesCategory.addPermission(new Permission('variables:update', 'Update')) - variablesCategory.addPermission(new Permission('variables:delete', 'Delete')) + variablesCategory.addPermission(new Permission('variables:view', 'View', true, true, true)) + variablesCategory.addPermission(new Permission('variables:create', 'Create', true, true, true)) + variablesCategory.addPermission(new Permission('variables:update', 'Update', true, true, true)) + variablesCategory.addPermission(new Permission('variables:delete', 'Delete', true, true, true)) this.categories.push(variablesCategory) const apikeysCategory = new PermissionCategory('apikeys') - apikeysCategory.addPermission(new Permission('apikeys:view', 'View')) - apikeysCategory.addPermission(new Permission('apikeys:create', 'Create')) - apikeysCategory.addPermission(new Permission('apikeys:update', 'Update')) - apikeysCategory.addPermission(new Permission('apikeys:delete', 'Delete')) - apikeysCategory.addPermission(new Permission('apikeys:import', 'Import')) + apikeysCategory.addPermission(new Permission('apikeys:view', 'View', true, true, true)) + apikeysCategory.addPermission(new Permission('apikeys:create', 'Create', true, true, true)) + apikeysCategory.addPermission(new Permission('apikeys:update', 'Update', true, true, true)) + apikeysCategory.addPermission(new Permission('apikeys:delete', 'Delete', true, true, true)) + apikeysCategory.addPermission(new Permission('apikeys:import', 'Import', true, true, true)) this.categories.push(apikeysCategory) const documentStoresCategory = new PermissionCategory('documentStores') - documentStoresCategory.addPermission(new Permission('documentStores:view', 'View')) - documentStoresCategory.addPermission(new Permission('documentStores:create', 'Create')) - documentStoresCategory.addPermission(new Permission('documentStores:update', 'Update')) - documentStoresCategory.addPermission(new Permission('documentStores:delete', 'Delete Document Store')) - documentStoresCategory.addPermission(new Permission('documentStores:add-loader', 'Add Document Loader')) - documentStoresCategory.addPermission(new Permission('documentStores:delete-loader', 'Delete Document Loader')) - documentStoresCategory.addPermission(new Permission('documentStores:preview-process', 'Preview & Process Document Chunks')) - documentStoresCategory.addPermission(new Permission('documentStores:upsert-config', 'Upsert Config')) + documentStoresCategory.addPermission(new Permission('documentStores:view', 'View', true, true, true)) + documentStoresCategory.addPermission(new Permission('documentStores:create', 'Create', true, true, true)) + documentStoresCategory.addPermission(new Permission('documentStores:update', 'Update', true, true, true)) + documentStoresCategory.addPermission(new Permission('documentStores:delete', 'Delete Document Store', true, true, true)) + documentStoresCategory.addPermission(new Permission('documentStores:add-loader', 'Add Document Loader', true, true, true)) + documentStoresCategory.addPermission(new Permission('documentStores:delete-loader', 'Delete Document Loader', true, true, true)) + documentStoresCategory.addPermission( + new Permission('documentStores:preview-process', 'Preview & Process Document Chunks', true, true, true) + ) + documentStoresCategory.addPermission(new Permission('documentStores:upsert-config', 'Upsert Config', true, true, true)) this.categories.push(documentStoresCategory) const datasetsCategory = new PermissionCategory('datasets') - datasetsCategory.addPermission(new Permission('datasets:view', 'View')) - datasetsCategory.addPermission(new Permission('datasets:create', 'Create')) - datasetsCategory.addPermission(new Permission('datasets:update', 'Update')) - datasetsCategory.addPermission(new Permission('datasets:delete', 'Delete')) + datasetsCategory.addPermission(new Permission('datasets:view', 'View', false, true, true)) + datasetsCategory.addPermission(new Permission('datasets:create', 'Create', false, true, true)) + datasetsCategory.addPermission(new Permission('datasets:update', 'Update', false, true, true)) + datasetsCategory.addPermission(new Permission('datasets:delete', 'Delete', false, true, true)) this.categories.push(datasetsCategory) const executionsCategory = new PermissionCategory('executions') - executionsCategory.addPermission(new Permission('executions:view', 'View')) - executionsCategory.addPermission(new Permission('executions:delete', 'Delete')) + executionsCategory.addPermission(new Permission('executions:view', 'View', true, true, true)) + executionsCategory.addPermission(new Permission('executions:delete', 'Delete', true, true, true)) this.categories.push(executionsCategory) const evaluatorsCategory = new PermissionCategory('evaluators') - evaluatorsCategory.addPermission(new Permission('evaluators:view', 'View')) - evaluatorsCategory.addPermission(new Permission('evaluators:create', 'Create')) - evaluatorsCategory.addPermission(new Permission('evaluators:update', 'Update')) - evaluatorsCategory.addPermission(new Permission('evaluators:delete', 'Delete')) + evaluatorsCategory.addPermission(new Permission('evaluators:view', 'View', false, true, true)) + evaluatorsCategory.addPermission(new Permission('evaluators:create', 'Create', false, true, true)) + evaluatorsCategory.addPermission(new Permission('evaluators:update', 'Update', false, true, true)) + evaluatorsCategory.addPermission(new Permission('evaluators:delete', 'Delete', false, true, true)) this.categories.push(evaluatorsCategory) const evaluationsCategory = new PermissionCategory('evaluations') - evaluationsCategory.addPermission(new Permission('evaluations:view', 'View')) - evaluationsCategory.addPermission(new Permission('evaluations:create', 'Create')) - evaluationsCategory.addPermission(new Permission('evaluations:update', 'Update')) - evaluationsCategory.addPermission(new Permission('evaluations:delete', 'Delete')) - evaluationsCategory.addPermission(new Permission('evaluations:run', 'Run Again')) + evaluationsCategory.addPermission(new Permission('evaluations:view', 'View', false, true, true)) + evaluationsCategory.addPermission(new Permission('evaluations:create', 'Create', false, true, true)) + evaluationsCategory.addPermission(new Permission('evaluations:update', 'Update', false, true, true)) + evaluationsCategory.addPermission(new Permission('evaluations:delete', 'Delete', false, true, true)) + evaluationsCategory.addPermission(new Permission('evaluations:run', 'Run Again', false, true, true)) this.categories.push(evaluationsCategory) const templatesCategory = new PermissionCategory('templates') - templatesCategory.addPermission(new Permission('templates:marketplace', 'View Marketplace Templates')) - templatesCategory.addPermission(new Permission('templates:custom', 'View Custom Templates')) - templatesCategory.addPermission(new Permission('templates:custom-delete', 'Delete Custom Template')) - templatesCategory.addPermission(new Permission('templates:toolexport', 'Export Tool as Template')) - templatesCategory.addPermission(new Permission('templates:flowexport', 'Export Flow as Template')) - templatesCategory.addPermission(new Permission('templates:custom-share', 'Share Custom Templates')) + templatesCategory.addPermission(new Permission('templates:marketplace', 'View Marketplace Templates', true, true, true)) + templatesCategory.addPermission(new Permission('templates:custom', 'View Custom Templates', true, true, true)) + templatesCategory.addPermission(new Permission('templates:custom-delete', 'Delete Custom Template', true, true, true)) + templatesCategory.addPermission(new Permission('templates:toolexport', 'Export Tool as Template', true, true, true)) + templatesCategory.addPermission(new Permission('templates:flowexport', 'Export Flow as Template', true, true, true)) + templatesCategory.addPermission(new Permission('templates:custom-share', 'Share Custom Templates', false, true, true)) this.categories.push(templatesCategory) const workspaceCategory = new PermissionCategory('workspace') - workspaceCategory.addPermission(new Permission('workspace:view', 'View')) - workspaceCategory.addPermission(new Permission('workspace:create', 'Create')) - workspaceCategory.addPermission(new Permission('workspace:update', 'Update')) - workspaceCategory.addPermission(new Permission('workspace:add-user', 'Add User')) - workspaceCategory.addPermission(new Permission('workspace:unlink-user', 'Remove User')) - workspaceCategory.addPermission(new Permission('workspace:delete', 'Delete')) - workspaceCategory.addPermission(new Permission('workspace:export', 'Export Data within Workspace')) - workspaceCategory.addPermission(new Permission('workspace:import', 'Import Data within Workspace')) + workspaceCategory.addPermission(new Permission('workspace:view', 'View', false, true, true)) + workspaceCategory.addPermission(new Permission('workspace:create', 'Create', false, true, true)) + workspaceCategory.addPermission(new Permission('workspace:update', 'Update', false, true, true)) + workspaceCategory.addPermission(new Permission('workspace:add-user', 'Add User', false, true, true)) + workspaceCategory.addPermission(new Permission('workspace:unlink-user', 'Remove User', false, true, true)) + workspaceCategory.addPermission(new Permission('workspace:delete', 'Delete', false, true, true)) + workspaceCategory.addPermission(new Permission('workspace:export', 'Export Data within Workspace', false, true, true)) + workspaceCategory.addPermission(new Permission('workspace:import', 'Import Data within Workspace', false, true, true)) this.categories.push(workspaceCategory) const adminCategory = new PermissionCategory('admin') - adminCategory.addPermission(new Permission('users:manage', 'Manage Users')) - adminCategory.addPermission(new Permission('roles:manage', 'Manage Roles')) - adminCategory.addPermission(new Permission('sso:manage', 'Manage SSO')) + adminCategory.addPermission(new Permission('users:manage', 'Manage Users', false, true, true)) + adminCategory.addPermission(new Permission('roles:manage', 'Manage Roles', false, true, true)) + adminCategory.addPermission(new Permission('sso:manage', 'Manage SSO', false, true, false)) this.categories.push(adminCategory) const logsCategory = new PermissionCategory('logs') - logsCategory.addPermission(new Permission('logs:view', 'View Logs', true)) + logsCategory.addPermission(new Permission('logs:view', 'View Logs', false, true, false)) this.categories.push(logsCategory) const loginActivityCategory = new PermissionCategory('loginActivity') - loginActivityCategory.addPermission(new Permission('loginActivity:view', 'View Login Activity', true)) - loginActivityCategory.addPermission(new Permission('loginActivity:delete', 'Delete Login Activity', true)) + loginActivityCategory.addPermission(new Permission('loginActivity:view', 'View Login Activity', false, true, false)) + loginActivityCategory.addPermission(new Permission('loginActivity:delete', 'Delete Login Activity', false, true, false)) this.categories.push(loginActivityCategory) } @@ -167,13 +169,21 @@ export class PermissionCategory { } export class Permission { - constructor(public name: string, public description: string, public isEnterprise: boolean = false) {} + constructor( + public name: string, + public description: string, + public isOpenSource: boolean = false, + public isEnterprise: boolean = false, + public isCloud: boolean = false + ) {} public toJSON() { return { key: this.name, value: this.description, - isEnterprise: this.isEnterprise + isOpenSource: this.isOpenSource, + isEnterprise: this.isEnterprise, + isCloud: this.isCloud } } } diff --git a/packages/ui/src/views/apikey/APIKeyDialog.jsx b/packages/ui/src/views/apikey/APIKeyDialog.jsx index f649db14938..c05078b20b4 100644 --- a/packages/ui/src/views/apikey/APIKeyDialog.jsx +++ b/packages/ui/src/views/apikey/APIKeyDialog.jsx @@ -1,28 +1,28 @@ -import { createPortal } from 'react-dom' +import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions' import PropTypes from 'prop-types' -import { useState, useEffect } from 'react' +import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { useDispatch } from 'react-redux' -import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' +import { StyledButton } from '@/ui-component/button/StyledButton' +import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' import { Box, - Typography, Button, Dialog, DialogActions, DialogContent, DialogTitle, - Stack, IconButton, OutlinedInput, - Popover + Popover, + Stack, + Typography } from '@mui/material' import { useTheme } from '@mui/material/styles' -import { StyledButton } from '@/ui-component/button/StyledButton' -import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' // Icons -import { IconX, IconCopy, IconKey } from '@tabler/icons-react' +import { IconCopy, IconKey, IconX } from '@tabler/icons-react' // API import apikeyApi from '@/api/apikey' @@ -30,6 +30,7 @@ import authApi from '@/api/auth' // Hooks import useApi from '@/hooks/useApi' +import { useConfig } from '@/store/context/ConfigContext' // utils import useNotifier from '@/utils/useNotifier' @@ -44,6 +45,7 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { const theme = useTheme() const dispatch = useDispatch() + const { isOpenSource, isEnterpriseLicensed, isCloud } = useConfig() // ==============================|| Snackbar ||============================== // @@ -89,6 +91,24 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { useEffect(() => { if (getAllPermissionsApi.data) { const permissionsData = getAllPermissionsApi.data + + // Filter permissions based on current platform + Object.keys(permissionsData).forEach((category) => { + permissionsData[category] = permissionsData[category].filter((permission) => { + if (isOpenSource) return permission.isOpenSource + if (isEnterpriseLicensed) return permission.isEnterprise + if (isCloud) return permission.isCloud + return false + }) + }) + + // Remove categories that have no permissions left + Object.keys(permissionsData).forEach((category) => { + if (permissionsData[category].length === 0) { + delete permissionsData[category] + } + }) + setPermissions(permissionsData) if (dialogProps.type === 'EDIT' && dialogProps.key) { diff --git a/packages/ui/src/views/roles/CreateEditRoleDialog.jsx b/packages/ui/src/views/roles/CreateEditRoleDialog.jsx index 9dcb900009a..a9760e78e89 100644 --- a/packages/ui/src/views/roles/CreateEditRoleDialog.jsx +++ b/packages/ui/src/views/roles/CreateEditRoleDialog.jsx @@ -1,18 +1,18 @@ -import { createPortal } from 'react-dom' +import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions' import PropTypes from 'prop-types' -import { useState, useEffect } from 'react' +import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { useDispatch, useSelector } from 'react-redux' -import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' // Material -import { Box, Typography, OutlinedInput, Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material' +import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, OutlinedInput, Typography } from '@mui/material' // Project imports import { StyledButton } from '@/ui-component/button/StyledButton' import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' // Icons -import { IconX, IconUser } from '@tabler/icons-react' +import { IconUser, IconX } from '@tabler/icons-react' // API import authApi from '@/api/auth' @@ -34,7 +34,7 @@ const CreateEditRoleDialog = ({ show, dialogProps, onCancel, onConfirm, setError const portalElement = document.getElementById('portal') const dispatch = useDispatch() - const { isEnterpriseLicensed } = useConfig() + const { isOpenSource, isEnterpriseLicensed, isCloud } = useConfig() // ==============================|| Snackbar ||============================== // @@ -158,18 +158,20 @@ const CreateEditRoleDialog = ({ show, dialogProps, onCancel, onConfirm, setError setRoleName(dialogData.name) setRoleDescription(dialogData.description) const permissions = getAllPermissionsApi.data - // Filter out enterprise permissions if not licensed - if (!isEnterpriseLicensed) { - Object.keys(permissions).forEach((category) => { - permissions[category] = permissions[category].filter((permission) => !permission.isEnterprise) - }) - // Remove categories that have no permissions left - Object.keys(permissions).forEach((category) => { - if (permissions[category].length === 0) { - delete permissions[category] - } + Object.keys(permissions).forEach((category) => { + permissions[category] = permissions[category].filter((permission) => { + if (isOpenSource) return permission.isOpenSource + if (isEnterpriseLicensed) return permission.isEnterprise + if (isCloud) return permission.isCloud + return false // fallback - show nothing if no platform is set }) - } + }) + // Remove categories that have no permissions left + Object.keys(permissions).forEach((category) => { + if (permissions[category].length === 0) { + delete permissions[category] + } + }) setPermissions(permissions) if ((dialogProps.type === 'EDIT' || dialogProps.type === 'VIEW') && dialogProps.data) { const dialogDataPermissions = JSON.parse(dialogData.permissions) From e135f4b315b02692fbb321cf7f83f8139c3ff416 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Wed, 17 Dec 2025 17:50:28 +0800 Subject: [PATCH 16/29] chore(migrations): lower API key permission value --- .../migrations/mariadb/1765360298674-AddApiKeyPermission.ts | 2 +- .../migrations/mysql/1765360298674-AddApiKeyPermission.ts | 2 +- .../migrations/postgres/1765360298674-AddApiKeyPermission.ts | 2 +- .../migrations/sqlite/1765360298674-AddApiKeyPermission.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts index 6e8271bd272..d03fad1681a 100644 --- a/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts @@ -11,7 +11,7 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { await queryRunner.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${columnName}\` TEXT NOT NULL DEFAULT ('[]');`) const permission = - '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","credentials:share","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","datasets:view","datasets:create","datasets:update","datasets:delete","executions:view","executions:delete","evaluators:view","evaluators:create","evaluators:update","evaluators:delete","evaluations:view","evaluations:create","evaluations:update","evaluations:delete","evaluations:run","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport","templates:custom-share","workspace:view","workspace:create","workspace:update","workspace:add-user","workspace:unlink-user","workspace:delete","workspace:export","workspace:import","users:manage","roles:manage",null,"admin:view"]' + '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]' await queryRunner.query(`UPDATE \`${tableName}\` SET \`${columnName}\` = '${permission}';`) } diff --git a/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts index 6e8271bd272..d03fad1681a 100644 --- a/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts @@ -11,7 +11,7 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { await queryRunner.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${columnName}\` TEXT NOT NULL DEFAULT ('[]');`) const permission = - '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","credentials:share","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","datasets:view","datasets:create","datasets:update","datasets:delete","executions:view","executions:delete","evaluators:view","evaluators:create","evaluators:update","evaluators:delete","evaluations:view","evaluations:create","evaluations:update","evaluations:delete","evaluations:run","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport","templates:custom-share","workspace:view","workspace:create","workspace:update","workspace:add-user","workspace:unlink-user","workspace:delete","workspace:export","workspace:import","users:manage","roles:manage",null,"admin:view"]' + '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]' await queryRunner.query(`UPDATE \`${tableName}\` SET \`${columnName}\` = '${permission}';`) } diff --git a/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts index 2f8c27d5793..bf43ad4a2bf 100644 --- a/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts @@ -11,7 +11,7 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" TEXT NOT NULL DEFAULT '[]';`) const permission = - '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","credentials:share","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","datasets:view","datasets:create","datasets:update","datasets:delete","executions:view","executions:delete","evaluators:view","evaluators:create","evaluators:update","evaluators:delete","evaluations:view","evaluations:create","evaluations:update","evaluations:delete","evaluations:run","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport","templates:custom-share","workspace:view","workspace:create","workspace:update","workspace:add-user","workspace:unlink-user","workspace:delete","workspace:export","workspace:import","users:manage","roles:manage",null,"admin:view"]' + '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]' await queryRunner.query(`UPDATE "${tableName}" SET "${columnName}" = '${permission}';`) } diff --git a/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts index 2f8c27d5793..bf43ad4a2bf 100644 --- a/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts @@ -11,7 +11,7 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" TEXT NOT NULL DEFAULT '[]';`) const permission = - '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","credentials:share","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","datasets:view","datasets:create","datasets:update","datasets:delete","executions:view","executions:delete","evaluators:view","evaluators:create","evaluators:update","evaluators:delete","evaluations:view","evaluations:create","evaluations:update","evaluations:delete","evaluations:run","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport","templates:custom-share","workspace:view","workspace:create","workspace:update","workspace:add-user","workspace:unlink-user","workspace:delete","workspace:export","workspace:import","users:manage","roles:manage",null,"admin:view"]' + '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]' await queryRunner.query(`UPDATE "${tableName}" SET "${columnName}" = '${permission}';`) } From 29dcb4c00a1a686ffffbf81205e01cae3df428dd Mon Sep 17 00:00:00 2001 From: yau-wd Date: Thu, 18 Dec 2025 16:39:07 +0800 Subject: [PATCH 17/29] Update packages/ui/src/views/apikey/APIKeyDialog.jsx Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/ui/src/views/apikey/APIKeyDialog.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/views/apikey/APIKeyDialog.jsx b/packages/ui/src/views/apikey/APIKeyDialog.jsx index c05078b20b4..49caca9f1d4 100644 --- a/packages/ui/src/views/apikey/APIKeyDialog.jsx +++ b/packages/ui/src/views/apikey/APIKeyDialog.jsx @@ -153,7 +153,7 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { } else { const viewPermissionKey = `${category}:view` if (key !== viewPermissionKey) { - const hasEnabledPermissions = Object.keys(updatedCategoryPermissions).some( + const hasEnabledPermissions = Object.entries(updatedCategoryPermissions).some( ([permissionKey, isEnabled]) => permissionKey !== viewPermissionKey && isEnabled ) if (hasEnabledPermissions) { From 51500b7488ada7d43570a8b653f6fea5173df3ca Mon Sep 17 00:00:00 2001 From: yau-wd Date: Thu, 18 Dec 2025 16:39:20 +0800 Subject: [PATCH 18/29] Update packages/ui/src/views/apikey/APIKeyDialog.jsx Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/ui/src/views/apikey/APIKeyDialog.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/views/apikey/APIKeyDialog.jsx b/packages/ui/src/views/apikey/APIKeyDialog.jsx index 49caca9f1d4..ce8a0ae5d1e 100644 --- a/packages/ui/src/views/apikey/APIKeyDialog.jsx +++ b/packages/ui/src/views/apikey/APIKeyDialog.jsx @@ -160,7 +160,7 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { updatedCategoryPermissions[viewPermissionKey] = true } } else { - const hasEnabledPermissions = Object.keys(updatedCategoryPermissions).some( + const hasEnabledPermissions = Object.entries(updatedCategoryPermissions).some( ([permissionKey, isEnabled]) => permissionKey === viewPermissionKey && isEnabled ) if (hasEnabledPermissions) { From 38ce74daf2064232b88a0f154ca7f32be8ce4ae3 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Mon, 22 Dec 2025 17:37:47 +0800 Subject: [PATCH 19/29] refactor: remove deprecated APIKEY_PATH and APIKEY migration --- docker/.env.example | 2 - docker/worker/.env.example | 2 - packages/server/.env.example | 2 - packages/server/src/index.ts | 52 ++++++------ packages/server/src/utils/apiKey.ts | 123 ---------------------------- 5 files changed, 24 insertions(+), 157 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index 2240edeb8a5..214481f2fac 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -1,7 +1,5 @@ PORT=3000 -# APIKEY_PATH=/your_apikey_path/.flowise # (will be deprecated by end of 2025) - ############################################################################################################ ############################################## DATABASE #################################################### ############################################################################################################ diff --git a/docker/worker/.env.example b/docker/worker/.env.example index 0e4b0c0dcf6..c873248084d 100644 --- a/docker/worker/.env.example +++ b/docker/worker/.env.example @@ -1,7 +1,5 @@ WORKER_PORT=5566 -# APIKEY_PATH=/your_apikey_path/.flowise # (will be deprecated by end of 2025) - ############################################################################################################ ############################################## DATABASE #################################################### ############################################################################################################ diff --git a/packages/server/.env.example b/packages/server/.env.example index 282e4cd33fc..cfac53ddac4 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -1,7 +1,5 @@ PORT=3000 -# APIKEY_PATH=/your_apikey_path/.flowise # (will be deprecated by end of 2025) - ############################################################################################################ ############################################## DATABASE #################################################### ############################################################################################################ diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index d956b4bf2b4..06db90e301e 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,39 +1,38 @@ -import express, { Request, Response } from 'express' -import path from 'path' +import { ExpressAdapter } from '@bull-board/express' +import cookieParser from 'cookie-parser' import cors from 'cors' +import express, { Request, Response } from 'express' +import 'global-agent/bootstrap' import http from 'http' -import cookieParser from 'cookie-parser' +import path from 'path' import { DataSource } from 'typeorm' -import { MODE, Platform } from './Interface' -import { getNodeModulesPackagePath, getEncryptionKey } from './utils' -import logger, { expressRequestLogger } from './utils/logger' -import { getDataSource } from './DataSource' -import { NodesPool } from './NodesPool' -import { ChatFlow } from './database/entities/ChatFlow' -import { CachePool } from './CachePool' import { AbortControllerPool } from './AbortControllerPool' -import { RateLimiterManager } from './utils/rateLimit' -import { getAllowedIframeOrigins, getCorsOptions, sanitizeMiddleware } from './utils/XSS' -import { Telemetry } from './utils/telemetry' -import flowiseApiV1Router from './routes' -import errorHandlerMiddleware from './middlewares/errors' -import { WHITELIST_URLS } from './utils/constants' +import { CachePool } from './CachePool' +import { ChatFlow } from './database/entities/ChatFlow' +import { getDataSource } from './DataSource' +import { Organization } from './enterprise/database/entities/organization.entity' +import { Workspace } from './enterprise/database/entities/workspace.entity' +import { LoggedInUser } from './enterprise/Interface.Enterprise' import { initializeJwtCookieMiddleware, verifyToken } from './enterprise/middleware/passport' import { IdentityManager } from './IdentityManager' -import { SSEStreamer } from './utils/SSEStreamer' -import { validateAPIKey } from './utils/validateKey' -import { LoggedInUser } from './enterprise/Interface.Enterprise' +import { MODE, Platform } from './Interface' import { IMetricsProvider } from './Interface.Metrics' -import { Prometheus } from './metrics/Prometheus' import { OpenTelemetry } from './metrics/OpenTelemetry' +import { Prometheus } from './metrics/Prometheus' +import errorHandlerMiddleware from './middlewares/errors' +import { NodesPool } from './NodesPool' import { QueueManager } from './queue/QueueManager' import { RedisEventSubscriber } from './queue/RedisEventSubscriber' -import 'global-agent/bootstrap' +import flowiseApiV1Router from './routes' import { UsageCacheManager } from './UsageCacheManager' -import { Workspace } from './enterprise/database/entities/workspace.entity' -import { Organization } from './enterprise/database/entities/organization.entity' -import { migrateApiKeysFromJsonToDb } from './utils/apiKey' -import { ExpressAdapter } from '@bull-board/express' +import { getEncryptionKey, getNodeModulesPackagePath } from './utils' +import { WHITELIST_URLS } from './utils/constants' +import logger, { expressRequestLogger } from './utils/logger' +import { RateLimiterManager } from './utils/rateLimit' +import { SSEStreamer } from './utils/SSEStreamer' +import { Telemetry } from './utils/telemetry' +import { validateAPIKey } from './utils/validateKey' +import { getAllowedIframeOrigins, getCorsOptions, sanitizeMiddleware } from './utils/XSS' declare global { namespace Express { @@ -147,9 +146,6 @@ export class App { logger.info('🔗 [server]: Redis event subscriber connected successfully') } - // TODO: Remove this by end of 2025 - await migrateApiKeysFromJsonToDb(this.AppDataSource, this.identityManager.getPlatformType()) - logger.info('🎉 [server]: All initialization steps completed successfully!') } catch (error) { logger.error('❌ [server]: Error during Data Source initialization:', error) diff --git a/packages/server/src/utils/apiKey.ts b/packages/server/src/utils/apiKey.ts index 9aa5daa9b52..0f9794363ef 100644 --- a/packages/server/src/utils/apiKey.ts +++ b/packages/server/src/utils/apiKey.ts @@ -1,22 +1,4 @@ import { randomBytes, scryptSync, timingSafeEqual } from 'crypto' -import { ICommonObject } from 'flowise-components' -import fs from 'fs' -import path from 'path' -import { DataSource } from 'typeorm' -import { ApiKey } from '../database/entities/ApiKey' -import { Workspace } from '../enterprise/database/entities/workspace.entity' -import { v4 as uuidv4 } from 'uuid' -import { ChatFlow } from '../database/entities/ChatFlow' -import { addChatflowsCount } from './addChatflowsCount' -import { Platform } from '../Interface' - -/** - * Returns the api key path - * @returns {string} - */ -export const getAPIKeyPath = (): string => { - return process.env.APIKEY_PATH ? path.join(process.env.APIKEY_PATH, 'api.json') : path.join(__dirname, '..', '..', 'api.json') -} /** * Generate the api key @@ -49,108 +31,3 @@ export const compareKeys = (storedKey: string, suppliedKey: string): boolean => const buffer = scryptSync(suppliedKey, salt, 64) as Buffer return timingSafeEqual(Buffer.from(hashedPassword, 'hex'), buffer) } - -/** - * Get API keys - * @returns {Promise} - */ -export const getAPIKeys = async (): Promise => { - try { - const content = await fs.promises.readFile(getAPIKeyPath(), 'utf8') - return JSON.parse(content) - } catch (error) { - return [] - } -} - -/** - * Get API Key details - * @param {string} apiKey - * @returns {Promise} - */ -export const getApiKey = async (apiKey: string) => { - const existingAPIKeys = await getAPIKeys() - const keyIndex = existingAPIKeys.findIndex((key) => key.apiKey === apiKey) - if (keyIndex < 0) return undefined - return existingAPIKeys[keyIndex] -} - -export const migrateApiKeysFromJsonToDb = async (appDataSource: DataSource, platformType: Platform) => { - if (platformType === Platform.CLOUD) { - return - } - - if (!process.env.APIKEY_STORAGE_TYPE || process.env.APIKEY_STORAGE_TYPE === 'json') { - const keys = await getAPIKeys() - if (keys.length > 0) { - try { - // Get all available workspaces - const workspaces = await appDataSource.getRepository(Workspace).find() - - for (const key of keys) { - const existingKey = await appDataSource.getRepository(ApiKey).findOneBy({ - apiKey: key.apiKey - }) - - // Only add if key doesn't already exist in DB - if (!existingKey) { - // Create a new API key for each workspace - if (workspaces.length > 0) { - for (const workspace of workspaces) { - const newKey = new ApiKey() - newKey.id = uuidv4() - newKey.apiKey = key.apiKey - newKey.apiSecret = key.apiSecret - newKey.keyName = key.keyName - newKey.workspaceId = workspace.id - - const keyEntity = appDataSource.getRepository(ApiKey).create(newKey) - await appDataSource.getRepository(ApiKey).save(keyEntity) - - const chatflows = await appDataSource.getRepository(ChatFlow).findBy({ - apikeyid: key.id, - workspaceId: workspace.id - }) - - for (const chatflow of chatflows) { - chatflow.apikeyid = newKey.id - await appDataSource.getRepository(ChatFlow).save(chatflow) - } - - await addChatflowsCount(chatflows) - } - } else { - // If no workspaces exist, create the key without a workspace ID and later will be updated by setNullWorkspaceId - const newKey = new ApiKey() - newKey.id = uuidv4() - newKey.apiKey = key.apiKey - newKey.apiSecret = key.apiSecret - newKey.keyName = key.keyName - - const keyEntity = appDataSource.getRepository(ApiKey).create(newKey) - await appDataSource.getRepository(ApiKey).save(keyEntity) - - const chatflows = await appDataSource.getRepository(ChatFlow).findBy({ - apikeyid: key.id - }) - - for (const chatflow of chatflows) { - chatflow.apikeyid = newKey.id - await appDataSource.getRepository(ChatFlow).save(chatflow) - } - - await addChatflowsCount(chatflows) - } - } - } - - // Delete the JSON file - if (fs.existsSync(getAPIKeyPath())) { - fs.unlinkSync(getAPIKeyPath()) - } - } catch (error) { - console.error('Error migrating API keys from JSON to DB', error) - } - } - } -} From 36cb00218228cb981c1516a8d325e9dd47941acf Mon Sep 17 00:00:00 2001 From: yau-wd Date: Tue, 23 Dec 2025 19:15:43 +0800 Subject: [PATCH 20/29] refactor(apikey): remove functionality related to import apikey --- .../server/src/controllers/apikey/index.ts | 20 +- .../1765360298674-AddApiKeyPermission.ts | 2 +- .../1765360298674-AddApiKeyPermission.ts | 2 +- .../1765360298674-AddApiKeyPermission.ts | 2 +- .../1765360298674-AddApiKeyPermission.ts | 2 +- .../server/src/enterprise/rbac/Permissions.ts | 1 - packages/server/src/routes/apikey/index.ts | 1 - packages/server/src/services/apikey/index.ts | 126 +----------- packages/ui/src/api/apikey.js | 5 +- .../src/views/apikey/UploadJSONFileDialog.jsx | 186 ------------------ packages/ui/src/views/apikey/index.jsx | 76 ++----- 11 files changed, 22 insertions(+), 401 deletions(-) delete mode 100644 packages/ui/src/views/apikey/UploadJSONFileDialog.jsx diff --git a/packages/server/src/controllers/apikey/index.ts b/packages/server/src/controllers/apikey/index.ts index 8f499e2b6e1..f7a18f97c1d 100644 --- a/packages/server/src/controllers/apikey/index.ts +++ b/packages/server/src/controllers/apikey/index.ts @@ -79,23 +79,6 @@ const updateApiKey = async (req: Request, res: Response, next: NextFunction) => } } -// Import Keys from JSON file -const importKeys = async (req: Request, res: Response, next: NextFunction) => { - try { - if (typeof req.body === 'undefined' || !req.body.jsonFile) { - throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.importKeys - body not provided!`) - } - if (!req.user?.activeWorkspaceId) { - throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Workspace ID is required`) - } - const user = req.user as LoggedInUser - const apiResponse = await apikeyService.importKeys(user.permissions, user.isOrganizationAdmin, req.body) - return res.json(apiResponse) - } catch (error) { - next(error) - } -} - // Delete api key const deleteApiKey = async (req: Request, res: Response, next: NextFunction) => { try { @@ -130,6 +113,5 @@ export default { deleteApiKey, getAllApiKeys, updateApiKey, - verifyApiKey, - importKeys + verifyApiKey } diff --git a/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts index d03fad1681a..ecbaa6ff951 100644 --- a/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts @@ -11,7 +11,7 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { await queryRunner.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${columnName}\` TEXT NOT NULL DEFAULT ('[]');`) const permission = - '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]' + '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]' await queryRunner.query(`UPDATE \`${tableName}\` SET \`${columnName}\` = '${permission}';`) } diff --git a/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts index d03fad1681a..ecbaa6ff951 100644 --- a/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts @@ -11,7 +11,7 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { await queryRunner.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${columnName}\` TEXT NOT NULL DEFAULT ('[]');`) const permission = - '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]' + '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]' await queryRunner.query(`UPDATE \`${tableName}\` SET \`${columnName}\` = '${permission}';`) } diff --git a/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts index bf43ad4a2bf..56cb9aeca05 100644 --- a/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts @@ -11,7 +11,7 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" TEXT NOT NULL DEFAULT '[]';`) const permission = - '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]' + '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]' await queryRunner.query(`UPDATE "${tableName}" SET "${columnName}" = '${permission}';`) } diff --git a/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts index bf43ad4a2bf..56cb9aeca05 100644 --- a/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts @@ -11,7 +11,7 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" TEXT NOT NULL DEFAULT '[]';`) const permission = - '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","apikeys:import","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]' + '["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]' await queryRunner.query(`UPDATE "${tableName}" SET "${columnName}" = '${permission}';`) } diff --git a/packages/server/src/enterprise/rbac/Permissions.ts b/packages/server/src/enterprise/rbac/Permissions.ts index abcc4742adc..82369c4a271 100644 --- a/packages/server/src/enterprise/rbac/Permissions.ts +++ b/packages/server/src/enterprise/rbac/Permissions.ts @@ -64,7 +64,6 @@ export class Permissions { apikeysCategory.addPermission(new Permission('apikeys:create', 'Create', true, true, true)) apikeysCategory.addPermission(new Permission('apikeys:update', 'Update', true, true, true)) apikeysCategory.addPermission(new Permission('apikeys:delete', 'Delete', true, true, true)) - apikeysCategory.addPermission(new Permission('apikeys:import', 'Import', true, true, true)) this.categories.push(apikeysCategory) const documentStoresCategory = new PermissionCategory('documentStores') diff --git a/packages/server/src/routes/apikey/index.ts b/packages/server/src/routes/apikey/index.ts index ec9f1a2c9e5..339a82dfd20 100644 --- a/packages/server/src/routes/apikey/index.ts +++ b/packages/server/src/routes/apikey/index.ts @@ -5,7 +5,6 @@ const router = express.Router() // CREATE router.post('/', checkPermission('apikeys:create'), apikeyController.createApiKey) -router.post('/import', checkPermission('apikeys:import'), apikeyController.importKeys) // READ router.get('/', checkPermission('apikeys:view'), apikeyController.getAllApiKeys) diff --git a/packages/server/src/services/apikey/index.ts b/packages/server/src/services/apikey/index.ts index 7a9db44d497..3ffe8d4c0e2 100644 --- a/packages/server/src/services/apikey/index.ts +++ b/packages/server/src/services/apikey/index.ts @@ -1,8 +1,6 @@ import { StatusCodes } from 'http-status-codes' -import { IsNull, Not } from 'typeorm' import { v4 as uuidv4 } from 'uuid' import { ApiKey } from '../../database/entities/ApiKey' -import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getErrorMessage } from '../../errors/utils' import { addChatflowsCount } from '../../utils/addChatflowsCount' @@ -184,127 +182,6 @@ const deleteApiKey = async (id: string, workspaceId: string) => { } } -const importKeys = async (userPermissions: string[], isOrganizationAdmin: boolean, body: any) => { - try { - const jsonFile = body.jsonFile - const workspaceId = body.workspaceId - const splitDataURI = jsonFile.split(',') - if (splitDataURI[0] !== 'data:application/json;base64') { - throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Invalid dataURI`) - } - const bf = Buffer.from(splitDataURI[1] || '', 'base64') - const plain = bf.toString('utf8') - const keys = JSON.parse(plain) - - // Validate schema of imported keys - if (!Array.isArray(keys)) { - throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, `Invalid format: Expected an array of API keys`) - } - - const requiredFields = ['keyName', 'apiKey', 'apiSecret', 'createdAt', 'id'] - const optionalFields = ['permissions'] - for (let i = 0; i < keys.length; i++) { - const key = keys[i] - if (typeof key !== 'object' || key === null) { - throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, `Invalid format: Key at index ${i} is not an object`) - } - - for (const field of requiredFields) { - if (!(field in key)) { - throw new InternalFlowiseError( - StatusCodes.BAD_REQUEST, - `Invalid format: Key at index ${i} is missing required field '${field}'` - ) - } - if (typeof key[field] !== 'string') { - throw new InternalFlowiseError( - StatusCodes.BAD_REQUEST, - `Invalid format: Key at index ${i} field '${field}' must be a string` - ) - } - if (key[field].trim() === '') { - throw new InternalFlowiseError( - StatusCodes.BAD_REQUEST, - `Invalid format: Key at index ${i} field '${field}' cannot be empty` - ) - } - } - - // Validate optional fields if present - for (const field of optionalFields) { - if (field in key && typeof key[field] !== 'string') { - throw new InternalFlowiseError( - StatusCodes.BAD_REQUEST, - `Invalid format: Key at index ${i} field '${field}' must be a string` - ) - } - } - } - - const appServer = getRunningExpressApp() - const allApiKeys = await appServer.AppDataSource.getRepository(ApiKey).findBy(getWorkspaceSearchOptions(workspaceId)) - if (body.importMode === 'replaceAll') { - await appServer.AppDataSource.getRepository(ApiKey).delete({ - id: Not(IsNull()), - workspaceId: workspaceId - }) - } - if (body.importMode === 'errorIfExist') { - // if importMode is errorIfExist, check for existing keys and raise error before any modification to the DB - for (const key of keys) { - const keyNameExists = allApiKeys.find((k) => k.keyName === key.keyName) - if (keyNameExists) { - throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Key with name ${key.keyName} already exists`) - } - } - } - // iterate through the keys and add them to the database - for (const key of keys) { - const keyNameExists = allApiKeys.find((k) => k.keyName === key.keyName) - if (keyNameExists) { - const keyIndex = allApiKeys.findIndex((k) => k.keyName === key.keyName) - switch (body.importMode) { - case 'overwriteIfExist': - case 'replaceAll': { - const currentKey = allApiKeys[keyIndex] - currentKey.id = uuidv4() - currentKey.apiKey = key.apiKey - currentKey.apiSecret = key.apiSecret - currentKey.permissions = key.permissions || '[]' - currentKey.workspaceId = workspaceId - await appServer.AppDataSource.getRepository(ApiKey).save(currentKey) - break - } - case 'ignoreIfExist': { - // ignore this key and continue - continue - } - case 'errorIfExist': { - // should not reach here as we have already checked for existing keys - throw new Error(`Key with name ${key.keyName} already exists`) - } - default: { - throw new Error(`Unknown overwrite option ${body.importMode}`) - } - } - } else { - const newKey = new ApiKey() - newKey.id = uuidv4() - newKey.apiKey = key.apiKey - newKey.apiSecret = key.apiSecret - newKey.keyName = key.keyName - newKey.permissions = key.permissions || '[]' - newKey.workspaceId = workspaceId - const newKeyEntity = appServer.AppDataSource.getRepository(ApiKey).create(newKey) - await appServer.AppDataSource.getRepository(ApiKey).save(newKeyEntity) - } - } - return await getAllApiKeys(userPermissions, isOrganizationAdmin, workspaceId) - } catch (error) { - throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.importKeys - ${getErrorMessage(error)}`) - } -} - const verifyApiKey = async (paramApiKey: string): Promise => { try { const appServer = getRunningExpressApp() @@ -334,6 +211,5 @@ export default { updateApiKey, verifyApiKey, getApiKey, - getApiKeyById, - importKeys + getApiKeyById } diff --git a/packages/ui/src/api/apikey.js b/packages/ui/src/api/apikey.js index a8483e43d7d..cab8722e2e2 100644 --- a/packages/ui/src/api/apikey.js +++ b/packages/ui/src/api/apikey.js @@ -8,12 +8,9 @@ const updateAPI = (id, body) => client.put(`/apikey/${id}`, body) const deleteAPI = (id) => client.delete(`/apikey/${id}`) -const importAPI = (body) => client.post(`/apikey/import`, body) - export default { getAllAPIKeys, createNewAPI, updateAPI, - deleteAPI, - importAPI + deleteAPI } diff --git a/packages/ui/src/views/apikey/UploadJSONFileDialog.jsx b/packages/ui/src/views/apikey/UploadJSONFileDialog.jsx deleted file mode 100644 index 4d39d895a43..00000000000 --- a/packages/ui/src/views/apikey/UploadJSONFileDialog.jsx +++ /dev/null @@ -1,186 +0,0 @@ -import { createPortal } from 'react-dom' -import PropTypes from 'prop-types' -import { useState, useEffect } from 'react' -import { useDispatch } from 'react-redux' -import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' - -// Material -import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Box, Typography, Stack } from '@mui/material' - -// Project imports -import { StyledButton } from '@/ui-component/button/StyledButton' -import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' -import { File } from '@/ui-component/file/File' - -// Icons -import { IconFileUpload, IconX } from '@tabler/icons-react' - -// API -import apikeyAPI from '@/api/apikey' - -// utils -import useNotifier from '@/utils/useNotifier' - -// const -import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions' -import { Dropdown } from '@/ui-component/dropdown/Dropdown' - -const importModes = [ - { - label: 'Add & Overwrite', - name: 'overwriteIfExist', - description: 'Add keys and overwrite existing keys with the same name' - }, - { - label: 'Add & Ignore', - name: 'ignoreIfExist', - description: 'Add keys and ignore existing keys with the same name' - }, - { - label: 'Add & Verify', - name: 'errorIfExist', - description: 'Add Keys and throw error if key with same name exists' - }, - { - label: 'Replace All', - name: 'replaceAll', - description: 'Replace all keys with the imported keys' - } -] - -const UploadJSONFileDialog = ({ show, dialogProps, onCancel, onConfirm }) => { - const portalElement = document.getElementById('portal') - - const dispatch = useDispatch() - - // ==============================|| Snackbar ||============================== // - - useNotifier() - - const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) - const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) - - const [selectedFile, setSelectedFile] = useState() - const [importMode, setImportMode] = useState('overwrite') - - useEffect(() => { - return () => { - setSelectedFile() - } - }, [dialogProps]) - - useEffect(() => { - if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) - else dispatch({ type: HIDE_CANVAS_DIALOG }) - return () => dispatch({ type: HIDE_CANVAS_DIALOG }) - }, [show, dispatch]) - - const importKeys = async () => { - try { - const obj = { - importMode: importMode, - jsonFile: selectedFile - } - const createResp = await apikeyAPI.importAPI(obj) - if (createResp.data) { - enqueueSnackbar({ - message: 'Imported keys successfully!', - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ) - } - }) - onConfirm(createResp.data.id) - } - } catch (error) { - enqueueSnackbar({ - message: `Failed to import keys: ${ - typeof error.response.data === 'object' ? error.response.data.message : error.response.data - }`, - options: { - key: new Date().getTime() + Math.random(), - variant: 'error', - persist: true, - action: (key) => ( - - ) - } - }) - onCancel() - } - } - - const component = show ? ( - - -
- - Import API Keys -
-
- - - - - Import api.json file -  * - - - setSelectedFile(newValue)} - value={selectedFile ?? 'Choose a file to upload'} - /> - - - - - Import Mode -  * - - - setImportMode(newValue)} - value={importMode ?? 'choose an option'} - /> - - - - - - {dialogProps.confirmButtonName} - - - -
- ) : null - - return createPortal(component, portalElement) -} - -UploadJSONFileDialog.propTypes = { - show: PropTypes.bool, - dialogProps: PropTypes.object, - onCancel: PropTypes.func, - onConfirm: PropTypes.func -} - -export default UploadJSONFileDialog diff --git a/packages/ui/src/views/apikey/index.jsx b/packages/ui/src/views/apikey/index.jsx index 1e13be92fce..2a94fb624ab 100644 --- a/packages/ui/src/views/apikey/index.jsx +++ b/packages/ui/src/views/apikey/index.jsx @@ -1,15 +1,18 @@ -import * as PropTypes from 'prop-types' +import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions' import moment from 'moment/moment' -import { useEffect, useState } from 'react' +import * as PropTypes from 'prop-types' +import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' -import React from 'react' // material-ui import { - Button, Box, + Button, Chip, + Collapse, + IconButton, + Paper, + Popover, Skeleton, Stack, Table, @@ -17,25 +20,20 @@ import { TableContainer, TableHead, TableRow, - Paper, - IconButton, - Popover, - Collapse, Typography } from '@mui/material' import TableCell, { tableCellClasses } from '@mui/material/TableCell' -import { useTheme, styled } from '@mui/material/styles' +import { styled, useTheme } from '@mui/material/styles' // project imports +import ErrorBoundary from '@/ErrorBoundary' +import ViewHeader from '@/layout/MainLayout/ViewHeader' +import { StyledPermissionButton } from '@/ui-component/button/RBACButtons' import MainCard from '@/ui-component/cards/MainCard' -import APIKeyDialog from './APIKeyDialog' import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' -import ViewHeader from '@/layout/MainLayout/ViewHeader' -import ErrorBoundary from '@/ErrorBoundary' -import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons' -import { Available } from '@/ui-component/rbac/available' -import UploadJSONFileDialog from '@/views/apikey/UploadJSONFileDialog' import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination' +import { Available } from '@/ui-component/rbac/available' +import APIKeyDialog from './APIKeyDialog' // API import apiKeyApi from '@/api/apikey' @@ -49,19 +47,8 @@ import useConfirm from '@/hooks/useConfirm' import useNotifier from '@/utils/useNotifier' // Icons -import { - IconTrash, - IconEdit, - IconCopy, - IconChevronsUp, - IconChevronsDown, - IconX, - IconPlus, - IconEye, - IconEyeOff, - IconFileUpload -} from '@tabler/icons-react' import APIEmptySVG from '@/assets/images/api_empty.svg' +import { IconChevronsDown, IconChevronsUp, IconCopy, IconEdit, IconEye, IconEyeOff, IconPlus, IconTrash, IconX } from '@tabler/icons-react' // ==============================|| APIKey ||============================== // @@ -243,9 +230,6 @@ const APIKey = () => { const [showApiKeys, setShowApiKeys] = useState([]) const openPopOver = Boolean(anchorEl) - const [showUploadDialog, setShowUploadDialog] = useState(false) - const [uploadDialogProps, setUploadDialogProps] = useState({}) - const [search, setSearch] = useState('') /* Table Pagination */ @@ -320,17 +304,6 @@ const APIKey = () => { setShowDialog(true) } - const uploadDialog = () => { - const dialogProp = { - type: 'ADD', - cancelButtonName: 'Cancel', - confirmButtonName: 'Upload', - data: {} - } - setUploadDialogProps(dialogProp) - setShowUploadDialog(true) - } - const deleteKey = async (key) => { const confirmPayload = { title: `Delete`, @@ -385,7 +358,6 @@ const APIKey = () => { const onConfirm = () => { setShowDialog(false) - setShowUploadDialog(false) refresh(currentPage, pageLimit) } @@ -419,16 +391,6 @@ const APIKey = () => { title='API Keys' description='Flowise API & SDK authentication keys' > - } - id='btn_importApiKeys' - > - Import - { onConfirm={onConfirm} setError={setError} > - {showUploadDialog && ( - setShowUploadDialog(false)} - onConfirm={onConfirm} - > - )} ) From 2163e5be28dc9e253b81a466591f23a0269ce611 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Tue, 23 Dec 2025 22:42:07 +0800 Subject: [PATCH 21/29] feat(permission): filter out workspace and admin categories for non-ROLE --- .../src/enterprise/controllers/auth/index.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/server/src/enterprise/controllers/auth/index.ts b/packages/server/src/enterprise/controllers/auth/index.ts index 4f0478d64ff..bf1c742e99b 100644 --- a/packages/server/src/enterprise/controllers/auth/index.ts +++ b/packages/server/src/enterprise/controllers/auth/index.ts @@ -1,4 +1,5 @@ import { NextFunction, Request, Response } from 'express' +import { StatusCodes } from 'http-status-codes' import { Platform } from '../../../Interface' import { getRunningExpressApp } from '../../../utils/getRunningExpressApp' import { LoggedInUser } from '../../Interface.Enterprise' @@ -23,6 +24,21 @@ const getAllPermissions = async (req: Request, res: Response, next: NextFunction 'feat:workspaces': ['workspace:'] } + // Category filtering for non-ROLE type + if (type !== 'ROLE') { + const filteredPermissions: { [key: string]: { key: string; value: string }[] } = {} + + for (const [category, categoryPermissions] of Object.entries(allPermissions)) { + // Exclude workspace and admin categories + if (category !== 'workspace' && category !== 'admin') { + filteredPermissions[category] = categoryPermissions + } + } + + permissions = filteredPermissions + } + + // Feature-based filtering for Cloud platform if (type !== 'ROLE' && appServer.identityManager.getPlatformType() === Platform.CLOUD) { const userFeatures = user.features if (userFeatures) { @@ -40,7 +56,7 @@ const getAllPermissions = async (req: Request, res: Response, next: NextFunction // Filter out permissions based on disabled features const filteredPermissions: { [key: string]: { key: string; value: string }[] } = {} - for (const [category, categoryPermissions] of Object.entries(allPermissions)) { + for (const [category, categoryPermissions] of Object.entries(permissions)) { const filteredCategoryPermissions = (categoryPermissions as any[]).filter((permission) => { // Check if this permission starts with any disabled prefix const isDisabled = disabledPermissionPrefixes.some((prefix) => permission.key.startsWith(prefix)) @@ -57,6 +73,7 @@ const getAllPermissions = async (req: Request, res: Response, next: NextFunction } } + // User-level filtering for non-admin users if (type !== 'ROLE' && user.isOrganizationAdmin === false) { const userPermissions = user.permissions as string[] const filteredPermissions: { [key: string]: { key: string; value: string }[] } = {} @@ -74,7 +91,7 @@ const getAllPermissions = async (req: Request, res: Response, next: NextFunction permissions = filteredPermissions } - return res.json(permissions) + return res.status(StatusCodes.OK).json(permissions) } catch (error) { next(error) } From d01c9e9ce7fd3dbf2828fe25b9a0714e2bac638a Mon Sep 17 00:00:00 2001 From: yau-wd Date: Fri, 26 Dec 2025 20:43:11 +0800 Subject: [PATCH 22/29] feat(services/apikey): validate API key permissions during create and update --- .../server/src/controllers/apikey/index.ts | 25 +--- packages/server/src/services/apikey/index.ts | 118 ++++++++++++------ 2 files changed, 83 insertions(+), 60 deletions(-) diff --git a/packages/server/src/controllers/apikey/index.ts b/packages/server/src/controllers/apikey/index.ts index f7a18f97c1d..84552174e11 100644 --- a/packages/server/src/controllers/apikey/index.ts +++ b/packages/server/src/controllers/apikey/index.ts @@ -11,13 +11,7 @@ const getAllApiKeys = async (req: Request, res: Response, next: NextFunction) => const user = req.user as LoggedInUser const { page, limit } = getPageAndLimitParams(req) - const apiResponse = await apikeyService.getAllApiKeys( - user.permissions, - user.isOrganizationAdmin, - user.activeWorkspaceId, - page, - limit - ) + const apiResponse = await apikeyService.getAllApiKeys(user, page, limit) return res.json(apiResponse) } catch (error) { next(error) @@ -36,13 +30,7 @@ const createApiKey = async (req: Request, res: Response, next: NextFunction) => ) } const user = req.user as LoggedInUser - const apiResponse = await apikeyService.createApiKey( - user.permissions, - user.isOrganizationAdmin, - user.activeWorkspaceId, - req.body.keyName, - req.body.permissions - ) + const apiResponse = await apikeyService.createApiKey(user, req.body.keyName, req.body.permissions) return res.json(apiResponse) } catch (error) { next(error) @@ -65,14 +53,7 @@ const updateApiKey = async (req: Request, res: Response, next: NextFunction) => ) } const user = req.user as LoggedInUser - const apiResponse = await apikeyService.updateApiKey( - user.permissions, - user.isOrganizationAdmin, - user.activeWorkspaceId, - req.params.id, - req.body.keyName, - req.body.permissions - ) + const apiResponse = await apikeyService.updateApiKey(user, req.params.id, req.body.keyName, req.body.permissions) return res.json(apiResponse) } catch (error) { next(error) diff --git a/packages/server/src/services/apikey/index.ts b/packages/server/src/services/apikey/index.ts index 3ffe8d4c0e2..2ba43d9776d 100644 --- a/packages/server/src/services/apikey/index.ts +++ b/packages/server/src/services/apikey/index.ts @@ -1,27 +1,88 @@ import { StatusCodes } from 'http-status-codes' import { v4 as uuidv4 } from 'uuid' import { ApiKey } from '../../database/entities/ApiKey' +import { LoggedInUser } from '../../enterprise/Interface.Enterprise' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getErrorMessage } from '../../errors/utils' +import { Platform } from '../../Interface' import { addChatflowsCount } from '../../utils/addChatflowsCount' import { generateAPIKey, generateSecretHash } from '../../utils/apiKey' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import logger from '../../utils/logger' /** - * Validates that requested permissions do not exceed user's own permissions - * @param userPermissions - Array of permissions the user has - * @param isOrganizationAdmin - Whether the user is an organization admin + * Validates that requested permissions are allowed for API keys + * @param user - The logged-in user * @param permissions - JSON string of requested permissions * @param operation - The operation being performed (for error message) * @throws InternalFlowiseError if validation fails */ -function validatePermissions(userPermissions: string[], isOrganizationAdmin: boolean, permissions: string, operation: string) { - if (!isOrganizationAdmin) { - const requestedPermissions = JSON.parse(permissions) +function validatePermissions(user: LoggedInUser, permissions: string, operation: string) { + const requestedPermissions = JSON.parse(permissions) + + // API Keys should not have workspace or admin permissions + // This applies to ALL users, including admins (platform constraint) + const hasRestrictedPermissions = requestedPermissions.some((permission: string) => { + if (permission === null) return false + return permission.startsWith('workspace:') || permission.startsWith('admin:') + }) + + if (hasRestrictedPermissions) { + throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, `Cannot ${operation} API key with workspace or admin permissions`) + } + + // For Cloud platform, check feature-gated permissions + // This also applies to ALL users, including admins (platform constraint) + const appServer = getRunningExpressApp() + if (appServer.identityManager.getPlatformType() === Platform.CLOUD) { + if (!user.features) { + // On Cloud platform, user features should always exist + // Log the anomaly with context for debugging + logger.error( + `[server]: Missing user features on Cloud platform for ${operation} API key. ` + + `User: ${user.email || user.id}, ` + + `Organization: ${user.activeOrganizationId || 'unknown'}, ` + + `Subscription: ${user.activeOrganizationSubscriptionId || 'unknown'}, ` + + `Customer: ${user.activeOrganizationCustomerId || 'unknown'}, ` + + `Workspace: ${user.activeWorkspaceId || 'unknown'}` + ) + throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Unable to validate permissions: user features not available`) + } + + const featureToPermissionMap: { [key: string]: string[] } = { + 'feat:login-activity': ['loginActivity:'], + 'feat:logs': ['logs:'], + 'feat:roles': ['roles:'], + 'feat:share': ['credentials:share', 'templates:custom-share'], + 'feat:sso-config': ['sso:'], + 'feat:users': ['users:'], + 'feat:workspaces': ['workspace:'] + } + + const disabledFeatures = Object.entries(user.features).filter(([, value]) => value === 'false') + const disabledPermissionPrefixes: string[] = [] + disabledFeatures.forEach(([featureKey]) => { + const prefixes = featureToPermissionMap[featureKey] + if (prefixes) { + disabledPermissionPrefixes.push(...prefixes) + } + }) + + const hasDisabledFeaturePermissions = requestedPermissions.some((permission: string) => { + if (permission === null) return false + return disabledPermissionPrefixes.some((prefix) => permission.startsWith(prefix)) + }) + + if (hasDisabledFeaturePermissions) { + throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, `Cannot ${operation} API key with permissions for disabled features`) + } + } + + // User permission validation - only applies to non-admins (authorization check) + if (!user.isOrganizationAdmin) { // Check if all requested permissions are included in user permissions const hasInvalidPermissions = requestedPermissions.some( - (permission: string) => permission !== null && !userPermissions.includes(permission) + (permission: string) => permission !== null && !user.permissions.includes(permission) ) if (hasInvalidPermissions) { throw new InternalFlowiseError( @@ -36,13 +97,7 @@ function validatePermissions(userPermissions: string[], isOrganizationAdmin: boo * Get all API keys for a workspace * Non-admin users can only view API keys whose permissions are a subset of their own permissions */ -const getAllApiKeys = async ( - userPermissions: string[], - isOrganizationAdmin: boolean, - workspaceId: string, - page: number = -1, - limit: number = -1 -) => { +const getAllApiKeys = async (user: LoggedInUser, page: number = -1, limit: number = -1) => { try { const appServer = getRunningExpressApp() const queryBuilder = appServer.AppDataSource.getRepository(ApiKey) @@ -52,18 +107,18 @@ const getAllApiKeys = async ( queryBuilder.skip((page - 1) * limit) queryBuilder.take(limit) } - queryBuilder.andWhere('api_key.workspaceId = :workspaceId', { workspaceId }) + queryBuilder.andWhere('api_key.workspaceId = :workspaceId', { workspaceId: user.activeWorkspaceId }) const allKeys = await queryBuilder.getMany() // Filter keys based on user permissions let filteredKeys = allKeys - if (!isOrganizationAdmin) { + if (!user.isOrganizationAdmin) { // Non-admin users can only see API keys whose permissions are a subset of their own filteredKeys = allKeys.filter((key) => { try { const keyPermissions = JSON.parse(key.permissions) // Check if all key permissions are included in user permissions - return keyPermissions.every((permission: string) => permission === null || userPermissions.includes(permission)) + return keyPermissions.every((permission: string) => permission === null || user.permissions.includes(permission)) } catch (error) { // Log parsing errors to help with debugging malformed permissions logger.error( @@ -118,15 +173,9 @@ const getApiKeyById = async (apiKeyId: string) => { } } -const createApiKey = async ( - userPermissions: string[], - isOrganizationAdmin: boolean, - workspaceId: string, - keyName: string, - permissions: string -) => { +const createApiKey = async (user: LoggedInUser, keyName: string, permissions: string) => { // Validate permissions before creating the key - validatePermissions(userPermissions, isOrganizationAdmin, permissions, 'create') + validatePermissions(user, permissions, 'create') const apiKey = generateAPIKey() const apiSecret = generateSecretHash(apiKey) @@ -137,28 +186,21 @@ const createApiKey = async ( newKey.apiSecret = apiSecret newKey.keyName = keyName newKey.permissions = permissions - newKey.workspaceId = workspaceId + newKey.workspaceId = user.activeWorkspaceId const key = appServer.AppDataSource.getRepository(ApiKey).create(newKey) await appServer.AppDataSource.getRepository(ApiKey).save(key) - return await getAllApiKeys(userPermissions, isOrganizationAdmin, workspaceId) + return await getAllApiKeys(user) } // Update api key -const updateApiKey = async ( - userPermissions: string[], - isOrganizationAdmin: boolean, - workspaceId: string, - id: string, - keyName: string, - permissions: string -) => { +const updateApiKey = async (user: LoggedInUser, id: string, keyName: string, permissions: string) => { // Validate permissions before updating the key - validatePermissions(userPermissions, isOrganizationAdmin, permissions, 'update') + validatePermissions(user, permissions, 'update') const appServer = getRunningExpressApp() const currentKey = await appServer.AppDataSource.getRepository(ApiKey).findOneBy({ id: id, - workspaceId: workspaceId + workspaceId: user.activeWorkspaceId }) if (!currentKey) { throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `ApiKey ${currentKey} not found`) @@ -166,7 +208,7 @@ const updateApiKey = async ( currentKey.keyName = keyName currentKey.permissions = permissions await appServer.AppDataSource.getRepository(ApiKey).save(currentKey) - return await getAllApiKeys(userPermissions, isOrganizationAdmin, workspaceId) + return await getAllApiKeys(user) } const deleteApiKey = async (id: string, workspaceId: string) => { From 6b04658bab8ee20a8a7999416194f7991526a59a Mon Sep 17 00:00:00 2001 From: yau-wd Date: Mon, 29 Dec 2025 17:53:58 +0800 Subject: [PATCH 23/29] chore(migrations): clean up role permissions values --- .../1765360298674-AddApiKeyPermission.ts | 17 +++++++++++++++++ .../mysql/1765360298674-AddApiKeyPermission.ts | 17 +++++++++++++++++ .../1765360298674-AddApiKeyPermission.ts | 15 +++++++++++++++ .../sqlite/1765360298674-AddApiKeyPermission.ts | 15 +++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts index ecbaa6ff951..93a42dc892c 100644 --- a/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts @@ -1,4 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm' +import { Role } from '../../../enterprise/database/entities/role.entity' import { hasColumn } from '../../../utils/database.util' export class AddApiKeyPermission1765360298674 implements MigrationInterface { @@ -15,6 +16,22 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { await queryRunner.query(`UPDATE \`${tableName}\` SET \`${columnName}\` = '${permission}';`) } + + const sso = 'sso:manage' + const apikey = 'apikeys:import' + const itemsToRemove = [sso, apikey] + const roles: Role[] = await queryRunner.query( + `SELECT * FROM \`role\` WHERE \`${columnName}\` LIKE '%${sso}%' OR \`${columnName}\` LIKE '%${apikey}%';` + ) + if (roles.length > 0) { + for (const role of roles) { + let permissions = JSON.parse(role.permissions) + permissions = permissions.filter((permission: string) => !itemsToRemove.includes(permission)) + await queryRunner.query( + `UPDATE \`role\` SET \`${columnName}\` = '${JSON.stringify(permissions)}' WHERE \`id\` = '${role.id}';` + ) + } + } } public async down(): Promise {} diff --git a/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts index ecbaa6ff951..93a42dc892c 100644 --- a/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts @@ -1,4 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm' +import { Role } from '../../../enterprise/database/entities/role.entity' import { hasColumn } from '../../../utils/database.util' export class AddApiKeyPermission1765360298674 implements MigrationInterface { @@ -15,6 +16,22 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { await queryRunner.query(`UPDATE \`${tableName}\` SET \`${columnName}\` = '${permission}';`) } + + const sso = 'sso:manage' + const apikey = 'apikeys:import' + const itemsToRemove = [sso, apikey] + const roles: Role[] = await queryRunner.query( + `SELECT * FROM \`role\` WHERE \`${columnName}\` LIKE '%${sso}%' OR \`${columnName}\` LIKE '%${apikey}%';` + ) + if (roles.length > 0) { + for (const role of roles) { + let permissions = JSON.parse(role.permissions) + permissions = permissions.filter((permission: string) => !itemsToRemove.includes(permission)) + await queryRunner.query( + `UPDATE \`role\` SET \`${columnName}\` = '${JSON.stringify(permissions)}' WHERE \`id\` = '${role.id}';` + ) + } + } } public async down(): Promise {} diff --git a/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts index 56cb9aeca05..680c461b494 100644 --- a/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts @@ -1,4 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm' +import { Role } from '../../../enterprise/database/entities/role.entity' import { hasColumn } from '../../../utils/database.util' export class AddApiKeyPermission1765360298674 implements MigrationInterface { @@ -15,6 +16,20 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { await queryRunner.query(`UPDATE "${tableName}" SET "${columnName}" = '${permission}';`) } + + const sso = 'sso:manage' + const apikey = 'apikeys:import' + const itemsToRemove = [sso, apikey] + const roles: Role[] = await queryRunner.query( + `SELECT * FROM "role" WHERE "${columnName}" LIKE '%${sso}%' OR "${columnName}" LIKE '%${apikey}%';` + ) + if (roles.length > 0) { + for (const role of roles) { + let permissions = JSON.parse(role.permissions) + permissions = permissions.filter((permission: string) => !itemsToRemove.includes(permission)) + await queryRunner.query(`UPDATE "role" SET "${columnName}" = '${JSON.stringify(permissions)}' WHERE "id" = '${role.id}';`) + } + } } public async down(): Promise {} diff --git a/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts index 56cb9aeca05..680c461b494 100644 --- a/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts @@ -1,4 +1,5 @@ import { MigrationInterface, QueryRunner } from 'typeorm' +import { Role } from '../../../enterprise/database/entities/role.entity' import { hasColumn } from '../../../utils/database.util' export class AddApiKeyPermission1765360298674 implements MigrationInterface { @@ -15,6 +16,20 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { await queryRunner.query(`UPDATE "${tableName}" SET "${columnName}" = '${permission}';`) } + + const sso = 'sso:manage' + const apikey = 'apikeys:import' + const itemsToRemove = [sso, apikey] + const roles: Role[] = await queryRunner.query( + `SELECT * FROM "role" WHERE "${columnName}" LIKE '%${sso}%' OR "${columnName}" LIKE '%${apikey}%';` + ) + if (roles.length > 0) { + for (const role of roles) { + let permissions = JSON.parse(role.permissions) + permissions = permissions.filter((permission: string) => !itemsToRemove.includes(permission)) + await queryRunner.query(`UPDATE "role" SET "${columnName}" = '${JSON.stringify(permissions)}' WHERE "id" = '${role.id}';`) + } + } } public async down(): Promise {} From 16d996a0e93a9131016c45b52e4c719ec0984cd2 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Mon, 29 Dec 2025 20:28:07 +0800 Subject: [PATCH 24/29] fix(migrations): handle corrupted permissions gracefully --- .../mariadb/1765360298674-AddApiKeyPermission.ts | 9 ++++++++- .../mysql/1765360298674-AddApiKeyPermission.ts | 9 ++++++++- .../postgres/1765360298674-AddApiKeyPermission.ts | 9 ++++++++- .../sqlite/1765360298674-AddApiKeyPermission.ts | 9 ++++++++- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts index 93a42dc892c..bb623cc28a2 100644 --- a/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts @@ -1,6 +1,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm' import { Role } from '../../../enterprise/database/entities/role.entity' import { hasColumn } from '../../../utils/database.util' +import logger from '../../../utils/logger' export class AddApiKeyPermission1765360298674 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { @@ -25,7 +26,13 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { ) if (roles.length > 0) { for (const role of roles) { - let permissions = JSON.parse(role.permissions) + let permissions: string[] = [] + try { + permissions = JSON.parse(role.permissions) + } catch (error) { + logger.error(`AddApiKeyPermission1765360298674 error parsing permissions for role ${role.id}:`, error) + continue + } permissions = permissions.filter((permission: string) => !itemsToRemove.includes(permission)) await queryRunner.query( `UPDATE \`role\` SET \`${columnName}\` = '${JSON.stringify(permissions)}' WHERE \`id\` = '${role.id}';` diff --git a/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts index 93a42dc892c..bb623cc28a2 100644 --- a/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts @@ -1,6 +1,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm' import { Role } from '../../../enterprise/database/entities/role.entity' import { hasColumn } from '../../../utils/database.util' +import logger from '../../../utils/logger' export class AddApiKeyPermission1765360298674 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { @@ -25,7 +26,13 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { ) if (roles.length > 0) { for (const role of roles) { - let permissions = JSON.parse(role.permissions) + let permissions: string[] = [] + try { + permissions = JSON.parse(role.permissions) + } catch (error) { + logger.error(`AddApiKeyPermission1765360298674 error parsing permissions for role ${role.id}:`, error) + continue + } permissions = permissions.filter((permission: string) => !itemsToRemove.includes(permission)) await queryRunner.query( `UPDATE \`role\` SET \`${columnName}\` = '${JSON.stringify(permissions)}' WHERE \`id\` = '${role.id}';` diff --git a/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts index 680c461b494..aa4d3b0f703 100644 --- a/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts @@ -1,6 +1,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm' import { Role } from '../../../enterprise/database/entities/role.entity' import { hasColumn } from '../../../utils/database.util' +import logger from '../../../utils/logger' export class AddApiKeyPermission1765360298674 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { @@ -25,7 +26,13 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { ) if (roles.length > 0) { for (const role of roles) { - let permissions = JSON.parse(role.permissions) + let permissions: string[] = [] + try { + permissions = JSON.parse(role.permissions) + } catch (error) { + logger.error(`AddApiKeyPermission1765360298674 error parsing permissions for role ${role.id}:`, error) + continue + } permissions = permissions.filter((permission: string) => !itemsToRemove.includes(permission)) await queryRunner.query(`UPDATE "role" SET "${columnName}" = '${JSON.stringify(permissions)}' WHERE "id" = '${role.id}';`) } diff --git a/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts index 680c461b494..aa4d3b0f703 100644 --- a/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts +++ b/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts @@ -1,6 +1,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm' import { Role } from '../../../enterprise/database/entities/role.entity' import { hasColumn } from '../../../utils/database.util' +import logger from '../../../utils/logger' export class AddApiKeyPermission1765360298674 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { @@ -25,7 +26,13 @@ export class AddApiKeyPermission1765360298674 implements MigrationInterface { ) if (roles.length > 0) { for (const role of roles) { - let permissions = JSON.parse(role.permissions) + let permissions: string[] = [] + try { + permissions = JSON.parse(role.permissions) + } catch (error) { + logger.error(`AddApiKeyPermission1765360298674 error parsing permissions for role ${role.id}:`, error) + continue + } permissions = permissions.filter((permission: string) => !itemsToRemove.includes(permission)) await queryRunner.query(`UPDATE "role" SET "${columnName}" = '${JSON.stringify(permissions)}' WHERE "id" = '${role.id}';`) } From 92beda6a6e9707447d77aa2d31c9ddd46a75e818 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Mon, 29 Dec 2025 20:42:59 +0800 Subject: [PATCH 25/29] fix(server/src/index.ts): add error handling for API key permissions parsing --- packages/server/src/index.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 06db90e301e..d8bea7d0527 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -4,6 +4,7 @@ import cors from 'cors' import express, { Request, Response } from 'express' import 'global-agent/bootstrap' import http from 'http' +import { StatusCodes } from 'http-status-codes' import path from 'path' import { DataSource } from 'typeorm' import { AbortControllerPool } from './AbortControllerPool' @@ -256,10 +257,16 @@ export class App { const customerId = org.customerId as string const features = await this.identityManager.getFeaturesByPlan(subscriptionId) const productId = await this.identityManager.getProductIdFromSubscription(subscriptionId) - + let permissions: string[] = [] + try { + permissions = JSON.parse(apiKey.permissions) + } catch (error) { + logger.error(`Error parsing permissions for API key ${apiKey.id}:`, error) + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: 'Error parsing permissions for API key' }) + } // @ts-ignore req.user = { - permissions: [...JSON.parse(apiKey.permissions)], + permissions: permissions, features, activeOrganizationId: activeOrganizationId, activeOrganizationSubscriptionId: subscriptionId, From 61e765d2806b7383bab3fcd31cb159379d16b0cc Mon Sep 17 00:00:00 2001 From: yau-wd Date: Mon, 29 Dec 2025 20:51:05 +0800 Subject: [PATCH 26/29] fix(APIKeyDialog.jsx): handle corrupted permissions when editing API key --- packages/ui/src/views/apikey/APIKeyDialog.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/views/apikey/APIKeyDialog.jsx b/packages/ui/src/views/apikey/APIKeyDialog.jsx index ce8a0ae5d1e..a20bbdc6fc3 100644 --- a/packages/ui/src/views/apikey/APIKeyDialog.jsx +++ b/packages/ui/src/views/apikey/APIKeyDialog.jsx @@ -112,7 +112,12 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { setPermissions(permissionsData) if (dialogProps.type === 'EDIT' && dialogProps.key) { - const keyPermissions = JSON.parse(dialogProps.key.permissions || '[]') + let keyPermissions = [] + try { + keyPermissions = JSON.parse(dialogProps.key.permissions || '[]') + } catch (error) { + console.error('Failed to parse API key permissions for editing:', error) + } if (keyPermissions && keyPermissions.length > 0) { const tempSelectedPermissions = {} Object.keys(permissionsData).forEach((category) => { From 772eedeaa73ac447a3c4d904939e7f0d89230e22 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Mon, 29 Dec 2025 21:06:45 +0800 Subject: [PATCH 27/29] fix(views/apikey/index.jsx): handle corrupted permissions in API key list --- packages/ui/src/views/apikey/index.jsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/views/apikey/index.jsx b/packages/ui/src/views/apikey/index.jsx index 2a94fb624ab..c16225bccbb 100644 --- a/packages/ui/src/views/apikey/index.jsx +++ b/packages/ui/src/views/apikey/index.jsx @@ -76,6 +76,15 @@ function APIKeyRow(props) { const [open, setOpen] = useState(false) const theme = useTheme() + // Parse permissions with error handling + let permissions = [] + try { + permissions = JSON.parse(props.apiKey.permissions || '[]') + } catch (error) { + console.error('Failed to parse API key permissions:', error) + permissions = [] + } + return ( <> @@ -126,7 +135,7 @@ function APIKeyRow(props) { WebkitBoxOrient: 'vertical' }} > - {JSON.parse(props.apiKey.permissions || '[]').map((d, key) => ( + {permissions.map((d, key) => ( {d} {', '} From 84835d72c361005e7607b2c491f42ae20cad19bd Mon Sep 17 00:00:00 2001 From: yau-wd Date: Mon, 29 Dec 2025 21:14:10 +0800 Subject: [PATCH 28/29] fix(services/apikey): validate permissions JSON in API key operations --- packages/server/src/services/apikey/index.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/server/src/services/apikey/index.ts b/packages/server/src/services/apikey/index.ts index 2ba43d9776d..420eb30f45f 100644 --- a/packages/server/src/services/apikey/index.ts +++ b/packages/server/src/services/apikey/index.ts @@ -18,7 +18,15 @@ import logger from '../../utils/logger' * @throws InternalFlowiseError if validation fails */ function validatePermissions(user: LoggedInUser, permissions: string, operation: string) { - const requestedPermissions = JSON.parse(permissions) + let requestedPermissions: string[] + try { + requestedPermissions = JSON.parse(permissions) + } catch (error) { + throw new InternalFlowiseError( + StatusCodes.BAD_REQUEST, + `Error parsing permissions for ${operation} API key: ${getErrorMessage(error)}` + ) + } // API Keys should not have workspace or admin permissions // This applies to ALL users, including admins (platform constraint) From cc1876f5ec3e415f920b85522727ca65b81990f7 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Mon, 29 Dec 2025 21:18:03 +0800 Subject: [PATCH 29/29] Potential fix for code scanning alert no. 83: Clear-text logging of sensitive information Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- packages/server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index d8bea7d0527..9e826fc5c97 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -261,7 +261,7 @@ export class App { try { permissions = JSON.parse(apiKey.permissions) } catch (error) { - logger.error(`Error parsing permissions for API key ${apiKey.id}:`, error) + logger.error('Error parsing API key permissions', error) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: 'Error parsing permissions for API key' }) } // @ts-ignore