Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/frontend/@n8n/i18n/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2354,8 +2354,9 @@
"projectRoles.workflow:read": "View",
"projectRoles.workflow:read.tooltip": "View workflows within the project",
"projectRoles.workflow:execute": "Execute",
"projectRoles.workflow:execute.tooltip": "Execute workflows within the project",
"projectRoles.workflow:update": "Edit",
"projectRoles.workflow:update.tooltip": "Edit workflow content and execute workflows",
"projectRoles.workflow:update.tooltip": "Edit workflow content",
"projectRoles.workflow:create": "Create",
"projectRoles.workflow:create.tooltip": "Create new workflows",
"projectRoles.workflow:share": "Share",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ describe('RoleHoverPopover', () => {
it('should display permission count', () => {
const { getByText } = renderComponent();

expect(getByText('3/40 permissions')).toBeInTheDocument();
expect(getByText('3/41 permissions')).toBeInTheDocument();
});

it('should display role description when available', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,116 @@ describe('ProjectRoleView', () => {
});
});

describe('workflow:execute scope dependency', () => {
it('should render workflow:execute checkbox in the UI', async () => {
const { getByTestId } = renderComponent();

await waitFor(() =>
expect(getByTestId('scope-checkbox-workflow:execute')).toBeInTheDocument(),
);
});

it('should auto-check workflow:read when workflow:execute is checked and workflow:read is unchecked', async () => {
const { getByTestId } = renderComponent();

await waitFor(() =>
expect(getByTestId('scope-checkbox-workflow:execute')).toBeInTheDocument(),
);

const executeCheckbox = getByTestId('scope-checkbox-workflow:execute');
const readCheckbox = getByTestId('scope-checkbox-workflow:read');

// workflow:read starts checked (it's in defaultScopes), uncheck it first
await userEvent.click(readCheckbox);
expect(readCheckbox).not.toBeChecked();
expect(executeCheckbox).not.toBeChecked();

// Now check workflow:execute — should auto-check workflow:read
await userEvent.click(executeCheckbox);
expect(executeCheckbox).toBeChecked();
expect(readCheckbox).toBeChecked();
});

it('should not double-add workflow:read when workflow:execute is checked and workflow:read is already checked', async () => {
const { getByTestId } = renderComponent();

await waitFor(() =>
expect(getByTestId('scope-checkbox-workflow:execute')).toBeInTheDocument(),
);

const executeCheckbox = getByTestId('scope-checkbox-workflow:execute');
const readCheckbox = getByTestId('scope-checkbox-workflow:read');

// workflow:read is already checked (it's in defaultScopes)
expect(readCheckbox).toBeChecked();

await userEvent.click(executeCheckbox);
expect(executeCheckbox).toBeChecked();
// workflow:read should still be checked, not toggled off
expect(readCheckbox).toBeChecked();
});

it('should auto-uncheck workflow:execute when workflow:read is unchecked', async () => {
const { getByTestId } = renderComponent();

await waitFor(() =>
expect(getByTestId('scope-checkbox-workflow:execute')).toBeInTheDocument(),
);

const executeCheckbox = getByTestId('scope-checkbox-workflow:execute');
const readCheckbox = getByTestId('scope-checkbox-workflow:read');

// First enable workflow:execute
await userEvent.click(executeCheckbox);
expect(executeCheckbox).toBeChecked();
expect(readCheckbox).toBeChecked();

// Uncheck workflow:read — should auto-uncheck workflow:execute
await userEvent.click(readCheckbox);
expect(readCheckbox).not.toBeChecked();
expect(executeCheckbox).not.toBeChecked();
});

it('should not affect workflow:execute when workflow:read is unchecked and workflow:execute was not checked', async () => {
const { getByTestId } = renderComponent();

await waitFor(() =>
expect(getByTestId('scope-checkbox-workflow:execute')).toBeInTheDocument(),
);

const executeCheckbox = getByTestId('scope-checkbox-workflow:execute');
const readCheckbox = getByTestId('scope-checkbox-workflow:read');

// workflow:execute is not checked; uncheck workflow:read
expect(executeCheckbox).not.toBeChecked();
await userEvent.click(readCheckbox);
expect(readCheckbox).not.toBeChecked();
expect(executeCheckbox).not.toBeChecked();
});

it('should NOT auto-toggle workflow:execute when workflow:update is toggled', async () => {
const { getByTestId } = renderComponent();

await waitFor(() =>
expect(getByTestId('scope-checkbox-workflow:update')).toBeInTheDocument(),
);

const updateCheckbox = getByTestId('scope-checkbox-workflow:update');
const executeCheckbox = getByTestId('scope-checkbox-workflow:execute');

expect(executeCheckbox).not.toBeChecked();

await userEvent.click(updateCheckbox);
expect(updateCheckbox).toBeChecked();
// workflow:execute must remain unchanged
expect(executeCheckbox).not.toBeChecked();

await userEvent.click(updateCheckbox);
expect(updateCheckbox).not.toBeChecked();
expect(executeCheckbox).not.toBeChecked();
});
});

describe('External Secrets Scopes', () => {
it('should not render externalSecretsProvider scope type when forProjects is off', () => {
const { queryByText } = renderComponent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ const scopes = SCOPES;

function toggleScope(scope: string) {
const index = form.value.scopes.indexOf(scope);
const isBeingAdded = index === -1;

if (index !== -1) {
form.value.scopes.splice(index, 1);
} else {
Expand All @@ -155,8 +157,17 @@ function toggleScope(scope: string) {
toggleScope(scope.replace(':read', ':list'));
}

if (scope === 'workflow:update') {
toggleScope('workflow:execute');
// Dependency: workflow:execute requires workflow:read
if (scope === 'workflow:execute' && isBeingAdded) {
if (!form.value.scopes.includes('workflow:read')) {
toggleScope('workflow:read');
}
}

if (scope === 'workflow:read' && !isBeingAdded) {
if (form.value.scopes.includes('workflow:execute')) {
toggleScope('workflow:execute');
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const UI_OPERATIONS = {
folder: ['read', 'update', 'create', 'move', 'delete'],
workflow: [
'read',
'execute',
'update',
'create',
'publish',
Expand Down
Loading