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:
Jaakko Husso
2026-10-13 06:40:58 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 2562744611
commit 27fcb968fb
4 changed files with 71 additions and 7 deletions
@@ -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,