|
| 1 | +/** |
| 2 | + * @vitest-environment node |
| 3 | + */ |
| 4 | +import { describe, expect, it } from 'vitest' |
| 5 | +import { apiKeyOperations } from '@/lib/api-key/application/operations' |
| 6 | +import { mcpServerOperations } from '@/lib/mcp/application/operations' |
| 7 | +import { workflowOperations } from '@/lib/workflows/application/operations' |
| 8 | + |
| 9 | +/** |
| 10 | + * The deployment permission matrix, asserted in one place. |
| 11 | + * |
| 12 | + * `minimumRole` is declarative data, so a single-character edit silently widens |
| 13 | + * or narrows access with no failing test — the surrounding suites assert which |
| 14 | + * operation a handler invoked, not what role that operation demands. This pins |
| 15 | + * the boundary that matters: deploying a workflow needs `write`, while minting a |
| 16 | + * workspace API key (a credential that can invoke every deployed workflow in the |
| 17 | + * workspace) and installing a custom block (code that runs in every workflow) |
| 18 | + * stay `admin`. |
| 19 | + */ |
| 20 | +describe('deployment permission matrix', () => { |
| 21 | + describe('deployment lifecycle requires write', () => { |
| 22 | + it.each([ |
| 23 | + ['deploy', workflowOperations.deploy], |
| 24 | + ['undeploy', workflowOperations.undeploy], |
| 25 | + ['deployChat', workflowOperations.deployChat], |
| 26 | + ['undeployChat', workflowOperations.undeployChat], |
| 27 | + ['updatePublicApi', workflowOperations.updatePublicApi], |
| 28 | + ['activateVersion', workflowOperations.activateVersion], |
| 29 | + ['revertVersion', workflowOperations.revertVersion], |
| 30 | + ['updateVersion', workflowOperations.updateVersion], |
| 31 | + ])('workflowOperations.%s', (_name, operation) => { |
| 32 | + expect(operation.minimumRole).toBe('write') |
| 33 | + }) |
| 34 | + |
| 35 | + it.each([ |
| 36 | + ['createWorkflowServer', mcpServerOperations.createWorkflowDeploymentServer], |
| 37 | + ['updateWorkflowServer', mcpServerOperations.updateWorkflowDeploymentServer], |
| 38 | + ['deleteWorkflowServer', mcpServerOperations.deleteWorkflowDeploymentServer], |
| 39 | + ['deployTool', mcpServerOperations.deployWorkflowTool], |
| 40 | + ['undeployTool', mcpServerOperations.undeployWorkflowTool], |
| 41 | + ])('mcpServerOperations.%s', (_name, operation) => { |
| 42 | + expect(operation.minimumRole).toBe('write') |
| 43 | + }) |
| 44 | + }) |
| 45 | + |
| 46 | + describe('credential and code-injection surfaces stay admin', () => { |
| 47 | + it('minting a workspace API key requires admin', () => { |
| 48 | + expect(apiKeyOperations.createFromCopilot.minimumRole).toBe('admin') |
| 49 | + }) |
| 50 | + |
| 51 | + it('workflow policy (lock) requires admin', () => { |
| 52 | + expect(workflowOperations.updatePolicy.minimumRole).toBe('admin') |
| 53 | + }) |
| 54 | + }) |
| 55 | + |
| 56 | + /** |
| 57 | + * Scope note: this asserts the operation *declarations*, which govern every |
| 58 | + * surface routed through the operation registry. The v1 REST API |
| 59 | + * (`resolveV1DeploymentWorkflow`) predates the registry and runs its own |
| 60 | + * `validateWorkspaceAccess`, resolving a workspace key to its creator — so a |
| 61 | + * workspace key can still deploy there. That gap is pre-existing and tracked |
| 62 | + * separately; do not read these assertions as covering v1. |
| 63 | + */ |
| 64 | + describe('deployment operations reject workspace API keys', () => { |
| 65 | + it.each([ |
| 66 | + ['deploy', workflowOperations.deploy], |
| 67 | + ['undeploy', workflowOperations.undeploy], |
| 68 | + ['updatePublicApi', workflowOperations.updatePublicApi], |
| 69 | + ['activateVersion', workflowOperations.activateVersion], |
| 70 | + ['revertVersion', workflowOperations.revertVersion], |
| 71 | + ])( |
| 72 | + 'workflowOperations.%s denies workspace_api_key so a long-lived key cannot deploy', |
| 73 | + (_name, operation) => { |
| 74 | + expect(operation.workspaceApiKey).toBe('deny') |
| 75 | + } |
| 76 | + ) |
| 77 | + |
| 78 | + it('creating an API key from copilot denies workspace API keys', () => { |
| 79 | + expect(apiKeyOperations.createFromCopilot.workspaceApiKey).toBe('deny') |
| 80 | + }) |
| 81 | + }) |
| 82 | +}) |
0 commit comments