Skip to content

Commit 11f402b

Browse files
author
Theodore Li
committed
Personal key should only be used if its the owner of the workflow
1 parent bba16f2 commit 11f402b

File tree

1 file changed

+67
-14
lines changed

1 file changed

+67
-14
lines changed

packages/db/scripts/migrate-block-api-keys-to-byok.ts

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
// --map jina=jina --user user_abc123 --user user_def456
2121

2222
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
23-
import { readFileSync, writeFileSync } from 'fs'
23+
import { appendFileSync, readFileSync, writeFileSync } from 'fs'
2424
import { resolve } from 'path'
2525
import { eq, sql } from 'drizzle-orm'
2626
import { index, json, jsonb, pgTable, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'
@@ -155,6 +155,11 @@ async function decryptSecret(encryptedValue: string): Promise<string> {
155155
}
156156

157157
// ---------- Schema ----------
158+
const workspaceTable = pgTable('workspace', {
159+
id: text('id').primaryKey(),
160+
ownerId: text('owner_id').notNull(),
161+
})
162+
158163
const workflow = pgTable('workflow', {
159164
id: text('id').primaryKey(),
160165
userId: text('user_id').notNull(),
@@ -272,6 +277,7 @@ function parseToolInputValue(value: unknown): any[] {
272277
type RawKeyRef = {
273278
rawValue: string
274279
blockName: string
280+
workflowId: string
275281
workflowName: string
276282
userId: string
277283
}
@@ -289,10 +295,15 @@ const KEY_SOURCE_PRIORITY: Record<KeySource, number> = {
289295
personal: 2,
290296
}
291297

298+
interface ResolveKeyContext {
299+
workspaceId: string
300+
workspaceOwnerId: string | null
301+
}
302+
292303
async function resolveKey(
293304
ref: RawKeyRef,
294-
context: string,
295-
env: EnvLookup
305+
env: EnvLookup,
306+
ctx: ResolveKeyContext
296307
): Promise<{ key: string | null; source: KeySource; envVarFailed: boolean }> {
297308
if (!isEnvVarReference(ref.rawValue)) {
298309
return { key: ref.rawValue, source: 'plaintext', envVarFailed: false }
@@ -308,16 +319,24 @@ async function resolveKey(
308319
const encryptedValue = wsValue ?? personalValue
309320
const source: KeySource = wsValue ? 'workspace' : 'personal'
310321

322+
const logPrefix =
323+
`workspace=${ctx.workspaceId} owner=${ctx.workspaceOwnerId ?? 'unknown'}` +
324+
` workflow=${ref.workflowId} user=${ref.userId}`
325+
311326
if (!encryptedValue) {
312-
console.warn(` [WARN] Env var "${varName}" not found (${context})`)
327+
console.warn(
328+
` [WARN] Env var "${varName}" not found — ${logPrefix} "${ref.blockName}" in "${ref.workflowName}"`
329+
)
313330
return { key: null, source, envVarFailed: true }
314331
}
315332

316333
try {
317334
const decrypted = await decryptSecret(encryptedValue)
318335
return { key: decrypted, source, envVarFailed: false }
319336
} catch (error) {
320-
console.warn(` [WARN] Failed to decrypt env var "${varName}" (${context}): ${error}`)
337+
console.warn(
338+
` [WARN] Failed to decrypt env var "${varName}" — ${logPrefix} "${ref.blockName}" in "${ref.workflowName}": ${error}`
339+
)
321340
return { key: null, source, envVarFailed: true }
322341
}
323342
}
@@ -379,8 +398,8 @@ async function run() {
379398
console.log(`Found ${workspaceIds.length} workspaces with candidate blocks\n`)
380399

381400
const outPath = resolve('migrate-byok-workspace-ids.txt')
382-
writeFileSync(outPath, `${workspaceIds.join('\n')}\n`)
383-
console.log(`[DRY RUN] Wrote ${workspaceIds.length} workspace IDs to ${outPath}\n`)
401+
writeFileSync(outPath, '')
402+
console.log(`[DRY RUN] Will write workspace IDs with keys to ${outPath}\n`)
384403
} else {
385404
const raw = readFileSync(resolve(FROM_FILE!), 'utf-8')
386405
workspaceIds = raw
@@ -411,7 +430,16 @@ async function run() {
411430
)})${userFilter}`
412431
)
413432

414-
console.log(`[Workspace ${workspaceId}] ${blocks.length} blocks`)
433+
const wsRows = await db
434+
.select({ ownerId: workspaceTable.ownerId })
435+
.from(workspaceTable)
436+
.where(eq(workspaceTable.id, workspaceId))
437+
.limit(1)
438+
const workspaceOwnerId = wsRows[0]?.ownerId ?? null
439+
440+
console.log(
441+
`[Workspace ${workspaceId}] ${blocks.length} blocks, owner=${workspaceOwnerId ?? 'unknown'}`
442+
)
415443

416444
// 2a. Extract all raw key references grouped by provider
417445
const providerKeys = new Map<string, RawKeyRef[]>()
@@ -427,6 +455,7 @@ async function run() {
427455
refs.push({
428456
rawValue: val,
429457
blockName: block.blockName,
458+
workflowId: block.workflowId,
430459
workflowName: block.workflowName,
431460
userId: block.userId,
432461
})
@@ -447,6 +476,7 @@ async function run() {
447476
refs.push({
448477
rawValue: toolApiKey,
449478
blockName: `${block.blockName} > tool "${tool.title || toolType}"`,
479+
workflowId: block.workflowId,
450480
workflowName: block.workflowName,
451481
userId: block.userId,
452482
})
@@ -461,6 +491,10 @@ async function run() {
461491
continue
462492
}
463493

494+
if (DRY_RUN) {
495+
appendFileSync(resolve('migrate-byok-workspace-ids.txt'), `${workspaceId}\n`)
496+
}
497+
464498
// 2b. Load env vars only if this workspace has env var references
465499
const needsEnvVars = [...providerKeys.values()]
466500
.flat()
@@ -504,11 +538,21 @@ async function run() {
504538
for (const [providerId, refs] of providerKeys) {
505539
// Resolve all keys for this provider to check for conflicts
506540
const resolved: { ref: RawKeyRef; key: string; source: KeySource }[] = []
541+
const resolveCtx: ResolveKeyContext = { workspaceId, workspaceOwnerId }
507542
for (const ref of refs) {
508-
const context = `"${ref.blockName}" in "${ref.workflowName}"`
509-
const { key, source, envVarFailed } = await resolveKey(ref, context, envLookup)
543+
const { key, source, envVarFailed } = await resolveKey(ref, envLookup, resolveCtx)
510544
if (envVarFailed) stats.envVarFailures++
511-
if (key?.trim()) resolved.push({ ref, key: key.trim(), source })
545+
if (!key?.trim()) continue
546+
547+
// For personal env vars, only use the workspace owner's — never another user's
548+
if (source === 'personal' && ref.userId !== workspaceOwnerId) {
549+
console.log(
550+
` [SKIP-PERSONAL] Ignoring non-owner personal key from user=${ref.userId} workflow=${ref.workflowId} "${ref.blockName}" in "${ref.workflowName}"`
551+
)
552+
continue
553+
}
554+
555+
resolved.push({ ref, key: key.trim(), source })
512556
}
513557

514558
if (resolved.length === 0) continue
@@ -522,12 +566,18 @@ async function run() {
522566
stats.conflicts++
523567
console.log(` [CONFLICT] provider "${providerId}": ${distinctKeys.size} distinct keys`)
524568
for (const { ref, key, source } of resolved) {
569+
const isOwner = ref.userId === workspaceOwnerId ? ' (owner)' : ''
525570
const display = isEnvVarReference(ref.rawValue)
526571
? `${ref.rawValue} -> ${maskKey(key)}`
527572
: maskKey(ref.rawValue)
528-
console.log(` [${source}] "${ref.blockName}" in "${ref.workflowName}": ${display}`)
573+
console.log(
574+
` [${source}] user=${ref.userId}${isOwner} workflow=${ref.workflowId} "${ref.blockName}" in "${ref.workflowName}": ${display}`
575+
)
529576
}
530-
console.log(` Using highest-priority key (${resolved[0].source})`)
577+
const chosenIsOwner = resolved[0].ref.userId === workspaceOwnerId ? ', owner' : ''
578+
console.log(
579+
` Using highest-priority key (${resolved[0].source}${chosenIsOwner}, user=${resolved[0].ref.userId})`
580+
)
531581
}
532582

533583
// Use the highest-priority resolved key
@@ -586,7 +636,10 @@ async function run() {
586636
console.log(` Env var resolution failures: ${stats.envVarFailures}`)
587637

588638
if (DRY_RUN) {
589-
console.log('\n[DRY RUN] No changes were made to the database.')
639+
console.log(
640+
`\n[DRY RUN] Wrote ${stats.workspacesProcessed} workspace IDs (with keys) to migrate-byok-workspace-ids.txt`
641+
)
642+
console.log('[DRY RUN] No changes were made to the database.')
590643
console.log('Run without --dry-run to apply changes.')
591644
} else {
592645
console.log('\nMigration completed successfully!')

0 commit comments

Comments
 (0)