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("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, }; }