feat(device-core): add authorized project read model
This commit is contained in:
@@ -237,6 +237,77 @@ test("management API exposes a repository idempotency conflict without retrying"
|
||||
}
|
||||
});
|
||||
|
||||
test("project query is service-authenticated and forwards only the trusted actor", async () => {
|
||||
let queriedActor;
|
||||
const runtime = await startTestServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async () => ({ replayed: false, result: {} }),
|
||||
listAccessibleProjects: async (actor) => {
|
||||
queriedActor = actor;
|
||||
return [{ projectRef: "project:11111111-1111-4111-8111-111111111111" }];
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const unauthorized = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/query/projects`,
|
||||
);
|
||||
assert.equal(unauthorized.status, 401);
|
||||
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/query/projects`,
|
||||
{ headers: managementHeaders() },
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal((await response.json()).projects.length, 1);
|
||||
assert.deepEqual(queriedActor, {
|
||||
userRef: "user:engineer",
|
||||
hubRole: "admin",
|
||||
groupRefs: ["group:engineers", "group:operators"],
|
||||
ownerScopes: [{ scopeKind: "company", ownerRef: "client:example" }],
|
||||
});
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("project workspace query accepts only a canonical project path", async () => {
|
||||
const projectId = "11111111-1111-4111-8111-111111111111";
|
||||
let queried;
|
||||
const runtime = await startTestServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async () => ({ replayed: false, result: {} }),
|
||||
getProjectWorkspace: async (actor, id) => {
|
||||
queried = { actor, id };
|
||||
return { project: { projectRef: `project:${id}` }, devices: [] };
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/query/projects/${projectId}/workspace`,
|
||||
{ headers: managementHeaders() },
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal((await response.json()).workspace.devices.length, 0);
|
||||
assert.equal(queried.id, projectId);
|
||||
|
||||
const invalid = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/query/projects/not-a-project/workspace`,
|
||||
{ headers: managementHeaders() },
|
||||
);
|
||||
assert.equal(invalid.status, 404);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("discovery ingest is closed by default", async () => {
|
||||
const runtime = await startTestServer({
|
||||
repository: {
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
getDeviceProjectWorkspace,
|
||||
listAccessibleDeviceProjects,
|
||||
} from "../src/project-query-repository.mjs";
|
||||
import { normalizeManagementActor } from "../src/project-management.mjs";
|
||||
|
||||
const projectId = "11111111-1111-4111-8111-111111111111";
|
||||
const actor = normalizeManagementActor({
|
||||
userRef: "user:device-admin",
|
||||
hubRole: "admin",
|
||||
groupRefs: ["group:engineers"],
|
||||
ownerScopes: [],
|
||||
});
|
||||
const timestamp = "2026-08-10T00:00:00.000Z";
|
||||
|
||||
test("project list applies direct-grant precedence and returns bounded summaries", async () => {
|
||||
const client = {
|
||||
async query(sql) {
|
||||
assert.match(sql, /from device_projects p/);
|
||||
return {
|
||||
rows: [
|
||||
projectGrantRow({
|
||||
grant_id: "22222222-2222-4222-8222-222222222222",
|
||||
principal_kind: "group",
|
||||
principal_ref: "group:engineers",
|
||||
project_role: "engineer",
|
||||
}),
|
||||
projectGrantRow({
|
||||
grant_id: "33333333-3333-4333-8333-333333333333",
|
||||
principal_kind: "user",
|
||||
principal_ref: "user:device-admin",
|
||||
project_role: "viewer",
|
||||
}),
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const projects = await listAccessibleDeviceProjects(client, actor);
|
||||
assert.equal(projects.length, 1);
|
||||
assert.equal(projects[0].access.projectRole, "viewer");
|
||||
assert.equal(projects[0].access.capabilities.includes("collection.manage"), false);
|
||||
assert.deepEqual(projects[0].counts, {
|
||||
devices: 3,
|
||||
collections: 2,
|
||||
discoveries: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test("project workspace returns only masked identity projections", async () => {
|
||||
const client = workspaceClient();
|
||||
const workspace = await getDeviceProjectWorkspace(client, actor, projectId);
|
||||
|
||||
assert.equal(workspace.project.projectRef, `project:${projectId}`);
|
||||
assert.equal(workspace.devices[0].identifier.masked, "***********0001");
|
||||
assert.equal(workspace.discoveries[0].identifier.masked, "***********0001");
|
||||
assert.equal(workspace.enrollments[0].expectedIdentifier.masked, "***********0001");
|
||||
const serialized = JSON.stringify(workspace);
|
||||
assert.equal(serialized.includes("hmac-sha256"), false);
|
||||
assert.equal(serialized.includes("ndc-credref"), false);
|
||||
});
|
||||
|
||||
test("project read source never selects identifier digests or credential refs", async () => {
|
||||
const source = await readFile(
|
||||
new URL("../src/project-query-repository.mjs", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/\b(?:identifier_digest|expected_identifier_digest|credential_ref)\b/,
|
||||
);
|
||||
});
|
||||
|
||||
function workspaceClient() {
|
||||
let grantReads = 0;
|
||||
return {
|
||||
async query(sql) {
|
||||
if (/from device_projects p/.test(sql)) {
|
||||
return { rows: [projectGrantRow()] };
|
||||
}
|
||||
if (/from device_project_grants/.test(sql)) {
|
||||
grantReads += 1;
|
||||
return { rows: [storedGrantRow()] };
|
||||
}
|
||||
if (/from device_instances di/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "44444444-4444-4444-8444-444444444444",
|
||||
device_key: "pilot-device",
|
||||
display_name: "Pilot device",
|
||||
model_profile_ref: "vendor.model.v1",
|
||||
lifecycle_state: "online",
|
||||
identifier_kind: "imei",
|
||||
identifier_masked: "***********0001",
|
||||
session_state: "online",
|
||||
last_seen_at: timestamp,
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
if (/from device_collections dc/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "55555555-5555-4555-8555-555555555555",
|
||||
collection_key: "pilot-fleet",
|
||||
name: "Pilot fleet",
|
||||
description: null,
|
||||
lifecycle_state: "active",
|
||||
member_count: "1",
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
if (/from device_discoveries dd/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "66666666-6666-4666-8666-666666666666",
|
||||
identifier_kind: "imei",
|
||||
identifier_masked: "***********0001",
|
||||
model_profile_ref: "vendor.model.v1",
|
||||
protocol: "INTERNAL",
|
||||
lifecycle_state: "quarantine",
|
||||
first_observed_at: timestamp,
|
||||
last_observed_at: timestamp,
|
||||
enrollment_intent_id: "77777777-7777-4777-8777-777777777777",
|
||||
claimed_device_id: null,
|
||||
}] };
|
||||
}
|
||||
if (/from device_enrollment_intents dei/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "77777777-7777-4777-8777-777777777777",
|
||||
enrollment_key: "pilot-enrollment",
|
||||
display_name: "Pilot device",
|
||||
model_profile_ref: "vendor.model.v1",
|
||||
expected_identifier_kind: "imei",
|
||||
expected_identifier_masked: "***********0001",
|
||||
lifecycle_state: "observed",
|
||||
observed_discovery_id: "66666666-6666-4666-8666-666666666666",
|
||||
claimed_device_id: null,
|
||||
expires_at: null,
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
throw new Error(`unexpected_query:${sql}`);
|
||||
},
|
||||
get grantReads() {
|
||||
return grantReads;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function projectGrantRow(overrides = {}) {
|
||||
return {
|
||||
id: projectId,
|
||||
project_key: "pilot-project",
|
||||
name: "Pilot project",
|
||||
description: null,
|
||||
lifecycle_state: "active",
|
||||
owner_scope_id: "88888888-8888-4888-8888-888888888888",
|
||||
scope_kind: "company",
|
||||
owner_ref: "client:dctouch",
|
||||
owner_display_name: "DCTOUCH",
|
||||
owner_lifecycle_state: "active",
|
||||
grant_id: "33333333-3333-4333-8333-333333333333",
|
||||
principal_kind: "user",
|
||||
principal_ref: "user:device-admin",
|
||||
project_role: "viewer",
|
||||
capability_allow: [],
|
||||
capability_deny: [],
|
||||
grant_lifecycle_state: "active",
|
||||
device_count: "3",
|
||||
collection_count: "2",
|
||||
discovery_count: "1",
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function storedGrantRow() {
|
||||
return {
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
principal_kind: "user",
|
||||
principal_ref: "user:device-admin",
|
||||
project_role: "admin",
|
||||
capability_allow: [],
|
||||
capability_deny: [],
|
||||
lifecycle_state: "active",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user