diff --git a/migrations/20240812131527-add-get-items-permissions.js b/migrations/20240812131527-add-get-items-permissions.js new file mode 100644 index 00000000..09bf3814 --- /dev/null +++ b/migrations/20240812131527-add-get-items-permissions.js @@ -0,0 +1,35 @@ +'use strict'; + +const { v4 } = require('uuid'); + +const newPermission = 'GET_ITEMS'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + const existingRoles = await queryInterface.sequelize.query( + `SELECT * FROM roles`, + { + type: queryInterface.sequelize.QueryTypes.SELECT, + }, + ); + + for (const role of existingRoles) { + await queryInterface.bulkInsert('permissions', [ + { + id: v4(), + role_id: role.id, + name: newPermission, + created_at: new Date(), + updated_at: new Date(), + }, + ]); + } + }, + + async down(queryInterface, Sequelize) { + await queryInterface.bulkDelete('permissions', { + name: newPermission, + }); + }, +}; diff --git a/src/modules/folder/folder.controller.ts b/src/modules/folder/folder.controller.ts index 7dc471a7..be8cc9a5 100644 --- a/src/modules/folder/folder.controller.ts +++ b/src/modules/folder/folder.controller.ts @@ -674,7 +674,6 @@ export class FolderController { value: 'folder', }, ]) - @RequiredSharingPermissions(SharingActionName.GetItems) @WorkspacesInBehalfValidationFolder() async getFolderAncestors( @UserDecorator() user: User, diff --git a/src/modules/sharing/sharing.controller.ts b/src/modules/sharing/sharing.controller.ts index 79739f28..e8446223 100644 --- a/src/modules/sharing/sharing.controller.ts +++ b/src/modules/sharing/sharing.controller.ts @@ -58,7 +58,10 @@ import { ThrottlerGuard } from '../../guards/throttler.guard'; import { SetSharingPasswordDto } from './dto/set-sharing-password.dto'; import { UuidDto } from '../../common/uuid.dto'; import { HttpExceptionFilter } from '../../lib/http/http-exception.filter'; -import { WorkspacesInBehalfGuard } from '../workspaces/guards/workspaces-resources-in-behalf.decorator'; +import { + WorkspaceResourcesAction, + WorkspacesInBehalfGuard, +} from '../workspaces/guards/workspaces-resources-in-behalf.decorator'; import { GetDataFromRequest } from '../../common/extract-data-from-request'; @ApiTags('Sharing') @@ -121,6 +124,8 @@ export class SharingController { description: 'Id of the sharing', type: String, }) + @GetDataFromRequest([{ sourceKey: 'params', fieldName: 'sharingId' }]) + @WorkspacesInBehalfGuard(WorkspaceResourcesAction.ModifySharingById) @ApiOkResponse({ description: 'Sets/edit password for public sharings' }) async setPublicSharingPassword( @UserDecorator() user: User, @@ -145,6 +150,8 @@ export class SharingController { type: String, }) @ApiOkResponse({ description: 'Remove ' }) + @GetDataFromRequest([{ sourceKey: 'params', fieldName: 'sharingId' }]) + @WorkspacesInBehalfGuard(WorkspaceResourcesAction.ModifySharingById) async removePublicSharingPassword( @UserDecorator() user: User, @Param('sharingId') sharingId: Sharing['id'], diff --git a/src/modules/workspaces/guards/workspaces-resources-in-behalf.decorator.ts b/src/modules/workspaces/guards/workspaces-resources-in-behalf.decorator.ts index d2d2161a..7d579cba 100644 --- a/src/modules/workspaces/guards/workspaces-resources-in-behalf.decorator.ts +++ b/src/modules/workspaces/guards/workspaces-resources-in-behalf.decorator.ts @@ -11,6 +11,7 @@ export interface ValidationOptions { export enum WorkspaceResourcesAction { AddItemsToTrash = 'addItemsToTrash', DeleteItemsFromTrash = 'deleteItemsFromTrash', + ModifySharingById = 'modifySharingById', Default = 'default', } diff --git a/src/modules/workspaces/guards/workspaces-resources-in-items-in-behalf.guard.spec.ts b/src/modules/workspaces/guards/workspaces-resources-in-items-in-behalf.guard.spec.ts index c987fd94..3a50e7d4 100644 --- a/src/modules/workspaces/guards/workspaces-resources-in-items-in-behalf.guard.spec.ts +++ b/src/modules/workspaces/guards/workspaces-resources-in-items-in-behalf.guard.spec.ts @@ -322,6 +322,35 @@ describe('WorkspacesResourcesItemsInBehalfGuard', () => { expect(hasPermissions).toBeTruthy(); }); }); + + describe('hasUserAccessToSharing', () => { + it('When sharing item is found and creator is requester, it should return true', async () => { + const user = newUser(); + + workspaceUseCases.getWorkspaceItemBySharingId.mockResolvedValue( + newWorkspaceItemUser({ createdBy: user.uuid }), + ); + + const hasPermissions = await guard.hasUserAccessToSharing(user, { + sharingId: v4(), + }); + expect(hasPermissions).toBeTruthy(); + }); + + it('When sharing item is found and creator is not requester, it should return false', async () => { + const user = newUser(); + const notCreator = newUser(); + + workspaceUseCases.getWorkspaceItemBySharingId.mockResolvedValue( + newWorkspaceItemUser({ createdBy: user.uuid }), + ); + + const hasPermissions = await guard.hasUserAccessToSharing(notCreator, { + sharingId: v4(), + }); + expect(hasPermissions).toBeFalsy(); + }); + }); }); const createMockExecutionContext = ( diff --git a/src/modules/workspaces/guards/workspaces-resources-in-items-in-behalf.guard.ts b/src/modules/workspaces/guards/workspaces-resources-in-items-in-behalf.guard.ts index 00137406..8853a8c0 100644 --- a/src/modules/workspaces/guards/workspaces-resources-in-items-in-behalf.guard.ts +++ b/src/modules/workspaces/guards/workspaces-resources-in-items-in-behalf.guard.ts @@ -45,6 +45,8 @@ export class WorkspacesResourcesItemsInBehalfGuard implements CanActivate { this.hasUserTrashPermissions.bind(this), [WorkspaceResourcesAction.DeleteItemsFromTrash]: this.hasUserTrashPermissions.bind(this), + [WorkspaceResourcesAction.ModifySharingById]: + this.hasUserAccessToSharing.bind(this), [WorkspaceResourcesAction.Default]: this.hasUserPermissions.bind(this), }; @@ -153,6 +155,15 @@ export class WorkspacesResourcesItemsInBehalfGuard implements CanActivate { return isCreator; } + async hasUserAccessToSharing(requester: User, data: any): Promise { + const { sharingId } = data; + + const item = + await this.workspaceUseCases.getWorkspaceItemBySharingId(sharingId); + + return !!item?.isOwnedBy(requester); + } + private decodeWorkspaceToken(token: string): DecodedWorkspaceToken { try { const decoded = verifyWithDefaultSecret(token) as DecodedWorkspaceToken; diff --git a/src/modules/workspaces/workspaces.usecase.spec.ts b/src/modules/workspaces/workspaces.usecase.spec.ts index 7ee7e4ef..dfd8e438 100644 --- a/src/modules/workspaces/workspaces.usecase.spec.ts +++ b/src/modules/workspaces/workspaces.usecase.spec.ts @@ -3560,6 +3560,30 @@ describe('WorkspacesUsecases', () => { }); }); + describe('getWorkspaceItemBySharingId', () => { + it('When sharing is not valid, then it should throw', async () => { + const sharingId = v4(); + + jest.spyOn(sharingUseCases, 'findSharingBy').mockResolvedValue(null); + + await expect( + service.getWorkspaceItemBySharingId(sharingId), + ).rejects.toThrow(BadRequestException); + }); + + it('When sharing is valid, then it should return item', async () => { + const sharing = newSharing(); + const item = newWorkspaceItemUser(); + + jest.spyOn(sharingUseCases, 'findSharingBy').mockResolvedValue(sharing); + jest.spyOn(workspaceRepository, 'getItemBy').mockResolvedValue(item); + + const result = await service.getWorkspaceItemBySharingId(sharing.id); + + expect(result).toBe(item); + }); + }); + describe('teams', () => { describe('createTeam', () => { it('When workspace is not found, then fail', async () => { diff --git a/src/modules/workspaces/workspaces.usecase.ts b/src/modules/workspaces/workspaces.usecase.ts index ec31c40c..ce213773 100644 --- a/src/modules/workspaces/workspaces.usecase.ts +++ b/src/modules/workspaces/workspaces.usecase.ts @@ -1619,6 +1619,20 @@ export class WorkspacesUsecases { return this.workspaceRepository.findWorkspaceResourcesOwner(workspace.id); } + async getWorkspaceItemBySharingId(sharingId: Sharing['id']) { + const sharing = await this.sharingUseCases.findSharingBy({ id: sharingId }); + + if (!sharing) { + throw new BadRequestException('Sharing does not exist'); + } + + const item = await this.workspaceRepository.getItemBy({ + itemId: sharing.itemId, + }); + + return item; + } + async isUserCreatorOfItem( requester: User, itemId: WorkspaceItemUser['itemId'],