215 lines
6.9 KiB
JavaScript
215 lines
6.9 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { createDeviceManagerAuth } from "./device-manager-auth.mjs";
|
|
|
|
const internalToken = "launcher-internal-token-must-stay-server-side";
|
|
const launcherSessionId = "launcher-session-id-must-stay-server-side";
|
|
|
|
test("Launcher handoff becomes an opaque Device Manager session and trusted actor", async () => {
|
|
const calls = [];
|
|
const auth = createDeviceManagerAuth({
|
|
env: productionEnv(),
|
|
fetchImpl: async (url, init) => {
|
|
calls.push({ url: String(url), init });
|
|
return jsonResponse(200, {
|
|
ok: true,
|
|
launcherSessionId,
|
|
access: {
|
|
allowed: true,
|
|
hubRole: "owner",
|
|
ownerScopes: [
|
|
{
|
|
scopeKind: "company",
|
|
ownerRef: "client:client_dctouch",
|
|
displayName: "DC Touch",
|
|
},
|
|
{
|
|
scopeKind: "personal",
|
|
ownerRef: "user:user_root",
|
|
displayName: "DC SUDO",
|
|
},
|
|
],
|
|
},
|
|
user: {
|
|
id: "user_root",
|
|
email: "root@example.test",
|
|
name: "DC SUDO",
|
|
groups: ["nodedc:superadmin", "nodedc:device-core:admin"],
|
|
},
|
|
});
|
|
},
|
|
});
|
|
const response = mockResponse();
|
|
await auth.handleHandoff(
|
|
{ method: "GET", headers: {} },
|
|
response,
|
|
new URL("https://device.example.test/auth/nodedc/handoff?token=handoff-secret&next_path=%2F"),
|
|
);
|
|
|
|
assert.equal(response.statusCode, 302);
|
|
assert.equal(calls.length, 1);
|
|
assert.equal(calls[0].init.headers.Authorization, `Bearer ${internalToken}`);
|
|
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
token: "handoff-secret",
|
|
serviceSlug: "device-core",
|
|
});
|
|
const cookie = String(response.getHeader("set-cookie")).split(";", 1)[0];
|
|
assert.match(cookie, /^nodedc_device_manager_session=[A-Za-z0-9_-]{40,}$/);
|
|
assert.equal(cookie.includes("user_root"), false);
|
|
assert.equal(cookie.includes(launcherSessionId), false);
|
|
|
|
const request = { method: "GET", headers: { cookie, accept: "application/json" } };
|
|
const authorized = await auth.authorize(
|
|
request,
|
|
mockResponse(),
|
|
new URL("https://device.example.test/api/device-manager/session"),
|
|
);
|
|
assert.equal(authorized, false);
|
|
const context = auth.currentContext(request);
|
|
assert.equal(context.actor.userRef, "user:user_root");
|
|
assert.equal(context.actor.hubRole, "owner");
|
|
assert.deepEqual(context.actor.ownerScopes, [
|
|
{
|
|
scopeKind: "company",
|
|
ownerRef: "client:client_dctouch",
|
|
displayName: "DC Touch",
|
|
},
|
|
{
|
|
scopeKind: "personal",
|
|
ownerRef: "user:user_root",
|
|
displayName: "DC SUDO",
|
|
},
|
|
]);
|
|
assert.deepEqual(context.actor.groupRefs, [
|
|
"group:nodedc:device-core:admin",
|
|
"group:nodedc:superadmin",
|
|
]);
|
|
assert.equal(JSON.stringify(context).includes(launcherSessionId), false);
|
|
assert.equal(JSON.stringify(context).includes(internalToken), false);
|
|
});
|
|
|
|
test("invalid identity and explicit Device Core block never produce an actor", async () => {
|
|
for (const user of [
|
|
{ id: "?", groups: ["nodedc:device-core:admin"] },
|
|
{ id: "valid-user", groups: ["nodedc:superadmin", "nodedc:device-core:blocked"] },
|
|
]) {
|
|
const auth = createDeviceManagerAuth({
|
|
env: productionEnv(),
|
|
fetchImpl: async () => jsonResponse(200, {
|
|
ok: true,
|
|
launcherSessionId,
|
|
access: {
|
|
allowed: true,
|
|
hubRole: "admin",
|
|
ownerScopes: [],
|
|
},
|
|
user,
|
|
}),
|
|
});
|
|
const handoff = mockResponse();
|
|
await auth.handleHandoff(
|
|
{ method: "GET", headers: {} },
|
|
handoff,
|
|
new URL("https://device.example.test/auth/nodedc/handoff?token=handoff-secret"),
|
|
);
|
|
const cookie = String(handoff.getHeader("set-cookie")).split(";", 1)[0];
|
|
const request = { method: "GET", headers: { cookie, accept: "application/json" } };
|
|
const response = mockResponse();
|
|
assert.equal(await auth.authorize(
|
|
request,
|
|
response,
|
|
new URL("https://device.example.test/api/device-manager/session"),
|
|
), true);
|
|
assert.equal(response.statusCode, 403);
|
|
}
|
|
});
|
|
|
|
test("production auth fails closed when Launcher omits trusted Device Core access", async () => {
|
|
const auth = createDeviceManagerAuth({
|
|
env: productionEnv(),
|
|
fetchImpl: async () => jsonResponse(200, {
|
|
ok: true,
|
|
launcherSessionId,
|
|
user: {
|
|
id: "user_root",
|
|
email: "root@example.test",
|
|
name: "DC SUDO",
|
|
groups: ["nodedc:superadmin"],
|
|
},
|
|
}),
|
|
});
|
|
const handoff = mockResponse();
|
|
await auth.handleHandoff(
|
|
{ method: "GET", headers: {} },
|
|
handoff,
|
|
new URL("https://device.example.test/auth/nodedc/handoff?token=handoff-secret"),
|
|
);
|
|
const cookie = String(handoff.getHeader("set-cookie")).split(";", 1)[0];
|
|
const response = mockResponse();
|
|
assert.equal(await auth.authorize(
|
|
{ method: "GET", headers: { cookie, accept: "application/json" } },
|
|
response,
|
|
new URL("https://device.example.test/api/device-manager/session"),
|
|
), true);
|
|
assert.equal(response.statusCode, 403);
|
|
assert.equal(JSON.parse(response.body).error, "device_manager_access_denied");
|
|
});
|
|
|
|
test("an injected file-backed token takes precedence over broad platform env tokens", async () => {
|
|
const calls = [];
|
|
const auth = createDeviceManagerAuth({
|
|
env: { ...productionEnv(), NODEDC_INTERNAL_ACCESS_TOKEN: "broad-platform-token" },
|
|
internalToken: "scoped-file-token",
|
|
fetchImpl: async (url, init) => {
|
|
calls.push({ url, init });
|
|
return jsonResponse(200, {
|
|
ok: true,
|
|
launcherSessionId,
|
|
access: { allowed: true, hubRole: "member", ownerScopes: [] },
|
|
user: {
|
|
id: "device-member",
|
|
email: "member@example.test",
|
|
name: "Device Member",
|
|
groups: ["nodedc:device-core:access"],
|
|
},
|
|
});
|
|
},
|
|
});
|
|
await auth.handleHandoff(
|
|
{ method: "GET", headers: {} },
|
|
mockResponse(),
|
|
new URL("https://device.example.test/auth/nodedc/handoff?token=handoff-secret"),
|
|
);
|
|
assert.equal(calls[0].init.headers.Authorization, "Bearer scoped-file-token");
|
|
});
|
|
|
|
function productionEnv() {
|
|
return {
|
|
NODE_ENV: "production",
|
|
NODEDC_DEVICE_MANAGER_AUTH_REQUIRED: "true",
|
|
NODEDC_DEVICE_MANAGER_COOKIE_SECURE: "false",
|
|
NODEDC_LAUNCHER_BASE_URL: "https://launcher.example.test",
|
|
NODEDC_LAUNCHER_INTERNAL_URL: "http://launcher.internal.test",
|
|
NODEDC_INTERNAL_ACCESS_TOKEN: internalToken,
|
|
};
|
|
}
|
|
|
|
function mockResponse() {
|
|
const headers = new Map();
|
|
return {
|
|
statusCode: 200,
|
|
body: "",
|
|
setHeader(name, value) { headers.set(String(name).toLowerCase(), value); },
|
|
getHeader(name) { return headers.get(String(name).toLowerCase()); },
|
|
end(body = "") { this.body = String(body); },
|
|
};
|
|
}
|
|
|
|
function jsonResponse(status, body) {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|