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/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 677d689311c..84552174e11 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,12 +8,10 @@ import { getPageAndLimitParams } from '../../utils/pagination' // Get api keys const getAllApiKeys = async (req: Request, res: Response, next: NextFunction) => { try { - const autoCreateNewKey = true + 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, autoCreateNewKey, page, limit) + + const apiResponse = await apikeyService.getAllApiKeys(user, page, limit) return res.json(apiResponse) } catch (error) { next(error) @@ -24,10 +23,14 @@ 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.user?.activeWorkspaceId) { - throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Workspace ID is required`) + if (!req.body.permissions || typeof req.body.permissions !== 'string') { + throw new InternalFlowiseError( + StatusCodes.PRECONDITION_FAILED, + `Error: apikeyController.createApiKey - permissions not provided!` + ) } - const apiResponse = await apikeyService.createApiKey(req.body.keyName, req.user?.activeWorkspaceId) + const user = req.user as LoggedInUser + const apiResponse = await apikeyService.createApiKey(user, req.body.keyName, req.body.permissions) return res.json(apiResponse) } catch (error) { next(error) @@ -43,27 +46,14 @@ 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.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) - return res.json(apiResponse) - } catch (error) { - next(error) - } -} - -// 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`) + if (!req.body.permissions || typeof req.body.permissions !== 'string') { + throw new InternalFlowiseError( + StatusCodes.PRECONDITION_FAILED, + `Error: apikeyController.updateApiKey - permissions not provided!` + ) } - req.body.workspaceId = req.user?.activeWorkspaceId - const apiResponse = await apikeyService.importKeys(req.body) + const user = req.user as LoggedInUser + const apiResponse = await apikeyService.updateApiKey(user, req.params.id, req.body.keyName, req.body.permissions) return res.json(apiResponse) } catch (error) { next(error) @@ -104,6 +94,5 @@ export default { deleteApiKey, getAllApiKeys, updateApiKey, - verifyApiKey, - importKeys + verifyApiKey } 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/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts b/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts new file mode 100644 index 00000000000..bb623cc28a2 --- /dev/null +++ b/packages/server/src/database/migrations/mariadb/1765360298674-AddApiKeyPermission.ts @@ -0,0 +1,45 @@ +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 { + const tableName = 'apikey' + const columnName = 'permissions' + + 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","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}';`) + } + + 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: 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}';` + ) + } + } + } + + 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..bb623cc28a2 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1765360298674-AddApiKeyPermission.ts @@ -0,0 +1,45 @@ +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 { + const tableName = 'apikey' + const columnName = 'permissions' + + 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","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}';`) + } + + 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: 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}';` + ) + } + } + } + + 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..aa4d3b0f703 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1765360298674-AddApiKeyPermission.ts @@ -0,0 +1,43 @@ +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 { + const tableName = 'apikey' + const columnName = 'permissions' + + 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","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}';`) + } + + 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: 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}';`) + } + } + } + + 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..aa4d3b0f703 --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1765360298674-AddApiKeyPermission.ts @@ -0,0 +1,43 @@ +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 { + const tableName = 'apikey' + const columnName = 'permissions' + + 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","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}';`) + } + + 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: 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}';`) + } + } + } + + 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 ] 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/enterprise/controllers/auth/index.ts b/packages/server/src/enterprise/controllers/auth/index.ts index 304b7eee6e5..bf1c742e99b 100644 --- a/packages/server/src/enterprise/controllers/auth/index.ts +++ b/packages/server/src/enterprise/controllers/auth/index.ts @@ -1,10 +1,97 @@ 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' 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 + + // 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:'] + } + + // 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) { + 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(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)) + return !isDisabled + }) + + // Only include category if it has remaining permissions + if (filteredCategoryPermissions.length > 0) { + filteredPermissions[category] = filteredCategoryPermissions + } + } + + permissions = filteredPermissions + } + } + + // 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 }[] } = {} + + for (const [category, categoryPermissions] of Object.entries(permissions)) { + const filteredCategoryPermissions = (categoryPermissions as any[]).filter((permission) => + userPermissions?.includes(permission.key) + ) + + if (filteredCategoryPermissions.length > 0) { + filteredPermissions[category] = filteredCategoryPermissions + } + } + + permissions = filteredPermissions + } + + return res.status(StatusCodes.OK).json(permissions) } catch (error) { next(error) } diff --git a/packages/server/src/enterprise/rbac/Permissions.ts b/packages/server/src/enterprise/rbac/Permissions.ts index e44f541a820..82369c4a271 100644 --- a/packages/server/src/enterprise/rbac/Permissions.ts +++ b/packages/server/src/enterprise/rbac/Permissions.ts @@ -6,138 +6,139 @@ 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)) 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 +168,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/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/server/src/index.ts b/packages/server/src/index.ts index 258be4cbd53..9e826fc5c97 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,40 +1,39 @@ -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 { DataSource, IsNull } 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 { StatusCodes } from 'http-status-codes' +import path from 'path' +import { DataSource } from 'typeorm' 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 { GeneralRole, Role } from './enterprise/database/entities/role.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 { @@ -148,9 +147,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) @@ -236,27 +232,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({ @@ -269,17 +257,23 @@ 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 API key permissions', error) + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: 'Error parsing permissions for API key' }) + } // @ts-ignore req.user = { - permissions: [...JSON.parse(ownerRole.permissions)], + permissions: 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/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 5e009c92774..420eb30f45f 100644 --- a/packages/server/src/services/apikey/index.ts +++ b/packages/server/src/services/apikey/index.ts @@ -1,41 +1,151 @@ import { StatusCodes } from 'http-status-codes' -import { generateAPIKey, generateSecretHash } from '../../utils/apiKey' -import { addChatflowsCount } from '../../utils/addChatflowsCount' +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 { ApiKey } from '../../database/entities/ApiKey' -import { Not, IsNull } from 'typeorm' -import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils' -import { v4 as uuidv4 } from 'uuid' +import logger from '../../utils/logger' -const getAllApiKeysFromDB = async (workspaceId: string, page: number = -1, limit: number = -1) => { +/** + * 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(user: LoggedInUser, permissions: string, operation: string) { + 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) + 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() - 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) + 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`) + } } - queryBuilder.andWhere('api_key.workspaceId = :workspaceId', { workspaceId }) - const [data, total] = await queryBuilder.getManyAndCount() - const keysWithChatflows = await addChatflowsCount(data) - if (page > 0 && limit > 0) { - return { total, data: keysWithChatflows } - } else { - return keysWithChatflows + // 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 && !user.permissions.includes(permission) + ) + if (hasInvalidPermissions) { + throw new InternalFlowiseError( + StatusCodes.FORBIDDEN, + `Cannot ${operation} API key with permissions that exceed your own permissions` + ) + } } } -const getAllApiKeys = async (workspaceId: string, autoCreateNewKey?: boolean, page: number = -1, limit: number = -1) => { +/** + * 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 (user: LoggedInUser, 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) + 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: user.activeWorkspaceId }) + const allKeys = await queryBuilder.getMany() + + // Filter keys based on user permissions + let filteredKeys = allKeys + 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 || user.permissions.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 + } + }) + } + + const keysWithChatflows = await addChatflowsCount(filteredKeys) + + if (page > 0 && limit > 0) { + return { total: filteredKeys.length, data: keysWithChatflows } + } else { + return keysWithChatflows } - return keys } catch (error) { throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.getAllApiKeys - ${getErrorMessage(error)}`) } @@ -71,42 +181,42 @@ const getApiKeyById = async (apiKeyId: string) => { } } -const createApiKey = async (keyName: string, workspaceId: 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.workspaceId = workspaceId - const key = appServer.AppDataSource.getRepository(ApiKey).create(newKey) - await appServer.AppDataSource.getRepository(ApiKey).save(key) - return await getAllApiKeysFromDB(workspaceId) - } catch (error) { - throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.createApiKey - ${getErrorMessage(error)}`) - } +const createApiKey = async (user: LoggedInUser, keyName: string, permissions: string) => { + // Validate permissions before creating the key + validatePermissions(user, permissions, 'create') + + 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 = user.activeWorkspaceId + const key = appServer.AppDataSource.getRepository(ApiKey).create(newKey) + await appServer.AppDataSource.getRepository(ApiKey).save(key) + return await getAllApiKeys(user) } // Update api key -const updateApiKey = async (id: string, keyName: string, workspaceId: 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 - await appServer.AppDataSource.getRepository(ApiKey).save(currentKey) - return await getAllApiKeysFromDB(workspaceId) - } catch (error) { - throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.updateApiKey - ${getErrorMessage(error)}`) +const updateApiKey = async (user: LoggedInUser, id: string, keyName: string, permissions: string) => { + // Validate permissions before updating the key + validatePermissions(user, permissions, 'update') + + const appServer = getRunningExpressApp() + const currentKey = await appServer.AppDataSource.getRepository(ApiKey).findOneBy({ + id: id, + workspaceId: user.activeWorkspaceId + }) + 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(user) } const deleteApiKey = async (id: string, workspaceId: string) => { @@ -122,114 +232,6 @@ const deleteApiKey = async (id: string, workspaceId: string) => { } } -const importKeys = async (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'] - 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` - ) - } - } - } - - 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.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.workspaceId = workspaceId - const newKeyEntity = appServer.AppDataSource.getRepository(ApiKey).create(newKey) - await appServer.AppDataSource.getRepository(ApiKey).save(newKeyEntity) - } - } - return await getAllApiKeysFromDB(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() @@ -259,6 +261,5 @@ export default { updateApiKey, verifyApiKey, getApiKey, - getApiKeyById, - importKeys + getApiKeyById } 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) - } - } - } -} diff --git a/packages/server/src/utils/database.util.ts b/packages/server/src/utils/database.util.ts new file mode 100644 index 00000000000..7ae71b1798e --- /dev/null +++ b/packages/server/src/utils/database.util.ts @@ -0,0 +1,13 @@ +import { QueryRunner } from 'typeorm' + +export async function hasColumn(queryRunner: QueryRunner, tableName: string, columnName: string): Promise { + const table = await queryRunner.getTable(tableName) + + if (!table) { + throw new Error(`Table ${tableName} not found`) + } + + const hasColumn = table.columns.some((column) => column.name === columnName) + + return hasColumn +} 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 } } 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/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.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..a20bbdc6fc3 100644 --- a/packages/ui/src/views/apikey/APIKeyDialog.jsx +++ b/packages/ui/src/views/apikey/APIKeyDialog.jsx @@ -1,39 +1,51 @@ -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' // Icons -import { IconX, IconCopy } from '@tabler/icons-react' +import { IconCopy, IconKey, IconX } from '@tabler/icons-react' // API import apikeyApi from '@/api/apikey' +import authApi from '@/api/auth' + +// Hooks +import useApi from '@/hooks/useApi' +import { useConfig } from '@/store/context/ConfigContext' // 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') const theme = useTheme() const dispatch = useDispatch() + const { isOpenSource, isEnterpriseLicensed, isCloud } = useConfig() // ==============================|| Snackbar ||============================== // @@ -45,6 +57,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 +68,166 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { } else if (dialogProps.type === 'ADD') { setKeyName('') } + getAllPermissionsApi.request('API_KEY') + 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 + + // 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) { + 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) => { + 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.entries(updatedCategoryPermissions).some( + ([permissionKey, isEnabled]) => permissionKey !== viewPermissionKey && isEnabled + ) + if (hasEnabledPermissions) { + updatedCategoryPermissions[viewPermissionKey] = true + } + } else { + const hasEnabledPermissions = Object.entries(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 +266,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 +317,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 +407,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 +479,7 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => { {dialogProps.confirmButtonName} +
) : null 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 e6f8881aded..c16225bccbb 100644 --- a/packages/ui/src/views/apikey/index.jsx +++ b/packages/ui/src/views/apikey/index.jsx @@ -1,14 +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' // material-ui import { - Button, Box, + Button, Chip, + Collapse, + IconButton, + Paper, + Popover, Skeleton, Stack, Table, @@ -16,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' @@ -48,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 ||============================== // @@ -88,13 +76,22 @@ 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 ( <> {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 +121,29 @@ function APIKeyRow(props) { + + + + {permissions.map((d, key) => ( + + {d} + {', '} + + ))} + + + {props.apiKey.chatFlows.length}{' '} {props.apiKey.chatFlows.length > 0 && ( @@ -150,7 +170,7 @@ function APIKeyRow(props) { {open && ( - + @@ -219,9 +239,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 */ @@ -296,17 +313,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`, @@ -361,7 +367,6 @@ const APIKey = () => { const onConfirm = () => { setShowDialog(false) - setShowUploadDialog(false) refresh(currentPage, pageLimit) } @@ -395,16 +400,6 @@ const APIKey = () => { title='API Keys' description='Flowise API & SDK authentication keys' > - } - id='btn_importApiKeys' - > - Import - { Key Name API Key + Permissions Usage Updated @@ -471,6 +467,9 @@ const APIKey = () => { + + + @@ -491,6 +490,9 @@ const APIKey = () => { + + + @@ -541,14 +543,6 @@ const APIKey = () => { onConfirm={onConfirm} setError={setError} > - {showUploadDialog && ( - setShowUploadDialog(false)} - onConfirm={onConfirm} - > - )} ) diff --git a/packages/ui/src/views/roles/CreateEditRoleDialog.jsx b/packages/ui/src/views/roles/CreateEditRoleDialog.jsx index 9592966c72d..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 ||============================== // @@ -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('') @@ -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) 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({})