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 ( + + +
+ {canUpdateFolder && } + {canDeleteFolder && } +
+
+ + } + > +
+ +
+
+ ); +}; 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'} + >
setNewFolderName(e.target.value)} className="form-control" placeholder="folder name" - disabled={isCreating} + disabled={isCreating || isUpdating} + value={newFolderName} maxLength={25} />
- -
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); }