Check that a schedule's zone belongs to the caller's workspace

Creating a schedule validates every reference it carries against the caller's workspace — content,
widget, layout, playlist all go through checkRefInWorkspace. zone_id was the one polymorphic
reference left out of that list, so a schedule could be pointed at a zone belonging to another
workspace's layout.

It needed its own check rather than a sixth entry in the table: layout_zones has no workspace_id
column of its own. A zone belongs to a layout, and the layout carries the workspace, so the
ownership question has to be answered through that join. A zone on a platform-template layout
(workspace_id IS NULL) is allowed, matching how the other references treat templates.

882 server tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
This commit is contained in:
Claude 2026-07-30 21:54:24 -05:00
parent 4a65c4cec7
commit 80c8c81fb8

View file

@ -94,6 +94,19 @@ function workspaceAccess(req, workspaceId) {
// / layout / playlist refs (where workspace_id IS NULL is the platform-template
// path and is always allowed) and for devices / device_groups (where
// workspace_id is required - those tables never carry template rows).
// layout_zones has no workspace_id of its own — a zone belongs to a layout, and the layout carries
// the workspace. zone_id was the one polymorphic reference missing from the ownership checks, so a
// schedule could be pointed at a zone in someone else's workspace.
function checkZoneInWorkspace(zoneId, workspaceId) {
const row = db.prepare(
'SELECT l.workspace_id FROM layout_zones z JOIN layouts l ON l.id = z.layout_id WHERE z.id = ?'
).get(zoneId);
if (!row) return { status: 404, error: 'zone not found' };
if (row.workspace_id === workspaceId) return null;
if (row.workspace_id == null) return null; // platform-template layout
return { status: 403, error: 'zone is not in this workspace' };
}
function checkRefInWorkspace(table, id, workspaceId, opts = { allowNullWorkspace: false }) {
const row = db.prepare(`SELECT workspace_id FROM ${table} WHERE id = ?`).get(id);
if (!row) return { status: 404, error: `${table.replace(/_/g, ' ').slice(0, -1)} not found` };
@ -243,6 +256,10 @@ router.post('/', (req, res) => {
const err = checkRefInWorkspace(table, id, targetWorkspaceId, { allowNullWorkspace: allowNull });
if (err) return res.status(err.status).json({ error: err.error });
}
if (zone_id) {
const zErr = checkZoneInWorkspace(zone_id, targetWorkspaceId);
if (zErr) return res.status(zErr.status).json({ error: zErr.error });
}
const id = uuidv4();
db.prepare(`