fix: Add folder scopes to global owner and admin roles (#19230)

This commit is contained in:
Stephen Wright
2025-09-08 08:08:21 +01:00
committed by GitHub
parent ed8fd32692
commit 2113532946
4 changed files with 59 additions and 9 deletions

View File

@@ -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',

View File

@@ -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) {

View File

@@ -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 () => {

View File

@@ -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', () => {