first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
const TEST_API_KEY = 'test-api-key';
|
||||
|
||||
test.describe('Access Control Boundaries', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Identity & Access' },
|
||||
],
|
||||
}, () => {
|
||||
test('should prevent credential editing by sharee', async ({ n8n, api }) => {
|
||||
const member = await api.publicApi.createUser({
|
||||
email: `member-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Member',
|
||||
});
|
||||
|
||||
const credentialName = `Owner Credential ${nanoid()}`;
|
||||
const credential = await api.credentials.createCredential({
|
||||
name: credentialName,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: TEST_API_KEY },
|
||||
});
|
||||
|
||||
const memberApi = await api.createApiForUser(member);
|
||||
const memberProject = await memberApi.projects.getMyPersonalProject();
|
||||
await api.credentials.shareCredential(credential.id, [memberProject.id]);
|
||||
|
||||
const memberN8n = await n8n.start.withUser(member);
|
||||
await memberN8n.navigate.toCredentials();
|
||||
await memberN8n.credentials.cards.clickCredentialCard(credentialName);
|
||||
|
||||
await expect(memberN8n.credentials.credentialModal.getCredentialName()).toContainText(
|
||||
credentialName,
|
||||
);
|
||||
await expect(memberN8n.credentials.credentialModal.getSaveButton()).toBeHidden();
|
||||
});
|
||||
|
||||
test('should allow admin full access to credentials created by others', async ({ n8n, api }) => {
|
||||
const member = await api.publicApi.createUser({
|
||||
email: `member-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Member',
|
||||
});
|
||||
const admin = await api.publicApi.createUser({
|
||||
email: `admin-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Admin',
|
||||
role: 'global:admin',
|
||||
});
|
||||
|
||||
const credentialName = `Member Credential ${nanoid()}`;
|
||||
const memberApi = await api.createApiForUser(member);
|
||||
await memberApi.credentials.createCredential({
|
||||
name: credentialName,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: TEST_API_KEY },
|
||||
});
|
||||
|
||||
const adminN8n = await n8n.start.withUser(admin);
|
||||
await adminN8n.navigate.toCredentials();
|
||||
await adminN8n.credentials.cards.clickCredentialCard(credentialName);
|
||||
|
||||
await expect(adminN8n.credentials.credentialModal.getCredentialName()).toContainText(
|
||||
credentialName,
|
||||
);
|
||||
|
||||
const apiKeyInput = adminN8n.credentials.credentialModal.getFieldInput('apiKey');
|
||||
await expect(apiKeyInput).toHaveValue(/__n8n_BLANK_VALUE_/);
|
||||
|
||||
await adminN8n.credentials.credentialModal.changeTab('Sharing');
|
||||
await expect(adminN8n.credentials.credentialModal.getUsersSelect()).toBeVisible();
|
||||
|
||||
await adminN8n.credentials.credentialModal.getUsersSelect().click();
|
||||
// Personal projects show "Name (Personal space)" instead of email
|
||||
const adminName = `${admin.firstName} ${admin.lastName}`;
|
||||
await expect(
|
||||
adminN8n.credentials.credentialModal.getVisibleDropdown().getByText(adminName),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should prevent access to private workflows via direct URL', async ({ api }) => {
|
||||
const member = await api.publicApi.createUser({
|
||||
email: `member-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Member',
|
||||
});
|
||||
const workflow = await api.workflows.createWorkflow({
|
||||
name: `Private Workflow ${nanoid()}`,
|
||||
nodes: [
|
||||
{
|
||||
id: 'manual-trigger',
|
||||
name: 'Manual Trigger',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [100, 200],
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
});
|
||||
|
||||
const memberApi = await api.createApiForUser(member);
|
||||
const response = await memberApi.request.get(`/rest/workflows/${workflow.id}`);
|
||||
expect(response.status()).toBe(403);
|
||||
});
|
||||
|
||||
test('should enforce project isolation for team projects', async ({ n8n, api }) => {
|
||||
await api.setMaxTeamProjectsQuota(-1);
|
||||
|
||||
const devProject = await api.projects.createProject(`Development ${nanoid()}`);
|
||||
const testProject = await api.projects.createProject(`Testing ${nanoid()}`);
|
||||
|
||||
const devCredName = `Dev Credential ${nanoid()}`;
|
||||
await api.credentials.createCredential({
|
||||
name: devCredName,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: TEST_API_KEY },
|
||||
projectId: devProject.id,
|
||||
});
|
||||
|
||||
const testCredName = `Test Credential ${nanoid()}`;
|
||||
await api.credentials.createCredential({
|
||||
name: testCredName,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: TEST_API_KEY },
|
||||
projectId: testProject.id,
|
||||
});
|
||||
|
||||
await n8n.navigate.toProject(testProject.id);
|
||||
await n8n.projectTabs.clickWorkflowsTab();
|
||||
await n8n.workflows.clickNewWorkflowButtonFromProject();
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Notion');
|
||||
await n8n.canvas.getFirstAction().click();
|
||||
|
||||
await n8n.ndv.getNodeCredentialsSelect().click();
|
||||
const credentialDropdown = n8n.ndv.getVisiblePopper();
|
||||
await expect(credentialDropdown.getByText(testCredName)).toBeVisible();
|
||||
await expect(credentialDropdown.getByText(devCredName)).toBeHidden();
|
||||
});
|
||||
|
||||
test('should prevent sharing team project workflows', async ({ n8n, api }) => {
|
||||
const teamProject = await api.projects.createProject(`Team Project ${nanoid()}`);
|
||||
const teamWorkflow = await api.workflows.createInProject(teamProject.id, {
|
||||
name: `Team Workflow ${nanoid()}`,
|
||||
});
|
||||
|
||||
await n8n.navigate.toWorkflow(teamWorkflow.id);
|
||||
await n8n.canvas.openShareModal();
|
||||
|
||||
// Team project workflows cannot be shared - no user selector shown
|
||||
await expect(n8n.workflowSharingModal.getUsersSelect()).toBeHidden();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
const TEST_API_KEY = 'test-api-key';
|
||||
|
||||
test.describe('Credential Sharing', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Identity & Access' },
|
||||
],
|
||||
}, () => {
|
||||
test('should share credential with another user via UI', async ({ n8n, api }) => {
|
||||
const member = await api.publicApi.createUser({
|
||||
email: `member-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Member',
|
||||
});
|
||||
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentials.addResource.credential();
|
||||
await n8n.credentials.selectCredentialType('Notion API');
|
||||
await n8n.credentials.credentialModal.fillField('apiKey', TEST_API_KEY);
|
||||
const credentialName = `Test Credential ${nanoid()}`;
|
||||
await n8n.credentials.credentialModal.renameCredential(credentialName);
|
||||
await n8n.credentials.credentialModal.save();
|
||||
|
||||
await n8n.credentials.credentialModal.changeTab('Sharing');
|
||||
await n8n.credentials.credentialModal.addUserToSharing(member.email);
|
||||
await n8n.credentials.credentialModal.saveSharing();
|
||||
await n8n.credentials.credentialModal.close();
|
||||
|
||||
const memberN8n = await n8n.start.withUser(member);
|
||||
await memberN8n.navigate.toCredentials();
|
||||
await expect(memberN8n.credentials.cards.getCredential(credentialName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should share credential with another user via API', async ({ api }) => {
|
||||
await api.enableProjectFeatures();
|
||||
|
||||
const member = await api.publicApi.createUser({
|
||||
email: `member-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Member',
|
||||
});
|
||||
|
||||
const credential = await api.credentials.createCredential({
|
||||
name: `Test Credential ${nanoid()}`,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: TEST_API_KEY },
|
||||
});
|
||||
|
||||
const memberApi = await api.createApiForUser(member);
|
||||
const memberProject = await memberApi.projects.getMyPersonalProject();
|
||||
await api.credentials.shareCredential(credential.id, [memberProject.id]);
|
||||
|
||||
const memberCredentials = await memberApi.credentials.getCredentials();
|
||||
const sharedCredential = memberCredentials.find((c) => c.id === credential.id);
|
||||
expect(sharedCredential).toBeTruthy();
|
||||
expect(sharedCredential?.name).toBe(credential.name);
|
||||
});
|
||||
|
||||
test('should show shared credential with proper permissions in node credential dropdown', async ({
|
||||
n8n,
|
||||
api,
|
||||
}) => {
|
||||
const member = await api.publicApi.createUser({
|
||||
email: `member-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Member',
|
||||
});
|
||||
|
||||
const credential = await api.credentials.createCredential({
|
||||
name: `Test Credential ${nanoid()}`,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: TEST_API_KEY },
|
||||
});
|
||||
|
||||
const memberApi = await api.createApiForUser(member);
|
||||
const memberProject = await memberApi.projects.getMyPersonalProject();
|
||||
await api.credentials.shareCredential(credential.id, [memberProject.id]);
|
||||
|
||||
const memberN8n = await n8n.start.withUser(member);
|
||||
await memberN8n.navigate.toWorkflow('new');
|
||||
await memberN8n.canvas.addNode('Notion');
|
||||
await memberN8n.canvas.getFirstAction().click();
|
||||
|
||||
await expect(memberN8n.ndv.getCredentialSelect()).toHaveValue(credential.name);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
const TEST_API_KEY = 'test-api-key';
|
||||
|
||||
test.describe('Credential Visibility Rules', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Identity & Access' },
|
||||
],
|
||||
}, () => {
|
||||
test('should only show credentials from the same team project', async ({ n8n, api }) => {
|
||||
await n8n.navigate.toCredentials();
|
||||
const personalCredName = `Personal Credential ${nanoid()}`;
|
||||
await n8n.credentialsComposer.createFromList(
|
||||
'Notion API',
|
||||
{ apiKey: TEST_API_KEY },
|
||||
{
|
||||
name: personalCredName,
|
||||
},
|
||||
);
|
||||
|
||||
const devProject = await n8n.projectComposer.createProject(`Development ${nanoid()}`);
|
||||
await n8n.projectTabs.clickCredentialsTab();
|
||||
const devCredName = `Dev Credential ${nanoid()}`;
|
||||
await n8n.credentialsComposer.createFromList(
|
||||
'Notion API',
|
||||
{ apiKey: TEST_API_KEY },
|
||||
{ projectId: devProject.projectId, name: devCredName },
|
||||
);
|
||||
|
||||
const testProject = await api.projects.createProject(`Test ${nanoid()}`);
|
||||
await n8n.navigate.toProject(testProject.id);
|
||||
await n8n.projectTabs.clickCredentialsTab();
|
||||
const testCredName = `Test Credential ${nanoid()}`;
|
||||
await n8n.credentialsComposer.createFromList(
|
||||
'Notion API',
|
||||
{ apiKey: TEST_API_KEY },
|
||||
{ projectId: testProject.id, name: testCredName },
|
||||
);
|
||||
|
||||
await n8n.navigate.toProject(testProject.id);
|
||||
await n8n.projectTabs.clickWorkflowsTab();
|
||||
await n8n.workflows.clickNewWorkflowButtonFromProject();
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Notion');
|
||||
await n8n.canvas.getFirstAction().click();
|
||||
|
||||
// Only test project credential should be visible
|
||||
await n8n.ndv.getNodeCredentialsSelect().click();
|
||||
const credentialDropdown = n8n.ndv.getVisiblePopper();
|
||||
await expect(credentialDropdown.getByText(testCredName)).toBeVisible();
|
||||
await expect(credentialDropdown.getByText(personalCredName)).toBeHidden();
|
||||
await expect(credentialDropdown.getByText(devCredName)).toBeHidden();
|
||||
});
|
||||
|
||||
test('should show personal and shared credentials for members', async ({ n8n, api }) => {
|
||||
const member = await api.publicApi.createUser({
|
||||
email: `member-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Member',
|
||||
});
|
||||
|
||||
const ownerCredName = `Owner Credential ${nanoid()}`;
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentials.addResource.credential();
|
||||
await n8n.credentials.selectCredentialType('Notion API');
|
||||
await n8n.credentials.credentialModal.fillField('apiKey', TEST_API_KEY);
|
||||
await n8n.credentials.credentialModal.renameCredential(ownerCredName);
|
||||
await n8n.credentials.credentialModal.save();
|
||||
|
||||
await n8n.credentials.credentialModal.changeTab('Sharing');
|
||||
await n8n.credentials.credentialModal.addUserToSharing(member.email);
|
||||
await n8n.credentials.credentialModal.saveSharing();
|
||||
await n8n.credentials.credentialModal.close();
|
||||
|
||||
const memberN8n = await n8n.start.withUser(member);
|
||||
await memberN8n.navigate.toCredentials();
|
||||
const memberCredName = `Member Credential ${nanoid()}`;
|
||||
await memberN8n.credentialsComposer.createFromList(
|
||||
'Notion API',
|
||||
{ apiKey: TEST_API_KEY },
|
||||
{
|
||||
name: memberCredName,
|
||||
},
|
||||
);
|
||||
|
||||
await memberN8n.navigate.toWorkflow('new');
|
||||
await memberN8n.canvas.addNode('Manual Trigger');
|
||||
await memberN8n.canvas.addNode('Notion');
|
||||
await memberN8n.canvas.getFirstAction().click();
|
||||
|
||||
// Both own credential and shared credential should be visible
|
||||
await memberN8n.ndv.getNodeCredentialsSelect().click();
|
||||
const credentialDropdown = memberN8n.ndv.getVisiblePopper();
|
||||
await expect(credentialDropdown.getByText(ownerCredName)).toBeVisible();
|
||||
await expect(credentialDropdown.getByText(memberCredName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should only show own credentials in shared workflow for members', async ({ n8n, api }) => {
|
||||
const member = await api.publicApi.createUser({
|
||||
email: `member-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Member',
|
||||
});
|
||||
|
||||
const ownerCredName = `Owner Credential ${nanoid()}`;
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentialsComposer.createFromList(
|
||||
'Notion API',
|
||||
{ apiKey: TEST_API_KEY },
|
||||
{ name: ownerCredName },
|
||||
);
|
||||
|
||||
const workflow = await api.workflows.createWorkflow({
|
||||
name: `Test Workflow ${nanoid()}`,
|
||||
nodes: [
|
||||
{
|
||||
id: 'manual-trigger',
|
||||
name: 'Manual Trigger',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [100, 200],
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
});
|
||||
|
||||
const memberApi = await api.createApiForUser(member);
|
||||
const memberProject = await memberApi.projects.getMyPersonalProject();
|
||||
await api.workflows.shareWorkflow(workflow.id, [memberProject.id]);
|
||||
|
||||
const memberN8n = await n8n.start.withUser(member);
|
||||
await memberN8n.navigate.toCredentials();
|
||||
const memberCredName = `Member Credential ${nanoid()}`;
|
||||
await memberN8n.credentialsComposer.createFromList(
|
||||
'Notion API',
|
||||
{ apiKey: TEST_API_KEY },
|
||||
{
|
||||
name: memberCredName,
|
||||
},
|
||||
);
|
||||
|
||||
await memberN8n.navigate.toWorkflow(workflow.id);
|
||||
await expect(memberN8n.canvas.getCanvasNodes().first()).toBeVisible();
|
||||
|
||||
await memberN8n.canvas.addNode('Notion');
|
||||
await memberN8n.canvas.getFirstAction().click();
|
||||
|
||||
// Member should see their own credential but NOT the owner's unshared credential
|
||||
await memberN8n.ndv.getNodeCredentialsSelect().click();
|
||||
const credentialDropdown = memberN8n.ndv.getVisiblePopper();
|
||||
await expect(credentialDropdown.getByText(memberCredName)).toBeVisible();
|
||||
await expect(credentialDropdown.getByText(ownerCredName)).toBeHidden();
|
||||
});
|
||||
|
||||
test('should show owner and workflow creator credentials for global owner in shared workflows', async ({
|
||||
n8n,
|
||||
api,
|
||||
}) => {
|
||||
const member = await api.publicApi.createUser({
|
||||
email: `member-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Member',
|
||||
});
|
||||
|
||||
const memberApi = await api.createApiForUser(member);
|
||||
const memberCredName = `Member Credential ${nanoid()}`;
|
||||
await memberApi.credentials.createCredential({
|
||||
name: memberCredName,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: TEST_API_KEY },
|
||||
});
|
||||
|
||||
const workflow = await memberApi.workflows.createWorkflow({
|
||||
name: `Test Workflow ${nanoid()}`,
|
||||
nodes: [
|
||||
{
|
||||
id: 'manual-trigger',
|
||||
name: 'Manual Trigger',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [100, 200],
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
});
|
||||
|
||||
const ownerProject = await api.projects.getMyPersonalProject();
|
||||
await memberApi.workflows.shareWorkflow(workflow.id, [ownerProject.id]);
|
||||
|
||||
const ownerCredName = `Owner Credential ${nanoid()}`;
|
||||
await api.credentials.createCredential({
|
||||
name: ownerCredName,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: TEST_API_KEY },
|
||||
});
|
||||
|
||||
await n8n.navigate.toWorkflow(workflow.id);
|
||||
await expect(n8n.canvas.getCanvasNodes().first()).toBeVisible();
|
||||
|
||||
await n8n.canvas.addNode('Notion');
|
||||
await n8n.canvas.getFirstAction().click();
|
||||
|
||||
// Global owner should see both their own credential and the workflow creator's credential
|
||||
await n8n.ndv.getNodeCredentialsSelect().click();
|
||||
const credentialOptions = n8n.ndv.getVisiblePopper().locator('li');
|
||||
await expect(credentialOptions.filter({ hasText: ownerCredName })).toBeVisible();
|
||||
await expect(credentialOptions.filter({ hasText: memberCredName })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show all personal credentials for global owner in own workflows', async ({
|
||||
n8n,
|
||||
api,
|
||||
}) => {
|
||||
const member = await api.publicApi.createUser({
|
||||
email: `member-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Member',
|
||||
});
|
||||
|
||||
const memberApi = await api.createApiForUser(member);
|
||||
const memberCredName = `Member Credential ${nanoid()}`;
|
||||
await memberApi.credentials.createCredential({
|
||||
name: memberCredName,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: TEST_API_KEY },
|
||||
});
|
||||
|
||||
await n8n.navigate.toWorkflow('new');
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Notion');
|
||||
await n8n.canvas.getFirstAction().click();
|
||||
|
||||
// Owner should see member's credential (global owner privilege)
|
||||
await n8n.ndv.getNodeCredentialsSelect().click();
|
||||
const credentialOptions = n8n.ndv.getVisiblePopper().locator('li');
|
||||
await expect(credentialOptions.filter({ hasText: memberCredName })).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('Workflow Sharing', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Identity & Access' },
|
||||
],
|
||||
}, () => {
|
||||
test('should share workflow with another user via UI', async ({ n8n, api }) => {
|
||||
const member = await api.publicApi.createUser({
|
||||
email: `member-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Member',
|
||||
});
|
||||
|
||||
await n8n.navigate.toWorkflow('new');
|
||||
const workflowName = `Test Workflow ${nanoid()}`;
|
||||
await n8n.canvas.setWorkflowName(workflowName);
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
|
||||
await n8n.canvas.openShareModal();
|
||||
await n8n.workflowSharingModal.addUser(member.email);
|
||||
await n8n.workflowSharingModal.save();
|
||||
|
||||
const memberN8n = await n8n.start.withUser(member);
|
||||
await memberN8n.navigate.toWorkflows();
|
||||
await expect(memberN8n.workflows.cards.getWorkflow(workflowName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should share workflow with another user via API', async ({ api }) => {
|
||||
const member = await api.publicApi.createUser({
|
||||
email: `member-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Member',
|
||||
});
|
||||
|
||||
const workflow = await api.workflows.createWorkflow({
|
||||
name: `Test Workflow ${nanoid()}`,
|
||||
nodes: [
|
||||
{
|
||||
id: 'manual-trigger',
|
||||
name: 'Manual Trigger',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [100, 200],
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
});
|
||||
|
||||
const memberApi = await api.createApiForUser(member);
|
||||
const memberProject = await memberApi.projects.getMyPersonalProject();
|
||||
await api.workflows.shareWorkflow(workflow.id, [memberProject.id]);
|
||||
|
||||
const memberWorkflows = await memberApi.workflows.getWorkflows();
|
||||
const sharedWorkflow = memberWorkflows.find((w: { id: string }) => w.id === workflow.id);
|
||||
expect(sharedWorkflow).toBeTruthy();
|
||||
expect(sharedWorkflow?.name).toBe(workflow.name);
|
||||
});
|
||||
|
||||
test('should allow shared user to edit shared workflow', async ({ n8n, api }) => {
|
||||
const member = await api.publicApi.createUser({
|
||||
email: `member-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Member',
|
||||
});
|
||||
|
||||
const workflow = await api.workflows.createWorkflow({
|
||||
name: `Test Workflow ${nanoid()}`,
|
||||
nodes: [
|
||||
{
|
||||
id: 'manual-trigger',
|
||||
name: 'Manual Trigger',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [100, 200],
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
});
|
||||
|
||||
const memberApi = await api.createApiForUser(member);
|
||||
const memberProject = await memberApi.projects.getMyPersonalProject();
|
||||
await api.workflows.shareWorkflow(workflow.id, [memberProject.id]);
|
||||
|
||||
const memberN8n = await n8n.start.withUser(member);
|
||||
await memberN8n.navigate.toWorkflow(workflow.id);
|
||||
await expect(memberN8n.canvas.getCanvasNodes()).toHaveCount(1);
|
||||
|
||||
await memberN8n.canvas.addNode('Code', { action: 'Code in JavaScript', closeNDV: true });
|
||||
await expect(memberN8n.canvas.getCanvasNodes()).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('should prevent access to workflow when user is not shared', async ({ api }) => {
|
||||
await api.enableProjectFeatures();
|
||||
|
||||
const member = await api.publicApi.createUser({
|
||||
email: `member-${nanoid()}@test.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'Member',
|
||||
});
|
||||
|
||||
const workflow = await api.workflows.createWorkflow({
|
||||
name: `Private Workflow ${nanoid()}`,
|
||||
nodes: [
|
||||
{
|
||||
id: 'manual-trigger',
|
||||
name: 'Manual Trigger',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [100, 200],
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
});
|
||||
|
||||
const memberApi = await api.createApiForUser(member);
|
||||
const response = await memberApi.request.get(`/rest/workflows/${workflow.id}`);
|
||||
// With project features enabled, unauthorized access returns 403 (Forbidden)
|
||||
expect(response.status()).toBe(403);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user