feat(device-core): add authorized project read model

This commit is contained in:
Codex
2026-08-10 20:10:29 +03:00
parent 43dc9b1f45
commit f5d7916338
7 changed files with 629 additions and 3 deletions
+6 -2
View File
@@ -46,8 +46,12 @@ the remaining activation gates. The staged admission update keeps the relay
opaque but requires a public IPv4 source and bounds its source table and bytes
per direction; it does not enable router/NAT exposure.
The Foundry `Device Manager` is a canonical page template using a server-owned
`device-plane-control` binding. It is not a service in this directory.
The standalone Hub application `Device Core` / `Device Manager` is the human
control-plane shell. Its server-owned BFF calls the disabled-by-default
management and query API in `device-control-core`; browsers never receive the
Core bearer token and never author actor, role, group or owner-scope headers.
Foundry remains a downstream consumer for project-approved device data and is
not the device registry or administration boundary.
Run the foundation tests:
@@ -149,6 +149,61 @@ export function createControlCoreApp({
});
}
if (
request.method === "GET"
&& requestUrl.pathname === "/internal/v1/query/projects"
) {
if (!managementApiEnabled) {
return writeJson(response, 404, {
ok: false,
error: "device_management_api_disabled",
});
}
if (!matchesBearer(request.headers.authorization, managementToken)) {
return writeJson(response, 401, {
ok: false,
error: "device_management_auth_required",
});
}
if (typeof repository.listAccessibleProjects !== "function") {
return writeJson(response, 503, {
ok: false,
error: "device_query_repository_unavailable",
});
}
const actor = managementActorFromHeaders(request.headers);
const projects = await repository.listAccessibleProjects(actor);
return writeJson(response, 200, { ok: true, projects });
}
const workspaceProjectId = projectWorkspaceId(requestUrl.pathname);
if (request.method === "GET" && workspaceProjectId) {
if (!managementApiEnabled) {
return writeJson(response, 404, {
ok: false,
error: "device_management_api_disabled",
});
}
if (!matchesBearer(request.headers.authorization, managementToken)) {
return writeJson(response, 401, {
ok: false,
error: "device_management_auth_required",
});
}
if (typeof repository.getProjectWorkspace !== "function") {
return writeJson(response, 503, {
ok: false,
error: "device_query_repository_unavailable",
});
}
const actor = managementActorFromHeaders(request.headers);
const workspace = await repository.getProjectWorkspace(
actor,
workspaceProjectId,
);
return writeJson(response, 200, { ok: true, workspace });
}
if (
request.method === "POST"
&& requestUrl.pathname === "/internal/v1/device-discoveries:observe"
@@ -208,6 +263,13 @@ export function createControlCoreApp({
return server;
}
function projectWorkspaceId(pathname) {
const match = pathname.match(
/^\/internal\/v1\/query\/projects\/([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\/workspace$/i,
);
return match?.[1]?.toLowerCase() ?? null;
}
function managementActorFromHeaders(headers) {
return normalizeManagementActor({
userRef: singleHeader(headers["x-nodedc-user-ref"]),
@@ -507,7 +507,8 @@ async function transferDevice(client, actor, command) {
export async function findProjectWithCapability(client, actor, projectId, capability) {
const result = await client.query(
`select p.id, p.owner_scope_id, p.lifecycle_state,
`select p.id, p.owner_scope_id, p.project_key, p.name, p.description,
p.lifecycle_state, p.created_at, p.updated_at,
os.scope_kind, os.owner_ref, os.display_name as owner_display_name,
os.lifecycle_state as owner_lifecycle_state
from device_projects p
@@ -14,6 +14,10 @@ import {
import {
isControlResourceManagementCommand,
} from "./control-resource-management.mjs";
import {
getDeviceProjectWorkspace,
listAccessibleDeviceProjects,
} from "./project-query-repository.mjs";
import {
applyInfrastructureManagementCommand,
authorizeInfrastructureManagementReplay,
@@ -168,6 +172,33 @@ export class PostgresDeviceRepository {
}
}
async listAccessibleProjects(actor) {
return this.#executeRead((client) =>
listAccessibleDeviceProjects(client, actor)
);
}
async getProjectWorkspace(actor, projectId) {
return this.#executeRead((client) =>
getDeviceProjectWorkspace(client, actor, projectId)
);
}
async #executeRead(operation) {
const client = await this.pool.connect();
try {
await client.query("begin transaction read only");
const result = await operation(client);
await client.query("commit");
return result;
} catch (error) {
await client.query("rollback").catch(() => undefined);
throw mapPostgresError(error);
} finally {
client.release();
}
}
async close() {
await this.pool.end();
}
@@ -0,0 +1,265 @@
import {
resolveProjectAccess,
toProjectRef,
} from "./project-management.mjs";
import { findProjectWithCapability } from "./lifecycle-repository.mjs";
export async function listAccessibleDeviceProjects(client, actor) {
const result = await client.query(
`select p.id, p.project_key, p.name, p.description,
p.lifecycle_state, p.created_at, p.updated_at,
os.id as owner_scope_id, os.scope_kind, os.owner_ref,
os.display_name as owner_display_name,
g.id as grant_id, g.principal_kind, g.principal_ref,
g.project_role, g.capability_allow, g.capability_deny,
g.lifecycle_state as grant_lifecycle_state,
(select count(*)::bigint from device_instances di
where di.project_id = p.id) as device_count,
(select count(*)::bigint from device_collections dc
where dc.project_id = p.id and dc.lifecycle_state = 'active') as collection_count,
(select count(*)::bigint from device_discoveries dd
where dd.project_id = p.id and dd.lifecycle_state = 'quarantine') as discovery_count
from device_projects p
join device_owner_scopes os on os.id = p.owner_scope_id
join device_project_grants g on g.project_id = p.id
where p.lifecycle_state <> 'archived'
and os.lifecycle_state = 'active'
and g.lifecycle_state = 'active'
and (
(g.principal_kind = 'user' and g.principal_ref = $1)
or (g.principal_kind = 'group' and g.principal_ref = any($2::text[]))
)
order by os.display_name, p.name, g.created_at, g.id`,
[actor.userRef, actor.groupRefs],
);
const projects = new Map();
for (const row of result.rows) {
const entry = projects.get(row.id) ?? { row, grants: [] };
entry.grants.push(grantView(row));
projects.set(row.id, entry);
}
return [...projects.values()].flatMap(({ row, grants }) => {
const access = resolveProjectAccess({ actor, grants });
return access.allowed ? [projectSummaryView(row, access)] : [];
});
}
export async function getDeviceProjectWorkspace(client, actor, projectId) {
const project = await findProjectWithCapability(
client,
actor,
projectId,
"project.read",
);
const grantsResult = await client.query(
`select id, principal_kind, principal_ref, project_role,
capability_allow, capability_deny, lifecycle_state
from device_project_grants
where project_id = $1
order by created_at, id`,
[projectId],
);
const access = resolveProjectAccess({
actor,
grants: grantsResult.rows.map(storedGrantView),
});
const devices = await client.query(
`select di.id, di.device_key, di.display_name, di.model_profile_ref,
di.lifecycle_state, di.created_at, di.updated_at,
identifier.identifier_kind, identifier.identifier_masked,
session.lifecycle_state as session_state,
session.last_seen_at
from device_instances di
left join lateral (
select dri.identifier_kind, dri.identifier_masked
from device_restricted_identifiers dri
where dri.device_id = di.id
and dri.lifecycle_state = 'active'
order by dri.is_primary desc, dri.created_at
limit 1
) identifier on true
left join lateral (
select ds.lifecycle_state, ds.last_seen_at
from device_sessions ds
where ds.device_id = di.id
and ds.lifecycle_state in ('connecting', 'online')
order by ds.last_seen_at desc
limit 1
) session on true
where di.project_id = $1
order by di.display_name, di.id`,
[projectId],
);
const collections = await client.query(
`select dc.id, dc.collection_key, dc.name, dc.description,
dc.lifecycle_state, dc.created_at, dc.updated_at,
count(dcm.device_id)::bigint as member_count
from device_collections dc
left join device_collection_members dcm
on dcm.collection_id = dc.id and dcm.project_id = dc.project_id
where dc.project_id = $1
group by dc.id
order by dc.name, dc.id`,
[projectId],
);
const discoveries = await client.query(
`select dd.id, dd.identifier_kind, dd.identifier_masked,
dd.model_profile_ref, dd.protocol, dd.lifecycle_state,
dd.first_observed_at, dd.last_observed_at,
dd.enrollment_intent_id, dd.claimed_device_id
from device_discoveries dd
where dd.project_id = $1
order by dd.last_observed_at desc, dd.id`,
[projectId],
);
const enrollments = await client.query(
`select dei.id, dei.enrollment_key, dei.display_name,
dei.model_profile_ref, dei.expected_identifier_kind,
dei.expected_identifier_masked, dei.lifecycle_state,
dei.observed_discovery_id, dei.claimed_device_id,
dei.expires_at, dei.created_at, dei.updated_at
from device_enrollment_intents dei
where dei.project_id = $1
order by dei.updated_at desc, dei.id`,
[projectId],
);
return {
project: projectSummaryView(project, access),
devices: devices.rows.map(deviceView),
collections: collections.rows.map(collectionView),
discoveries: discoveries.rows.map(discoveryView),
enrollments: enrollments.rows.map(enrollmentView),
};
}
function projectSummaryView(row, access) {
return {
projectRef: toProjectRef(row.id),
projectKey: row.project_key,
name: row.name,
description: row.description ?? null,
lifecycleState: row.lifecycle_state,
ownerScope: {
ownerScopeRef: `owner-scope:${row.owner_scope_id}`,
scopeKind: row.scope_kind,
ownerRef: row.owner_ref,
displayName: row.owner_display_name,
},
access: {
projectRole: access.projectRole,
capabilities: access.capabilities,
},
counts: {
devices: numericCount(row.device_count),
collections: numericCount(row.collection_count),
discoveries: numericCount(row.discovery_count),
},
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function deviceView(row) {
return {
deviceRef: `device:${row.id}`,
deviceKey: row.device_key,
displayName: row.display_name,
modelProfileRef: row.model_profile_ref,
lifecycleState: row.lifecycle_state,
identifier: row.identifier_masked
? { kind: row.identifier_kind, masked: row.identifier_masked }
: null,
session: row.session_state
? { state: row.session_state, lastSeenAt: toIso(row.last_seen_at) }
: null,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function collectionView(row) {
return {
collectionRef: `collection:${row.id}`,
collectionKey: row.collection_key,
name: row.name,
description: row.description ?? null,
lifecycleState: row.lifecycle_state,
memberCount: numericCount(row.member_count),
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function discoveryView(row) {
return {
discoveryRef: `discovery:${row.id}`,
identifier: { kind: row.identifier_kind, masked: row.identifier_masked },
modelProfileRef: row.model_profile_ref,
protocol: row.protocol,
lifecycleState: row.lifecycle_state,
enrollmentIntentRef: row.enrollment_intent_id
? `enrollment:${row.enrollment_intent_id}`
: null,
claimedDeviceRef: row.claimed_device_id ? `device:${row.claimed_device_id}` : null,
firstObservedAt: toIso(row.first_observed_at),
lastObservedAt: toIso(row.last_observed_at),
};
}
function enrollmentView(row) {
return {
enrollmentIntentRef: `enrollment:${row.id}`,
enrollmentKey: row.enrollment_key,
displayName: row.display_name,
modelProfileRef: row.model_profile_ref,
expectedIdentifier: {
kind: row.expected_identifier_kind,
masked: row.expected_identifier_masked,
},
lifecycleState: row.lifecycle_state,
observedDiscoveryRef: row.observed_discovery_id
? `discovery:${row.observed_discovery_id}`
: null,
claimedDeviceRef: row.claimed_device_id ? `device:${row.claimed_device_id}` : null,
expiresAt: toIso(row.expires_at),
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function grantView(row) {
return storedGrantView({
id: row.grant_id,
principal_kind: row.principal_kind,
principal_ref: row.principal_ref,
project_role: row.project_role,
capability_allow: row.capability_allow,
capability_deny: row.capability_deny,
lifecycle_state: row.grant_lifecycle_state,
});
}
function storedGrantView(row) {
return {
grantRef: `grant:${row.id}`,
principalKind: row.principal_kind,
principalRef: row.principal_ref,
projectRole: row.project_role,
capabilityAllow: row.capability_allow ?? [],
capabilityDeny: row.capability_deny ?? [],
lifecycleState: row.lifecycle_state,
};
}
function numericCount(value) {
const count = Number(value ?? 0);
return Number.isSafeInteger(count) && count >= 0 ? count : 0;
}
function toIso(value) {
return value == null ? null : new Date(value).toISOString();
}
@@ -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",
};
}