feat(device-core): add control resource ledger
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
DEVICE_CONTROL_RESOURCE_COMMAND_KINDS,
|
||||
normalizeControlResourceManagementCommand,
|
||||
} from "../src/control-resource-management.mjs";
|
||||
import {
|
||||
ALL_DEVICE_MANAGEMENT_COMMAND_KINDS,
|
||||
normalizeDeviceManagementCommand,
|
||||
} from "../src/management-command.mjs";
|
||||
|
||||
const projectRef = "project:11111111-1111-4111-8111-111111111111";
|
||||
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
|
||||
const collectionRef = "collection:33333333-3333-4333-8333-333333333333";
|
||||
const bindingRef = "binding:44444444-4444-4444-8444-444444444444";
|
||||
const revisionRef =
|
||||
"configuration-revision:55555555-5555-4555-8555-555555555555";
|
||||
|
||||
test("control resource commands join the strict idempotent surface", () => {
|
||||
for (const kind of DEVICE_CONTROL_RESOURCE_COMMAND_KINDS) {
|
||||
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes(kind), true);
|
||||
}
|
||||
assert.equal(
|
||||
normalizeDeviceManagementCommand(
|
||||
"device_binding.ensure",
|
||||
bindingInput(),
|
||||
).projectId,
|
||||
projectRef.slice("project:".length),
|
||||
);
|
||||
});
|
||||
|
||||
test("binding input is source-scoped and cannot claim external approval", () => {
|
||||
const command = normalizeControlResourceManagementCommand(
|
||||
"device_binding.ensure",
|
||||
bindingInput(),
|
||||
);
|
||||
|
||||
assert.deepEqual(command.source, {
|
||||
kind: "collection",
|
||||
id: collectionRef.slice("collection:".length),
|
||||
});
|
||||
assert.deepEqual(command.capabilities, ["inspect", "observe"]);
|
||||
assert.equal("lifecycleState" in command, false);
|
||||
assert.equal("externalApprovalRef" in command, false);
|
||||
assert.throws(
|
||||
() => normalizeControlResourceManagementCommand(
|
||||
"device_binding.ensure",
|
||||
{ ...bindingInput(), externalApprovalRef: "approval:forged" },
|
||||
),
|
||||
/device_management_command_field_unexpected:externalApprovalRef/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeControlResourceManagementCommand(
|
||||
"device_binding.ensure",
|
||||
{ ...bindingInput(), targetRef: "ndc-credref:must-not-be-a-target" },
|
||||
),
|
||||
/device_binding_target_ref_invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test("binding revoke uses only project, binding and bounded reason refs", () => {
|
||||
const command = normalizeControlResourceManagementCommand(
|
||||
"device_binding.revoke",
|
||||
{
|
||||
projectRef,
|
||||
bindingRef,
|
||||
resolutionCode: "operator.unbound",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(command.bindingId, bindingRef.slice("binding:".length));
|
||||
assert.equal(command.resolutionCode, "operator.unbound");
|
||||
});
|
||||
|
||||
test("configuration is canonical, bounded and secret-free before hashing", () => {
|
||||
const first = normalizeControlResourceManagementCommand(
|
||||
"device_configuration_revision.create",
|
||||
{
|
||||
projectRef,
|
||||
deviceRef,
|
||||
configuration: {
|
||||
reporting_interval_seconds: 15,
|
||||
motion: { enabled: true, threshold: 3.5 },
|
||||
channels: ["gps", "voltage"],
|
||||
},
|
||||
changeSummary: "Pilot reporting profile",
|
||||
},
|
||||
);
|
||||
const reordered = normalizeControlResourceManagementCommand(
|
||||
"device_configuration_revision.create",
|
||||
{
|
||||
projectRef,
|
||||
deviceRef,
|
||||
configuration: {
|
||||
channels: ["gps", "voltage"],
|
||||
motion: { threshold: 3.5, enabled: true },
|
||||
reporting_interval_seconds: 15,
|
||||
},
|
||||
changeSummary: "Pilot reporting profile",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(first.configurationDigest, reordered.configurationDigest);
|
||||
assert.equal(Object.isFrozen(first.configuration.motion), true);
|
||||
assert.throws(
|
||||
() => normalizeControlResourceManagementCommand(
|
||||
"device_configuration_revision.create",
|
||||
{
|
||||
projectRef,
|
||||
deviceRef,
|
||||
configuration: { api_token: "forbidden" },
|
||||
},
|
||||
),
|
||||
/forbidden_device_field/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeControlResourceManagementCommand(
|
||||
"device_configuration_revision.create",
|
||||
{
|
||||
projectRef,
|
||||
deviceRef,
|
||||
configuration: { tracker_imei: "000000000000001" },
|
||||
},
|
||||
),
|
||||
/safe_projection_contains_unmasked_imei/,
|
||||
);
|
||||
});
|
||||
|
||||
test("desired configuration binds one exact immutable revision", () => {
|
||||
const command = normalizeControlResourceManagementCommand(
|
||||
"device_configuration_desired.set",
|
||||
{
|
||||
projectRef,
|
||||
deviceRef,
|
||||
configurationRevisionRef: revisionRef,
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(command.deviceId, deviceRef.slice("device:".length));
|
||||
assert.equal(
|
||||
command.configurationRevisionId,
|
||||
revisionRef.slice("configuration-revision:".length),
|
||||
);
|
||||
assert.equal("applied" in command, false);
|
||||
});
|
||||
|
||||
function bindingInput() {
|
||||
return {
|
||||
projectRef,
|
||||
bindingKey: "robot2b-map",
|
||||
displayName: "Robot2B map binding",
|
||||
source: { kind: "collection", ref: collectionRef },
|
||||
targetKind: "foundry.application",
|
||||
targetRef: "foundry-application:robot2b-test",
|
||||
capabilities: ["observe", "inspect"],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const schemaUrl = new URL(
|
||||
"../migrations/010_device_control_resources.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const commandsUrl = new URL(
|
||||
"../migrations/011_device_control_resource_commands.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
|
||||
const appUrl = new URL("../src/app.mjs", import.meta.url);
|
||||
|
||||
test("control resource schema separates bindings, revisions and current state", async () => {
|
||||
const sql = await readFile(schemaUrl, "utf8");
|
||||
|
||||
assert.match(sql, /create table if not exists device_resource_bindings/);
|
||||
assert.match(sql, /pending_external_approval/);
|
||||
assert.match(sql, /external_approval_digest/);
|
||||
assert.match(sql, /device_binding_source_scope_mismatch/);
|
||||
assert.match(sql, /create table if not exists device_configuration_revisions/);
|
||||
assert.match(sql, /create table if not exists device_configuration_state/);
|
||||
assert.match(sql, /device_configuration_revision_scope_mismatch/);
|
||||
assert.match(sql, /unique \(device_id, revision_number\)/);
|
||||
});
|
||||
|
||||
test("command ledger has honest ordered states without a transport API", async () => {
|
||||
const sql = await readFile(schemaUrl, "utf8");
|
||||
const app = await readFile(appUrl, "utf8");
|
||||
|
||||
assert.match(sql, /create table if not exists device_commands/);
|
||||
assert.match(sql, /create table if not exists device_command_events/);
|
||||
for (const state of [
|
||||
"draft",
|
||||
"planned",
|
||||
"awaiting_confirmation",
|
||||
"queued",
|
||||
"dispatched",
|
||||
"acknowledged",
|
||||
"verified",
|
||||
"failed",
|
||||
"expired",
|
||||
"unknown",
|
||||
]) {
|
||||
assert.match(sql, new RegExp(`'${state}'`));
|
||||
}
|
||||
assert.match(sql, /device_command_initial_event_invalid/);
|
||||
assert.match(sql, /device_command_event_sequence_invalid/);
|
||||
assert.match(sql, /device_command_event_transition_invalid/);
|
||||
assert.match(sql, /device_command_event_projection_mismatch/);
|
||||
assert.match(sql, /device_command_events_current_projection_guard/);
|
||||
assert.match(sql, /device_command_current_projection_mismatch/);
|
||||
assert.doesNotMatch(app, /device-commands:(?:plan|confirm|dispatch)/);
|
||||
});
|
||||
|
||||
test("configuration, command history and audit are append-only", async () => {
|
||||
const sql = await readFile(schemaUrl, "utf8");
|
||||
|
||||
for (const table of [
|
||||
"device_configuration_revisions",
|
||||
"device_command_events",
|
||||
"device_audit_events",
|
||||
]) {
|
||||
assert.match(
|
||||
sql,
|
||||
new RegExp(`${table}_immutable_guard[\\s\\S]*before update or delete or truncate`),
|
||||
);
|
||||
}
|
||||
assert.match(sql, /device_immutable_record_mutation_forbidden/);
|
||||
assert.match(sql, /device_transfer_active_resource_binding/);
|
||||
assert.match(sql, /device_transfer_applied_configuration/);
|
||||
assert.match(sql, /device_transfer_nonterminal_command/);
|
||||
});
|
||||
|
||||
test("control resource schema contains no seeded device or raw secret material", async () => {
|
||||
const sql = await readFile(schemaUrl, "utf8");
|
||||
|
||||
assert.doesNotMatch(sql, /insert\s+into/i);
|
||||
assert.doesNotMatch(sql, /dcctouch|arusnavi|gelios|\bb2\b|imei/i);
|
||||
assert.doesNotMatch(sql, /password\s+text|token\s+text|secret\s+text|raw_command|raw_packet/i);
|
||||
});
|
||||
|
||||
test("commands extend receipts only after their schema", async () => {
|
||||
const commands = await readFile(commandsUrl, "utf8");
|
||||
const repository = await readFile(repositoryUrl, "utf8");
|
||||
|
||||
for (const kind of [
|
||||
"device_binding.ensure",
|
||||
"device_binding.revoke",
|
||||
"device_configuration_revision.create",
|
||||
"device_configuration_desired.set",
|
||||
]) {
|
||||
assert.match(commands, new RegExp(`'${kind.replace(".", "\\.")}'`));
|
||||
}
|
||||
const schemaIndex = repository.indexOf("010_device_control_resources.sql");
|
||||
const commandsIndex = repository.indexOf(
|
||||
"011_device_control_resource_commands.sql",
|
||||
);
|
||||
assert.notEqual(schemaIndex, -1);
|
||||
assert.notEqual(commandsIndex, -1);
|
||||
assert.ok(schemaIndex < commandsIndex);
|
||||
});
|
||||
@@ -0,0 +1,313 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { assertSafeProjection } from "../../../packages/device-protocol-contract/src/index.mjs";
|
||||
import { normalizeDeviceManagementCommand } from "../src/management-command.mjs";
|
||||
import { PostgresDeviceRepository } from "../src/postgres-repository.mjs";
|
||||
import { normalizeManagementActor } from "../src/project-management.mjs";
|
||||
|
||||
const now = new Date("2026-08-10T00:00:00.000Z");
|
||||
const projectId = "11111111-1111-4111-8111-111111111111";
|
||||
const ownerId = "22222222-2222-4222-8222-222222222222";
|
||||
const deviceId = "33333333-3333-4333-8333-333333333333";
|
||||
const collectionId = "44444444-4444-4444-8444-444444444444";
|
||||
const bindingId = "55555555-5555-4555-8555-555555555555";
|
||||
const revisionId = "66666666-6666-4666-8666-666666666666";
|
||||
|
||||
test("creates only a pending collection binding owned by the source project", async () => {
|
||||
const actor = managementActor();
|
||||
const command = normalizeDeviceManagementCommand("device_binding.ensure", {
|
||||
projectRef: `project:${projectId}`,
|
||||
bindingKey: "robot2b-map",
|
||||
displayName: "Robot2B map binding",
|
||||
source: { kind: "collection", ref: `collection:${collectionId}` },
|
||||
targetKind: "foundry.application",
|
||||
targetRef: "foundry-application:robot2b-test",
|
||||
capabilities: ["observe", "inspect"],
|
||||
});
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-binding"),
|
||||
projectStep(),
|
||||
grantsStep(actor),
|
||||
step("from device_collections", {
|
||||
rows: [{ id: collectionId, project_id: projectId, lifecycle_state: "active" }],
|
||||
}),
|
||||
step("insert into device_resource_bindings", {
|
||||
rows: [bindingRow({ source_kind: "collection", device_id: null })],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device_binding.ensure",
|
||||
command,
|
||||
digestCharacter: "a",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.binding.lifecycleState, "pending_external_approval");
|
||||
assert.equal(result.result.binding.source.ref, `collection:${collectionId}`);
|
||||
assert.equal("externalApprovalRef" in result.result.binding, false);
|
||||
assertSafeProjection(result.result);
|
||||
assert.equal(client.remaining(), 0);
|
||||
});
|
||||
|
||||
test("creates an immutable configuration revision from the active profile schema", async () => {
|
||||
const actor = managementActor();
|
||||
const command = createConfigurationCommand();
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-configuration-revision"),
|
||||
projectStep(),
|
||||
grantsStep(actor),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
step("from device_model_profiles", {
|
||||
rows: [{
|
||||
profile_ref: "vendor.model.protocol.v1",
|
||||
schema_artifact_ref: "schema:vendor.model.protocol.v1",
|
||||
lifecycle_state: "active",
|
||||
}],
|
||||
}),
|
||||
step("from device_configuration_revisions", {
|
||||
rows: [{ next_revision: "1" }],
|
||||
}),
|
||||
step("insert into device_configuration_revisions", {
|
||||
rows: [configurationRevisionRow({
|
||||
configuration_digest: command.configurationDigest,
|
||||
configuration: command.configuration,
|
||||
})],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device_configuration_revision.create",
|
||||
command,
|
||||
digestCharacter: "b",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.configurationRevision.revisionNumber, 1);
|
||||
assert.deepEqual(result.result.configurationRevision.configuration, {
|
||||
reporting_interval_seconds: 15,
|
||||
});
|
||||
assert.equal(
|
||||
result.result.configurationRevision.schemaArtifactRef,
|
||||
"schema:vendor.model.protocol.v1",
|
||||
);
|
||||
assertSafeProjection(result.result);
|
||||
assert.equal(client.remaining(), 0);
|
||||
});
|
||||
|
||||
test("sets desired configuration without claiming runtime apply", async () => {
|
||||
const actor = managementActor();
|
||||
const command = normalizeDeviceManagementCommand(
|
||||
"device_configuration_desired.set",
|
||||
{
|
||||
projectRef: `project:${projectId}`,
|
||||
deviceRef: `device:${deviceId}`,
|
||||
configurationRevisionRef: `configuration-revision:${revisionId}`,
|
||||
},
|
||||
);
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-configuration-desired"),
|
||||
projectStep(),
|
||||
grantsStep(actor),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
step("from device_configuration_revisions", {
|
||||
rows: [configurationRevisionRow()],
|
||||
}),
|
||||
step("from device_configuration_state", { rows: [] }),
|
||||
step("insert into device_configuration_state", {
|
||||
rows: [{
|
||||
device_id: deviceId,
|
||||
project_id: projectId,
|
||||
desired_revision_id: revisionId,
|
||||
applied_revision_id: null,
|
||||
}],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device_configuration_desired.set",
|
||||
command,
|
||||
digestCharacter: "c",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.changed, true);
|
||||
assert.equal(
|
||||
result.result.configurationState.desiredConfigurationRevisionRef,
|
||||
`configuration-revision:${revisionId}`,
|
||||
);
|
||||
assert.equal(
|
||||
result.result.configurationState.appliedConfigurationRevisionRef,
|
||||
null,
|
||||
);
|
||||
assert.equal("applied" in result.result, false);
|
||||
assert.equal(client.remaining(), 0);
|
||||
});
|
||||
|
||||
function createConfigurationCommand() {
|
||||
return normalizeDeviceManagementCommand(
|
||||
"device_configuration_revision.create",
|
||||
{
|
||||
projectRef: `project:${projectId}`,
|
||||
deviceRef: `device:${deviceId}`,
|
||||
configuration: { reporting_interval_seconds: 15 },
|
||||
changeSummary: "Pilot reporting profile",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function managementActor() {
|
||||
return normalizeManagementActor({
|
||||
userRef: "user:device-engineer",
|
||||
hubRole: "member",
|
||||
groupRefs: [],
|
||||
ownerScopes: [],
|
||||
});
|
||||
}
|
||||
|
||||
function projectStep() {
|
||||
return step("from device_projects p", {
|
||||
rows: [{
|
||||
id: projectId,
|
||||
owner_scope_id: ownerId,
|
||||
lifecycle_state: "active",
|
||||
scope_kind: "company",
|
||||
owner_ref: "client:example-company",
|
||||
owner_display_name: "Example Company",
|
||||
owner_lifecycle_state: "active",
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
function grantsStep(actor) {
|
||||
return step("from device_project_grants", {
|
||||
rows: [{
|
||||
id: "77777777-7777-4777-8777-777777777777",
|
||||
principal_kind: "user",
|
||||
principal_ref: actor.userRef,
|
||||
project_role: "engineer",
|
||||
capability_allow: [],
|
||||
capability_deny: [],
|
||||
lifecycle_state: "active",
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
function deviceRow() {
|
||||
return {
|
||||
id: deviceId,
|
||||
contour_id: null,
|
||||
owner_scope_id: ownerId,
|
||||
project_id: projectId,
|
||||
model_profile_ref: "vendor.model.protocol.v1",
|
||||
lifecycle_state: "claimed",
|
||||
};
|
||||
}
|
||||
|
||||
function bindingRow(overrides = {}) {
|
||||
return {
|
||||
id: bindingId,
|
||||
owner_scope_id: ownerId,
|
||||
project_id: projectId,
|
||||
binding_key: "robot2b-map",
|
||||
display_name: "Robot2B map binding",
|
||||
source_kind: "device",
|
||||
device_id: deviceId,
|
||||
collection_id: collectionId,
|
||||
target_kind: "foundry.application",
|
||||
target_ref: "foundry-application:robot2b-test",
|
||||
capabilities: ["inspect", "observe"],
|
||||
lifecycle_state: "pending_external_approval",
|
||||
source_approved_at: now,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function configurationRevisionRow(overrides = {}) {
|
||||
return {
|
||||
id: revisionId,
|
||||
owner_scope_id: ownerId,
|
||||
project_id: projectId,
|
||||
device_id: deviceId,
|
||||
revision_number: "1",
|
||||
model_profile_ref: "vendor.model.protocol.v1",
|
||||
schema_artifact_ref: "schema:vendor.model.protocol.v1",
|
||||
configuration_digest: `sha256:${"d".repeat(64)}`,
|
||||
configuration: { reporting_interval_seconds: 15 },
|
||||
change_summary: "Pilot reporting profile",
|
||||
created_at: now,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function receiptStep(id) {
|
||||
return step("insert into device_management_command_receipts", {
|
||||
rows: [{ id }],
|
||||
});
|
||||
}
|
||||
|
||||
function commandInput({ actor, commandKind, command, digestCharacter }) {
|
||||
return {
|
||||
idempotencyKey: `phase25-${commandKind.replaceAll(".", "-")}-0001`,
|
||||
commandKind,
|
||||
requestDigest: `sha256:${digestCharacter.repeat(64)}`,
|
||||
actor,
|
||||
command,
|
||||
};
|
||||
}
|
||||
|
||||
function repositoryWithClient(client) {
|
||||
return new PostgresDeviceRepository({
|
||||
pool: {
|
||||
query: async () => ({ rows: [] }),
|
||||
connect: async () => client,
|
||||
end: async () => undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function step(includes, result = { rows: [] }) {
|
||||
return { includes, result };
|
||||
}
|
||||
|
||||
function scriptedClient(steps) {
|
||||
const queue = [...steps];
|
||||
return {
|
||||
released: false,
|
||||
async query(sql) {
|
||||
const next = queue.shift();
|
||||
assert.ok(next, `Unexpected query: ${sql}`);
|
||||
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
|
||||
return next.result;
|
||||
},
|
||||
release() {
|
||||
this.released = true;
|
||||
},
|
||||
remaining() {
|
||||
return queue.length;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -111,6 +111,10 @@ test("authorized transfer preserves history and detaches source collections", as
|
||||
grantsStep(actor, "owner"),
|
||||
step("from device_sessions", { rows: [{ active: false }] }),
|
||||
step("from device_credential_bindings", { rows: [{ active: false }] }),
|
||||
step("from device_resource_bindings", { rows: [{ active: false }] }),
|
||||
step("from device_configuration_state", { rows: [] }),
|
||||
step("from device_commands", { rows: [{ active: false }] }),
|
||||
step("delete from device_configuration_state", { rows: [], rowCount: 0 }),
|
||||
step("delete from device_collection_members", { rows: [], rowCount: 2 }),
|
||||
step("update device_instances", {
|
||||
rows: [deviceRow({
|
||||
@@ -139,6 +143,7 @@ test("authorized transfer preserves history and detaches source collections", as
|
||||
assert.equal(result.result.device.projectRef, `project:${targetProjectId}`);
|
||||
assert.equal(result.result.detachedCollectionCount, 2);
|
||||
assert.equal(result.result.transferredIdentifierCount, 1);
|
||||
assert.equal(result.result.clearedDesiredConfiguration, false);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
@@ -173,6 +178,104 @@ test("transfer fails closed while a credential binding is active", async () => {
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("transfer fails closed while a resource binding is pending approval", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = transferCommand();
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-transfer-resource-bound"),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
projectStep(sourceProjectId, sourceOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
projectStep(targetProjectId, targetOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
step("from device_sessions", { rows: [{ active: false }] }),
|
||||
step("from device_credential_bindings", { rows: [{ active: false }] }),
|
||||
step("from device_resource_bindings", { rows: [{ active: true }] }),
|
||||
step("rollback"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
await assert.rejects(
|
||||
repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device.transfer",
|
||||
command,
|
||||
digestCharacter: "1",
|
||||
})),
|
||||
/device_transfer_active_resource_binding/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
});
|
||||
|
||||
test("transfer fails closed with applied configuration", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = transferCommand();
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-transfer-applied-config"),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
projectStep(sourceProjectId, sourceOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
projectStep(targetProjectId, targetOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
step("from device_sessions", { rows: [{ active: false }] }),
|
||||
step("from device_credential_bindings", { rows: [{ active: false }] }),
|
||||
step("from device_resource_bindings", { rows: [{ active: false }] }),
|
||||
step("from device_configuration_state", {
|
||||
rows: [{
|
||||
desired_revision_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
applied_revision_id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
||||
}],
|
||||
}),
|
||||
step("rollback"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
await assert.rejects(
|
||||
repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device.transfer",
|
||||
command,
|
||||
digestCharacter: "2",
|
||||
})),
|
||||
/device_transfer_applied_configuration/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
});
|
||||
|
||||
test("transfer fails closed with a nonterminal command", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = transferCommand();
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-transfer-command-active"),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
projectStep(sourceProjectId, sourceOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
projectStep(targetProjectId, targetOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
step("from device_sessions", { rows: [{ active: false }] }),
|
||||
step("from device_credential_bindings", { rows: [{ active: false }] }),
|
||||
step("from device_resource_bindings", { rows: [{ active: false }] }),
|
||||
step("from device_configuration_state", { rows: [] }),
|
||||
step("from device_commands", { rows: [{ active: true }] }),
|
||||
step("rollback"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
await assert.rejects(
|
||||
repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device.transfer",
|
||||
command,
|
||||
digestCharacter: "3",
|
||||
})),
|
||||
/device_transfer_nonterminal_command/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
});
|
||||
|
||||
test("reject resolves both quarantine and enrollment without exposing a digest", async () => {
|
||||
const actor = managementActor("member");
|
||||
const command = normalizeDeviceManagementCommand("discovery.reject", {
|
||||
|
||||
@@ -15,8 +15,11 @@ test("management surface is internal, POST-only and disabled by default", async
|
||||
assert.match(source, /\/internal\/v1\/management\/projects:ensure/);
|
||||
assert.match(source, /\/internal\/v1\/management\/collections:ensure/);
|
||||
assert.match(source, /\/internal\/v1\/management\/project-grants:upsert/);
|
||||
assert.match(source, /\/internal\/v1\/management\/device-bindings:ensure/);
|
||||
assert.match(source, /\/internal\/v1\/management\/device-configuration-revisions:create/);
|
||||
assert.match(source, /request\.method === "POST" && managementCommandKind/);
|
||||
assert.doesNotMatch(source, /\/api\/public\/.*management/);
|
||||
assert.doesNotMatch(source, /device-commands:(?:plan|confirm|dispatch)/);
|
||||
});
|
||||
|
||||
test("management token remains file-backed and is not enabled by current Compose", async () => {
|
||||
|
||||
@@ -138,6 +138,28 @@ test("matching group grants combine bounded operator and engineer capabilities",
|
||||
assert.equal(access.capabilities.includes("access.manage"), false);
|
||||
});
|
||||
|
||||
test("configuration mutation belongs to engineer and admin, not operator", () => {
|
||||
const engineer = resolveProjectAccess({
|
||||
actor: actor({ groupRefs: ["group:engineers"] }),
|
||||
grants: [grant({
|
||||
principalKind: "group",
|
||||
principalRef: "group:engineers",
|
||||
projectRole: "engineer",
|
||||
})],
|
||||
});
|
||||
const operator = resolveProjectAccess({
|
||||
actor: actor({ groupRefs: ["group:operators"] }),
|
||||
grants: [grant({
|
||||
principalKind: "group",
|
||||
principalRef: "group:operators",
|
||||
projectRole: "operator",
|
||||
})],
|
||||
});
|
||||
|
||||
assert.equal(engineer.capabilities.includes("configuration.manage"), true);
|
||||
assert.equal(operator.capabilities.includes("configuration.manage"), false);
|
||||
});
|
||||
|
||||
test("Hub ceiling and explicit deny prevent privilege escalation", () => {
|
||||
const ownerGrant = grant({
|
||||
grantRef: "grant:owner",
|
||||
|
||||
Reference in New Issue
Block a user