diff --git a/packages/@n8n/permissions/src/roles/scopes/global-scopes.ee.ts b/packages/@n8n/permissions/src/roles/scopes/global-scopes.ee.ts index 5c167bbf0e..e5f367a72c 100644 --- a/packages/@n8n/permissions/src/roles/scopes/global-scopes.ee.ts +++ b/packages/@n8n/permissions/src/roles/scopes/global-scopes.ee.ts @@ -78,6 +78,11 @@ export const GLOBAL_OWNER_SCOPES: Scope[] = [ 'project:delete', 'insights:list', 'folder:move', + 'folder:read', + 'folder:update', + 'folder:delete', + 'folder:create', + 'folder:list', 'oidc:manage', 'dataStore:list', 'role:manage', diff --git a/packages/cli/src/controllers/folder.controller.ts b/packages/cli/src/controllers/folder.controller.ts index 51746e52d3..3137d6f33a 100644 --- a/packages/cli/src/controllers/folder.controller.ts +++ b/packages/cli/src/controllers/folder.controller.ts @@ -18,8 +18,9 @@ import { Put, Param, Licensed, + Middleware, } from '@n8n/decorators'; -import { Response } from 'express'; +import { NextFunction, Response } from 'express'; import { UserError } from 'n8n-workflow'; import { FolderNotFoundError } from '@/errors/folder-not-found.error'; @@ -28,14 +29,32 @@ import { InternalServerError } from '@/errors/response-errors/internal-server.er import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { FolderService } from '@/services/folder.service'; import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee'; +import { ProjectService } from '@/services/project.service.ee'; @RestController('/projects/:projectId/folders') export class ProjectController { constructor( private readonly folderService: FolderService, private readonly enterpriseWorkflowService: EnterpriseWorkflowService, + private readonly projectService: ProjectService, ) {} + @Middleware() + async validateProjectExists( + req: AuthenticatedRequest<{ projectId: string }>, + res: Response, + next: NextFunction, + ) { + try { + const { projectId } = req.params; + await this.projectService.getProject(projectId); + next(); + } catch (e) { + res.status(404).send('Project not found'); + return; + } + } + @Post('/') @ProjectScope('folder:create') @Licensed('feat:folders') @@ -44,8 +63,10 @@ export class ProjectController { _res: Response, @Body payload: CreateFolderDto, ) { + const { projectId } = req.params; + try { - const folder = await this.folderService.createFolder(payload, req.params.projectId); + const folder = await this.folderService.createFolder(payload, projectId); return folder; } catch (e) { if (e instanceof FolderNotFoundError) { diff --git a/packages/cli/test/integration/folder/folder.controller.test.ts b/packages/cli/test/integration/folder/folder.controller.test.ts index 29dd4759ce..100bfd0923 100644 --- a/packages/cli/test/integration/folder/folder.controller.test.ts +++ b/packages/cli/test/integration/folder/folder.controller.test.ts @@ -87,7 +87,7 @@ describe('POST /projects/:projectId/folders', () => { name: 'Test Folder', }; - await authOwnerAgent.post('/projects/non-existing-id/folders').send(payload).expect(403); + await authOwnerAgent.post('/projects/non-existing-id/folders').send(payload).expect(404); }); test('should not create folder when name is empty', async () => { @@ -278,7 +278,7 @@ describe('GET /projects/:projectId/folders/:folderId/tree', () => { }); test('should not get folder tree when project does not exist', async () => { - await authOwnerAgent.get('/projects/non-existing-id/folders/some-folder-id/tree').expect(403); + await authOwnerAgent.get('/projects/non-existing-id/folders/some-folder-id/tree').expect(404); }); test('should not get folder tree when folder does not exist', async () => { @@ -418,7 +418,7 @@ describe('GET /projects/:projectId/folders/:folderId/credentials', () => { test('should not get folder credentials when project does not exist', async () => { await authOwnerAgent .get('/projects/non-existing-id/folders/some-folder-id/credentials') - .expect(403); + .expect(404); }); test('should not get folder credentials when folder does not exist', async () => { @@ -545,7 +545,7 @@ describe('PATCH /projects/:projectId/folders/:folderId', () => { await authOwnerAgent .patch('/projects/non-existing-id/folders/some-folder-id') .send(payload) - .expect(403); + .expect(404); }); test('should not update folder when folder does not exist', async () => { @@ -1005,7 +1005,7 @@ describe('DELETE /projects/:projectId/folders/:folderId', () => { await authOwnerAgent .delete('/projects/non-existing-id/folders/some-folder-id') .send({}) - .expect(403); + .expect(404); }); test('should not delete folder when folder does not exist', async () => { @@ -1303,7 +1303,7 @@ describe('GET /projects/:projectId/folders', () => { }); test('should not list folders when project does not exist', async () => { - await authOwnerAgent.get('/projects/non-existing-id/folders').expect(403); + await authOwnerAgent.get('/projects/non-existing-id/folders').expect(404); }); test('should not list folders if user has no access to project', async () => { @@ -1731,7 +1731,7 @@ describe('GET /projects/:projectId/folders/content', () => { test('should not list folders when project does not exist', async () => { await authOwnerAgent .get('/projects/non-existing-id/folders/no-existing-id/content') - .expect(403); + .expect(404); }); test('should not return folder content if user has no access to project', async () => { diff --git a/packages/cli/test/integration/project.api.test.ts b/packages/cli/test/integration/project.api.test.ts index 5b4a08c6c8..5943813d5f 100644 --- a/packages/cli/test/integration/project.api.test.ts +++ b/packages/cli/test/integration/project.api.test.ts @@ -853,6 +853,30 @@ describe('GET /project/:projectId', () => { role: 'project:admin', }); }); + + test('should have correct folder scopes when, as an admin / owner, I fetch a project created by a different user', async () => { + const [ownerUser, testUser1] = await Promise.all([createOwner(), createUser()]); + + const createdProject = await createTeamProject(undefined, testUser1); + + const memberAgent = testServer.authAgentFor(ownerUser); + + const resp = await memberAgent.get(`/projects/${createdProject.id}`); + expect(resp.status).toBe(200); + + expect(resp.body.data.id).toBe(createdProject.id); + expect(resp.body.data.name).toBe(createdProject.name); + + expect(resp.body.data.scopes).toEqual( + expect.arrayContaining([ + 'folder:read', + 'folder:update', + 'folder:delete', + 'folder:create', + 'folder:list', + ]), + ); + }); }); describe('DELETE /project/:projectId', () => {