Merge pull request #579 from pikasTech/fix/device-lease-fk-order-20260529
fix: create device lease holder session first
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user