mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
fix: Check license in folder controller (no-changelog) (#16937)
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
Put,
|
Put,
|
||||||
Param,
|
Param,
|
||||||
|
Licensed,
|
||||||
} from '@n8n/decorators';
|
} from '@n8n/decorators';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { UserError } from 'n8n-workflow';
|
import { UserError } from 'n8n-workflow';
|
||||||
@@ -37,6 +38,7 @@ export class ProjectController {
|
|||||||
|
|
||||||
@Post('/')
|
@Post('/')
|
||||||
@ProjectScope('folder:create')
|
@ProjectScope('folder:create')
|
||||||
|
@Licensed('feat:folders')
|
||||||
async createFolder(
|
async createFolder(
|
||||||
req: AuthenticatedRequest<{ projectId: string }>,
|
req: AuthenticatedRequest<{ projectId: string }>,
|
||||||
_res: Response,
|
_res: Response,
|
||||||
@@ -55,6 +57,7 @@ export class ProjectController {
|
|||||||
|
|
||||||
@Get('/:folderId/tree')
|
@Get('/:folderId/tree')
|
||||||
@ProjectScope('folder:read')
|
@ProjectScope('folder:read')
|
||||||
|
@Licensed('feat:folders')
|
||||||
async getFolderTree(
|
async getFolderTree(
|
||||||
req: AuthenticatedRequest<{ projectId: string; folderId: string }>,
|
req: AuthenticatedRequest<{ projectId: string; folderId: string }>,
|
||||||
_res: Response,
|
_res: Response,
|
||||||
@@ -74,6 +77,7 @@ export class ProjectController {
|
|||||||
|
|
||||||
@Get('/:folderId/credentials')
|
@Get('/:folderId/credentials')
|
||||||
@ProjectScope('folder:read')
|
@ProjectScope('folder:read')
|
||||||
|
@Licensed('feat:folders')
|
||||||
async getFolderUsedCredentials(
|
async getFolderUsedCredentials(
|
||||||
req: AuthenticatedRequest<{ projectId: string; folderId: string }>,
|
req: AuthenticatedRequest<{ projectId: string; folderId: string }>,
|
||||||
_res: Response,
|
_res: Response,
|
||||||
@@ -97,6 +101,7 @@ export class ProjectController {
|
|||||||
|
|
||||||
@Patch('/:folderId')
|
@Patch('/:folderId')
|
||||||
@ProjectScope('folder:update')
|
@ProjectScope('folder:update')
|
||||||
|
@Licensed('feat:folders')
|
||||||
async updateFolder(
|
async updateFolder(
|
||||||
req: AuthenticatedRequest<{ projectId: string; folderId: string }>,
|
req: AuthenticatedRequest<{ projectId: string; folderId: string }>,
|
||||||
_res: Response,
|
_res: Response,
|
||||||
@@ -118,6 +123,7 @@ export class ProjectController {
|
|||||||
|
|
||||||
@Delete('/:folderId')
|
@Delete('/:folderId')
|
||||||
@ProjectScope('folder:delete')
|
@ProjectScope('folder:delete')
|
||||||
|
@Licensed('feat:folders')
|
||||||
async deleteFolder(
|
async deleteFolder(
|
||||||
req: AuthenticatedRequest<{ projectId: string; folderId: string }>,
|
req: AuthenticatedRequest<{ projectId: string; folderId: string }>,
|
||||||
_res: Response,
|
_res: Response,
|
||||||
@@ -139,6 +145,7 @@ export class ProjectController {
|
|||||||
|
|
||||||
@Get('/')
|
@Get('/')
|
||||||
@ProjectScope('folder:list')
|
@ProjectScope('folder:list')
|
||||||
|
@Licensed('feat:folders')
|
||||||
async listFolders(
|
async listFolders(
|
||||||
req: AuthenticatedRequest<{ projectId: string }>,
|
req: AuthenticatedRequest<{ projectId: string }>,
|
||||||
res: Response,
|
res: Response,
|
||||||
@@ -153,6 +160,7 @@ export class ProjectController {
|
|||||||
|
|
||||||
@Get('/:folderId/content')
|
@Get('/:folderId/content')
|
||||||
@ProjectScope('folder:read')
|
@ProjectScope('folder:read')
|
||||||
|
@Licensed('feat:folders')
|
||||||
async getFolderContent(req: AuthenticatedRequest<{ projectId: string; folderId: string }>) {
|
async getFolderContent(req: AuthenticatedRequest<{ projectId: string; folderId: string }>) {
|
||||||
const { projectId, folderId } = req.params;
|
const { projectId, folderId } = req.params;
|
||||||
|
|
||||||
@@ -174,6 +182,7 @@ export class ProjectController {
|
|||||||
|
|
||||||
@Put('/:folderId/transfer')
|
@Put('/:folderId/transfer')
|
||||||
@ProjectScope('folder:move')
|
@ProjectScope('folder:move')
|
||||||
|
@Licensed('feat:folders')
|
||||||
async transferFolderToProject(
|
async transferFolderToProject(
|
||||||
req: AuthenticatedRequest,
|
req: AuthenticatedRequest,
|
||||||
_res: unknown,
|
_res: unknown,
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ let workflowRepository: WorkflowRepository;
|
|||||||
const activeWorkflowManager = mockInstance(ActiveWorkflowManager);
|
const activeWorkflowManager = mockInstance(ActiveWorkflowManager);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
testServer.license.enable('feat:folders');
|
||||||
|
|
||||||
await testDb.truncate(['Folder', 'SharedWorkflow', 'TagEntity', 'Project', 'ProjectRelation']);
|
await testDb.truncate(['Folder', 'SharedWorkflow', 'TagEntity', 'Project', 'ProjectRelation']);
|
||||||
|
|
||||||
projectRepository = Container.get(ProjectRepository);
|
projectRepository = Container.get(ProjectRepository);
|
||||||
@@ -66,6 +68,18 @@ beforeEach(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /projects/:projectId/folders', () => {
|
describe('POST /projects/:projectId/folders', () => {
|
||||||
|
test('should now create folder if license does not allow it', async () => {
|
||||||
|
testServer.license.disable('feat:folders');
|
||||||
|
const project = await createTeamProject(undefined, owner);
|
||||||
|
await linkUserToProject(member, project, 'project:viewer');
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: 'Test Folder',
|
||||||
|
};
|
||||||
|
|
||||||
|
await authMemberAgent.post(`/projects/${project.id}/folders`).send(payload).expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
test('should not create folder when project does not exist', async () => {
|
test('should not create folder when project does not exist', async () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: 'Test Folder',
|
name: 'Test Folder',
|
||||||
@@ -235,6 +249,32 @@ describe('POST /projects/:projectId/folders', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /projects/:projectId/folders/:folderId/tree', () => {
|
describe('GET /projects/:projectId/folders/:folderId/tree', () => {
|
||||||
|
test('should not retrieve folder tree if license does not allow it', async () => {
|
||||||
|
testServer.license.disable('feat:folders');
|
||||||
|
|
||||||
|
const project = await createTeamProject('test', owner);
|
||||||
|
const rootFolder = await createFolder(project, { name: 'Root' });
|
||||||
|
|
||||||
|
const childFolder1 = await createFolder(project, {
|
||||||
|
name: 'Child 1',
|
||||||
|
parentFolder: rootFolder,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createFolder(project, {
|
||||||
|
name: 'Child 2',
|
||||||
|
parentFolder: rootFolder,
|
||||||
|
});
|
||||||
|
|
||||||
|
const grandchildFolder = await createFolder(project, {
|
||||||
|
name: 'Grandchild',
|
||||||
|
parentFolder: childFolder1,
|
||||||
|
});
|
||||||
|
|
||||||
|
await authOwnerAgent
|
||||||
|
.get(`/projects/${project.id}/folders/${grandchildFolder.id}/tree`)
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
test('should not get folder tree when project does not exist', async () => {
|
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(403);
|
||||||
});
|
});
|
||||||
@@ -311,6 +351,68 @@ describe('GET /projects/:projectId/folders/:folderId/tree', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /projects/:projectId/folders/:folderId/credentials', () => {
|
describe('GET /projects/:projectId/folders/:folderId/credentials', () => {
|
||||||
|
test('should not retrieve folder tree if license does not allow it', async () => {
|
||||||
|
testServer.license.disable('feat:folders');
|
||||||
|
|
||||||
|
const project = await createTeamProject('test', owner);
|
||||||
|
const rootFolder = await createFolder(project, { name: 'Root' });
|
||||||
|
|
||||||
|
const childFolder1 = await createFolder(project, {
|
||||||
|
name: 'Child 1',
|
||||||
|
parentFolder: rootFolder,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createFolder(project, {
|
||||||
|
name: 'Child 2',
|
||||||
|
parentFolder: rootFolder,
|
||||||
|
});
|
||||||
|
|
||||||
|
const grandchildFolder = await createFolder(project, {
|
||||||
|
name: 'Grandchild',
|
||||||
|
parentFolder: childFolder1,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const folder of [rootFolder, childFolder1, grandchildFolder]) {
|
||||||
|
const credential = await createCredentials(
|
||||||
|
{
|
||||||
|
name: `Test credential ${folder.name}`,
|
||||||
|
data: '',
|
||||||
|
type: 'test',
|
||||||
|
},
|
||||||
|
project,
|
||||||
|
);
|
||||||
|
|
||||||
|
await createWorkflow(
|
||||||
|
{
|
||||||
|
name: 'Test Workflow',
|
||||||
|
parentFolder: folder,
|
||||||
|
active: false,
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
parameters: {},
|
||||||
|
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||||
|
typeVersion: 1.2,
|
||||||
|
position: [0, 0],
|
||||||
|
id: faker.string.uuid(),
|
||||||
|
name: 'OpenAI Chat Model',
|
||||||
|
credentials: {
|
||||||
|
openAiApi: {
|
||||||
|
id: credential.id,
|
||||||
|
name: credential.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
owner,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await authOwnerAgent
|
||||||
|
.get(`/projects/${project.id}/folders/${childFolder1.id}/credentials`)
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
test('should not get folder credentials when project does not exist', async () => {
|
test('should not get folder credentials when project does not exist', async () => {
|
||||||
await authOwnerAgent
|
await authOwnerAgent
|
||||||
.get('/projects/non-existing-id/folders/some-folder-id/credentials')
|
.get('/projects/non-existing-id/folders/some-folder-id/credentials')
|
||||||
@@ -416,6 +518,23 @@ describe('GET /projects/:projectId/folders/:folderId/credentials', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PATCH /projects/:projectId/folders/:folderId', () => {
|
describe('PATCH /projects/:projectId/folders/:folderId', () => {
|
||||||
|
test('should not update folder if license does not allow it', async () => {
|
||||||
|
testServer.license.disable('feat:folders');
|
||||||
|
|
||||||
|
const project = await createTeamProject(undefined, owner);
|
||||||
|
const folder = await createFolder(project, { name: 'Original Name' });
|
||||||
|
await linkUserToProject(member, project, 'project:editor');
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: 'Updated Folder Name',
|
||||||
|
};
|
||||||
|
|
||||||
|
await authMemberAgent
|
||||||
|
.patch(`/projects/${project.id}/folders/${folder.id}`)
|
||||||
|
.send(payload)
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
test('should not update folder when project does not exist', async () => {
|
test('should not update folder when project does not exist', async () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: 'Updated Folder Name',
|
name: 'Updated Folder Name',
|
||||||
@@ -868,6 +987,18 @@ describe('PATCH /projects/:projectId/folders/:folderId', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /projects/:projectId/folders/:folderId', () => {
|
describe('DELETE /projects/:projectId/folders/:folderId', () => {
|
||||||
|
test('should not delete folder if license does not allow it', async () => {
|
||||||
|
testServer.license.disable('feat:folders');
|
||||||
|
|
||||||
|
const project = await createTeamProject(undefined, owner);
|
||||||
|
const folder = await createFolder(project);
|
||||||
|
|
||||||
|
await authOwnerAgent
|
||||||
|
.delete(`/projects/${project.id}/folders/${folder.id}`)
|
||||||
|
.send({})
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
test('should not delete folder when project does not exist', async () => {
|
test('should not delete folder when project does not exist', async () => {
|
||||||
await authOwnerAgent
|
await authOwnerAgent
|
||||||
.delete('/projects/non-existing-id/folders/some-folder-id')
|
.delete('/projects/non-existing-id/folders/some-folder-id')
|
||||||
@@ -1159,6 +1290,16 @@ describe('DELETE /projects/:projectId/folders/:folderId', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /projects/:projectId/folders', () => {
|
describe('GET /projects/:projectId/folders', () => {
|
||||||
|
test('should not retrieve folder if license does not allow it', async () => {
|
||||||
|
testServer.license.disable('feat:folders');
|
||||||
|
|
||||||
|
const project = await createTeamProject('test project', owner);
|
||||||
|
await linkUserToProject(member, project, 'project:viewer');
|
||||||
|
await createFolder(project, { name: 'Test Folder' });
|
||||||
|
|
||||||
|
await authMemberAgent.get(`/projects/${project.id}/folders`).expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
test('should not list folders when project does not exist', async () => {
|
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(403);
|
||||||
});
|
});
|
||||||
@@ -1570,6 +1711,16 @@ describe('GET /projects/:projectId/folders', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /projects/:projectId/folders/content', () => {
|
describe('GET /projects/:projectId/folders/content', () => {
|
||||||
|
test('should not retrieve folder content if license does not allow it', async () => {
|
||||||
|
testServer.license.disable('feat:folders');
|
||||||
|
|
||||||
|
const project = await createTeamProject('test project', owner);
|
||||||
|
await linkUserToProject(member, project, 'project:viewer');
|
||||||
|
const folder = await createFolder(project, { name: 'Test Folder' });
|
||||||
|
|
||||||
|
await authMemberAgent.get(`/projects/${project.id}/folders/${folder.id}/content`).expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
test('should not list folders when project does not exist', async () => {
|
test('should not list folders when project does not exist', async () => {
|
||||||
await authOwnerAgent
|
await authOwnerAgent
|
||||||
.get('/projects/non-existing-id/folders/no-existing-id/content')
|
.get('/projects/non-existing-id/folders/no-existing-id/content')
|
||||||
@@ -1634,6 +1785,31 @@ describe('GET /projects/:projectId/folders/content', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /projects/:projectId/folders/:folderId/transfer', () => {
|
describe('PUT /projects/:projectId/folders/:folderId/transfer', () => {
|
||||||
|
test('should not transfer folder if license does not allow it', async () => {
|
||||||
|
testServer.license.disable('feat:folders');
|
||||||
|
|
||||||
|
const admin = await createUser({ role: 'global:admin' });
|
||||||
|
const sourceProject = await createTeamProject('source project', admin);
|
||||||
|
const destinationProject = await createTeamProject('destination project', member);
|
||||||
|
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
|
||||||
|
|
||||||
|
const credential = await saveCredential(randomCredentialPayload(), {
|
||||||
|
project: sourceProject,
|
||||||
|
role: 'credential:owner',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await testServer
|
||||||
|
.authAgentFor(owner)
|
||||||
|
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
|
||||||
|
.send({
|
||||||
|
destinationProjectId: destinationProject.id,
|
||||||
|
destinationParentFolderId: '0',
|
||||||
|
shareCredentials: [credential.id],
|
||||||
|
})
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
test('cannot transfer into the same project', async () => {
|
test('cannot transfer into the same project', async () => {
|
||||||
const sourceProject = await createTeamProject('source project', member);
|
const sourceProject = await createTeamProject('source project', member);
|
||||||
const destinationProject = await createTeamProject('Team Project', member);
|
const destinationProject = await createTeamProject('Team Project', member);
|
||||||
|
|||||||
Reference in New Issue
Block a user