From 99102f921b759c9d0765ab27330fe3ac4b841e06 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 29 May 2026 19:59:29 +0800 Subject: [PATCH] fix: create device lease holder session first --- internal/cloud/access-control.test.ts | 62 +++++++++++++++++++++++++++ internal/cloud/access-control.ts | 34 ++++++++++----- 2 files changed, 86 insertions(+), 10 deletions(-) diff --git a/internal/cloud/access-control.test.ts b/internal/cloud/access-control.test.ts index 24ae1e8a..dfa77716 100644 --- a/internal/cloud/access-control.test.ts +++ b/internal/cloud/access-control.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { createServer } from "node:http"; import { test } from "bun:test"; +import { createAccessController } from "./access-control.ts"; import { createCloudApiServer } from "./server.ts"; const INTERNAL_TOKEN = "test-internal-token"; @@ -302,6 +303,67 @@ test("cloud api protects device-pod routes when access control is required", asy } }); +test("cloud api creates device lease holder session before lease insert", async () => { + const accessController = createAccessController({ + env: { + HWLAB_ACCESS_CONTROL_REQUIRED: "1", + HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin", + HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass" + }, + now: () => "2026-05-28T00:00:00.000Z" + }); + const store = accessController.store; + const leaseOrder = []; + const holderSessions = new Set(); + const recordAgentSessionOwner = store.recordAgentSessionOwner.bind(store); + store.recordAgentSessionOwner = async (input) => { + leaseOrder.push(`record:${input.sessionId}`); + holderSessions.add(input.sessionId); + return recordAgentSessionOwner(input); + }; + const acquireDeviceLease = store.acquireDeviceLease.bind(store); + store.acquireDeviceLease = async (input) => { + leaseOrder.push(`acquire:${input.holderSessionId}`); + if (!holderSessions.has(input.holderSessionId)) throw new Error("holder_session_fk_missing"); + return acquireDeviceLease(input); + }; + const server = createCloudApiServer({ + env: { + HWLAB_ACCESS_CONTROL_REQUIRED: "1", + HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin", + HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass" + }, + accessController, + now: () => "2026-05-28T00:00:00.000Z" + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + + try { + const { port } = server.address(); + const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" }); + const userCreate = await postJson(port, "/v1/admin/users", { username: "alice", password: "alice-pass" }, adminLogin.cookie); + await postJson(port, "/v1/admin/device-pods", { + devicePodId: "device-pod-71-freq", + profile: { schemaVersion: 1, target: { id: "target-71-freq" }, route: { gatewaySessionId: "gws_unused" } } + }, adminLogin.cookie); + await postJson(port, "/v1/admin/device-pod-grants", { devicePodId: "device-pod-71-freq", userId: userCreate.body.user.id }, adminLogin.cookie); + + const aliceLogin = await postJson(port, "/auth/login", { username: "alice", password: "alice-pass" }); + const lease = await postJson(port, "/v1/device-pods/device-pod-71-freq/leases", { + agentSessionId: "ses_fk_order_test", + reason: "fk order smoke", + ttlSeconds: 60 + }, aliceLogin.cookie); + + assert.equal(lease.status, 201); + assert.equal(lease.body.acquired, true); + assert.equal(lease.body.lease.holderSessionId, "ses_fk_order_test"); + assert.deepEqual(leaseOrder, ["record:ses_fk_order_test", "acquire:ses_fk_order_test"]); + } finally { + await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); + } +}); + test("cloud api dispatches authorized device jobs to the internal device-pod executor", async () => { const executorRequests = []; const executor = createServer(async (request, response) => { diff --git a/internal/cloud/access-control.ts b/internal/cloud/access-control.ts index c77514e0..2b131afb 100644 --- a/internal/cloud/access-control.ts +++ b/internal/cloud/access-control.ts @@ -523,16 +523,8 @@ class AccessController { const expiresAt = new Date(Date.parse(now) + ttlSeconds * 1000).toISOString(); const token = randomBytes(32).toString("base64url"); const holderSessionId = safeAgentSessionId(body.agentSessionId) || `ses_devicepod_${randomUUID()}`; - const lease = await this.store.acquireDeviceLease?.({ - devicePodId: pod.id, - holderSessionId, - holderUserId: actor.id, - leaseTokenHash: sha256(token), - now, - expiresAt - }); - if (!lease) { - const current = await this.store.getActiveDeviceLease?.(pod.id, now); + const current = await this.store.getActiveDeviceLease?.(pod.id, now); + if (current && current.holderUserId !== actor.id) { return sendJson(response, 409, { ok: false, status: 409, @@ -554,6 +546,28 @@ class AccessController { session: { source: "device-pod-lease", reason: textOr(body.reason, "") }, now }); + const lease = await this.store.acquireDeviceLease?.({ + devicePodId: pod.id, + holderSessionId, + holderUserId: actor.id, + leaseTokenHash: sha256(token), + now, + expiresAt + }); + if (!lease) { + const active = await this.store.getActiveDeviceLease?.(pod.id, now); + return sendJson(response, 409, { + ok: false, + status: 409, + error: { + code: "device_lease_conflict", + message: "Device Pod already has an active lease held by another actor" + }, + lease: publicDeviceLease(active), + devicePodId: pod.id, + profileHash: pod.profileHash + }); + } return sendJson(response, 201, { serviceId: CLOUD_API_SERVICE_ID, contractVersion: "device-pod-lease-v1",