diff --git a/cypress/constants/selectors/common.js b/cypress/constants/selectors/common.js
index ecf2466f4c..f76e5e5f89 100644
--- a/cypress/constants/selectors/common.js
+++ b/cypress/constants/selectors/common.js
@@ -6,6 +6,7 @@ export const commonSelectors={
firstWidget:"[data-cy=widget-list]:eq(0)",
canvas:"[data-cy=real-canvas]",
appCardOptions: "[data-cy=app-card-menu-icon]",
+ folderItemOptions: "[data-cy=folder-item-menu-icon]",
deleteApp: "[data-cy=card-options] :nth-child(5)>span",
confirmButton: "[data-cy=confirm-yes-button]",
autoSave: "[data-cy=autosave-indicator]",
diff --git a/frontend/src/HomePage/FolderMenu.jsx b/frontend/src/HomePage/FolderMenu.jsx
new file mode 100644
index 0000000000..5d7d02ec0e
--- /dev/null
+++ b/frontend/src/HomePage/FolderMenu.jsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
+import Popover from 'react-bootstrap/Popover';
+
+export const FolderMenu = function FolderMenu({
+ deleteFolder,
+ editFolder,
+ canDeleteFolder,
+ canUpdateFolder,
+ onMenuOpen,
+ darkMode,
+}) {
+ const closeMenu = () => {
+ document.body.click();
+ };
+ const Field = ({ text, onClick, customClass }) => {
+ return (
+
+ {
+ closeMenu();
+ onClick();
+ }}
+ >
+ {text}
+
+
+ );
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/src/HomePage/Folders.jsx b/frontend/src/HomePage/Folders.jsx
index b10683767a..3a37488ebe 100644
--- a/frontend/src/HomePage/Folders.jsx
+++ b/frontend/src/HomePage/Folders.jsx
@@ -1,7 +1,10 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import { folderService } from '@/_services';
import { toast } from 'react-hot-toast';
import Modal from './Modal';
+import { FolderMenu } from './FolderMenu';
+import useHover from '@/_hooks/useHover';
+import { ConfirmDialog } from '@/_components';
export const Folders = function Folders({
folders,
@@ -10,9 +13,27 @@ export const Folders = function Folders({
folderChanged,
foldersChanged,
canCreateFolder,
+ canUpdateFolder,
+ canDeleteFolder,
darkMode,
}) {
const [isLoading, setLoadingStatus] = useState(foldersLoading);
+ const [isMenuOpen, setMenuOpen] = useState(false);
+ const [hoverRef, isHovered] = useHover();
+ const [focused, setFocused] = useState(false);
+
+ const onMenuToggle = useCallback(
+ (status) => {
+ setMenuOpen(!!status);
+ !status && !isHovered && setFocused(false);
+ },
+ [isHovered]
+ );
+
+ useEffect(() => {
+ !isMenuOpen && setFocused(!!isHovered);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isHovered]);
useEffect(() => {
setLoadingStatus(foldersLoading);
@@ -20,26 +41,28 @@ export const Folders = function Folders({
const [showForm, setShowForm] = useState(false);
const [isCreating, setCreationStatus] = useState(false);
+ const [isDeleting, setDeletionStatus] = useState(false);
+ const [isUpdating, setUpdationStatus] = useState(false);
+ const [deletingFolder, setDeletingFolder] = useState(null);
+ const [updatingFolder, setUpdatingFolder] = useState(null);
const [newFolderName, setNewFolderName] = useState('');
+ const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
+ const [showUpdateForm, setShowUpdateForm] = useState(false);
const [activeFolder, setActiveFolder] = useState(currentFolder || {});
function saveFolder() {
- if (!newFolderName || !newFolderName.trim()) {
- toast.error("folder name can't be empty.", {
- position: 'top-center',
+ if (validateName()) {
+ setCreationStatus(true);
+ folderService.create(newFolderName).then(() => {
+ toast.success('Folder created.', {
+ position: 'top-center',
+ });
+ setCreationStatus(false);
+ setShowForm(false);
+ setNewFolderName('');
+ foldersChanged();
});
- return;
}
- setCreationStatus(true);
- folderService.create(newFolderName).then(() => {
- toast.success('folder created.', {
- position: 'top-center',
- });
- setCreationStatus(false);
- setShowForm(false);
- setNewFolderName('');
- foldersChanged();
- });
}
function handleFolderChange(folder) {
@@ -47,8 +70,85 @@ export const Folders = function Folders({
folderChanged(folder);
}
+ function deleteFolder(folder) {
+ setShowDeleteConfirmation(true);
+ setDeletingFolder(folder);
+ }
+
+ function updateFolder(folder) {
+ setNewFolderName(folder.name);
+ setShowUpdateForm(true);
+ setUpdatingFolder(folder);
+ }
+
+ function executeDeletion() {
+ setDeletionStatus(true);
+ folderService
+ .deleteFolder(deletingFolder.id)
+ .then(() => {
+ toast.success('Folder has been deleted.', {
+ position: 'top-center',
+ });
+ setShowDeleteConfirmation(false);
+ setDeletionStatus(false);
+ foldersChanged();
+ })
+ .catch(({ error }) => {
+ toast.error(error);
+ setShowDeleteConfirmation(false);
+ setDeletionStatus(false);
+ });
+ }
+
+ function cancelDeleteDialog() {
+ setShowDeleteConfirmation(false);
+ setDeletingFolder(null);
+ }
+
+ function validateName() {
+ if (!newFolderName?.trim()) {
+ toast.error("Folder name can't be empty.", {
+ position: 'top-center',
+ });
+ return false;
+ }
+ return true;
+ }
+
+ function executeEditFolder() {
+ if (validateName()) {
+ setUpdationStatus(true);
+ folderService
+ .updateFolder(newFolderName, updatingFolder.id)
+ .then(() => {
+ toast.success('Folder has been updated.', {
+ position: 'top-center',
+ });
+ setUpdationStatus(false);
+ setShowUpdateForm(false);
+ setNewFolderName('');
+ foldersChanged();
+ })
+ .catch(({ error }) => {
+ toast.error(error);
+ setNewFolderName('');
+ setUpdationStatus(false);
+ });
+ }
+ }
+
return (
+
executeDeletion()}
+ onCancel={() => cancelDeleteDialog()}
+ darkMode={darkMode}
+ />
+
Folders
{canCreateFolder && (
-
setShowForm(true)}>
+
{
+ setNewFolderName('');
+ setShowForm(true);
+ }}
+ >
+ Create new folder
)}
@@ -88,22 +194,40 @@ export const Folders = function Folders({
? folders.map((folder, index) => (
handleFolderChange(folder)}
+ } ${darkMode && 'dark'} ${focused ? ' highlight' : ''}`}
>
-
-
-
- {`${folder.name}${folder.count > 0 ? ` (${folder.count})` : ''}`}
+ handleFolderChange(folder)} className="flex-grow-1">
+
+
+
+ {`${folder.name}${folder.count > 0 ? ` (${folder.count})` : ''}`}
+
+
+ {(canDeleteFolder || canUpdateFolder) && (
+ deleteFolder(folder)}
+ editFolder={() => updateFolder(folder)}
+ darkMode={darkMode}
+ />
+ )}
+
))
: !isLoading && (
You haven't created any folders. Use folders to organize your apps
)}
-
setShowForm(false)} title="Create folder">
+ (showUpdateForm ? setShowUpdateForm(false) : setShowForm(false))}
+ title={showUpdateForm ? 'Update Folder' : 'Create folder'}
+ >
-
diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx
index bc909c60c6..2686f9eb4e 100644
--- a/frontend/src/HomePage/HomePage.jsx
+++ b/frontend/src/HomePage/HomePage.jsx
@@ -274,6 +274,14 @@ class HomePage extends React.Component {
return this.canAnyGroupPerformAction('folder_create', this.state.currentUser.group_permissions);
};
+ canDeleteFolder = () => {
+ return this.canAnyGroupPerformAction('folder_delete', this.state.currentUser.group_permissions);
+ };
+
+ canUpdateFolder = () => {
+ return this.canAnyGroupPerformAction('folder_update', this.state.currentUser.group_permissions);
+ };
+
cancelDeleteAppDialog = () => {
this.setState({
isDeletingApp: false,
@@ -622,6 +630,8 @@ class HomePage extends React.Component {
folderChanged={this.folderChanged}
foldersChanged={this.foldersChanged}
canCreateFolder={this.canCreateFolder()}
+ canDeleteFolder={this.canDeleteFolder()}
+ canUpdateFolder={this.canUpdateFolder()}
darkMode={this.props.darkMode}
/>
diff --git a/frontend/src/ManageGroupPermissionResources/ManageGroupPermissionResources.jsx b/frontend/src/ManageGroupPermissionResources/ManageGroupPermissionResources.jsx
index a9587b792a..757b00c11f 100644
--- a/frontend/src/ManageGroupPermissionResources/ManageGroupPermissionResources.jsx
+++ b/frontend/src/ManageGroupPermissionResources/ManageGroupPermissionResources.jsx
@@ -280,6 +280,10 @@ class ManageGroupPermissionResources extends React.Component {
selectedUserIds,
} = this.state;
+ const folder_permission = groupPermission
+ ? groupPermission.folder_create && groupPermission.folder_delete && groupPermission.folder_update
+ : false;
+
const appSelectOptions = appsNotInGroup.map((app) => {
return { name: app.name, value: app.id };
});
@@ -606,13 +610,15 @@ class ManageGroupPermissionResources extends React.Component {
type="checkbox"
onChange={() => {
this.updateGroupPermission(groupPermission.id, {
- folder_create: !groupPermission.folder_create,
+ folder_create: !folder_permission,
+ folder_delete: !folder_permission,
+ folder_update: !folder_permission,
});
}}
- checked={groupPermission.folder_create}
+ checked={folder_permission}
disabled={groupPermission.group === 'admin'}
/>
-
Create
+
Create/Update/Delete
diff --git a/frontend/src/_services/folder.service.js b/frontend/src/_services/folder.service.js
index 637ede3480..0d2e177e85 100644
--- a/frontend/src/_services/folder.service.js
+++ b/frontend/src/_services/folder.service.js
@@ -3,9 +3,11 @@ import { authHeader, handleResponse } from '@/_helpers';
export const folderService = {
create,
+ deleteFolder,
getAll,
addToFolder,
removeAppFromFolder,
+ updateFolder,
};
function getAll(searchKey = '') {
@@ -26,6 +28,27 @@ function create(name) {
return fetch(`${config.apiUrl}/folders`, requestOptions).then(handleResponse);
}
+function updateFolder(name, id) {
+ const body = {
+ name,
+ };
+
+ const requestOptions = {
+ method: 'PUT',
+ headers: authHeader(),
+ body: JSON.stringify(body),
+ };
+ return fetch(`${config.apiUrl}/folders/${id}`, requestOptions).then(handleResponse);
+}
+
+function deleteFolder(id) {
+ const requestOptions = {
+ method: 'DELETE',
+ headers: authHeader(),
+ };
+ return fetch(`${config.apiUrl}/folders/${id}`, requestOptions).then(handleResponse);
+}
+
function addToFolder(appId, folderId) {
const body = {
app_id: appId,
diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss
index 0b05ad7212..e4dafc1e89 100644
--- a/frontend/src/_styles/theme.scss
+++ b/frontend/src/_styles/theme.scss
@@ -4164,6 +4164,25 @@ input[type="text"] {
color: #0565ff;
cursor: pointer;
}
+
+ .menu-ico {
+ cursor: pointer;
+ padding: 3px;
+ border-radius: 13px;
+ &__open {
+ background-color: #d2ddec;
+ }
+ img {
+ padding: 0px;
+ height: 14px;
+ width: 14px;
+ vertical-align: unset;
+ }
+ }
+
+ .menu-ico:hover {
+ background-color: #d2ddec;
+ }
}
/**
diff --git a/server/migrations/1653391166172-AddFolderUpdateAndDeletePermissionsToGroupPermissions.ts b/server/migrations/1653391166172-AddFolderUpdateAndDeletePermissionsToGroupPermissions.ts
new file mode 100644
index 0000000000..ed8f444f07
--- /dev/null
+++ b/server/migrations/1653391166172-AddFolderUpdateAndDeletePermissionsToGroupPermissions.ts
@@ -0,0 +1,30 @@
+import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
+
+export class AddFolderUpdateAndDeletePermissionToGroupPermissions1653391166172 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.addColumn(
+ 'group_permissions',
+ new TableColumn({
+ name: 'folder_delete',
+ type: 'boolean',
+ default: false,
+ isNullable: false,
+ })
+ );
+
+ await queryRunner.addColumn(
+ 'group_permissions',
+ new TableColumn({
+ name: 'folder_update',
+ type: 'boolean',
+ default: false,
+ isNullable: false,
+ })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.dropColumn('group_permissions', 'folder_delete');
+ await queryRunner.dropColumn('group_permissions', 'folder_update');
+ }
+}
diff --git a/server/migrations/1653474337657-BackfillFolderDeleteFolderUpdatePermissionsAsTruthyForAdminGroup.ts b/server/migrations/1653474337657-BackfillFolderDeleteFolderUpdatePermissionsAsTruthyForAdminGroup.ts
new file mode 100644
index 0000000000..d58310bc4b
--- /dev/null
+++ b/server/migrations/1653474337657-BackfillFolderDeleteFolderUpdatePermissionsAsTruthyForAdminGroup.ts
@@ -0,0 +1,20 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+import { GroupPermission } from '../src/entities/group_permission.entity';
+
+export class BackfillFolderDeleteFolderUpdatePermissionsAsTruthyForAdminGroup1653474337657
+ implements MigrationInterface
+{
+ public async up(queryRunner: QueryRunner): Promise {
+ const entityManager = queryRunner.manager;
+ const GroupPermissionRepostory = entityManager.getRepository(GroupPermission);
+
+ await GroupPermissionRepostory.update({ group: 'admin' }, { folderUpdate: true, folderDelete: true });
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ const entityManager = queryRunner.manager;
+ const GroupPermissionRepostory = entityManager.getRepository(GroupPermission);
+
+ await GroupPermissionRepostory.update({ group: 'admin' }, { folderUpdate: false, folderDelete: false });
+ }
+}
diff --git a/server/src/controllers/folders.controller.ts b/server/src/controllers/folders.controller.ts
index 8a91585fce..1b4a93e952 100644
--- a/server/src/controllers/folders.controller.ts
+++ b/server/src/controllers/folders.controller.ts
@@ -1,4 +1,4 @@
-import { Controller, Get, Post, Query, Request, UseGuards, Body } from '@nestjs/common';
+import { Controller, Get, Post, Query, Request, UseGuards, Body, Delete, Param, Put } from '@nestjs/common';
import { decamelizeKeys } from 'humps';
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
import { FoldersService } from '../services/folders.service';
@@ -6,6 +6,7 @@ import { ForbiddenException } from '@nestjs/common';
import { FoldersAbilityFactory } from 'src/modules/casl/abilities/folders-ability.factory';
import { Folder } from 'src/entities/folder.entity';
import { CreateFolderDto } from '@dto/create-folder.dto';
+import { User } from 'src/decorators/user.decorator';
@Controller('folders')
export class FoldersController {
@@ -31,4 +32,28 @@ export class FoldersController {
const folder = await this.foldersService.create(req.user, folderName);
return decamelizeKeys(folder);
}
+
+ @UseGuards(JwtAuthGuard)
+ @Put(':id')
+ async update(@User() user, @Param('id') id, @Body('name') folderName: string) {
+ const ability = await this.foldersAbilityFactory.folderActions(user, {});
+
+ if (!ability.can('updateFolder', Folder)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+ const folder = await this.foldersService.update(id, folderName);
+ return decamelizeKeys(folder);
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @Delete(':id')
+ async delete(@User() user, @Param('id') id) {
+ const ability = await this.foldersAbilityFactory.folderActions(user, {});
+
+ if (!ability.can('deleteFolder', Folder)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ return await this.foldersService.delete(user, id);
+ }
}
diff --git a/server/src/entities/group_permission.entity.ts b/server/src/entities/group_permission.entity.ts
index 0583ec6730..4bb08192b9 100644
--- a/server/src/entities/group_permission.entity.ts
+++ b/server/src/entities/group_permission.entity.ts
@@ -37,6 +37,12 @@ export class GroupPermission extends BaseEntity {
@Column({ name: 'folder_create', default: false })
folderCreate: boolean;
+ @Column({ name: 'folder_delete', default: false })
+ folderDelete: boolean;
+
+ @Column({ name: 'folder_update', default: false })
+ folderUpdate: boolean;
+
@CreateDateColumn({ default: () => 'now()', name: 'created_at' })
createdAt: Date;
diff --git a/server/src/modules/casl/abilities/folders-ability.factory.ts b/server/src/modules/casl/abilities/folders-ability.factory.ts
index d25ace9708..59c43ec7f7 100644
--- a/server/src/modules/casl/abilities/folders-ability.factory.ts
+++ b/server/src/modules/casl/abilities/folders-ability.factory.ts
@@ -4,7 +4,7 @@ import { Injectable } from '@nestjs/common';
import { UsersService } from 'src/services/users.service';
import { Folder } from 'src/entities/folder.entity';
-type Actions = 'createFolder';
+type Actions = 'createFolder' | 'updateFolder' | 'deleteFolder';
type Subjects = InferSubjects | 'all';
@@ -21,6 +21,14 @@ export class FoldersAbilityFactory {
can('createFolder', Folder);
}
+ if (await this.usersService.userCan(user, 'update', 'Folder')) {
+ can('updateFolder', Folder);
+ }
+
+ if (await this.usersService.userCan(user, 'delete', 'Folder')) {
+ can('deleteFolder', Folder);
+ }
+
return build({
detectSubjectType: (item) => item.constructor as ExtractSubjectType,
});
diff --git a/server/src/services/folders.service.ts b/server/src/services/folders.service.ts
index 15c2588390..3a1c2282d0 100644
--- a/server/src/services/folders.service.ts
+++ b/server/src/services/folders.service.ts
@@ -1,9 +1,9 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { App } from 'src/entities/app.entity';
import { FolderApp } from 'src/entities/folder_app.entity';
import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
-import { Brackets, createQueryBuilder, Repository } from 'typeorm';
+import { Brackets, createQueryBuilder, Repository, UpdateResult } from 'typeorm';
import { User } from '../../src/entities/user.entity';
import { Folder } from '../entities/folder.entity';
import { UsersService } from './users.service';
@@ -31,6 +31,10 @@ export class FoldersService {
);
}
+ async update(folderId: string, folderName: string): Promise {
+ return this.foldersRepository.update({ id: folderId }, { name: folderName });
+ }
+
async allFolders(user: User): Promise {
if (await this.usersService.hasGroup(user, 'admin')) {
return await this.foldersRepository.find({
@@ -292,4 +296,36 @@ export class FoldersService {
return viewableAppsInFolder;
}
+
+ async delete(user: User, id: string) {
+ const folder = await this.foldersRepository.findOneOrFail({ id, organizationId: user.organizationId });
+ const allViewableApps = await createQueryBuilder(App, 'apps')
+ .select('apps.id')
+ .innerJoin('apps.groupPermissions', 'group_permissions')
+ .innerJoin('apps.appGroupPermissions', 'app_group_permissions')
+ .innerJoin(
+ UserGroupPermission,
+ 'user_group_permissions',
+ 'app_group_permissions.group_permission_id = user_group_permissions.group_permission_id'
+ )
+ .where('user_group_permissions.user_id = :userId', { userId: user.id })
+ .andWhere('app_group_permissions.read = :value', { value: true })
+ .orWhere('apps.user_id = :userId', {
+ value: true,
+ organizationId: user.organizationId,
+ userId: user.id,
+ })
+ .getMany();
+
+ const allViewableAppIds = allViewableApps.map((app) => app.id);
+
+ folder.folderApps.map((folderApp: FolderApp) => {
+ if (!allViewableAppIds.includes(folderApp.appId)) {
+ throw new ForbiddenException(
+ 'Applications not authorised for you are included in the folder, please contact administrator to remove them and try again'
+ );
+ }
+ });
+ return await this.foldersRepository.delete({ id, organizationId: user.organizationId });
+ }
}
diff --git a/server/src/services/group_permissions.service.ts b/server/src/services/group_permissions.service.ts
index 18f146ab9a..8e55147325 100644
--- a/server/src/services/group_permissions.service.ts
+++ b/server/src/services/group_permissions.service.ts
@@ -123,7 +123,17 @@ export class GroupPermissionsService {
},
});
- const { app_create, app_delete, add_apps, remove_apps, add_users, remove_users, folder_create } = body;
+ const {
+ app_create,
+ app_delete,
+ add_apps,
+ remove_apps,
+ add_users,
+ remove_users,
+ folder_create,
+ folder_delete,
+ folder_update,
+ } = body;
await getManager().transaction(async (manager) => {
// update group permissions
@@ -131,6 +141,8 @@ export class GroupPermissionsService {
...(typeof app_create === 'boolean' && { appCreate: app_create }),
...(typeof app_delete === 'boolean' && { appDelete: app_delete }),
...(typeof folder_create === 'boolean' && { folderCreate: folder_create }),
+ ...(typeof folder_delete === 'boolean' && { folderDelete: folder_delete }),
+ ...(typeof folder_update === 'boolean' && { folderUpdate: folder_update }),
};
if (Object.keys(groupPermissionUpdateParams).length !== 0) {
await manager.update(GroupPermission, groupPermissionId, groupPermissionUpdateParams);
diff --git a/server/src/services/organizations.service.ts b/server/src/services/organizations.service.ts
index 163d4fbea5..eaa55ec477 100644
--- a/server/src/services/organizations.service.ts
+++ b/server/src/services/organizations.service.ts
@@ -93,6 +93,8 @@ export class OrganizationsService {
appCreate: isAdmin,
appDelete: isAdmin,
folderCreate: isAdmin,
+ folderUpdate: isAdmin,
+ folderDelete: isAdmin,
});
await this.groupPermissionsRepository.save(groupPermission);
createdGroupPermissions.push(groupPermission);
diff --git a/server/src/services/seeds.service.ts b/server/src/services/seeds.service.ts
index 35958b2c8d..2a6d1f9d7c 100644
--- a/server/src/services/seeds.service.ts
+++ b/server/src/services/seeds.service.ts
@@ -75,6 +75,8 @@ export class SeedsService {
appCreate: group == 'admin',
appDelete: group == 'admin',
folderCreate: group == 'admin',
+ folderUpdate: group == 'admin',
+ folderDelete: group == 'admin',
});
await manager.save(groupPermission);
diff --git a/server/src/services/users.service.ts b/server/src/services/users.service.ts
index 733c9f8769..df6a7e9d7e 100644
--- a/server/src/services/users.service.ts
+++ b/server/src/services/users.service.ts
@@ -311,6 +311,12 @@ export class UsersService {
case 'create':
permissionGrant = this.canAnyGroupPerformAction('folderCreate', await this.groupPermissions(user));
break;
+ case 'update':
+ permissionGrant = this.canAnyGroupPerformAction('folderUpdate', await this.groupPermissions(user));
+ break;
+ case 'delete':
+ permissionGrant = this.canAnyGroupPerformAction('folderDelete', await this.groupPermissions(user));
+ break;
default:
permissionGrant = false;
break;
diff --git a/server/test/controllers/app.e2e-spec.ts b/server/test/controllers/app.e2e-spec.ts
index c6181f3c1b..2c773b5046 100644
--- a/server/test/controllers/app.e2e-spec.ts
+++ b/server/test/controllers/app.e2e-spec.ts
@@ -76,11 +76,15 @@ describe('Authentication', () => {
expect(adminGroup.appCreate).toBeTruthy();
expect(adminGroup.appDelete).toBeTruthy();
expect(adminGroup.folderCreate).toBeTruthy();
+ expect(adminGroup.folderUpdate).toBeTruthy();
+ expect(adminGroup.folderDelete).toBeTruthy();
const allUserGroup = groupPermissions.find((x) => x.group == 'all_users');
expect(allUserGroup.appCreate).toBeFalsy();
expect(allUserGroup.appDelete).toBeFalsy();
expect(allUserGroup.folderCreate).toBeFalsy();
+ expect(allUserGroup.folderUpdate).toBeFalsy();
+ expect(allUserGroup.folderDelete).toBeFalsy();
});
describe('Single organization operations', () => {
beforeEach(async () => {
@@ -220,11 +224,15 @@ describe('Authentication', () => {
expect(adminGroup.appCreate).toBeTruthy();
expect(adminGroup.appDelete).toBeTruthy();
expect(adminGroup.folderCreate).toBeTruthy();
+ expect(adminGroup.folderUpdate).toBeTruthy();
+ expect(adminGroup.folderDelete).toBeTruthy();
const allUserGroup = groupPermissions.find((x) => x.group == 'all_users');
expect(allUserGroup.appCreate).toBeFalsy();
expect(allUserGroup.appDelete).toBeFalsy();
expect(allUserGroup.folderCreate).toBeFalsy();
+ expect(allUserGroup.folderUpdate).toBeFalsy();
+ expect(allUserGroup.folderDelete).toBeFalsy();
});
it('authenticate if valid credentials', async () => {
await request(app.getHttpServer())
@@ -380,6 +388,8 @@ describe('Authentication', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_delete',
+ 'folder_update',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -441,6 +451,8 @@ describe('Authentication', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_delete',
+ 'folder_update',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
diff --git a/server/test/controllers/folders.e2e-spec.ts b/server/test/controllers/folders.e2e-spec.ts
index 623ab826e4..af94321eb9 100644
--- a/server/test/controllers/folders.e2e-spec.ts
+++ b/server/test/controllers/folders.e2e-spec.ts
@@ -294,6 +294,114 @@ describe('folders controller', () => {
});
});
+ describe('PUT /api/folders/:id', () => {
+ it('should be able to update an existing folder if group is admin or has update permission in the same organization', async () => {
+ const adminUserData = await createUser(nestApp, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const developerUserData = await createUser(nestApp, {
+ email: 'dev@tooljet.io',
+ groups: ['all_users', 'developer'],
+ organization: adminUserData.organization,
+ });
+
+ const viewerUserData = await createUser(nestApp, {
+ email: 'viewer@tooljet.io',
+ groups: ['viewer', 'all_users'],
+ organization: adminUserData.organization,
+ });
+
+ const developerGroup = await getManager().findOneOrFail(GroupPermission, {
+ where: { group: 'developer' },
+ });
+
+ await getManager().update(GroupPermission, developerGroup.id, {
+ folderUpdate: true,
+ });
+
+ const folder = await getManager().save(Folder, {
+ name: 'Folder1',
+ organizationId: adminUserData.organization.id,
+ });
+
+ for (const userData of [adminUserData, developerUserData]) {
+ await request(nestApp.getHttpServer())
+ .put(`/api/folders/${folder.id}`)
+ .set('Authorization', authHeaderForUser(userData.user))
+ .send({ name: 'My folder' })
+ .expect(200);
+
+ const updatedFolder = await getManager().findOne(Folder, folder.id);
+
+ expect(updatedFolder.name).toEqual('My folder');
+ }
+
+ await request(nestApp.getHttpServer())
+ .put(`/api/folders/${folder.id}`)
+ .set('Authorization', authHeaderForUser(viewerUserData.user))
+ .send({ name: 'My folder' })
+ .expect(403);
+ });
+ });
+
+ describe('DELETE /api/folders/:id', () => {
+ it('should be able to delete an existing folder if group is admin or has delete permission in the same organization', async () => {
+ const adminUserData = await createUser(nestApp, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const developerUserData = await createUser(nestApp, {
+ email: 'dev@tooljet.io',
+ groups: ['all_users', 'developer'],
+ organization: adminUserData.organization,
+ });
+
+ const viewerUserData = await createUser(nestApp, {
+ email: 'viewer@tooljet.io',
+ groups: ['viewer', 'all_users'],
+ organization: adminUserData.organization,
+ });
+
+ const developerGroup = await getManager().findOneOrFail(GroupPermission, {
+ where: { group: 'developer' },
+ });
+
+ await getManager().update(GroupPermission, developerGroup.id, {
+ folderDelete: true,
+ });
+
+ for (const userData of [adminUserData, developerUserData]) {
+ const folder = await getManager().save(Folder, {
+ name: 'Folder1',
+ organizationId: adminUserData.organization.id,
+ });
+
+ const preCount = await getManager().count(Folder);
+
+ await request(nestApp.getHttpServer())
+ .delete(`/api/folders/${folder.id}`)
+ .set('Authorization', authHeaderForUser(userData.user))
+ .send()
+ .expect(200);
+
+ const postCount = await getManager().count(Folder);
+ expect(postCount).toEqual(preCount - 1);
+ }
+
+ const folder = await getManager().save(Folder, {
+ name: 'Folder1',
+ organizationId: adminUserData.organization.id,
+ });
+
+ await request(nestApp.getHttpServer())
+ .delete(`/api/folders/${folder.id}`)
+ .set('Authorization', authHeaderForUser(viewerUserData.user))
+ .send()
+ .expect(403);
+ });
+ });
+
afterAll(async () => {
await nestApp.close();
});
diff --git a/server/test/controllers/oauth.e2e-spec.ts b/server/test/controllers/oauth.e2e-spec.ts
index ca280c3f61..ddf8185a62 100644
--- a/server/test/controllers/oauth.e2e-spec.ts
+++ b/server/test/controllers/oauth.e2e-spec.ts
@@ -165,6 +165,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -235,6 +237,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -295,6 +299,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -372,6 +378,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -449,6 +457,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_delete',
+ 'folder_update',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -617,6 +627,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -702,6 +714,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -784,6 +798,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -856,6 +872,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -948,6 +966,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -1038,6 +1058,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -1128,6 +1150,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_delete',
+ 'folder_update',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -1261,6 +1285,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -1331,6 +1357,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -1391,6 +1419,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -1468,6 +1498,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -1545,6 +1577,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_delete',
+ 'folder_update',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -1713,6 +1747,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -1798,6 +1834,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -1880,6 +1918,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -1952,6 +1992,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -2044,6 +2086,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -2134,6 +2178,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_update',
+ 'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@@ -2224,6 +2270,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
+ 'folder_delete',
+ 'folder_update',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
diff --git a/server/test/test.helper.ts b/server/test/test.helper.ts
index a013474b2b..426d4f3a6e 100644
--- a/server/test/test.helper.ts
+++ b/server/test/test.helper.ts
@@ -321,6 +321,8 @@ export async function maybeCreateDefaultGroupPermissions(nestApp, organizationId
appCreate: group == 'admin',
appDelete: group == 'admin',
folderCreate: group == 'admin',
+ folderUpdate: group == 'admin',
+ folderDelete: group == 'admin',
});
await groupPermissionRepository.save(groupPermission);
}