Skip to content

Commit

Permalink
fix(editor): Connect up new project viewer role to the FE (#9913)
Browse files Browse the repository at this point in the history
  • Loading branch information
cstuncsik authored Aug 13, 2024
1 parent 56c4692 commit 117e2d9
Show file tree
Hide file tree
Showing 56 changed files with 1,473 additions and 1,146 deletions.
21 changes: 12 additions & 9 deletions cypress/composables/projects.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CredentialsModal, WorkflowPage } from '../pages';
import { getVisibleSelect } from '../utils';

const workflowPage = new WorkflowPage();
const credentialsModal = new CredentialsModal();
Expand All @@ -11,18 +12,25 @@ export const getProjectTabs = () => cy.getByTestId('project-tabs').find('a');
export const getProjectTabWorkflows = () => getProjectTabs().filter('a[href$="/workflows"]');
export const getProjectTabCredentials = () => getProjectTabs().filter('a[href$="/credentials"]');
export const getProjectTabSettings = () => getProjectTabs().filter('a[href$="/settings"]');
export const getProjectSettingsNameInput = () => cy.getByTestId('project-settings-name-input');
export const getProjectSettingsNameInput = () =>
cy.getByTestId('project-settings-name-input').find('input');
export const getProjectSettingsSaveButton = () => cy.getByTestId('project-settings-save-button');
export const getProjectSettingsCancelButton = () =>
cy.getByTestId('project-settings-cancel-button');
export const getProjectSettingsDeleteButton = () =>
cy.getByTestId('project-settings-delete-button');
export const getProjectMembersSelect = () => cy.getByTestId('project-members-select');
export const addProjectMember = (email: string) => {
export const addProjectMember = (email: string, role?: string) => {
getProjectMembersSelect().click();
getProjectMembersSelect().get('.el-select-dropdown__item').contains(email.toLowerCase()).click();

if (role) {
cy.getByTestId(`user-list-item-${email}`)
.find('[data-test-id="projects-settings-user-role-select"]')
.click();
getVisibleSelect().find('li').contains(role).click();
}
};
export const getProjectNameInput = () => cy.get('#projectName').find('input');
export const getResourceMoveModal = () => cy.getByTestId('project-move-resource-modal');
export const getResourceMoveConfirmModal = () =>
cy.getByTestId('project-move-resource-confirm-modal');
Expand All @@ -31,12 +39,7 @@ export const getProjectMoveSelect = () => cy.getByTestId('project-move-resource-
export function createProject(name: string) {
getAddProjectButton().click();

getProjectNameInput()
.should('be.visible')
.should('be.focused')
.should('have.value', 'My project')
.clear()
.type(name);
getProjectSettingsNameInput().should('be.visible').clear().type(name);
getProjectSettingsSaveButton().click();
}

Expand Down
11 changes: 10 additions & 1 deletion cypress/e2e/12-canvas-actions.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,16 @@ describe('Canvas Actions', () => {
});
});

it('should delete connections by pressing the delete button', () => {
it('should delete node by pressing keyboard backspace', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).click();
cy.get('body').type('{backspace}');
WorkflowPage.getters.nodeConnections().should('have.length', 0);
});

it('should delete connections by clicking on the delete button', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
Expand Down
39 changes: 29 additions & 10 deletions cypress/e2e/17-sharing.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
describe('Credential Usage in Cross Shared Workflows', () => {
beforeEach(() => {
cy.resetDatabase();
cy.enableFeature('sharing');
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
Expand All @@ -274,11 +275,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {
});

it('should only show credentials from the same team project', () => {
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);

// Create a notion credential in the home project
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
Expand All @@ -305,10 +301,36 @@ describe('Credential Usage in Cross Shared Workflows', () => {
getVisibleSelect().find('li').should('have.length', 2);
});

it('should only show credentials in their personal project for members', () => {
// Create a notion credential as the owner
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');

// Create another notion credential as the owner, but share it with member
// 0
credentialsPage.getters.createCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API', false);
credentialsModal.actions.changeTab('Sharing');
credentialsModal.actions.addUser(INSTANCE_MEMBERS[0].email);
credentialsModal.actions.saveSharing();

// As the member, create a new notion credential and a workflow
cy.signinAsMember();
cy.visit(credentialsPage.url);
credentialsPage.getters.createCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
cy.visit(workflowsPage.url);
workflowsPage.actions.createWorkflowFromCard();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);

// Only the own credential the shared one (+ the 'Create new' option)
// should be in the dropdown
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
});

it('should only show credentials in their personal project for members if the workflow was shared with them', () => {
const workflowName = 'Test workflow';
cy.enableFeature('sharing');
cy.reload();

// Create a notion credential as the owner and a workflow that is shared
// with member 0
Expand Down Expand Up @@ -339,7 +361,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {

it("should show all credentials from all personal projects the workflow's been shared into for the global owner", () => {
const workflowName = 'Test workflow';
cy.enableFeature('sharing');

// As member 1, create a new notion credential. This should not show up.
cy.signinAsMember(1);
Expand Down Expand Up @@ -384,8 +405,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {
});

it('should show all personal credentials if the global owner owns the workflow', () => {
cy.enableFeature('sharing');

// As member 0, create a new notion credential.
cy.signinAsMember();
cy.visit(credentialsPage.url);
Expand Down
110 changes: 101 additions & 9 deletions cypress/e2e/39-projects.cy.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import {
INSTANCE_MEMBERS,
INSTANCE_OWNER,
MANUAL_TRIGGER_NODE_NAME,
NOTION_NODE_NAME,
} from '../constants';
import { INSTANCE_MEMBERS, MANUAL_TRIGGER_NODE_NAME, NOTION_NODE_NAME } from '../constants';
import {
WorkflowsPage,
WorkflowPage,
CredentialsModal,
CredentialsPage,
WorkflowExecutionsTab,
NDV,
MainSidebar,
} from '../pages';
import * as projects from '../composables/projects';
import { getVisibleSelect } from '../utils';
import { getVisibleDropdown, getVisibleModalOverlay, getVisibleSelect } from '../utils';

const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage();
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
const executionsTab = new WorkflowExecutionsTab();
const ndv = new NDV();
const mainSidebar = new MainSidebar();

describe('Projects', { disableAutoLogin: true }, () => {
before(() => {
Expand Down Expand Up @@ -241,6 +238,26 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects.getMenuItems().should('not.exist');
});

it('should not show viewer role if not licensed', () => {
cy.signinAsOwner();
cy.visit(workflowsPage.url);

projects.getMenuItems().first().click();
projects.getProjectTabSettings().click();

cy.get(
`[data-test-id="user-list-item-${INSTANCE_MEMBERS[0].email}"] [data-test-id="projects-settings-user-role-select"]`,
).click();

cy.get('.el-select-dropdown__item.is-disabled')
.should('contain.text', 'Viewer')
.get('span:contains("Upgrade")')
.filter(':visible')
.click();

getVisibleModalOverlay().should('contain.text', 'Upgrade to unlock additional roles');
});

describe('when starting from scratch', () => {
beforeEach(() => {
cy.resetDatabase();
Expand All @@ -257,7 +274,7 @@ describe('Projects', { disableAutoLogin: true }, () => {

// Create a project and add a credential to it
cy.intercept('POST', '/rest/projects').as('projectCreate');
projects.getAddProjectButton().should('contain', 'Add project').should('be.visible').click();
projects.getAddProjectButton().click();
cy.wait('@projectCreate');
projects.getMenuItems().should('have.length', 1);
projects.getMenuItems().first().click();
Expand Down Expand Up @@ -418,7 +435,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
});

it('should move resources between projects', () => {
cy.signin(INSTANCE_OWNER);
cy.signinAsOwner();
cy.visit(workflowsPage.url);

// Create a workflow and a credential in the Home project
Expand Down Expand Up @@ -563,5 +580,80 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('have.length', 2);
});

it('should handle viewer role', () => {
cy.enableFeature('projectRole:viewer');
cy.signinAsOwner();
cy.visit(workflowsPage.url);

projects.createProject('Development');
projects.addProjectMember(INSTANCE_MEMBERS[0].email, 'Viewer');
projects.getProjectSettingsSaveButton().click();

projects.getProjectTabWorkflows().click();
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_4_executions_view.json', 'WF with random error');
executionsTab.actions.createManualExecutions(2);
executionsTab.actions.toggleNodeEnabled('Error');
executionsTab.actions.createManualExecutions(2);
workflowPage.actions.saveWorkflowUsingKeyboardShortcut();

projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Notion API');

mainSidebar.actions.openUserMenu();
cy.getByTestId('user-menu-item-logout').click();

cy.get('input[name="email"]').type(INSTANCE_MEMBERS[0].email);
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
cy.getByTestId('form-submit-button').click();

mainSidebar.getters.executions().click();
cy.getByTestId('global-execution-list-item').first().find('td:last button').click();
getVisibleDropdown()
.find('li')
.filter(':contains("Retry")')
.should('have.class', 'is-disabled');
getVisibleDropdown()
.find('li')
.filter(':contains("Delete")')
.should('have.class', 'is-disabled');

projects.getMenuItems().first().click();
cy.getByTestId('workflow-card-name').should('be.visible').first().click();
workflowPage.getters.nodeViewRoot().should('be.visible');
workflowPage.getters.executeWorkflowButton().should('not.exist');
workflowPage.getters.nodeCreatorPlusButton().should('not.exist');
workflowPage.getters.canvasNodes().should('have.length', 3).last().click();
cy.get('body').type('{backspace}');
workflowPage.getters.canvasNodes().should('have.length', 3).last().rightclick();
getVisibleDropdown()
.find('li')
.should('be.visible')
.filter(
':contains("Open"), :contains("Copy"), :contains("Select all"), :contains("Clear selection")',
)
.should('not.have.class', 'is-disabled');
cy.get('body').type('{esc}');

executionsTab.actions.switchToExecutionsTab();
cy.getByTestId('retry-execution-button')
.should('be.visible')
.find('.is-disabled')
.should('exist');
cy.get('button:contains("Debug")').should('be.disabled');
cy.get('button[title="Retry execution"]').should('be.disabled');
cy.get('button[title="Delete this execution"]').should('be.disabled');

projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().filter(':contains("Notion")').click();
cy.getByTestId('node-credentials-config-container')
.should('be.visible')
.find('input')
.should('not.have.length');
});
});
});
23 changes: 23 additions & 0 deletions packages/@n8n/permissions/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const DEFAULT_OPERATIONS = ['create', 'read', 'update', 'delete', 'list'] as const;
export const RESOURCES = {
auditLogs: ['manage'] as const,
banner: ['dismiss'] as const,
communityPackage: ['install', 'uninstall', 'update', 'list', 'manage'] as const,
credential: ['share', 'move', ...DEFAULT_OPERATIONS] as const,
externalSecretsProvider: ['sync', ...DEFAULT_OPERATIONS] as const,
externalSecret: ['list', 'use'] as const,
eventBusDestination: ['test', ...DEFAULT_OPERATIONS] as const,
ldap: ['sync', 'manage'] as const,
license: ['manage'] as const,
logStreaming: ['manage'] as const,
orchestration: ['read', 'list'] as const,
project: [...DEFAULT_OPERATIONS] as const,
saml: ['manage'] as const,
securityAudit: ['generate'] as const,
sourceControl: ['pull', 'push', 'manage'] as const,
tag: [...DEFAULT_OPERATIONS] as const,
user: ['resetPassword', 'changeRole', ...DEFAULT_OPERATIONS] as const,
variable: [...DEFAULT_OPERATIONS] as const,
workersView: ['manage'] as const,
workflow: ['share', 'execute', 'move', ...DEFAULT_OPERATIONS] as const,
} as const;
1 change: 1 addition & 0 deletions packages/@n8n/permissions/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export type * from './types';
export * from './constants';
export * from './hasScope';
export * from './combineScopes';
26 changes: 4 additions & 22 deletions packages/@n8n/permissions/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,7 @@
export type DefaultOperations = 'create' | 'read' | 'update' | 'delete' | 'list';
export type Resource =
| 'auditLogs'
| 'banner'
| 'communityPackage'
| 'credential'
| 'externalSecretsProvider'
| 'externalSecret'
| 'eventBusDestination'
| 'ldap'
| 'license'
| 'logStreaming'
| 'orchestration'
| 'project'
| 'saml'
| 'securityAudit'
| 'sourceControl'
| 'tag'
| 'user'
| 'variable'
| 'workersView'
| 'workflow';
import type { DEFAULT_OPERATIONS, RESOURCES } from './constants';

export type DefaultOperations = (typeof DEFAULT_OPERATIONS)[number];
export type Resource = keyof typeof RESOURCES;

export type ResourceScope<
R extends Resource,
Expand Down
Loading

0 comments on commit 117e2d9

Please sign in to comment.