fix(realtime): stop read-only members persisting block positions (#6174)

* fix(realtime): stop read-only members persisting block positions

The socket operation ACL granted the `read` role block.updatePosition and
blocks.batchUpdatePositions on the premise that they were ephemeral cursor
sync. They are not: both are followed by persistWorkflowOperation, which
UPDATEs workflow_blocks.positionX/positionY and bumps workflow.updatedAt. A
member holding only `read` on a workspace could therefore permanently rewrite
the coordinates of every block of every workflow in it — a write across the
read/write boundary.

Live cursors ride their own `cursor-update` event, and the smooth-drag
broadcast is the UNCOMMITTED position path, which returns before persisting and
never consults the role table — so the read role needs no grant at all.

* test(realtime): assert the role ACL against production, not a fixture

The shared ROLE_ALLOWED_OPERATIONS fixture still listed the two position
operations for the read role, and three tests compared that fixture against
itself — so they certified whatever it said, including the grants this PR
removes. They now assert checkRolePermission over the protocol's complete
operation list (the fixture's copy omits subblock/variable/admin-only ops), plus
one test that pins the fixture to the production ACL so the two cannot drift
apart again.
This commit is contained in:
Waleed
2026-08-01 17:51:10 -07:00
committed by GitHub
parent ccc2ec9507
commit e5d2f7d80a
4 changed files with 266 additions and 42 deletions
@@ -307,11 +307,18 @@ export type SocketOperation = (typeof SOCKET_OPERATIONS)[number]
/**
* Operations allowed for each role.
*
* A convenience mirror for fixtures — NOT the authority. The real ACL lives in
* `apps/realtime/src/middleware/permissions.ts`; assert against
* `checkRolePermission` rather than this table, or a drift between the two turns
* into a test that certifies whatever the fixture happens to say. (`read` listed
* the two position operations here while production had already granted them for
* real; both are persisted writes and neither role should hold them.)
*/
export const ROLE_ALLOWED_OPERATIONS: Record<PermissionType, readonly SocketOperation[]> = {
admin: SOCKET_OPERATIONS,
write: SOCKET_OPERATIONS,
read: [BLOCK_OPERATIONS.UPDATE_POSITION, BLOCKS_OPERATIONS.BATCH_UPDATE_POSITIONS],
read: [],
}
/**