mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-01 10:14:56 -05:00
fix(socket): add server-side lock validation and admin-only permissions
1. BATCH_TOGGLE_LOCKED now requires admin role - non-admin users with write role can no longer bypass UI restriction via direct socket messages 2. BATCH_REMOVE_BLOCKS now validates lock status server-side - filters out protected blocks (locked or inside locked parent) before deletion 3. Remove duplicate/outdated comment in regenerateBlockIds Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -625,44 +625,74 @@ async function handleBlocksOperationTx(
|
||||
|
||||
logger.info(`Batch removing ${ids.length} blocks from workflow ${workflowId}`)
|
||||
|
||||
// Fetch all blocks to check lock status and filter out protected blocks
|
||||
const allBlocks = await tx
|
||||
.select({
|
||||
id: workflowBlocks.id,
|
||||
type: workflowBlocks.type,
|
||||
locked: workflowBlocks.locked,
|
||||
data: workflowBlocks.data,
|
||||
})
|
||||
.from(workflowBlocks)
|
||||
.where(eq(workflowBlocks.workflowId, workflowId))
|
||||
|
||||
type BlockRecord = (typeof allBlocks)[number]
|
||||
const blocksById: Record<string, BlockRecord> = Object.fromEntries(
|
||||
allBlocks.map((b: BlockRecord) => [b.id, b])
|
||||
)
|
||||
|
||||
// Helper to check if a block is protected (locked or inside locked parent)
|
||||
const isProtected = (blockId: string): boolean => {
|
||||
const block = blocksById[blockId]
|
||||
if (!block) return false
|
||||
if (block.locked) return true
|
||||
const parentId = (block.data as Record<string, unknown> | null)?.parentId as
|
||||
| string
|
||||
| undefined
|
||||
if (parentId && blocksById[parentId]?.locked) return true
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter out protected blocks from deletion request
|
||||
const deletableIds = ids.filter((id) => !isProtected(id))
|
||||
if (deletableIds.length === 0) {
|
||||
logger.info('All requested blocks are protected, skipping deletion')
|
||||
return
|
||||
}
|
||||
|
||||
if (deletableIds.length < ids.length) {
|
||||
logger.info(
|
||||
`Filtered out ${ids.length - deletableIds.length} protected blocks from deletion`
|
||||
)
|
||||
}
|
||||
|
||||
// Collect all block IDs including children of subflows
|
||||
const allBlocksToDelete = new Set<string>(ids)
|
||||
const allBlocksToDelete = new Set<string>(deletableIds)
|
||||
|
||||
for (const id of ids) {
|
||||
const blockToRemove = await tx
|
||||
.select({ type: workflowBlocks.type })
|
||||
.from(workflowBlocks)
|
||||
.where(and(eq(workflowBlocks.id, id), eq(workflowBlocks.workflowId, workflowId)))
|
||||
.limit(1)
|
||||
|
||||
if (blockToRemove.length > 0 && isSubflowBlockType(blockToRemove[0].type)) {
|
||||
const childBlocks = await tx
|
||||
.select({ id: workflowBlocks.id })
|
||||
.from(workflowBlocks)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowBlocks.workflowId, workflowId),
|
||||
sql`${workflowBlocks.data}->>'parentId' = ${id}`
|
||||
)
|
||||
)
|
||||
|
||||
childBlocks.forEach((child: { id: string }) => allBlocksToDelete.add(child.id))
|
||||
for (const id of deletableIds) {
|
||||
const block = blocksById[id]
|
||||
if (block && isSubflowBlockType(block.type)) {
|
||||
// Include all children of the subflow (they should be deleted with parent)
|
||||
for (const b of allBlocks) {
|
||||
const parentId = (b.data as Record<string, unknown> | null)?.parentId
|
||||
if (parentId === id) {
|
||||
allBlocksToDelete.add(b.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blockIdsArray = Array.from(allBlocksToDelete)
|
||||
|
||||
// Collect parent IDs BEFORE deleting blocks
|
||||
// Collect parent IDs BEFORE deleting blocks (use blocksById, already fetched)
|
||||
const parentIds = new Set<string>()
|
||||
for (const id of ids) {
|
||||
const parentInfo = await tx
|
||||
.select({ parentId: sql<string | null>`${workflowBlocks.data}->>'parentId'` })
|
||||
.from(workflowBlocks)
|
||||
.where(and(eq(workflowBlocks.id, id), eq(workflowBlocks.workflowId, workflowId)))
|
||||
.limit(1)
|
||||
|
||||
if (parentInfo.length > 0 && parentInfo[0].parentId) {
|
||||
parentIds.add(parentInfo[0].parentId)
|
||||
for (const id of deletableIds) {
|
||||
const block = blocksById[id]
|
||||
const parentId = (block?.data as Record<string, unknown> | null)?.parentId as
|
||||
| string
|
||||
| undefined
|
||||
if (parentId) {
|
||||
parentIds.add(parentId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@ import {
|
||||
|
||||
const logger = createLogger('SocketPermissions')
|
||||
|
||||
// All write operations (admin and write roles have same permissions)
|
||||
// Admin-only operations (require admin role)
|
||||
const ADMIN_ONLY_OPERATIONS: string[] = [BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED]
|
||||
|
||||
// Write operations (admin and write roles both have these permissions)
|
||||
const WRITE_OPERATIONS: string[] = [
|
||||
// Block operations
|
||||
BLOCK_OPERATIONS.UPDATE_POSITION,
|
||||
@@ -30,7 +33,6 @@ const WRITE_OPERATIONS: string[] = [
|
||||
BLOCKS_OPERATIONS.BATCH_REMOVE_BLOCKS,
|
||||
BLOCKS_OPERATIONS.BATCH_TOGGLE_ENABLED,
|
||||
BLOCKS_OPERATIONS.BATCH_TOGGLE_HANDLES,
|
||||
BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED,
|
||||
BLOCKS_OPERATIONS.BATCH_UPDATE_PARENT,
|
||||
// Edge operations
|
||||
EDGE_OPERATIONS.ADD,
|
||||
@@ -52,7 +54,7 @@ const READ_OPERATIONS: string[] = [
|
||||
|
||||
// Define operation permissions based on role
|
||||
const ROLE_PERMISSIONS: Record<string, string[]> = {
|
||||
admin: WRITE_OPERATIONS,
|
||||
admin: [...ADMIN_ONLY_OPERATIONS, ...WRITE_OPERATIONS],
|
||||
write: WRITE_OPERATIONS,
|
||||
read: READ_OPERATIONS,
|
||||
}
|
||||
|
||||
@@ -520,7 +520,6 @@ export function regenerateBlockIds(
|
||||
}
|
||||
} else {
|
||||
// Parent doesn't exist anywhere OR parent is locked - clear the relationship
|
||||
// Parent doesn't exist anywhere - clear the relationship
|
||||
block.data = { ...block.data, parentId: undefined, extent: undefined }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user