mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-16 15:56:41 +08:00
fix(core): Require workflow create scope for single node runs in n8n Assistant (#40908)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2562744611
commit
27fcb968fb
+59
@@ -0,0 +1,59 @@
|
||||
# Require create and execute scopes for standalone node runs
|
||||
|
||||
Date: 2026-10-12
|
||||
|
||||
Status: Active
|
||||
|
||||
Decision Owner: Assistant
|
||||
|
||||
Source: https://linear.app/n8n/issue/AST-1705
|
||||
|
||||
## Context
|
||||
|
||||
The n8n Assistant can run one node standalone. The caller supplies the node type, the node
|
||||
parameters, the credentials, and the input items. The service puts the node in a temporary
|
||||
workflow in the target project, runs it, and deletes the workflow.
|
||||
|
||||
`workflow:execute` lets a user run a workflow that already exists. The manual run endpoint
|
||||
loads the workflow from the database and ignores a definition in the request body. A user
|
||||
who has `workflow:execute` but not `workflow:create` or `workflow:update` can only run
|
||||
workflows that someone else wrote.
|
||||
|
||||
A standalone node run does not run a stored workflow. The caller writes the node, so the
|
||||
run is workflow authoring and execution in one step.
|
||||
|
||||
## Decision
|
||||
|
||||
A standalone node run requires both `workflow:create` and `workflow:execute` in the target
|
||||
project. The user must hold both scopes. One of the two is not sufficient.
|
||||
|
||||
The main reason: the caller defines the node. That is the same power as creating a workflow
|
||||
and running it. The required scopes match those two actions.
|
||||
|
||||
The check runs before the service resolves the node or reads a credential.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **Keep `workflow:execute` alone.** We rejected it because it lets a role that cannot
|
||||
author workflows run any node with any parameters. That breaks the contract of an
|
||||
execute-only role.
|
||||
2. **Require `workflow:update` with `workflow:execute`.** We rejected it because
|
||||
`workflow:update` applies to a workflow that exists, and this technically creates a new
|
||||
workflow it runs.
|
||||
|
||||
## Consequences
|
||||
|
||||
1. A custom role that has `workflow:execute` but not `workflow:create` cannot use standalone
|
||||
node runs. Such a user can still run stored workflows through the Assistant.
|
||||
2. The built-in project roles do not change in behavior. Each role that has
|
||||
`workflow:execute` also has `workflow:create`. A personal project owner has both scopes.
|
||||
3. Any new path that runs a caller-defined node or workflow must require `workflow:create`
|
||||
with `workflow:execute`.
|
||||
|
||||
## Links
|
||||
|
||||
RFC: -
|
||||
|
||||
Documentation: -
|
||||
|
||||
Related ADRs: -
|
||||
@@ -1970,7 +1970,7 @@ describe('executeNodeService adapter', () => {
|
||||
mockedUserHasScopes.mockReset();
|
||||
});
|
||||
|
||||
it('runs the node in the bound project after asserting execute scope there', async () => {
|
||||
it('runs the node in the bound project after asserting create and execute scopes there', async () => {
|
||||
mockedUserHasScopes.mockResolvedValue(true);
|
||||
const executeNodeService = {
|
||||
run: vi.fn().mockResolvedValue({ status: 'success', output: [] }),
|
||||
@@ -1980,16 +1980,19 @@ describe('executeNodeService adapter', () => {
|
||||
const context = service.createContext(mockUser, { projectId: 'team-project-1' });
|
||||
await context.executeNodeService?.execute(executeRequest);
|
||||
|
||||
expect(mockedUserHasScopes).toHaveBeenCalledWith(mockUser, ['workflow:execute'], false, {
|
||||
projectId: 'team-project-1',
|
||||
});
|
||||
expect(mockedUserHasScopes).toHaveBeenCalledWith(
|
||||
mockUser,
|
||||
['workflow:create', 'workflow:execute'],
|
||||
false,
|
||||
{ projectId: 'team-project-1' },
|
||||
);
|
||||
expect(executeNodeService.run).toHaveBeenCalledWith(
|
||||
mockUser,
|
||||
expect.objectContaining({ projectId: 'team-project-1' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not run when the user lacks execute scope in the bound project', async () => {
|
||||
it('does not run when the user lacks the required scopes in the bound project', async () => {
|
||||
mockedUserHasScopes.mockResolvedValue(false);
|
||||
const executeNodeService = { run: vi.fn() };
|
||||
const { service, mockUser } = createNodeAdapterServiceForTests([], { executeNodeService });
|
||||
|
||||
@@ -888,7 +888,9 @@ export class InstanceAiAdapterService {
|
||||
return {
|
||||
execute: async (request) => {
|
||||
this.assertInstanceNotReadOnly('executions');
|
||||
const projectId = await resolveProjectId(['workflow:execute']);
|
||||
// The caller supplies the node type and parameters, so a standalone run
|
||||
// authors a workflow. See ADR-20261012-require-create-and-execute-scopes-for-standalone-node-runs.
|
||||
const projectId = await resolveProjectId(['workflow:create', 'workflow:execute']);
|
||||
const result = await executeNodeService.run(user, {
|
||||
type: request.type,
|
||||
version: request.version,
|
||||
|
||||
@@ -34,7 +34,7 @@ const defaultRuleSettings: RuleSettingsMap = {
|
||||
'adr-conventions': {
|
||||
enabled: true,
|
||||
severity: 'error',
|
||||
options: { allowedOwners: ['API & OEM', 'Catalysts'] },
|
||||
options: { allowedOwners: ['API & OEM', 'Assistant', 'Catalysts'] },
|
||||
},
|
||||
'catalog-violations': {
|
||||
enabled: true,
|
||||
|
||||
Reference in New Issue
Block a user