Skip to content

Commit 84d2d6b

Browse files
authored
Harden Project dependency graph and packages
1 parent 46e93f2 commit 84d2d6b

18 files changed

Lines changed: 1463 additions & 286 deletions

File tree

‎packages/server/src/ai/tools/budibase/rows.ts‎

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,28 @@ const buildCollisionSafeToolName = (tableId: string, action: string) => {
378378
return `${sanitizedTableId.substring(0, tableIdLength)}${suffix}`
379379
}
380380

381+
export const getRowToolNames = (tableId: string): Record<string, string> => {
382+
const sanitizedTableId = tableId.replace(/[^A-Za-z0-9_-]/g, "_")
383+
const truncatedToolNames = Object.fromEntries(
384+
Object.keys(ROW_TOOL).map(action => [
385+
action,
386+
`${sanitizedTableId}_${action}`.substring(0, MAX_TOOL_NAME_LENGTH),
387+
])
388+
)
389+
const hasToolNameCollision =
390+
new Set(Object.values(truncatedToolNames)).size !==
391+
Object.keys(truncatedToolNames).length
392+
if (!hasToolNameCollision) {
393+
return truncatedToolNames
394+
}
395+
return Object.fromEntries(
396+
Object.keys(ROW_TOOL).map(action => [
397+
action,
398+
buildCollisionSafeToolName(tableId, action),
399+
])
400+
)
401+
}
402+
381403
export const createRowTools = ({
382404
tableId,
383405
tableName,
@@ -404,23 +426,12 @@ export const createRowTools = ({
404426
const schemaSummary = buildSchemaSummary(writableFields)
405427
const dataSchema = buildRowDataSchema(writableFields, schemaSummary)
406428
const searchInputSchema = buildSearchInputSchema(schemaSummary)
429+
const toolNames = getRowToolNames(tableId)
407430
const fields = getAgentTableFields(tableSchema)
408-
const sanitizedTableId = tableId.replace(/[^A-Za-z0-9_-]/g, "_")
409-
const truncatedToolNames = Object.fromEntries(
410-
Object.keys(ROW_TOOL).map(action => [
411-
action,
412-
`${sanitizedTableId}_${action}`.substring(0, MAX_TOOL_NAME_LENGTH),
413-
])
414-
)
415-
const hasToolNameCollision =
416-
new Set(Object.values(truncatedToolNames)).size !==
417-
Object.keys(truncatedToolNames).length
418431

419432
return Object.entries(ROW_TOOL).map(([action, def]) => {
420433
const description = `${formatActionLabel(action)} in "${tableName}". ${def.description}`
421-
const toolName = hasToolNameCollision
422-
? buildCollisionSafeToolName(tableId, action)
423-
: truncatedToolNames[action]
434+
const toolName = toolNames[action]
424435
let inputSchema = def.inputSchema
425436
if (action === "create_row") {
426437
inputSchema = z.object({ data: dataSchema })

‎packages/server/src/api/routes/tests/datasource.spec.ts‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,94 @@ describe("/datasources", () => {
515515
)
516516
})
517517

518+
it("detects embedded env var references", async () => {
519+
const ds = await config.api.datasource.create({
520+
type: "datasource",
521+
name: "REST embedded env var",
522+
source: SourceName.REST,
523+
config: {
524+
url: "https://{{ env.HOST }}/api",
525+
},
526+
})
527+
528+
expect(ds.usesEnvironmentVariables).toBe(true)
529+
})
530+
531+
it("scrubs secrets containing mixed literals and env var references", async () => {
532+
const ds = await config.api.datasource.create({
533+
type: "datasource",
534+
name: "REST mixed env secret",
535+
source: SourceName.REST,
536+
config: {
537+
authConfigs: [
538+
{
539+
_id: generator.guid(),
540+
name: "Mixed Env Auth",
541+
type: RestAuthType.BASIC,
542+
config: {
543+
username: "{{ env.USERNAME }}",
544+
password: "prefix {{ env.PASSWORD }}",
545+
},
546+
},
547+
],
548+
},
549+
})
550+
551+
expect(ds.usesEnvironmentVariables).toBe(true)
552+
expect(ds.config!.authConfigs[0].config.password).toBe(
553+
PASSWORD_REPLACEMENT
554+
)
555+
})
556+
557+
it("preserves secrets composed of adjacent env var references", async () => {
558+
const password = "{{ env.PASSWORD_PREFIX }}{{ env.PASSWORD_SUFFIX }}"
559+
const ds = await config.api.datasource.create({
560+
type: "datasource",
561+
name: "REST adjacent env secrets",
562+
source: SourceName.REST,
563+
config: {
564+
authConfigs: [
565+
{
566+
_id: generator.guid(),
567+
name: "Adjacent Env Auth",
568+
type: RestAuthType.BASIC,
569+
config: {
570+
username: "{{ env.USERNAME }}",
571+
password,
572+
},
573+
},
574+
],
575+
},
576+
})
577+
578+
expect(ds.config!.authConfigs[0].config.password).toBe(password)
579+
})
580+
581+
it("scrubs secrets combining env and non-env bindings", async () => {
582+
const ds = await config.api.datasource.create({
583+
type: "datasource",
584+
name: "REST mixed binding secret",
585+
source: SourceName.REST,
586+
config: {
587+
authConfigs: [
588+
{
589+
_id: generator.guid(),
590+
name: "Mixed Binding Auth",
591+
type: RestAuthType.BASIC,
592+
config: {
593+
username: "{{ env.USERNAME }}",
594+
password: "{{ env.PASSWORD }}{{ user.password }}",
595+
},
596+
},
597+
],
598+
},
599+
})
600+
601+
expect(ds.config!.authConfigs[0].config.password).toBe(
602+
PASSWORD_REPLACEMENT
603+
)
604+
})
605+
518606
it("scrubs sensitive longform fields in get response", async () => {
519607
const privateKey = [
520608
"-----BEGIN PRIVATE KEY-----",

0 commit comments

Comments
 (0)