diff --git a/lib/tasks/__tests__/validateDeleteTaskRequest.test.ts b/lib/tasks/__tests__/validateDeleteTaskRequest.test.ts index 1b6f9207..0b846e48 100644 --- a/lib/tasks/__tests__/validateDeleteTaskRequest.test.ts +++ b/lib/tasks/__tests__/validateDeleteTaskRequest.test.ts @@ -62,4 +62,29 @@ describe("validateDeleteTaskRequest", () => { const res = await validateDeleteTaskRequest(request); expect(res).toBe(forbidden); }); + + it("forwards body account_id to validateAuthContext as the override (chat#1918)", async () => { + const OTHER = "999e4567-e89b-12d3-a456-426614174999"; + const request = new NextRequest("http://localhost/api/tasks", { + method: "DELETE", + headers: { "Content-Type": "application/json", "x-api-key": "test-key" }, + body: JSON.stringify({ id: TASK_ID, account_id: OTHER }), + }); + await validateDeleteTaskRequest(request); + expect(validateAuthContext).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ accountId: OTHER }), + ); + }); + + it("rejects a non-UUID account_id with 400", async () => { + const request = new NextRequest("http://localhost/api/tasks", { + method: "DELETE", + headers: { "Content-Type": "application/json", "x-api-key": "test-key" }, + body: JSON.stringify({ id: TASK_ID, account_id: "not-a-uuid" }), + }); + const result = await validateDeleteTaskRequest(request); + expect(result).toBeInstanceOf(NextResponse); + expect((result as NextResponse).status).toBe(400); + }); }); diff --git a/lib/tasks/validateDeleteTaskBody.ts b/lib/tasks/validateDeleteTaskBody.ts index 3136f27d..2ffc5b44 100644 --- a/lib/tasks/validateDeleteTaskBody.ts +++ b/lib/tasks/validateDeleteTaskBody.ts @@ -4,6 +4,13 @@ import { z } from "zod"; export const deleteTaskBodySchema = z.object({ id: z.string().uuid("id must be a valid UUID").describe("UUID of the task to delete"), + account_id: z + .string() + .uuid("account_id must be a valid UUID") + .optional() + .describe( + "Account context for org/API-key rules; authorized via validateAuthContext. Not a raw column override.", + ), }); export type DeleteTaskBody = z.infer; diff --git a/lib/tasks/validateDeleteTaskRequest.ts b/lib/tasks/validateDeleteTaskRequest.ts index 5247515e..ff872444 100644 --- a/lib/tasks/validateDeleteTaskRequest.ts +++ b/lib/tasks/validateDeleteTaskRequest.ts @@ -40,7 +40,12 @@ export async function validateDeleteTaskRequest( ); } - const authContext = await validateAuthContext(request); + // Mirror GET/POST/PATCH: an org or admin key may act in a member account's + // context. Without this, an admin key could create and edit a customer's task + // but got 403 deleting it (chat#1918). + const authContext = await validateAuthContext(request, { + accountId: validationResult.data.account_id, + }); if (authContext instanceof NextResponse) { return authContext; }