diff --git a/cypress/e2e/17-sharing.cy.ts b/cypress/e2e/17-sharing.cy.ts index 84365153c7..6aa8b64e7e 100644 --- a/cypress/e2e/17-sharing.cy.ts +++ b/cypress/e2e/17-sharing.cy.ts @@ -126,8 +126,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { cy.visit(workflowW2Url); cy.waitForLoad(); - cy.wait(1000); - cy.get('.el-notification').contains('Could not find workflow').should('be.visible'); + cy.location('pathname', { timeout: 10000 }).should('eq', '/entity-not-authorized/workflow'); }); it('should have access to W1, W2, as U1', () => { diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index b5a1a9a51e..e90e387d62 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -708,6 +708,11 @@ "error": "Error", "error.goBack": "Go back", "error.pageNotFound": "Oops, couldn’t find that", + "error.entityNotFound.title": "{entity} not found", + "error.entityNotFound.text": "We couldn’t find the {entity} you were looking for. Make sure you have the correct URL.", + "error.entityNotFound.action": "Go to overview", + "error.entityUnAuthorized.title": "You need access", + "error.entityUnAuthorized.content": "You don't have permission to view this {entity}. Please contact the person who shared this link to request access.", "executions.ExecutionStatus": "Execution status", "executions.concurrency.docsLink": "https://docs.n8n.io/hosting/scaling/concurrency-control/", "executionDetails.additionalActions": "Additional Actions", diff --git a/packages/frontend/editor-ui/src/constants.ts b/packages/frontend/editor-ui/src/constants.ts index dde5f5f12d..effa8c7346 100644 --- a/packages/frontend/editor-ui/src/constants.ts +++ b/packages/frontend/editor-ui/src/constants.ts @@ -571,6 +571,8 @@ export const enum VIEWS { SHARED_WITH_ME = 'SharedWithMe', SHARED_WORKFLOWS = 'SharedWorkflows', SHARED_CREDENTIALS = 'SharedCredentials', + ENTITY_NOT_FOUND = 'EntityNotFound', + ENTITY_UNAUTHORIZED = 'EntityUnAuthorized', } export const EDITABLE_CANVAS_VIEWS = [VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW, VIEWS.EXECUTION_DEBUG]; diff --git a/packages/frontend/editor-ui/src/router.ts b/packages/frontend/editor-ui/src/router.ts index 42df95c95a..867a567a43 100644 --- a/packages/frontend/editor-ui/src/router.ts +++ b/packages/frontend/editor-ui/src/router.ts @@ -24,6 +24,8 @@ import TestRunDetailView from '@/views/Evaluations.ee/TestRunDetailView.vue'; const ChangePasswordView = async () => await import('./views/ChangePasswordView.vue'); const ErrorView = async () => await import('./views/ErrorView.vue'); +const EntityNotFound = async () => await import('./views/EntityNotFound.vue'); +const EntityUnAuthorised = async () => await import('./views/EntityUnAuthorised.vue'); const ForgotMyPasswordView = async () => await import('./views/ForgotMyPasswordView.vue'); const MainHeader = async () => await import('@/components/MainHeader/MainHeader.vue'); const MainSidebar = async () => await import('@/components/MainSidebar.vue'); @@ -721,6 +723,24 @@ export const routes: RouteRecordRaw[] = [ }, ...projectsRoutes, ...insightsRoutes, + { + path: '/entity-not-found/:entityType(credential|workflow)', + props: true, + name: VIEWS.ENTITY_NOT_FOUND, + components: { + default: EntityNotFound, + sidebar: MainSidebar, + }, + }, + { + path: '/entity-not-authorized/:entityType(credential|workflow)', + props: true, + name: VIEWS.ENTITY_UNAUTHORIZED, + components: { + default: EntityUnAuthorised, + sidebar: MainSidebar, + }, + }, { path: '/:pathMatch(.*)*', name: VIEWS.NOT_FOUND, diff --git a/packages/frontend/editor-ui/src/views/CredentialsView.test.ts b/packages/frontend/editor-ui/src/views/CredentialsView.test.ts index 3c6b470140..c993cf5f9c 100644 --- a/packages/frontend/editor-ui/src/views/CredentialsView.test.ts +++ b/packages/frontend/editor-ui/src/views/CredentialsView.test.ts @@ -32,6 +32,16 @@ const router = createRouter({ name: VIEWS.CREDENTIALS, component: { template: '
' }, }, + { + path: '/entity-un-authorized', + name: VIEWS.ENTITY_UNAUTHORIZED, + component: { template: '' }, + }, + { + path: '/entity-not-found', + name: VIEWS.ENTITY_NOT_FOUND, + component: { template: '' }, + }, ], }); @@ -189,6 +199,41 @@ describe('CredentialsView', () => { }); }); + it("should redirect to unauthorized page if user doesn't have read or update permissions", async () => { + const replaceSpy = vi.spyOn(router, 'replace'); + const credentialsStore = mockedStore(useCredentialsStore); + credentialsStore.getCredentialById = vi.fn().mockImplementation(() => ({ + id: 'abc123', + name: 'test', + type: 'test', + createdAt: '2021-05-05T00:00:00Z', + updatedAt: '2021-05-05T00:00:00Z', + scopes: [], + })); + const { rerender } = renderComponent(); + await rerender({ credentialId: 'abc123' }); + expect(replaceSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: VIEWS.ENTITY_UNAUTHORIZED, + params: { entityType: 'credential' }, + }), + ); + }); + + it("should redirect to not found page if the credential doesn't exist", async () => { + const replaceSpy = vi.spyOn(router, 'replace'); + const credentialsStore = mockedStore(useCredentialsStore); + credentialsStore.getCredentialById = vi.fn().mockImplementation(() => undefined); + const { rerender } = renderComponent(); + await rerender({ credentialId: 'abc123' }); + expect(replaceSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: VIEWS.ENTITY_NOT_FOUND, + params: { entityType: 'credential' }, + }), + ); + }); + describe('filters', () => { it('should filter by type', async () => { await router.push({ name: VIEWS.CREDENTIALS, query: { type: ['test'] } }); diff --git a/packages/frontend/editor-ui/src/views/CredentialsView.vue b/packages/frontend/editor-ui/src/views/CredentialsView.vue index af56889c6c..0a3ac9e3d9 100644 --- a/packages/frontend/editor-ui/src/views/CredentialsView.vue +++ b/packages/frontend/editor-ui/src/views/CredentialsView.vue @@ -6,7 +6,6 @@ import ResourcesListLayout, { } from '@/components/layouts/ResourcesListLayout.vue'; import ProjectHeader from '@/components/Projects/ProjectHeader.vue'; import { useDocumentTitle } from '@/composables/useDocumentTitle'; -import { useI18n } from '@n8n/i18n'; import { useProjectPages } from '@/composables/useProjectPages'; import { useTelemetry } from '@/composables/useTelemetry'; import { @@ -31,6 +30,7 @@ import { useUsersStore } from '@/stores/users.store'; import type { Project } from '@/types/projects.types'; import { isCredentialsResource } from '@/utils/typeGuards'; import { N8nCheckbox } from '@n8n/design-system'; +import { useI18n } from '@n8n/i18n'; import pickBy from 'lodash/pickBy'; import type { ICredentialType, ICredentialsDecrypted } from 'n8n-workflow'; import { CREDENTIAL_EMPTY_VALUE } from 'n8n-workflow'; @@ -169,15 +169,26 @@ const maybeCreateCredential = () => { } }; -const maybeEditCredential = () => { +const maybeEditCredential = async () => { if (!!props.credentialId && props.credentialId !== 'create') { const credential = credentialsStore.getCredentialById(props.credentialId); const credentialPermissions = getResourcePermissions(credential?.scopes).credential; - if (credential && (credentialPermissions.update || credentialPermissions.read)) { - uiStore.openExistingCredential(props.credentialId); - } else { - void router.replace({ name: VIEWS.HOMEPAGE }); + if (!credential) { + return await router.replace({ + name: VIEWS.ENTITY_NOT_FOUND, + params: { entityType: 'credential' }, + }); } + + if (credentialPermissions.update || credentialPermissions.read) { + uiStore.openExistingCredential(props.credentialId); + return; + } + + return await router.replace({ + name: VIEWS.ENTITY_UNAUTHORIZED, + params: { entityType: 'credential' }, + }); } }; @@ -200,7 +211,7 @@ const initialize = async () => { await Promise.all(loadPromises); maybeCreateCredential(); - maybeEditCredential(); + await maybeEditCredential(); loading.value = false; }; @@ -225,7 +236,7 @@ watch( () => props.credentialId, () => { maybeCreateCredential(); - maybeEditCredential(); + void maybeEditCredential(); }, ); diff --git a/packages/frontend/editor-ui/src/views/EntityNotFound.vue b/packages/frontend/editor-ui/src/views/EntityNotFound.vue new file mode 100644 index 0000000000..6afed5e0e5 --- /dev/null +++ b/packages/frontend/editor-ui/src/views/EntityNotFound.vue @@ -0,0 +1,67 @@ + + + +