mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-31 08:46:58 +02:00
Some checks are pending
Build: Benchmark Image / build (push) Waiting to run
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.14.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (25.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Util: Sync API Docs / sync-public-api (push) Waiting to run
443 lines
14 KiB
TypeScript
443 lines
14 KiB
TypeScript
import {
|
|
createTeamProject,
|
|
getPersonalProject,
|
|
linkUserToProject,
|
|
createWorkflow,
|
|
randomCredentialPayload,
|
|
testDb,
|
|
mockInstance,
|
|
} from '@n8n/backend-test-utils';
|
|
import type { Project, User, Role } from '@n8n/db';
|
|
|
|
import { UserManagementMailer } from '@/user-management/email';
|
|
|
|
import { createCustomRoleWithScopeSlugs, cleanupRolesAndScopes } from '../shared/db/roles';
|
|
import { createOwner, createMember } from '../shared/db/users';
|
|
import type { SuperAgentTest } from '../shared/types';
|
|
import * as utils from '../shared/utils/';
|
|
|
|
const testServer = utils.setupTestServer({
|
|
endpointGroups: ['workflows', 'credentials'],
|
|
enabledFeatures: ['feat:sharing', 'feat:customRoles'],
|
|
quotas: {
|
|
'quota:maxTeamProjects': -1,
|
|
},
|
|
});
|
|
|
|
// Foundation users and projects
|
|
let owner: User;
|
|
let testUser: User;
|
|
let teamProject: Project;
|
|
let testUserPersonalProject: Project;
|
|
|
|
// Custom role definitions (8 total - covering all combinations)
|
|
let workflowReadOnlyRole: Role;
|
|
let workflowAllOperationsRole: Role;
|
|
let credentialReadOnlyRole: Role;
|
|
let credentialAllOperationsRole: Role;
|
|
|
|
// Authentication agents
|
|
let ownerAgent: SuperAgentTest;
|
|
let testUserAgent: SuperAgentTest;
|
|
|
|
// Test data - created fresh for each test
|
|
let testWorkflowId: string;
|
|
let testWorkflowVersionId: string;
|
|
let testCredentialId: string;
|
|
|
|
describe('Resource Access Control Matrix Tests', () => {
|
|
beforeAll(async () => {
|
|
mockInstance(UserManagementMailer, {
|
|
invite: jest.fn(),
|
|
passwordReset: jest.fn(),
|
|
});
|
|
|
|
// Create foundation users
|
|
owner = await createOwner();
|
|
testUser = await createMember();
|
|
|
|
// Get projects
|
|
testUserPersonalProject = await getPersonalProject(testUser);
|
|
teamProject = await createTeamProject('Access Control Test Project', owner);
|
|
|
|
// Create authentication agents
|
|
ownerAgent = testServer.authAgentFor(owner);
|
|
testUserAgent = testServer.authAgentFor(testUser);
|
|
|
|
await utils.initCredentialsTypes();
|
|
|
|
// Create custom roles with specific scopes
|
|
workflowReadOnlyRole = await createCustomRoleWithScopeSlugs(
|
|
['workflow:read', 'workflow:list'],
|
|
{
|
|
roleType: 'project',
|
|
displayName: 'Workflow Read-Only',
|
|
description: 'Can only read and list workflows',
|
|
},
|
|
);
|
|
|
|
workflowAllOperationsRole = await createCustomRoleWithScopeSlugs(
|
|
[
|
|
'workflow:share',
|
|
'workflow:execute',
|
|
'workflow:read',
|
|
'workflow:list',
|
|
'workflow:create',
|
|
'workflow:update',
|
|
'workflow:delete',
|
|
],
|
|
{
|
|
roleType: 'project',
|
|
displayName: 'Workflow All Operations',
|
|
description: 'Full workflow access (CRUD + list)',
|
|
},
|
|
);
|
|
|
|
credentialReadOnlyRole = await createCustomRoleWithScopeSlugs(
|
|
['credential:read', 'credential:list'],
|
|
{
|
|
roleType: 'project',
|
|
displayName: 'Credential Read-Only',
|
|
description: 'Can only read and list credentials',
|
|
},
|
|
);
|
|
|
|
credentialAllOperationsRole = await createCustomRoleWithScopeSlugs(
|
|
[
|
|
'credential:read',
|
|
'credential:list',
|
|
'credential:create',
|
|
'credential:update',
|
|
'credential:delete',
|
|
],
|
|
{
|
|
roleType: 'project',
|
|
displayName: 'Credential All Operations',
|
|
description: 'Full credential access (CRUD + list)',
|
|
},
|
|
);
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Clean up any existing shared resources
|
|
await testDb.truncate(['SharedWorkflow', 'SharedCredentials', 'ProjectRelation']);
|
|
|
|
// Create fresh test data for each test
|
|
const workflow = await createWorkflow({ name: 'Matrix Test Workflow' }, teamProject);
|
|
testWorkflowId = workflow.id;
|
|
testWorkflowVersionId = workflow.versionId;
|
|
|
|
// Create test credential via owner
|
|
const credentialPayload = randomCredentialPayload();
|
|
const credentialResponse = await ownerAgent
|
|
.post('/credentials')
|
|
.send({ ...credentialPayload, projectId: teamProject.id })
|
|
.expect(200);
|
|
testCredentialId = credentialResponse.body.data.id;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await testDb.truncate(['User', 'ProjectRelation']);
|
|
await cleanupRolesAndScopes();
|
|
});
|
|
|
|
describe('Foundation Setup Validation', () => {
|
|
test('should have created all required custom roles', () => {
|
|
expect(workflowReadOnlyRole.scopes).toHaveLength(2);
|
|
expect(workflowAllOperationsRole.scopes).toHaveLength(7);
|
|
|
|
expect(credentialReadOnlyRole.scopes).toHaveLength(2);
|
|
expect(credentialAllOperationsRole.scopes).toHaveLength(5);
|
|
});
|
|
|
|
test('should have functional test setup', async () => {
|
|
expect(testWorkflowId).toBeDefined();
|
|
expect(testCredentialId).toBeDefined();
|
|
expect(teamProject).toBeDefined();
|
|
|
|
// Verify owner can access test data
|
|
await ownerAgent.get(`/workflows/${testWorkflowId}`).expect(200);
|
|
await ownerAgent.get(`/credentials/${testCredentialId}`).expect(200);
|
|
});
|
|
});
|
|
|
|
describe('Workflow Access Control Matrix', () => {
|
|
describe('Workflow Read-Only Role', () => {
|
|
beforeEach(async () => {
|
|
await linkUserToProject(testUser, teamProject, workflowReadOnlyRole.slug);
|
|
});
|
|
|
|
test('POST /workflows should return 400', async () => {
|
|
const workflowPayload = {
|
|
name: 'New Workflow',
|
|
active: false,
|
|
nodes: [
|
|
{
|
|
id: 'uuid-1234',
|
|
parameters: {},
|
|
name: 'Start',
|
|
type: 'n8n-nodes-base.manualTrigger',
|
|
typeVersion: 1,
|
|
position: [240, 300],
|
|
},
|
|
],
|
|
connections: {},
|
|
projectId: teamProject.id,
|
|
};
|
|
|
|
await testUserAgent.post('/workflows').send(workflowPayload).expect(400);
|
|
});
|
|
|
|
test('GET /workflows should return 200', async () => {
|
|
const response = await testUserAgent.get('/workflows').expect(200);
|
|
expect(Array.isArray(response.body.data)).toBe(true);
|
|
expect(response.body.data.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('GET /workflows/new should return 403', async () => {
|
|
await testUserAgent.get(`/workflows/new?projectId=${teamProject.id}`).expect(403);
|
|
});
|
|
|
|
test('GET /workflows/:id should return 200', async () => {
|
|
const response = await testUserAgent.get(`/workflows/${testWorkflowId}`).expect(200);
|
|
expect(response.body.data.name).toBe('Matrix Test Workflow');
|
|
});
|
|
|
|
test('PATCH /workflows/:id should return 403', async () => {
|
|
await testUserAgent
|
|
.patch(`/workflows/${testWorkflowId}`)
|
|
.send({ name: 'Updated Name', versionId: testWorkflowVersionId })
|
|
.expect(403);
|
|
});
|
|
|
|
test('DELETE /workflows/:id should return 403', async () => {
|
|
await testUserAgent.delete(`/workflows/${testWorkflowId}`).expect(403);
|
|
});
|
|
|
|
test('POST /workflows/:id/archive should return 403', async () => {
|
|
await testUserAgent.post(`/workflows/${testWorkflowId}/archive`).send().expect(403);
|
|
});
|
|
|
|
test('POST /workflows/:id/unarchive should return 403', async () => {
|
|
await testUserAgent.post(`/workflows/${testWorkflowId}/unarchive`).send().expect(403);
|
|
});
|
|
|
|
test('POST /workflows/:id/run should return 403', async () => {
|
|
const runPayload = {
|
|
workflowData: { id: testWorkflowId, name: 'Test', nodes: [], connections: {} },
|
|
};
|
|
await testUserAgent.post(`/workflows/${testWorkflowId}/run`).send(runPayload).expect(403);
|
|
});
|
|
|
|
test('PUT /workflows/:id/share should return 403', async () => {
|
|
await testUserAgent
|
|
.put(`/workflows/${testWorkflowId}/share`)
|
|
.send({ shareWithIds: [testUserPersonalProject.id] })
|
|
.expect(403);
|
|
});
|
|
|
|
test('PUT /workflows/:id/transfer should return 403', async () => {
|
|
await testUserAgent
|
|
.put(`/workflows/${testWorkflowId}/transfer`)
|
|
.send({ destinationProjectId: testUserPersonalProject.id })
|
|
.expect(403);
|
|
});
|
|
});
|
|
|
|
describe('Workflow All-Operations Role', () => {
|
|
beforeEach(async () => {
|
|
await linkUserToProject(testUser, teamProject, workflowAllOperationsRole.slug);
|
|
});
|
|
|
|
test('POST /workflows should return 200', async () => {
|
|
const workflowPayload = {
|
|
name: 'All-Ops Workflow',
|
|
active: false,
|
|
nodes: [
|
|
{
|
|
id: 'uuid-1234',
|
|
parameters: {},
|
|
name: 'Start',
|
|
type: 'n8n-nodes-base.manualTrigger',
|
|
typeVersion: 1,
|
|
position: [240, 300],
|
|
},
|
|
],
|
|
connections: {},
|
|
projectId: teamProject.id,
|
|
};
|
|
|
|
const response = await testUserAgent.post('/workflows').send(workflowPayload).expect(200);
|
|
expect(response.body.data.name).toBe('All-Ops Workflow');
|
|
});
|
|
|
|
test('GET /workflows should return 200', async () => {
|
|
const response = await testUserAgent.get('/workflows').expect(200);
|
|
expect(Array.isArray(response.body.data)).toBe(true);
|
|
expect(response.body.data.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('GET /workflows/new should return 200', async () => {
|
|
await testUserAgent.get(`/workflows/new?projectId=${teamProject.id}`).expect(200);
|
|
});
|
|
|
|
test('GET /workflows/:id should return 200', async () => {
|
|
const response = await testUserAgent.get(`/workflows/${testWorkflowId}`).expect(200);
|
|
expect(response.body.data.name).toBe('Matrix Test Workflow');
|
|
});
|
|
|
|
test('PATCH /workflows/:id should return 200', async () => {
|
|
const response = await testUserAgent
|
|
.patch(`/workflows/${testWorkflowId}`)
|
|
.send({ name: 'Updated by All-Ops', versionId: testWorkflowVersionId })
|
|
.expect(200);
|
|
expect(response.body.data.name).toBe('Updated by All-Ops');
|
|
});
|
|
|
|
test('POST /workflows/:id/archive should return 200', async () => {
|
|
// All-operations role (includes workflow:delete) can successfully archive workflows
|
|
// Archive operation is allowed when user has full CRUD permissions
|
|
await testUserAgent.post(`/workflows/${testWorkflowId}/archive`).send().expect(200);
|
|
});
|
|
|
|
test('DELETE /workflows/:id should return 200', async () => {
|
|
await testUserAgent.post(`/workflows/${testWorkflowId}/archive`).send().expect(200);
|
|
await testUserAgent.delete(`/workflows/${testWorkflowId}`).expect(200);
|
|
});
|
|
|
|
test('POST /workflows/:id/unarchive should return 200', async () => {
|
|
await testUserAgent.post(`/workflows/${testWorkflowId}/archive`).send().expect(200);
|
|
await testUserAgent.post(`/workflows/${testWorkflowId}/unarchive`).send().expect(200);
|
|
});
|
|
|
|
test('PUT /workflows/:id/share should return 200', async () => {
|
|
// Sharing requires workflow:share scope (not included in CRUD operations)
|
|
await testUserAgent
|
|
.put(`/workflows/${testWorkflowId}/share`)
|
|
.send({ shareWithIds: [testUserPersonalProject.id] })
|
|
.expect(200);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Credential Access Control Matrix', () => {
|
|
describe('Credential Read-Only Role', () => {
|
|
beforeEach(async () => {
|
|
await linkUserToProject(testUser, teamProject, credentialReadOnlyRole.slug);
|
|
});
|
|
|
|
test('GET /credentials should return 200', async () => {
|
|
const response = await testUserAgent.get('/credentials').expect(200);
|
|
expect(Array.isArray(response.body.data)).toBe(true);
|
|
expect(response.body.data.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('GET /credentials/new should return 200', async () => {
|
|
await testUserAgent.get('/credentials/new').expect(200);
|
|
});
|
|
|
|
test('GET /credentials/:id should return 200', async () => {
|
|
const response = await testUserAgent.get(`/credentials/${testCredentialId}`).expect(200);
|
|
expect(response.body.data).toBeDefined();
|
|
});
|
|
|
|
test('POST /credentials should return 403', async () => {
|
|
const credentialPayload = randomCredentialPayload();
|
|
await testUserAgent
|
|
.post('/credentials')
|
|
.send({ ...credentialPayload, projectId: teamProject.id })
|
|
.expect(403);
|
|
});
|
|
|
|
test('PATCH /credentials/:id should return 403', async () => {
|
|
const updatePayload = { name: 'Updated Credential Name' };
|
|
await testUserAgent
|
|
.patch(`/credentials/${testCredentialId}`)
|
|
.send(updatePayload)
|
|
.expect(403);
|
|
});
|
|
|
|
test('DELETE /credentials/:id should return 403', async () => {
|
|
await testUserAgent.delete(`/credentials/${testCredentialId}`).expect(403);
|
|
});
|
|
|
|
test('PUT /credentials/:id/share should return 403', async () => {
|
|
await testUserAgent
|
|
.put(`/credentials/${testCredentialId}/share`)
|
|
.send({ shareWithIds: [testUserPersonalProject.id] })
|
|
.expect(403);
|
|
});
|
|
|
|
test('PUT /credentials/:id/transfer should return 403', async () => {
|
|
await testUserAgent
|
|
.put(`/credentials/${testCredentialId}/transfer`)
|
|
.send({ destinationProjectId: testUserPersonalProject.id })
|
|
.expect(403);
|
|
});
|
|
});
|
|
|
|
describe('Credential All-Operations Role', () => {
|
|
beforeEach(async () => {
|
|
await linkUserToProject(testUser, teamProject, credentialAllOperationsRole.slug);
|
|
});
|
|
|
|
test('GET /credentials should return 200', async () => {
|
|
const response = await testUserAgent.get('/credentials').expect(200);
|
|
expect(Array.isArray(response.body.data)).toBe(true);
|
|
expect(response.body.data.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('GET /credentials/new should return 200', async () => {
|
|
await testUserAgent.get('/credentials/new').expect(200);
|
|
});
|
|
|
|
test('GET /credentials/:id should return 200', async () => {
|
|
const response = await testUserAgent.get(`/credentials/${testCredentialId}`).expect(200);
|
|
expect(response.body.data).toBeDefined();
|
|
});
|
|
|
|
test('POST /credentials should return 200', async () => {
|
|
const credentialPayload = randomCredentialPayload();
|
|
const response = await testUserAgent
|
|
.post('/credentials')
|
|
.send({ ...credentialPayload, projectId: teamProject.id })
|
|
.expect(200);
|
|
expect(response.body.data.name).toBe(credentialPayload.name);
|
|
});
|
|
|
|
test('PATCH /credentials/:id should return 200', async () => {
|
|
const original = await testUserAgent.get(`/credentials/${testCredentialId}`).expect(200);
|
|
|
|
const updatePayload = { ...original.body.data, name: 'Updated by All-Ops', data: {} };
|
|
|
|
const response = await testUserAgent
|
|
.patch(`/credentials/${testCredentialId}`)
|
|
.send(updatePayload)
|
|
.expect(200);
|
|
|
|
expect(response.body.data.name).toBe('Updated by All-Ops');
|
|
});
|
|
|
|
test('DELETE /credentials/:id should return 200', async () => {
|
|
await testUserAgent.delete(`/credentials/${testCredentialId}`).expect(200);
|
|
});
|
|
|
|
test('PUT /credentials/:id/share should return 403', async () => {
|
|
// Sharing requires credential:share scope (not included in CRUD operations)
|
|
await testUserAgent
|
|
.put(`/credentials/${testCredentialId}/share`)
|
|
.send({ shareWithIds: [testUserPersonalProject.id] })
|
|
.expect(403);
|
|
});
|
|
|
|
test('PUT /credentials/:id/transfer should return 403', async () => {
|
|
// Transfer requires credential:move scope (not included in CRUD operations)
|
|
await testUserAgent
|
|
.put(`/credentials/${testCredentialId}/transfer`)
|
|
.send({ destinationProjectId: testUserPersonalProject.id })
|
|
.expect(403);
|
|
});
|
|
});
|
|
});
|
|
});
|