Skip to content

Commit 3be6122

Browse files
fix(custom-block): allow cross-workspace exec, org-scope authority, keep field ids, hide disabled
1 parent 11bca6b commit 3be6122

4 files changed

Lines changed: 46 additions & 19 deletions

File tree

apps/sim/app/workspace/[workspaceId]/providers/custom-blocks-loader.tsx

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,27 @@ export function CustomBlocksLoader() {
2121

2222
useEffect(() => {
2323
hydrateClientCustomBlocks(
24-
(data ?? []).map((block) =>
25-
buildCustomBlockConfig(
26-
{
27-
type: block.type,
28-
name: block.name,
29-
description: block.description,
30-
workflowId: block.workflowId,
31-
exposedOutputs: block.exposedOutputs,
32-
},
33-
block.inputFields,
34-
{
35-
icon: getCustomBlockIcon(block.iconUrl),
36-
bgColor: block.iconUrl ? 'transparent' : undefined,
37-
}
24+
// Only enabled blocks are resolvable/executable server-side, so the client
25+
// overlay (toolbar, canvas, palette) must exclude disabled ones too — else
26+
// the block is offered but every run fails.
27+
(data ?? [])
28+
.filter((block) => block.enabled)
29+
.map((block) =>
30+
buildCustomBlockConfig(
31+
{
32+
type: block.type,
33+
name: block.name,
34+
description: block.description,
35+
workflowId: block.workflowId,
36+
exposedOutputs: block.exposedOutputs,
37+
},
38+
block.inputFields,
39+
{
40+
icon: getCustomBlockIcon(block.iconUrl),
41+
bgColor: block.iconUrl ? 'transparent' : undefined,
42+
}
43+
)
3844
)
39-
)
4045
)
4146
}, [data])
4247

apps/sim/executor/handlers/workflow/workflow-handler.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export class WorkflowBlockHandler implements BlockHandler {
113113
let loadUserId = ctx.userId
114114
let exposedOutputs: CustomBlockOutput[] = []
115115
if (isCustomBlock) {
116-
const authority = await getCustomBlockAuthority(blockTypeId as string)
116+
const authority = await getCustomBlockAuthority(blockTypeId as string, ctx.workspaceId)
117117
if (!authority) {
118118
throw new Error('This custom block is no longer available')
119119
}
@@ -163,7 +163,14 @@ export class WorkflowBlockHandler implements BlockHandler {
163163
throw new Error(`Child workflow ${workflowId} not found`)
164164
}
165165

166-
this.assertChildWorkflowInWorkspace(workflowId, childWorkflow.workspaceId, ctx.workspaceId)
166+
// Custom blocks are org-scoped and deliberately cross-workspace: the source
167+
// workflow lives in the publisher's workspace, not the consumer's. Their
168+
// boundary is the org overlay + `getCustomBlockAuthority`, so the
169+
// same-workspace assert (which guards regular workflow blocks) must be
170+
// skipped or every custom-block invocation from another workspace throws.
171+
if (!isCustomBlock) {
172+
this.assertChildWorkflowInWorkspace(workflowId, childWorkflow.workspaceId, ctx.workspaceId)
173+
}
167174

168175
childWorkflowName = childWorkflow.name || 'Unknown Workflow'
169176

apps/sim/lib/api/contracts/custom-blocks.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { workflowIdSchema, workspaceIdSchema } from '@/lib/api/contracts/primiti
33
import { defineRouteContract } from '@/lib/api/contracts/types'
44

55
const inputFieldSchema = z.object({
6+
/** Stable per-field id — preserved so client block configs key sub-blocks on it
7+
* (rename-safe wiring) instead of the display name. Absent on legacy fields. */
8+
id: z.string().optional(),
69
name: z.string(),
710
type: z.string(),
811
description: z.string().optional(),

apps/sim/lib/workflows/custom-blocks/operations.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,22 @@ export async function getCustomBlockById(id: string) {
140140
* deletes the workflow → the custom_block row, so there is never an orphaned block.
141141
* `null` when no enabled block matches the type.
142142
*/
143-
export async function getCustomBlockAuthority(type: string): Promise<{
143+
export async function getCustomBlockAuthority(
144+
type: string,
145+
consumerWorkspaceId: string | undefined
146+
): Promise<{
144147
workflowId: string
145148
organizationId: string
146149
ownerUserId: string
147150
exposedOutputs: CustomBlockOutput[]
148151
} | null> {
152+
// Scope resolution to the consumer's org: `(organizationId, type)` is the unique
153+
// key, so without the org filter a `custom_block_*` type smuggled in from another
154+
// org's serialized workflow could resolve and run that org's block.
155+
if (!consumerWorkspaceId) return null
156+
const consumerWs = await getWorkspaceWithOwner(consumerWorkspaceId)
157+
if (!consumerWs?.organizationId) return null
158+
149159
const [row] = await db
150160
.select({
151161
workflowId: customBlock.workflowId,
@@ -156,7 +166,9 @@ export async function getCustomBlockAuthority(type: string): Promise<{
156166
})
157167
.from(customBlock)
158168
.innerJoin(workflow, eq(workflow.id, customBlock.workflowId))
159-
.where(eq(customBlock.type, type))
169+
.where(
170+
and(eq(customBlock.type, type), eq(customBlock.organizationId, consumerWs.organizationId))
171+
)
160172
.limit(1)
161173

162174
if (!row || !row.enabled) return null

0 commit comments

Comments
 (0)