mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 09:28:31 +00:00
* fix: update permission key logic in defineAppVersionAbility function * fix: remove unused 'resource' from UserAllPermissions destructuring in defineAppVersionAbility function * test: add unit tests for defineAppVersionAbility function * refactor: formatting changes * test: add unit tests for different roles in defineAppVersionAbility function * test: fix review issues in defineAppVersionAbility unit tests * fix: update defineAppVersionAbility to use correct User type and improve permissions handling * fix: update user app permissions query to include MODULE type * fix: enhance app visibility logic for MODULE type in getAppsFor method * fix: improve environment synchronization and permissions handling in version management * fix: enhance permission handling for MODULE type apps across various services * fix: enhance permission checks for module and workflow apps in environment access logic * fix: enhance permission handling for MODULE apps and add support for end-user abilities * fix: remove debug logging from ability definition for data query apps * fix: enhance module folder permissions for builders and improve access checks * fix: enhance permission checks for folder updates in module context for builder roles
527 lines
20 KiB
TypeScript
527 lines
20 KiB
TypeScript
import { INestApplication } from '@nestjs/common';
|
|
import {
|
|
login,
|
|
initTestApp,
|
|
closeTestApp,
|
|
createUser,
|
|
createApplication,
|
|
createApplicationVersion,
|
|
createFolder,
|
|
addAppToFolder,
|
|
saveEntity,
|
|
findEntity,
|
|
} from 'test-helper';
|
|
import * as request from 'supertest';
|
|
import { Folder } from '@entities/folder.entity';
|
|
import { FolderApp } from '@entities/folder_app.entity';
|
|
import { WorkspaceBranch } from '@entities/workspace_branch.entity';
|
|
import { AppVersion } from '@entities/app_version.entity';
|
|
import { App } from '@entities/app.entity';
|
|
import { APP_TYPES } from '@modules/apps/constants';
|
|
|
|
async function setupOrganization(nestApp) {
|
|
const adminUserData = await createUser(nestApp, {
|
|
email: 'admin@tooljet.io',
|
|
groups: ['end-user', 'admin'],
|
|
});
|
|
const adminUser = adminUserData.user;
|
|
const organization = adminUserData.organization;
|
|
|
|
const app = await createApplication(nestApp, {
|
|
user: adminUser,
|
|
name: 'sample app',
|
|
isPublic: false,
|
|
});
|
|
|
|
return { adminUser, organization, app };
|
|
}
|
|
|
|
/** @group platform */
|
|
describe('FolderAppsController', () => {
|
|
let nestApp: INestApplication;
|
|
|
|
beforeAll(async () => {
|
|
({ app: nestApp } = await initTestApp({ edition: 'ee', plan: 'enterprise' }));
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.resetAllMocks();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await closeTestApp(nestApp);
|
|
}, 60_000);
|
|
|
|
describe('EE (plan: enterprise)', () => {
|
|
describe('POST /api/folder-apps | Add app to folder', () => {
|
|
it('should allow only authenticated users to add apps to folders', async () => {
|
|
await request(nestApp.getHttpServer()).post('/api/folder-apps').expect(401);
|
|
});
|
|
|
|
it('should add an app to a folder', async () => {
|
|
const { adminUser, app } = await setupOrganization(nestApp);
|
|
// create a new folder
|
|
const folder = await saveEntity(Folder, { name: 'folder', organizationId: adminUser.organizationId } as any);
|
|
|
|
const loggedUser = await login(nestApp);
|
|
|
|
const response = await request(nestApp.getHttpServer())
|
|
.post(`/api/folder-apps`)
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('Cookie', loggedUser.tokenCookie)
|
|
.send({ folder_id: folder.id, app_id: app.id });
|
|
|
|
expect(response.statusCode).toBe(201);
|
|
expect(response.body).toMatchObject({
|
|
app_id: app.id,
|
|
folder_id: folder.id,
|
|
});
|
|
expect(response.body.id).toBeDefined();
|
|
});
|
|
|
|
it('super admin should be able to add apps to folders in any organization', async () => {
|
|
const { adminUser, app } = await setupOrganization(nestApp);
|
|
// create a new folder
|
|
const folder = await saveEntity(Folder, { name: 'folder', organizationId: adminUser.organizationId } as any);
|
|
//super admin
|
|
const superAdminUserData = await createUser(nestApp, {
|
|
email: 'superadmin@tooljet.io',
|
|
groups: ['end-user', 'admin'],
|
|
userType: 'instance',
|
|
});
|
|
|
|
const loggedUser = await login(
|
|
nestApp,
|
|
superAdminUserData.user.email,
|
|
'password',
|
|
adminUser.defaultOrganizationId
|
|
);
|
|
superAdminUserData['tokenCookie'] = loggedUser.tokenCookie;
|
|
|
|
const response = await request(nestApp.getHttpServer())
|
|
.post(`/api/folder-apps`)
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('Cookie', superAdminUserData['tokenCookie'])
|
|
.send({ folder_id: folder.id, app_id: app.id });
|
|
|
|
expect(response.statusCode).toBe(201);
|
|
expect(response.body).toMatchObject({
|
|
app_id: app.id,
|
|
folder_id: folder.id,
|
|
});
|
|
expect(response.body.id).toBeDefined();
|
|
});
|
|
|
|
it('should not add an app to a folder more than once', async () => {
|
|
const { adminUser, app } = await setupOrganization(nestApp);
|
|
|
|
// create a new folder
|
|
const folder = await saveEntity(Folder, { name: 'folder', organizationId: adminUser.organizationId } as any);
|
|
|
|
const loggedUser = await login(nestApp);
|
|
|
|
await request(nestApp.getHttpServer())
|
|
.post(`/api/folder-apps`)
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('Cookie', loggedUser.tokenCookie)
|
|
.send({ folder_id: folder.id, app_id: app.id });
|
|
|
|
const response = await request(nestApp.getHttpServer())
|
|
.post(`/api/folder-apps`)
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('Cookie', loggedUser.tokenCookie)
|
|
.send({ folder_id: folder.id, app_id: app.id });
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(response.body.message).toBe('App has already been added to the folder');
|
|
});
|
|
});
|
|
|
|
describe('PUT /api/folder-apps/:id | Remove app from folder', () => {
|
|
it('should remove an app from a folder', async () => {
|
|
const { adminUser, app } = await setupOrganization(nestApp);
|
|
|
|
const loggedUser = await login(nestApp);
|
|
|
|
// create a new folder
|
|
const folder = await saveEntity(Folder, { name: 'folder', organizationId: adminUser.organizationId } as any);
|
|
// add app to folder
|
|
const folderApp = await saveEntity(FolderApp, { folderId: folder.id, appId: app.id } as any);
|
|
const response = await request(nestApp.getHttpServer())
|
|
.put(`/api/folder-apps/${folderApp.folderId}`)
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('Cookie', loggedUser.tokenCookie)
|
|
.send({ app_id: folderApp.appId });
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
});
|
|
|
|
it('super admin should be able to remove an app from a folder', async () => {
|
|
const { adminUser, app } = await setupOrganization(nestApp);
|
|
// create a new folder
|
|
const folder = await saveEntity(Folder, { name: 'folder', organizationId: adminUser.organizationId } as any);
|
|
// add app to folder
|
|
const folderApp = await saveEntity(FolderApp, { folderId: folder.id, appId: app.id } as any);
|
|
|
|
//super admin
|
|
const superAdminUserData = await createUser(nestApp, {
|
|
email: 'superadmin@tooljet.io',
|
|
groups: ['end-user', 'admin'],
|
|
userType: 'instance',
|
|
});
|
|
|
|
const loggedUser = await login(
|
|
nestApp,
|
|
superAdminUserData.user.email,
|
|
'password',
|
|
adminUser.defaultOrganizationId
|
|
);
|
|
superAdminUserData['tokenCookie'] = loggedUser.tokenCookie;
|
|
|
|
const response = await request(nestApp.getHttpServer())
|
|
.put(`/api/folder-apps/${folderApp.folderId}`)
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('Cookie', superAdminUserData['tokenCookie'])
|
|
.send({ app_id: folderApp.appId });
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Module branch-scoped folder listing tests
|
|
// =========================================================================
|
|
// Verifies that module apps in folders are properly branch-scoped when
|
|
// branchId is provided, ensuring cross-branch module leakage is prevented.
|
|
// =========================================================================
|
|
|
|
describe('GET /api/apps (type=module, folder context) | Module branch-scoped listing', () => {
|
|
it('should return only modules on the specified branch when branchId is provided', async () => {
|
|
const { adminUser } = await setupOrganization(nestApp);
|
|
const loggedUser = await login(nestApp);
|
|
|
|
// Create workspace branches
|
|
const branchA = await saveEntity(WorkspaceBranch, {
|
|
name: 'feature-a',
|
|
organizationId: adminUser.organizationId,
|
|
isDefault: false,
|
|
} as any);
|
|
|
|
const branchB = await saveEntity(WorkspaceBranch, {
|
|
name: 'feature-b',
|
|
organizationId: adminUser.organizationId,
|
|
isDefault: false,
|
|
} as any);
|
|
|
|
// Create module apps (skip env creation - already created by setupOrganization)
|
|
const moduleA = await createApplication(
|
|
nestApp,
|
|
{
|
|
user: adminUser,
|
|
name: 'Module A',
|
|
type: APP_TYPES.MODULE,
|
|
},
|
|
false
|
|
);
|
|
|
|
const moduleB = await createApplication(
|
|
nestApp,
|
|
{
|
|
user: adminUser,
|
|
name: 'Module B',
|
|
type: APP_TYPES.MODULE,
|
|
},
|
|
false
|
|
);
|
|
|
|
// Create versions on specific branches
|
|
const versionA = await createApplicationVersion(nestApp, moduleA, { name: 'v1' });
|
|
await saveEntity(AppVersion, {
|
|
id: versionA.id,
|
|
branchId: branchA.id,
|
|
} as any);
|
|
|
|
const versionB = await createApplicationVersion(nestApp, moduleB, { name: 'v1' });
|
|
await saveEntity(AppVersion, {
|
|
id: versionB.id,
|
|
branchId: branchB.id,
|
|
} as any);
|
|
|
|
// Create a module folder and add modules to it
|
|
const moduleFolder = await createFolder(nestApp, {
|
|
name: 'Module Folder',
|
|
type: APP_TYPES.MODULE,
|
|
organizationId: adminUser.organizationId,
|
|
});
|
|
|
|
await addAppToFolder(nestApp, moduleA, moduleFolder);
|
|
await addAppToFolder(nestApp, moduleB, moduleFolder);
|
|
|
|
// Fetch modules from folder with branchId=A
|
|
const responseBranchA = await request(nestApp.getHttpServer())
|
|
.get('/api/apps')
|
|
.query({ folder: moduleFolder.id, type: 'module' })
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('x-branch-id', branchA.id)
|
|
.set('Cookie', loggedUser.tokenCookie);
|
|
|
|
expect(responseBranchA.statusCode).toBe(200);
|
|
const appsBranchA = responseBranchA.body.apps;
|
|
expect(appsBranchA).toHaveLength(1);
|
|
expect(appsBranchA[0].id).toBe(moduleA.id);
|
|
expect(appsBranchA[0].name).toBe('Module A');
|
|
|
|
// Fetch modules from folder with branchId=B
|
|
const responseBranchB = await request(nestApp.getHttpServer())
|
|
.get('/api/apps')
|
|
.query({ folder: moduleFolder.id, type: 'module' })
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('x-branch-id', branchB.id)
|
|
.set('Cookie', loggedUser.tokenCookie);
|
|
|
|
expect(responseBranchB.statusCode).toBe(200);
|
|
const appsBranchB = responseBranchB.body.apps;
|
|
expect(appsBranchB).toHaveLength(1);
|
|
expect(appsBranchB[0].id).toBe(moduleB.id);
|
|
expect(appsBranchB[0].name).toBe('Module B');
|
|
});
|
|
|
|
it('should return empty list when no modules on that branch are in the folder', async () => {
|
|
const { adminUser } = await setupOrganization(nestApp);
|
|
const loggedUser = await login(nestApp);
|
|
|
|
const branchA = await saveEntity(WorkspaceBranch, {
|
|
name: 'isolated-branch',
|
|
organizationId: adminUser.organizationId,
|
|
isDefault: false,
|
|
} as any);
|
|
|
|
const moduleA = await createApplication(
|
|
nestApp,
|
|
{
|
|
user: adminUser,
|
|
name: 'Isolated Module',
|
|
type: APP_TYPES.MODULE,
|
|
},
|
|
false
|
|
);
|
|
|
|
const versionA = await createApplicationVersion(nestApp, moduleA, { name: 'v1' });
|
|
await saveEntity(AppVersion, {
|
|
id: versionA.id,
|
|
branchId: branchA.id,
|
|
} as any);
|
|
|
|
const moduleFolder = await createFolder(nestApp, {
|
|
name: 'Empty Folder',
|
|
type: APP_TYPES.MODULE,
|
|
organizationId: adminUser.organizationId,
|
|
});
|
|
|
|
// Add module but query with different branch
|
|
const branchC = await saveEntity(WorkspaceBranch, {
|
|
name: 'other-branch',
|
|
organizationId: adminUser.organizationId,
|
|
isDefault: false,
|
|
} as any);
|
|
|
|
await addAppToFolder(nestApp, moduleA, moduleFolder);
|
|
|
|
const response = await request(nestApp.getHttpServer())
|
|
.get('/api/apps')
|
|
.query({ folder: moduleFolder.id, type: 'module' })
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('x-branch-id', branchC.id)
|
|
.set('Cookie', loggedUser.tokenCookie);
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.body.apps).toHaveLength(0);
|
|
expect(response.body.meta.folder_count).toBe(0);
|
|
});
|
|
|
|
describe('Builder permissions on module folders', () => {
|
|
it('should allow a builder to add a module app to a module folder', async () => {
|
|
const adminUserData = await createUser(nestApp, {
|
|
email: 'admin-builder-add@tooljet.io',
|
|
groups: ['end-user', 'admin'],
|
|
});
|
|
const adminUser = adminUserData.user;
|
|
const organization = adminUserData.organization;
|
|
|
|
await createUser(nestApp, { email: 'builder-add@tooljet.io', groups: ['builder'], organization });
|
|
const { tokenCookie: builderCookie } = await login(nestApp, 'builder-add@tooljet.io', 'password');
|
|
|
|
const moduleApp = await createApplication(nestApp, { user: adminUser, name: 'module app', type: APP_TYPES.MODULE }, false);
|
|
|
|
const moduleFolder = await createFolder(nestApp, {
|
|
name: 'module folder',
|
|
type: APP_TYPES.MODULE,
|
|
organizationId: adminUser.organizationId,
|
|
});
|
|
|
|
const response = await request(nestApp.getHttpServer())
|
|
.post('/api/folder-apps')
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('Cookie', builderCookie)
|
|
.send({ folder_id: moduleFolder.id, app_id: moduleApp.id });
|
|
|
|
expect(response.statusCode).toBe(201);
|
|
});
|
|
|
|
it('should allow a builder to remove a module app from a module folder', async () => {
|
|
const adminUserData = await createUser(nestApp, {
|
|
email: 'admin-builder-remove@tooljet.io',
|
|
groups: ['end-user', 'admin'],
|
|
});
|
|
const adminUser = adminUserData.user;
|
|
const organization = adminUserData.organization;
|
|
|
|
await createUser(nestApp, { email: 'builder-remove@tooljet.io', groups: ['builder'], organization });
|
|
const { tokenCookie: builderCookie } = await login(nestApp, 'builder-remove@tooljet.io', 'password');
|
|
|
|
const moduleApp = await createApplication(nestApp, { user: adminUser, name: 'module app for removal', type: APP_TYPES.MODULE }, false);
|
|
|
|
const moduleFolder = await createFolder(nestApp, {
|
|
name: 'module folder for removal',
|
|
type: APP_TYPES.MODULE,
|
|
organizationId: adminUser.organizationId,
|
|
});
|
|
|
|
await addAppToFolder(nestApp, moduleApp, moduleFolder);
|
|
|
|
const response = await request(nestApp.getHttpServer())
|
|
.put(`/api/folder-apps/${moduleFolder.id}`)
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('Cookie', builderCookie)
|
|
.send({ app_id: moduleApp.id });
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
});
|
|
|
|
it('should not allow a builder to add a front-end app to a front-end folder without explicit permission', async () => {
|
|
const adminUserData = await createUser(nestApp, {
|
|
email: 'admin-builder-fe@tooljet.io',
|
|
groups: ['end-user', 'admin'],
|
|
});
|
|
const adminUser = adminUserData.user;
|
|
const organization = adminUserData.organization;
|
|
|
|
await createUser(nestApp, { email: 'builder-frontend@tooljet.io', groups: ['builder'], organization });
|
|
const { tokenCookie: builderCookie } = await login(nestApp, 'builder-frontend@tooljet.io', 'password');
|
|
|
|
const frontEndApp = await createApplication(nestApp, { user: adminUser, name: 'front-end app' }, false);
|
|
|
|
const frontEndFolder = await createFolder(nestApp, {
|
|
name: 'front-end folder',
|
|
type: APP_TYPES.FRONT_END,
|
|
organizationId: adminUser.organizationId,
|
|
});
|
|
|
|
const response = await request(nestApp.getHttpServer())
|
|
.post('/api/folder-apps')
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('Cookie', builderCookie)
|
|
.send({ folder_id: frontEndFolder.id, app_id: frontEndApp.id });
|
|
|
|
expect(response.statusCode).toBe(403);
|
|
});
|
|
|
|
it('should block adding a module app to a front-end folder even for admin', async () => {
|
|
const { adminUser } = await setupOrganization(nestApp);
|
|
const loggedUser = await login(nestApp);
|
|
|
|
const moduleApp = await createApplication(nestApp, { user: adminUser, name: 'module for mismatch', type: APP_TYPES.MODULE }, false);
|
|
|
|
const frontEndFolder = await createFolder(nestApp, {
|
|
name: 'fe folder for mismatch',
|
|
type: APP_TYPES.FRONT_END,
|
|
organizationId: adminUser.organizationId,
|
|
});
|
|
|
|
const response = await request(nestApp.getHttpServer())
|
|
.post('/api/folder-apps')
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('Cookie', loggedUser.tokenCookie)
|
|
.send({ folder_id: frontEndFolder.id, app_id: moduleApp.id });
|
|
|
|
expect(response.statusCode).toBe(403);
|
|
});
|
|
|
|
it('should block adding a front-end app to a module folder even for admin', async () => {
|
|
const { adminUser } = await setupOrganization(nestApp);
|
|
const loggedUser = await login(nestApp);
|
|
|
|
const frontEndApp = await createApplication(nestApp, { user: adminUser, name: 'fe app for mismatch' }, false);
|
|
|
|
const moduleFolder = await createFolder(nestApp, {
|
|
name: 'module folder for mismatch',
|
|
type: APP_TYPES.MODULE,
|
|
organizationId: adminUser.organizationId,
|
|
});
|
|
|
|
const response = await request(nestApp.getHttpServer())
|
|
.post('/api/folder-apps')
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('Cookie', loggedUser.tokenCookie)
|
|
.send({ folder_id: moduleFolder.id, app_id: frontEndApp.id });
|
|
|
|
expect(response.statusCode).toBe(403);
|
|
});
|
|
});
|
|
|
|
it('should align folder count with returned modules across pagination', async () => {
|
|
const { adminUser } = await setupOrganization(nestApp);
|
|
const loggedUser = await login(nestApp);
|
|
|
|
const branchA = await saveEntity(WorkspaceBranch, {
|
|
name: 'pagination-branch',
|
|
organizationId: adminUser.organizationId,
|
|
isDefault: false,
|
|
} as any);
|
|
|
|
// Create 3 modules, all on branchA
|
|
const modules = [];
|
|
for (let i = 0; i < 3; i++) {
|
|
const mod = await createApplication(
|
|
nestApp,
|
|
{
|
|
user: adminUser,
|
|
name: `Module ${i + 1}`,
|
|
type: APP_TYPES.MODULE,
|
|
},
|
|
false
|
|
);
|
|
const version = await createApplicationVersion(nestApp, mod, { name: 'v1' });
|
|
await saveEntity(AppVersion, {
|
|
id: version.id,
|
|
branchId: branchA.id,
|
|
} as any);
|
|
modules.push(mod);
|
|
}
|
|
|
|
const moduleFolder = await createFolder(nestApp, {
|
|
name: 'Pagination Folder',
|
|
type: APP_TYPES.MODULE,
|
|
organizationId: adminUser.organizationId,
|
|
});
|
|
|
|
for (const mod of modules) {
|
|
await addAppToFolder(nestApp, mod, moduleFolder);
|
|
}
|
|
|
|
// Fetch with page 1 (9 per page, so all 3 should fit)
|
|
const response = await request(nestApp.getHttpServer())
|
|
.get('/api/apps')
|
|
.query({ folder: moduleFolder.id, type: 'module', page: 1 })
|
|
.set('tj-workspace-id', adminUser.defaultOrganizationId)
|
|
.set('x-branch-id', branchA.id)
|
|
.set('Cookie', loggedUser.tokenCookie);
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.body.apps).toHaveLength(3);
|
|
expect(response.body.meta.folder_count).toBe(3);
|
|
expect(response.body.meta.total_pages).toBe(1);
|
|
});
|
|
});
|
|
});
|
|
});
|