Files
NODEDC_PLATFORM/device-plane/services/device-control-core/test/project-management.test.mjs
T

294 lines
8.1 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import {
assertActorCanManageOwnerScope,
assertGrantMutationAllowed,
assertProjectCapability,
normalizeManagementActor,
normalizeManagementCommand,
resolveProjectAccess,
} from "../src/project-management.mjs";
const projectRef = "project:11111111-1111-4111-8111-111111111111";
test("normalizes strict generic management commands without seeded entities", () => {
const project = normalizeManagementCommand("project.ensure", {
scopeKind: "company",
ownerRef: "client:example",
projectKey: "field-devices",
name: "Field Devices",
description: "Generic project",
});
assert.deepEqual(project, {
scopeKind: "company",
ownerRef: "client:example",
projectKey: "field-devices",
name: "Field Devices",
description: "Generic project",
});
assert.throws(
() => normalizeManagementCommand("project.ensure", {
scopeKind: "company",
ownerRef: "client:example",
projectKey: "field-devices",
name: "Field Devices",
rawPayload: "forbidden",
}),
/device_management_command_field_unexpected:rawPayload/,
);
});
test("company scope requires an asserted scope and Hub admin ceiling", () => {
const scope = { scopeKind: "company", ownerRef: "client:example" };
assert.doesNotThrow(() => assertActorCanManageOwnerScope(actor({
hubRole: "admin",
ownerScopes: [scope],
}), scope));
assert.throws(
() => assertActorCanManageOwnerScope(actor({
hubRole: "viewer",
ownerScopes: [scope],
}), scope),
/device_owner_scope_access_denied/,
);
assert.throws(
() => assertActorCanManageOwnerScope(actor({ hubRole: "owner" }), scope),
/device_owner_scope_access_denied/,
);
});
test("personal scope is isolated to the matching Hub user", () => {
const scope = { scopeKind: "personal", ownerRef: "user:engineer" };
assert.doesNotThrow(() => assertActorCanManageOwnerScope(
actor({ hubRole: "owner" }),
scope,
));
assert.throws(
() => assertActorCanManageOwnerScope(
actor({ hubRole: "member" }),
scope,
),
/device_owner_scope_access_denied/,
);
assert.throws(
() => assertActorCanManageOwnerScope(
actor({ userRef: "user:other", hubRole: "owner" }),
scope,
),
/device_owner_scope_access_denied/,
);
});
test("Hub owner has no project access without an explicit project grant", () => {
const access = resolveProjectAccess({
actor: actor({ hubRole: "owner" }),
grants: [],
});
assert.equal(access.allowed, false);
assert.deepEqual(access.capabilities, []);
});
test("a direct user grant overrides broader group grants", () => {
const access = resolveProjectAccess({
actor: actor({ hubRole: "owner", groupRefs: ["group:admins"] }),
grants: [
grant({
grantRef: "grant:group-admin",
principalKind: "group",
principalRef: "group:admins",
projectRole: "admin",
}),
grant({
grantRef: "grant:direct-viewer",
principalKind: "user",
principalRef: "user:engineer",
projectRole: "viewer",
}),
],
});
assert.equal(access.projectRole, "viewer");
assert.equal(access.capabilities.includes("access.manage"), false);
});
test("matching group grants combine bounded operator and engineer capabilities", () => {
const access = resolveProjectAccess({
actor: actor({
hubRole: "member",
groupRefs: ["group:operators", "group:engineers"],
}),
grants: [
grant({
grantRef: "grant:operator",
principalKind: "group",
principalRef: "group:operators",
projectRole: "operator",
}),
grant({
grantRef: "grant:engineer",
principalKind: "group",
principalRef: "group:engineers",
projectRole: "engineer",
}),
],
});
assert.equal(access.capabilities.includes("device.enroll"), true);
assert.equal(access.capabilities.includes("command.dispatch"), true);
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",
principalKind: "user",
principalRef: "user:engineer",
projectRole: "owner",
capabilityDeny: ["credential.manage"],
});
const hubAdmin = resolveProjectAccess({
actor: actor({ hubRole: "admin" }),
grants: [ownerGrant],
});
const hubOwner = resolveProjectAccess({
actor: actor({ hubRole: "owner" }),
grants: [ownerGrant],
});
assert.equal(hubAdmin.capabilities.includes("device.transfer"), false);
assert.equal(hubOwner.capabilities.includes("device.transfer"), true);
assert.equal(hubOwner.capabilities.includes("credential.manage"), false);
});
test("owner grant mutations require both Hub and project ownership authority", () => {
const grants = [grant({
grantRef: "grant:owner",
principalKind: "user",
principalRef: "user:engineer",
projectRole: "owner",
})];
const ownerCommand = normalizeManagementCommand("project_grant.upsert", {
projectRef,
principalKind: "user",
principalRef: "user:second-owner",
projectRole: "owner",
});
assert.throws(
() => assertGrantMutationAllowed(
actor({ hubRole: "admin" }),
grants,
ownerCommand,
),
/device_project_owner_transfer_denied/,
);
assert.doesNotThrow(() => assertGrantMutationAllowed(
actor({ hubRole: "owner" }),
grants,
ownerCommand,
));
});
test("grant normalization rejects group owners and capability overlap", () => {
assert.throws(
() => normalizeManagementCommand("project_grant.upsert", {
projectRef,
principalKind: "group",
principalRef: "group:owners",
projectRole: "owner",
}),
/device_project_owner_must_be_user/,
);
assert.throws(
() => normalizeManagementCommand("project_grant.upsert", {
projectRef,
principalKind: "user",
principalRef: "user:operator",
projectRole: "operator",
capabilityAllow: ["command.dispatch"],
capabilityDeny: ["command.dispatch"],
}),
/device_project_capability_overlap/,
);
});
test("capability checks fail closed for inactive or unrelated grants", () => {
assert.throws(
() => assertProjectCapability(
actor({ hubRole: "owner" }),
[grant({ lifecycleState: "revoked" })],
"project.read",
),
/device_project_capability_denied/,
);
});
test("denying project.read collapses every derived capability", () => {
const access = resolveProjectAccess({
actor: actor({ hubRole: "owner" }),
grants: [grant({
projectRole: "owner",
capabilityDeny: ["project.read"],
})],
});
assert.equal(access.allowed, false);
assert.deepEqual(access.capabilities, []);
assert.throws(
() => assertProjectCapability(
actor({ hubRole: "owner" }),
[grant({
projectRole: "owner",
capabilityDeny: ["project.read"],
})],
"access.manage",
),
/device_project_capability_denied/,
);
});
function actor(overrides = {}) {
return normalizeManagementActor({
userRef: "user:engineer",
hubRole: "member",
groupRefs: [],
ownerScopes: [],
...overrides,
});
}
function grant(overrides = {}) {
return {
grantRef: "grant:default",
principalKind: "user",
principalRef: "user:engineer",
projectRole: "viewer",
capabilityAllow: [],
capabilityDeny: [],
lifecycleState: "active",
...overrides,
};
}