Merge pull request #579 from pikasTech/fix/device-lease-fk-order-20260529

fix: create device lease holder session first
This commit is contained in:
Lyon
2026-05-29 20:00:15 +08:00
committed by GitHub
2 changed files with 86 additions and 10 deletions
+62
View File
@@ -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) => {
+24 -10
View File
@@ -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",