Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0e0b95b
feat(database.util.ts): add function to check if column exists in table
yau-wd Dec 11, 2025
7aa5739
feat(migrations): add permission column to apikey table
yau-wd Dec 11, 2025
cde6400
chore(services/apikey): remove auto-create default API key
yau-wd Dec 12, 2025
40fabc6
chore: remove auto-create default API key
yau-wd Dec 12, 2025
63e6784
fix(migrations): rename permission to permissions in apikey table
yau-wd Dec 12, 2025
28963b6
feat(services/apikey): add permissions to create and update
yau-wd Dec 12, 2025
ab252a2
feat(views/apikey): add permissions to create and update
yau-wd Dec 12, 2025
5e5f730
feat(api): use API key permissions instead of role permissions
yau-wd Dec 15, 2025
cc545ba
feat(permissions): filter by user.permissions except for ROLE type
yau-wd Dec 15, 2025
2e46bb6
feat(services/apikey): filter keys by user permissions in getAllApiKeys
yau-wd Dec 16, 2025
952807b
feat(services/apikey): limit API key permissions to user permissions …
yau-wd Dec 16, 2025
c0d9b69
Merge branch 'main' into feature/api-key-permission
yau-wd Dec 16, 2025
aacfad5
fix(database.util.ts): replace non-null assertion with explicit error…
yau-wd Dec 16, 2025
9d33318
feat(services/apikey): log errors for malformed API key permissions
yau-wd Dec 16, 2025
9e24e04
refactor(services/apikey): extract permission validation
yau-wd Dec 16, 2025
15d67b2
feat(permission): enhance permission filtering based on user features…
yau-wd Dec 17, 2025
e135f4b
chore(migrations): lower API key permission value
yau-wd Dec 17, 2025
29dcb4c
Update packages/ui/src/views/apikey/APIKeyDialog.jsx
yau-wd Dec 18, 2025
51500b7
Update packages/ui/src/views/apikey/APIKeyDialog.jsx
yau-wd Dec 18, 2025
38ce74d
refactor: remove deprecated APIKEY_PATH and APIKEY migration
yau-wd Dec 22, 2025
25a2d76
Merge branch 'main' into feature/api-key-permission
yau-wd Dec 23, 2025
36cb002
refactor(apikey): remove functionality related to import apikey
yau-wd Dec 23, 2025
2163e5b
feat(permission): filter out workspace and admin categories for non-ROLE
yau-wd Dec 23, 2025
281c241
Merge branch 'main' into feature/api-key-permission
yau-wd Dec 26, 2025
d01c9e9
feat(services/apikey): validate API key permissions during create and…
yau-wd Dec 26, 2025
6b04658
chore(migrations): clean up role permissions values
yau-wd Dec 29, 2025
16d996a
fix(migrations): handle corrupted permissions gracefully
yau-wd Dec 29, 2025
92beda6
fix(server/src/index.ts): add error handling for API key permissions …
yau-wd Dec 29, 2025
61e765d
fix(APIKeyDialog.jsx): handle corrupted permissions when editing API key
yau-wd Dec 29, 2025
772eede
fix(views/apikey/index.jsx): handle corrupted permissions in API key …
yau-wd Dec 29, 2025
84835d7
fix(services/apikey): validate permissions JSON in API key operations
yau-wd Dec 29, 2025
cc1876f
Potential fix for code scanning alert no. 83: Clear-text logging of s…
yau-wd Dec 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions docker/.env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
PORT=3000

# APIKEY_PATH=/your_apikey_path/.flowise # (will be deprecated by end of 2025)

############################################################################################################
############################################## DATABASE ####################################################
############################################################################################################
Expand Down
2 changes: 0 additions & 2 deletions docker/worker/.env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
WORKER_PORT=5566

# APIKEY_PATH=/your_apikey_path/.flowise # (will be deprecated by end of 2025)

############################################################################################################
############################################## DATABASE ####################################################
############################################################################################################
Expand Down
2 changes: 0 additions & 2 deletions packages/server/.env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
PORT=3000

# APIKEY_PATH=/your_apikey_path/.flowise # (will be deprecated by end of 2025)

############################################################################################################
############################################## DATABASE ####################################################
############################################################################################################
Expand Down
9 changes: 0 additions & 9 deletions packages/server/src/Interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 20 additions & 31 deletions packages/server/src/controllers/apikey/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
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'

// 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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -104,6 +94,5 @@ export default {
deleteApiKey,
getAllApiKeys,
updateApiKey,
verifyApiKey,
importKeys
verifyApiKey
}
6 changes: 4 additions & 2 deletions packages/server/src/database/entities/ApiKey.ts
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {}
}
4 changes: 3 additions & 1 deletion packages/server/src/database/migrations/mariadb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -108,5 +109,6 @@ export const mariadbMigrations = [
ModifyChatflowType1755066758601,
AddTextToSpeechToChatFlow1759419231100,
AddChatFlowNameIndex1759424809984,
FixDocumentStoreFileChunkLongText1765000000000
FixDocumentStoreFileChunkLongText1765000000000,
AddApiKeyPermission1765360298674
]
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {}
}
4 changes: 3 additions & 1 deletion packages/server/src/database/migrations/mysql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -110,5 +111,6 @@ export const mysqlMigrations = [
ModifyChatflowType1755066758601,
AddTextToSpeechToChatFlow1759419216034,
AddChatFlowNameIndex1759424828558,
FixDocumentStoreFileChunkLongText1765000000000
FixDocumentStoreFileChunkLongText1765000000000,
AddApiKeyPermission1765360298674
]
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {}
}
4 changes: 3 additions & 1 deletion packages/server/src/database/migrations/postgres/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -106,5 +107,6 @@ export const postgresMigrations = [
AddTextToSpeechToChatFlow1754986480347,
ModifyChatflowType1755066758601,
AddTextToSpeechToChatFlow1759419194331,
AddChatFlowNameIndex1759424903973
AddChatFlowNameIndex1759424903973,
AddApiKeyPermission1765360298674
]
Loading