


























@@ -181,6 +181,40 @@ describe("deviceHandlers", () => {
181181);
182182});
183183184+it("rejects removing mixed-role devices without admin scope", async () => {
185+getPairedDeviceMock.mockResolvedValue({
186+deviceId: "device-1",
187+role: "operator",
188+roles: ["operator", "node"],
189+tokens: {
190+operator: {
191+token: "operator-token",
192+role: "operator",
193+scopes: ["operator.pairing"],
194+createdAtMs: 100,
195+},
196+node: {
197+token: "node-token",
198+role: "node",
199+scopes: [],
200+createdAtMs: 100,
201+revokedAtMs: 200,
202+},
203+},
204+});
205+const opts = createOptions(
206+"device.pair.remove",
207+{ deviceId: "device-1" },
208+{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: true }) },
209+);
210+211+await deviceHandlers["device.pair.remove"](opts);
212+213+expect(removePairedDeviceMock).not.toHaveBeenCalled();
214+expect(opts.context.disconnectClientsForDevice).not.toHaveBeenCalled();
215+expectRespondedErrorMessage(opts, "device pairing removal denied");
216+});
217+184218it("disconnects active clients after revoking a device token", async () => {
185219revokeDeviceTokenMock.mockResolvedValue({
186220ok: true,
@@ -234,6 +268,20 @@ describe("deviceHandlers", () => {
234268);
235269});
236270271+it("rejects revoking node tokens without admin scope", async () => {
272+const opts = createOptions(
273+"device.token.revoke",
274+{ deviceId: "device-1", role: "node" },
275+{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: true }) },
276+);
277+278+await deviceHandlers["device.token.revoke"](opts);
279+280+expect(revokeDeviceTokenMock).not.toHaveBeenCalled();
281+expect(opts.context.disconnectClientsForDevice).not.toHaveBeenCalled();
282+expectRespondedErrorMessage(opts, "device token revocation denied");
283+});
284+237285it("treats normalized device ids as self-owned for token revocation", async () => {
238286revokeDeviceTokenMock.mockResolvedValue({
239287ok: true,
@@ -336,6 +384,65 @@ describe("deviceHandlers", () => {
336384);
337385});
338386387+it("allows pairing-scoped device sessions to manage their own operator token", async () => {
388+rotateDeviceTokenMock.mockResolvedValue({
389+ok: true,
390+entry: {
391+token: "rotated-token",
392+role: "operator",
393+scopes: ["operator.pairing"],
394+createdAtMs: 456,
395+rotatedAtMs: 789,
396+},
397+});
398+revokeDeviceTokenMock.mockResolvedValue({
399+ok: true,
400+entry: { role: "operator", revokedAtMs: 987 },
401+});
402+403+const rotateOpts = createOptions(
404+"device.token.rotate",
405+{ deviceId: "device-1", role: "operator", scopes: ["operator.pairing"] },
406+{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: true }) },
407+);
408+const revokeOpts = createOptions(
409+"device.token.revoke",
410+{ deviceId: "device-1", role: "operator" },
411+{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: true }) },
412+);
413+414+await deviceHandlers["device.token.rotate"](rotateOpts);
415+await deviceHandlers["device.token.revoke"](revokeOpts);
416+417+expect(rotateDeviceTokenMock).toHaveBeenCalledWith({
418+deviceId: "device-1",
419+role: "operator",
420+scopes: ["operator.pairing"],
421+callerScopes: ["operator.pairing"],
422+});
423+expect(revokeDeviceTokenMock).toHaveBeenCalledWith({
424+deviceId: "device-1",
425+role: "operator",
426+callerScopes: ["operator.pairing"],
427+});
428+expect(rotateOpts.respond).toHaveBeenCalledWith(
429+true,
430+{
431+deviceId: "device-1",
432+role: "operator",
433+token: "rotated-token",
434+scopes: ["operator.pairing"],
435+rotatedAtMs: 789,
436+},
437+undefined,
438+);
439+expect(revokeOpts.respond).toHaveBeenCalledWith(
440+true,
441+{ deviceId: "device-1", role: "operator", revokedAtMs: 987 },
442+undefined,
443+);
444+});
445+339446it("omits rotated tokens when an admin rotates another device token", async () => {
340447mockPairedOperatorDevice();
341448mockRotateOperatorTokenSuccess();
@@ -368,8 +475,27 @@ describe("deviceHandlers", () => {
368475});
369476370477it("rejects rotating a token for a role that was never approved", async () => {
371-mockPairedOperatorDevice();
372478rotateDeviceTokenMock.mockResolvedValue({ ok: false, reason: "unknown-device-or-role" });
479+const opts = createOptions(
480+"device.token.rotate",
481+{ deviceId: "device-1", role: "node" },
482+{ client: createClient(["operator.admin"], "admin-device", { isDeviceTokenAuth: true }) },
483+);
484+485+await deviceHandlers["device.token.rotate"](opts);
486+487+expect(rotateDeviceTokenMock).toHaveBeenCalledWith({
488+deviceId: "device-1",
489+role: "node",
490+scopes: undefined,
491+callerScopes: ["operator.admin"],
492+});
493+expect(opts.context.disconnectClientsForDevice).not.toHaveBeenCalled();
494+expectRespondedErrorMessage(opts, "device token rotation denied");
495+});
496+497+it("rejects rotating node tokens without admin scope", async () => {
498+mockPairedOperatorDevice();
373499const opts = createOptions(
374500"device.token.rotate",
375501{
@@ -387,12 +513,7 @@ describe("deviceHandlers", () => {
387513388514await deviceHandlers["device.token.rotate"](opts);
389515390-expect(rotateDeviceTokenMock).toHaveBeenCalledWith({
391-deviceId: "device-1",
392-role: "node",
393-scopes: undefined,
394-callerScopes: ["operator.pairing"],
395-});
516+expect(rotateDeviceTokenMock).not.toHaveBeenCalled();
396517expect(opts.context.disconnectClientsForDevice).not.toHaveBeenCalled();
397518expectRespondedErrorMessage(opts, "device token rotation denied");
398519});
@@ -688,6 +809,27 @@ describe("deviceHandlers", () => {
688809);
689810});
690811812+it("rejects approving node roles for the caller device without admin scope", async () => {
813+getPendingDevicePairingMock.mockResolvedValue({
814+requestId: "req-1",
815+deviceId: " device-1 ",
816+publicKey: "pk-1",
817+role: "node",
818+roles: ["node"],
819+ts: 100,
820+});
821+const opts = createOptions(
822+"device.pair.approve",
823+{ requestId: "req-1" },
824+{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: true }) },
825+);
826+827+await deviceHandlers["device.pair.approve"](opts);
828+829+expect(approveDevicePairingMock).not.toHaveBeenCalled();
830+expectRespondedErrorMessage(opts, "device pairing approval denied");
831+});
832+691833it("rejects rejecting another device from a non-admin device session", async () => {
692834getPendingDevicePairingMock.mockResolvedValue({
693835requestId: "req-2",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。