Skip to content

Commit 813ec9b

Browse files
waleedlatif1claude
andcommitted
fix(socket): add comprehensive lock validation across operations
Based on audit findings, adds lock validation to multiple operations: 1. BATCH_TOGGLE_HANDLES - now skips locked/protected blocks at: - Store layer (batchToggleHandles) - Collaborative hook (collaborativeBatchToggleBlockHandles) - Server socket handler 2. BATCH_ADD_BLOCKS - server now filters blocks being added to locked parent containers 3. BATCH_UPDATE_PARENT - server now: - Skips protected blocks (locked or inside locked container) - Prevents moving blocks into locked containers All validations use consistent isProtected() helper that checks both direct lock and parent container lock. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 52d9f31 commit 813ec9b

File tree

3 files changed

+147
-27
lines changed

3 files changed

+147
-27
lines changed

apps/sim/hooks/use-collaborative-workflow.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,12 +1019,25 @@ export function useCollaborativeWorkflow() {
10191019

10201020
if (ids.length === 0) return
10211021

1022+
const blocks = useWorkflowStore.getState().blocks
1023+
1024+
// Helper to check if a block is protected (locked or inside locked parent)
1025+
const isProtected = (blockId: string): boolean => {
1026+
const block = blocks[blockId]
1027+
if (!block) return false
1028+
if (block.locked) return true
1029+
const parentId = block.data?.parentId
1030+
if (parentId && blocks[parentId]?.locked) return true
1031+
return false
1032+
}
1033+
10221034
const previousStates: Record<string, boolean> = {}
10231035
const validIds: string[] = []
10241036

10251037
for (const id of ids) {
1026-
const block = useWorkflowStore.getState().blocks[id]
1027-
if (block) {
1038+
const block = blocks[id]
1039+
// Skip locked blocks and blocks inside locked containers
1040+
if (block && !isProtected(id)) {
10281041
previousStates[id] = block.horizontalHandles ?? false
10291042
validIds.push(id)
10301043
}

apps/sim/socket/database/operations.ts

Lines changed: 115 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,37 @@ async function handleBlocksOperationTx(
507507
})
508508

509509
if (blocks && blocks.length > 0) {
510-
const blockValues = blocks.map((block: Record<string, unknown>) => {
510+
// Fetch existing blocks to check for locked parents
511+
const existingBlocks = await tx
512+
.select({ id: workflowBlocks.id, locked: workflowBlocks.locked })
513+
.from(workflowBlocks)
514+
.where(eq(workflowBlocks.workflowId, workflowId))
515+
516+
type ExistingBlockRecord = (typeof existingBlocks)[number]
517+
const lockedParentIds = new Set(
518+
existingBlocks
519+
.filter((b: ExistingBlockRecord) => b.locked)
520+
.map((b: ExistingBlockRecord) => b.id)
521+
)
522+
523+
// Filter out blocks being added to locked parents
524+
const allowedBlocks = (blocks as Array<Record<string, unknown>>).filter((block) => {
525+
const parentId = (block.data as Record<string, unknown> | null)?.parentId as
526+
| string
527+
| undefined
528+
if (parentId && lockedParentIds.has(parentId)) {
529+
logger.info(`Skipping block ${block.id} - parent ${parentId} is locked`)
530+
return false
531+
}
532+
return true
533+
})
534+
535+
if (allowedBlocks.length === 0) {
536+
logger.info('All blocks filtered out due to locked parents, skipping add')
537+
break
538+
}
539+
540+
const blockValues = allowedBlocks.map((block: Record<string, unknown>) => {
511541
const blockId = block.id as string
512542
const mergedSubBlocks = mergeSubBlockValues(
513543
block.subBlocks as Record<string, unknown>,
@@ -538,7 +568,7 @@ async function handleBlocksOperationTx(
538568
// Create subflow entries for loop/parallel blocks (skip if already in payload)
539569
const loopIds = new Set(loops ? Object.keys(loops) : [])
540570
const parallelIds = new Set(parallels ? Object.keys(parallels) : [])
541-
for (const block of blocks) {
571+
for (const block of allowedBlocks) {
542572
const blockId = block.id as string
543573
if (block.type === 'loop' && !loopIds.has(blockId)) {
544574
await tx.insert(workflowSubflows).values({
@@ -567,7 +597,7 @@ async function handleBlocksOperationTx(
567597

568598
// Update parent subflow node lists
569599
const parentIds = new Set<string>()
570-
for (const block of blocks) {
600+
for (const block of allowedBlocks) {
571601
const parentId = (block.data as Record<string, unknown>)?.parentId as string | undefined
572602
if (parentId) {
573603
parentIds.add(parentId)
@@ -838,22 +868,53 @@ async function handleBlocksOperationTx(
838868

839869
logger.info(`Batch toggling handles for ${blockIds.length} blocks in workflow ${workflowId}`)
840870

841-
const blocks = await tx
842-
.select({ id: workflowBlocks.id, horizontalHandles: workflowBlocks.horizontalHandles })
871+
// Fetch all blocks to check lock status and filter out protected blocks
872+
const allBlocks = await tx
873+
.select({
874+
id: workflowBlocks.id,
875+
horizontalHandles: workflowBlocks.horizontalHandles,
876+
locked: workflowBlocks.locked,
877+
data: workflowBlocks.data,
878+
})
843879
.from(workflowBlocks)
844-
.where(and(eq(workflowBlocks.workflowId, workflowId), inArray(workflowBlocks.id, blockIds)))
880+
.where(eq(workflowBlocks.workflowId, workflowId))
845881

846-
for (const block of blocks) {
882+
type HandleBlockRecord = (typeof allBlocks)[number]
883+
const blocksById: Record<string, HandleBlockRecord> = Object.fromEntries(
884+
allBlocks.map((b: HandleBlockRecord) => [b.id, b])
885+
)
886+
887+
// Helper to check if a block is protected (locked or inside locked parent)
888+
const isProtected = (blockId: string): boolean => {
889+
const block = blocksById[blockId]
890+
if (!block) return false
891+
if (block.locked) return true
892+
const parentId = (block.data as Record<string, unknown> | null)?.parentId as
893+
| string
894+
| undefined
895+
if (parentId && blocksById[parentId]?.locked) return true
896+
return false
897+
}
898+
899+
// Filter to only toggle handles on unprotected blocks
900+
const blocksToToggle = blockIds.filter((id) => blocksById[id] && !isProtected(id))
901+
if (blocksToToggle.length === 0) {
902+
logger.info('All requested blocks are protected, skipping handles toggle')
903+
break
904+
}
905+
906+
for (const blockId of blocksToToggle) {
907+
const block = blocksById[blockId]
847908
await tx
848909
.update(workflowBlocks)
849910
.set({
850911
horizontalHandles: !block.horizontalHandles,
851912
updatedAt: new Date(),
852913
})
853-
.where(and(eq(workflowBlocks.id, block.id), eq(workflowBlocks.workflowId, workflowId)))
914+
.where(and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId)))
854915
}
855916

856-
logger.debug(`Batch toggled handles for ${blocks.length} blocks`)
917+
logger.debug(`Batch toggled handles for ${blocksToToggle.length} blocks`)
857918
break
858919
}
859920

@@ -930,19 +991,54 @@ async function handleBlocksOperationTx(
930991

931992
logger.info(`Batch updating parent for ${updates.length} blocks in workflow ${workflowId}`)
932993

994+
// Fetch all blocks to check lock status
995+
const allBlocks = await tx
996+
.select({
997+
id: workflowBlocks.id,
998+
locked: workflowBlocks.locked,
999+
data: workflowBlocks.data,
1000+
})
1001+
.from(workflowBlocks)
1002+
.where(eq(workflowBlocks.workflowId, workflowId))
1003+
1004+
type ParentBlockRecord = (typeof allBlocks)[number]
1005+
const blocksById: Record<string, ParentBlockRecord> = Object.fromEntries(
1006+
allBlocks.map((b: ParentBlockRecord) => [b.id, b])
1007+
)
1008+
1009+
// Helper to check if a block is protected (locked or inside locked parent)
1010+
const isProtected = (blockId: string): boolean => {
1011+
const block = blocksById[blockId]
1012+
if (!block) return false
1013+
if (block.locked) return true
1014+
const currentParentId = (block.data as Record<string, unknown> | null)?.parentId as
1015+
| string
1016+
| undefined
1017+
if (currentParentId && blocksById[currentParentId]?.locked) return true
1018+
return false
1019+
}
1020+
9331021
for (const update of updates) {
9341022
const { id, parentId, position } = update
9351023
if (!id) continue
9361024

1025+
// Skip protected blocks (locked or inside locked container)
1026+
if (isProtected(id)) {
1027+
logger.info(`Skipping block ${id} parent update - block is protected`)
1028+
continue
1029+
}
1030+
1031+
// Skip if trying to move into a locked container
1032+
if (parentId && blocksById[parentId]?.locked) {
1033+
logger.info(`Skipping block ${id} parent update - target parent ${parentId} is locked`)
1034+
continue
1035+
}
1036+
9371037
// Fetch current parent to update subflow node lists
938-
const [existing] = await tx
939-
.select({
940-
id: workflowBlocks.id,
941-
parentId: sql<string | null>`${workflowBlocks.data}->>'parentId'`,
942-
})
943-
.from(workflowBlocks)
944-
.where(and(eq(workflowBlocks.id, id), eq(workflowBlocks.workflowId, workflowId)))
945-
.limit(1)
1038+
const existing = blocksById[id]
1039+
const existingParentId = (existing?.data as Record<string, unknown> | null)?.parentId as
1040+
| string
1041+
| undefined
9461042

9471043
if (!existing) {
9481044
logger.warn(`Block ${id} not found for batch-update-parent`)
@@ -987,8 +1083,8 @@ async function handleBlocksOperationTx(
9871083
await updateSubflowNodeList(tx, workflowId, parentId)
9881084
}
9891085
// If the block had a previous parent, update that parent's node list as well
990-
if (existing?.parentId && existing.parentId !== parentId) {
991-
await updateSubflowNodeList(tx, workflowId, existing.parentId)
1086+
if (existingParentId && existingParentId !== parentId) {
1087+
await updateSubflowNodeList(tx, workflowId, existingParentId)
9921088
}
9931089
}
9941090

apps/sim/stores/workflows/workflow/store.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -412,13 +412,24 @@ export const useWorkflowStore = create<WorkflowStore>()(
412412
},
413413

414414
batchToggleHandles: (ids: string[]) => {
415-
const newBlocks = { ...get().blocks }
415+
const currentBlocks = get().blocks
416+
const newBlocks = { ...currentBlocks }
417+
418+
// Helper to check if a block is protected (locked or inside locked parent)
419+
const isProtected = (blockId: string): boolean => {
420+
const block = currentBlocks[blockId]
421+
if (!block) return false
422+
if (block.locked) return true
423+
const parentId = block.data?.parentId
424+
if (parentId && currentBlocks[parentId]?.locked) return true
425+
return false
426+
}
427+
416428
for (const id of ids) {
417-
if (newBlocks[id]) {
418-
newBlocks[id] = {
419-
...newBlocks[id],
420-
horizontalHandles: !newBlocks[id].horizontalHandles,
421-
}
429+
if (!newBlocks[id] || isProtected(id)) continue
430+
newBlocks[id] = {
431+
...newBlocks[id],
432+
horizontalHandles: !newBlocks[id].horizontalHandles,
422433
}
423434
}
424435
set({ blocks: newBlocks, edges: [...get().edges] })

0 commit comments

Comments
 (0)