feat(device-core): add authorized project read model
This commit is contained in:
@@ -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",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user