From 70d7ca5475e920ef1079b3ed305b0f7e9f7417bd Mon Sep 17 00:00:00 2001 From: Yannick Bungers Date: Sun, 6 Feb 2022 23:37:38 +0100 Subject: [PATCH] Move permission update functions to permissions controller Signed-off-by: Yannick Bungers --- src/api/public/notes/notes.controller.ts | 88 +-- src/notes/notes.service.spec.ts | 538 --------------- src/notes/notes.service.ts | 198 +----- src/permissions/permissions.module.ts | 15 +- src/permissions/permissions.service.spec.ts | 704 ++++++++++++++++++-- src/permissions/permissions.service.ts | 210 +++++- 6 files changed, 888 insertions(+), 865 deletions(-) diff --git a/src/api/public/notes/notes.controller.ts b/src/api/public/notes/notes.controller.ts index 2046915ab..47c08cef1 100644 --- a/src/api/public/notes/notes.controller.ts +++ b/src/api/public/notes/notes.controller.ts @@ -34,6 +34,7 @@ import { Note } from '../../../notes/note.entity'; import { NoteMediaDeletionDto } from '../../../notes/note.media-deletion.dto'; import { NotesService } from '../../../notes/notes.service'; import { Permission } from '../../../permissions/permissions.enum'; +import { PermissionsService } from '../../../permissions/permissions.service'; import { RevisionMetadataDto } from '../../../revisions/revision-metadata.dto'; import { RevisionDto } from '../../../revisions/revision.dto'; import { RevisionsService } from '../../../revisions/revisions.service'; @@ -61,6 +62,7 @@ export class NotesController { private revisionsService: RevisionsService, private historyService: HistoryService, private mediaService: MediaService, + private permissionService: PermissionsService, ) { this.logger.setContext(NotesController.name); } @@ -224,7 +226,7 @@ export class NotesController { @Body() updateDto: NotePermissionsUpdateDto, ): Promise { return await this.noteService.toNotePermissionsDto( - await this.noteService.updateNotePermissions(note, updateDto), + await this.permissionService.updateNotePermissions(note, updateDto), ); } @@ -266,22 +268,13 @@ export class NotesController { @Param('userName') username: string, @Body() canEdit: boolean, ): Promise { - try { - const permissionUser = await this.userService.getUserByUsername(username); - const returnedNote = await this.noteService.setUserPermission( - note, - permissionUser, - canEdit, - ); - return await this.noteService.toNotePermissionsDto(returnedNote); - } catch (e) { - if (e instanceof NotInDBError) { - throw new BadRequestException( - "Can't add user to permissions. User not known.", - ); - } - throw e; - } + const permissionUser = await this.userService.getUserByUsername(username); + const returnedNote = await this.permissionService.setUserPermission( + note, + permissionUser, + canEdit, + ); + return await this.noteService.toNotePermissionsDto(returnedNote); } @UseInterceptors(GetNoteInterceptor) @@ -304,7 +297,7 @@ export class NotesController { ): Promise { try { const permissionUser = await this.userService.getUserByUsername(username); - const returnedNote = await this.noteService.removeUserPermission( + const returnedNote = await this.permissionService.removeUserPermission( note, permissionUser, ); @@ -338,22 +331,13 @@ export class NotesController { @Param('groupName') groupName: string, @Body() canEdit: boolean, ): Promise { - try { - const permissionGroup = await this.groupService.getGroupByName(groupName); - const returnedNote = await this.noteService.setGroupPermission( - note, - permissionGroup, - canEdit, - ); - return await this.noteService.toNotePermissionsDto(returnedNote); - } catch (e) { - if (e instanceof NotInDBError) { - throw new BadRequestException( - "Can't add group to permissions. Group not known.", - ); - } - throw e; - } + const permissionGroup = await this.groupService.getGroupByName(groupName); + const returnedNote = await this.permissionService.setGroupPermission( + note, + permissionGroup, + canEdit, + ); + return await this.noteService.toNotePermissionsDto(returnedNote); } @UseInterceptors(GetNoteInterceptor) @@ -374,21 +358,12 @@ export class NotesController { @RequestNote() note: Note, @Param('groupName') groupName: string, ): Promise { - try { - const permissionGroup = await this.groupService.getGroupByName(groupName); - const returnedNote = await this.noteService.removeGroupPermission( - note, - permissionGroup, - ); - return await this.noteService.toNotePermissionsDto(returnedNote); - } catch (e) { - if (e instanceof NotInDBError) { - throw new BadRequestException( - "Can't remove group from permissions. Group not known.", - ); - } - throw e; - } + const permissionGroup = await this.groupService.getGroupByName(groupName); + const returnedNote = await this.permissionService.removeGroupPermission( + note, + permissionGroup, + ); + return await this.noteService.toNotePermissionsDto(returnedNote); } @UseInterceptors(GetNoteInterceptor) @@ -409,17 +384,10 @@ export class NotesController { @RequestNote() note: Note, @Body() newOwner: string, ): Promise { - try { - const owner = await this.userService.getUserByUsername(newOwner); - return await this.noteService.toNoteDto( - await this.noteService.changeOwner(note, owner), - ); - } catch (e) { - if (e instanceof NotInDBError) { - throw new BadRequestException("Can't set new owner. User not known."); - } - throw e; - } + const owner = await this.userService.getUserByUsername(newOwner); + return await this.noteService.toNoteDto( + await this.permissionService.changeOwner(note, owner), + ); } @UseInterceptors(GetNoteInterceptor) diff --git a/src/notes/notes.service.spec.ts b/src/notes/notes.service.spec.ts index cc02ae3bc..38469f6f9 100644 --- a/src/notes/notes.service.spec.ts +++ b/src/notes/notes.service.spec.ts @@ -16,7 +16,6 @@ import { AlreadyInDBError, ForbiddenIdError, NotInDBError, - PermissionsUpdateInconsistentError, } from '../errors/errors'; import { Group } from '../groups/group.entity'; import { GroupsModule } from '../groups/groups.module'; @@ -32,10 +31,6 @@ import { User } from '../users/user.entity'; import { UsersModule } from '../users/users.module'; import { Alias } from './alias.entity'; import { AliasService } from './alias.service'; -import { - NoteGroupPermissionUpdateDto, - NoteUserPermissionUpdateDto, -} from './note-permissions.dto'; import { Note } from './note.entity'; import { NotesService } from './notes.service'; import { Tag } from './tag.entity'; @@ -45,7 +40,6 @@ describe('NotesService', () => { let noteRepo: Repository; let revisionRepo: Repository; let userRepo: Repository; - let groupRepo: Repository; let forbiddenNoteId: string; beforeEach(async () => { @@ -122,8 +116,6 @@ describe('NotesService', () => { revisionRepo = module.get>( getRepositoryToken(Revision), ); - userRepo = module.get>(getRepositoryToken(User)); - groupRepo = module.get>(getRepositoryToken(Group)); }); it('should be defined', () => { @@ -357,536 +349,6 @@ describe('NotesService', () => { }); }); - describe('updateNotePermissions', () => { - const userPermissionUpdate = new NoteUserPermissionUpdateDto(); - userPermissionUpdate.username = 'hardcoded'; - userPermissionUpdate.canEdit = true; - const groupPermissionUpate = new NoteGroupPermissionUpdateDto(); - groupPermissionUpate.groupName = 'testGroup'; - groupPermissionUpate.canEdit = false; - const user = User.create(userPermissionUpdate.username, 'Testy') as User; - const group = Group.create( - groupPermissionUpate.groupName, - groupPermissionUpate.groupName, - false, - ) as Group; - const note = Note.create(user) as Note; - describe('works', () => { - it('with empty GroupPermissions and with empty UserPermissions', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const savedNote = await service.updateNotePermissions(note, { - sharedToUsers: [], - sharedToGroups: [], - }); - expect(await savedNote.userPermissions).toHaveLength(0); - expect(await savedNote.groupPermissions).toHaveLength(0); - }); - it('with empty GroupPermissions and with new UserPermissions', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user); - const savedNote = await service.updateNotePermissions(note, { - sharedToUsers: [userPermissionUpdate], - sharedToGroups: [], - }); - expect(await savedNote.userPermissions).toHaveLength(1); - expect((await savedNote.userPermissions)[0].user.username).toEqual( - userPermissionUpdate.username, - ); - expect((await savedNote.userPermissions)[0].canEdit).toEqual( - userPermissionUpdate.canEdit, - ); - expect(await savedNote.groupPermissions).toHaveLength(0); - }); - it('with empty GroupPermissions and with existing UserPermissions', async () => { - const noteWithPreexistingPermissions: Note = { ...note }; - noteWithPreexistingPermissions.userPermissions = Promise.resolve([ - { - note: noteWithPreexistingPermissions, - user: user, - canEdit: !userPermissionUpdate.canEdit, - }, - ]); - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user); - const savedNote = await service.updateNotePermissions(note, { - sharedToUsers: [userPermissionUpdate], - sharedToGroups: [], - }); - expect(await savedNote.userPermissions).toHaveLength(1); - expect((await savedNote.userPermissions)[0].user.username).toEqual( - userPermissionUpdate.username, - ); - expect((await savedNote.userPermissions)[0].canEdit).toEqual( - userPermissionUpdate.canEdit, - ); - expect(await savedNote.groupPermissions).toHaveLength(0); - }); - it('with new GroupPermissions and with empty UserPermissions', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group); - const savedNote = await service.updateNotePermissions(note, { - sharedToUsers: [], - sharedToGroups: [groupPermissionUpate], - }); - expect(await savedNote.userPermissions).toHaveLength(0); - expect((await savedNote.groupPermissions)[0].group.name).toEqual( - groupPermissionUpate.groupName, - ); - expect((await savedNote.groupPermissions)[0].canEdit).toEqual( - groupPermissionUpate.canEdit, - ); - }); - it('with new GroupPermissions and with new UserPermissions', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user); - jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group); - const savedNote = await service.updateNotePermissions(note, { - sharedToUsers: [userPermissionUpdate], - sharedToGroups: [groupPermissionUpate], - }); - expect((await savedNote.userPermissions)[0].user.username).toEqual( - userPermissionUpdate.username, - ); - expect((await savedNote.userPermissions)[0].canEdit).toEqual( - userPermissionUpdate.canEdit, - ); - expect((await savedNote.groupPermissions)[0].group.name).toEqual( - groupPermissionUpate.groupName, - ); - expect((await savedNote.groupPermissions)[0].canEdit).toEqual( - groupPermissionUpate.canEdit, - ); - }); - it('with new GroupPermissions and with existing UserPermissions', async () => { - const noteWithUserPermission: Note = { ...note }; - noteWithUserPermission.userPermissions = Promise.resolve([ - { - note: noteWithUserPermission, - user: user, - canEdit: !userPermissionUpdate.canEdit, - }, - ]); - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user); - jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group); - const savedNote = await service.updateNotePermissions( - noteWithUserPermission, - { - sharedToUsers: [userPermissionUpdate], - sharedToGroups: [groupPermissionUpate], - }, - ); - expect((await savedNote.userPermissions)[0].user.username).toEqual( - userPermissionUpdate.username, - ); - expect((await savedNote.userPermissions)[0].canEdit).toEqual( - userPermissionUpdate.canEdit, - ); - expect((await savedNote.groupPermissions)[0].group.name).toEqual( - groupPermissionUpate.groupName, - ); - expect((await savedNote.groupPermissions)[0].canEdit).toEqual( - groupPermissionUpate.canEdit, - ); - }); - it('with existing GroupPermissions and with empty UserPermissions', async () => { - const noteWithPreexistingPermissions: Note = { ...note }; - noteWithPreexistingPermissions.groupPermissions = Promise.resolve([ - { - note: noteWithPreexistingPermissions, - group: group, - canEdit: !groupPermissionUpate.canEdit, - }, - ]); - jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group); - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const savedNote = await service.updateNotePermissions( - noteWithPreexistingPermissions, - { - sharedToUsers: [], - sharedToGroups: [groupPermissionUpate], - }, - ); - expect(await savedNote.userPermissions).toHaveLength(0); - expect((await savedNote.groupPermissions)[0].group.name).toEqual( - groupPermissionUpate.groupName, - ); - expect((await savedNote.groupPermissions)[0].canEdit).toEqual( - groupPermissionUpate.canEdit, - ); - }); - it('with existing GroupPermissions and with new UserPermissions', async () => { - const noteWithPreexistingPermissions: Note = { ...note }; - noteWithPreexistingPermissions.groupPermissions = Promise.resolve([ - { - note: noteWithPreexistingPermissions, - group: group, - canEdit: !groupPermissionUpate.canEdit, - }, - ]); - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user); - jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group); - const savedNote = await service.updateNotePermissions( - noteWithPreexistingPermissions, - { - sharedToUsers: [userPermissionUpdate], - sharedToGroups: [groupPermissionUpate], - }, - ); - expect((await savedNote.userPermissions)[0].user.username).toEqual( - userPermissionUpdate.username, - ); - expect((await savedNote.userPermissions)[0].canEdit).toEqual( - userPermissionUpdate.canEdit, - ); - expect((await savedNote.groupPermissions)[0].group.name).toEqual( - groupPermissionUpate.groupName, - ); - expect((await savedNote.groupPermissions)[0].canEdit).toEqual( - groupPermissionUpate.canEdit, - ); - }); - it('with existing GroupPermissions and with existing UserPermissions', async () => { - const noteWithPreexistingPermissions: Note = { ...note }; - noteWithPreexistingPermissions.groupPermissions = Promise.resolve([ - { - note: noteWithPreexistingPermissions, - group: group, - canEdit: !groupPermissionUpate.canEdit, - }, - ]); - noteWithPreexistingPermissions.userPermissions = Promise.resolve([ - { - note: noteWithPreexistingPermissions, - user: user, - canEdit: !userPermissionUpdate.canEdit, - }, - ]); - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user); - jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group); - const savedNote = await service.updateNotePermissions( - noteWithPreexistingPermissions, - { - sharedToUsers: [userPermissionUpdate], - sharedToGroups: [groupPermissionUpate], - }, - ); - expect((await savedNote.userPermissions)[0].user.username).toEqual( - userPermissionUpdate.username, - ); - expect((await savedNote.userPermissions)[0].canEdit).toEqual( - userPermissionUpdate.canEdit, - ); - expect((await savedNote.groupPermissions)[0].group.name).toEqual( - groupPermissionUpate.groupName, - ); - expect((await savedNote.groupPermissions)[0].canEdit).toEqual( - groupPermissionUpate.canEdit, - ); - }); - }); - describe('fails:', () => { - it('userPermissions has duplicate entries', async () => { - await expect( - service.updateNotePermissions(note, { - sharedToUsers: [userPermissionUpdate, userPermissionUpdate], - sharedToGroups: [], - }), - ).rejects.toThrow(PermissionsUpdateInconsistentError); - }); - - it('groupPermissions has duplicate entries', async () => { - await expect( - service.updateNotePermissions(note, { - sharedToUsers: [], - sharedToGroups: [groupPermissionUpate, groupPermissionUpate], - }), - ).rejects.toThrow(PermissionsUpdateInconsistentError); - }); - - it('userPermissions and groupPermissions have duplicate entries', async () => { - await expect( - service.updateNotePermissions(note, { - sharedToUsers: [userPermissionUpdate, userPermissionUpdate], - sharedToGroups: [groupPermissionUpate, groupPermissionUpate], - }), - ).rejects.toThrow(PermissionsUpdateInconsistentError); - }); - }); - }); - - describe('setUserPermission', () => { - describe('works', () => { - it('with user not added before and editable', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const note = Note.create(null) as Note; - const user = User.create('test', 'Testy') as User; - const resultNote = await service.setUserPermission(note, user, true); - const noteUserPermission = NoteUserPermission.create(user, note, true); - expect((await resultNote.userPermissions)[0]).toStrictEqual( - noteUserPermission, - ); - }); - it('with user not added before and not editable', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const note = Note.create(null) as Note; - const user = User.create('test', 'Testy') as User; - const resultNote = await service.setUserPermission(note, user, false); - const noteUserPermission = NoteUserPermission.create(user, note, false); - expect((await resultNote.userPermissions)[0]).toStrictEqual( - noteUserPermission, - ); - }); - it('with user added before and editable', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const note = Note.create(null) as Note; - const user = User.create('test', 'Testy') as User; - note.userPermissions = Promise.resolve([ - NoteUserPermission.create(user, note, false), - ]); - - const resultNote = await service.setUserPermission(note, user, true); - const noteUserPermission = NoteUserPermission.create(user, note, true); - expect((await resultNote.userPermissions)[0]).toStrictEqual( - noteUserPermission, - ); - }); - it('with user added before and not editable', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const note = Note.create(null) as Note; - const user = User.create('test', 'Testy') as User; - note.userPermissions = Promise.resolve([ - NoteUserPermission.create(user, note, true), - ]); - const resultNote = await service.setUserPermission(note, user, false); - const noteUserPermission = NoteUserPermission.create(user, note, false); - expect((await resultNote.userPermissions)[0]).toStrictEqual( - noteUserPermission, - ); - }); - }); - }); - - describe('removeUserPermission', () => { - describe('works', () => { - it('with user added before and editable', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const note = Note.create(null) as Note; - const user = User.create('test', 'Testy') as User; - note.userPermissions = Promise.resolve([ - NoteUserPermission.create(user, note, true), - ]); - - const resultNote = await service.removeUserPermission(note, user); - expect((await resultNote.userPermissions).length).toStrictEqual(0); - }); - it('with user not added before and not editable', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const note = Note.create(null) as Note; - const user = User.create('test', 'Testy') as User; - note.userPermissions = Promise.resolve([ - NoteUserPermission.create(user, note, false), - ]); - const resultNote = await service.removeUserPermission(note, user); - expect((await resultNote.userPermissions).length).toStrictEqual(0); - }); - }); - }); - - describe('setGroupPermission', () => { - describe('works', () => { - it('with group not added before and editable', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const note = Note.create(null) as Note; - const group = Group.create('test', 'Testy', false) as Group; - const resultNote = await service.setGroupPermission(note, group, true); - const noteGroupPermission = NoteGroupPermission.create( - group, - note, - true, - ); - expect((await resultNote.groupPermissions)[0]).toStrictEqual( - noteGroupPermission, - ); - }); - it('with group not added before and not editable', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const note = Note.create(null) as Note; - const group = Group.create('test', 'Testy', false) as Group; - const resultNote = await service.setGroupPermission(note, group, false); - const noteGroupPermission = NoteGroupPermission.create( - group, - note, - false, - ); - expect((await resultNote.groupPermissions)[0]).toStrictEqual( - noteGroupPermission, - ); - }); - it('with group added before and editable', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const note = Note.create(null) as Note; - const group = Group.create('test', 'Testy', false) as Group; - note.groupPermissions = Promise.resolve([ - NoteGroupPermission.create(group, note, false), - ]); - - const resultNote = await service.setGroupPermission(note, group, true); - const noteGroupPermission = NoteGroupPermission.create( - group, - note, - true, - ); - expect((await resultNote.groupPermissions)[0]).toStrictEqual( - noteGroupPermission, - ); - }); - it('with group added before and not editable', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const note = Note.create(null) as Note; - const group = Group.create('test', 'Testy', false) as Group; - note.groupPermissions = Promise.resolve([ - NoteGroupPermission.create(group, note, true), - ]); - const resultNote = await service.setGroupPermission(note, group, false); - const noteGroupPermission = NoteGroupPermission.create( - group, - note, - false, - ); - expect((await resultNote.groupPermissions)[0]).toStrictEqual( - noteGroupPermission, - ); - }); - }); - }); - - describe('removeGroupPermission', () => { - describe('works', () => { - it('with user added before and editable', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const note = Note.create(null) as Note; - const group = Group.create('test', 'Testy', false) as Group; - note.groupPermissions = Promise.resolve([ - NoteGroupPermission.create(group, note, true), - ]); - - const resultNote = await service.removeGroupPermission(note, group); - expect((await resultNote.groupPermissions).length).toStrictEqual(0); - }); - it('with user not added before and not editable', async () => { - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const note = Note.create(null) as Note; - const group = Group.create('test', 'Testy', false) as Group; - note.groupPermissions = Promise.resolve([ - NoteGroupPermission.create(group, note, false), - ]); - const resultNote = await service.removeGroupPermission(note, group); - expect((await resultNote.groupPermissions).length).toStrictEqual(0); - }); - }); - }); - - describe('changeOwner', () => { - it('works', async () => { - const note = Note.create(null) as Note; - const user = User.create('test', 'Testy') as User; - jest - .spyOn(noteRepo, 'save') - .mockImplementationOnce(async (entry: Note) => { - return entry; - }); - const resultNote = await service.changeOwner(note, user); - expect(await resultNote.owner).toStrictEqual(user); - }); - }); - describe('toTagList', () => { it('works', async () => { const note = {} as Note; diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index f55337f1e..e4f35c8b4 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -12,26 +12,18 @@ import { AlreadyInDBError, ForbiddenIdError, NotInDBError, - PermissionsUpdateInconsistentError, } from '../errors/errors'; -import { Group } from '../groups/group.entity'; import { GroupsService } from '../groups/groups.service'; import { HistoryEntry } from '../history/history-entry.entity'; import { ConsoleLoggerService } from '../logger/console-logger.service'; -import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; -import { NoteUserPermission } from '../permissions/note-user-permission.entity'; import { Revision } from '../revisions/revision.entity'; import { RevisionsService } from '../revisions/revisions.service'; import { User } from '../users/user.entity'; import { UsersService } from '../users/users.service'; -import { checkArrayForDuplicates } from '../utils/arrayDuplicatCheck'; import { Alias } from './alias.entity'; import { AliasService } from './alias.service'; import { NoteMetadataDto } from './note-metadata.dto'; -import { - NotePermissionsDto, - NotePermissionsUpdateDto, -} from './note-permissions.dto'; +import { NotePermissionsDto } from './note-permissions.dto'; import { NoteDto } from './note.dto'; import { Note } from './note.entity'; import { Tag } from './tag.entity'; @@ -271,182 +263,6 @@ export class NotesService { return await this.noteRepository.save(note); } - /** - * @async - * Update a notes permissions. - * @param {Note} note - the note - * @param {NotePermissionsUpdateDto} newPermissions - the permissions the not should be set to - * @return {Note} the note with the new permissions - * @throws {NotInDBError} there is no note with this id or alias - * @throws {PermissionsUpdateInconsistentError} the new permissions specify a user or group twice. - */ - async updateNotePermissions( - note: Note, - newPermissions: NotePermissionsUpdateDto, - ): Promise { - const users = newPermissions.sharedToUsers.map( - (userPermission) => userPermission.username, - ); - - const groups = newPermissions.sharedToGroups.map( - (groupPermission) => groupPermission.groupName, - ); - - if (checkArrayForDuplicates(users) || checkArrayForDuplicates(groups)) { - this.logger.debug( - `The PermissionUpdate requested specifies the same user or group multiple times.`, - 'updateNotePermissions', - ); - throw new PermissionsUpdateInconsistentError( - 'The PermissionUpdate requested specifies the same user or group multiple times.', - ); - } - - note.userPermissions = Promise.resolve([]); - note.groupPermissions = Promise.resolve([]); - - // Create new userPermissions - for (const newUserPermission of newPermissions.sharedToUsers) { - const user = await this.usersService.getUserByUsername( - newUserPermission.username, - ); - const createdPermission = NoteUserPermission.create( - user, - note, - newUserPermission.canEdit, - ); - createdPermission.note = note; - (await note.userPermissions).push(createdPermission); - } - - // Create groupPermissions - for (const newGroupPermission of newPermissions.sharedToGroups) { - const group = await this.groupsService.getGroupByName( - newGroupPermission.groupName, - ); - const createdPermission = NoteGroupPermission.create( - group, - note, - newGroupPermission.canEdit, - ); - createdPermission.note = note; - (await note.groupPermissions).push(createdPermission); - } - - return await this.noteRepository.save(note); - } - - /** - * @async - * Set permission for a specific user on a note. - * @param {Note} note - the note - * @param {User} permissionUser - the user for which the permission should be set - * @param {boolean} canEdit - specifies if the user can edit the note - * @return {Note} the note with the new permission - */ - async setUserPermission( - note: Note, - permissionUser: User, - canEdit: boolean, - ): Promise { - const permissions = await note.userPermissions; - const permission = permissions.find( - (value: NoteUserPermission, index: number) => { - if (value.user.id == permissionUser.id) { - if (value.canEdit != canEdit) { - value.canEdit = canEdit; - permissions[index] = value; - } - return true; - } - }, - ); - if (permission == undefined) { - const noteUserPermission = NoteUserPermission.create( - permissionUser, - note, - canEdit, - ); - (await note.userPermissions).push(noteUserPermission); - } - return await this.noteRepository.save(note); - } - - /** - * @async - * Remove permission for a specific user on a note. - * @param {Note} note - the note - * @param {User} permissionUser - the user for which the permission should be set - * @return {Note} the note with the new permission - */ - async removeUserPermission(note: Note, permissionUser: User): Promise { - const permissions = await note.userPermissions; - const permissionsFiltered = permissions.filter( - (value: NoteUserPermission) => { - return value.user.id != permissionUser.id; - }, - ); - note.userPermissions = Promise.resolve(permissionsFiltered); - return await this.noteRepository.save(note); - } - - /** - * @async - * Set permission for a specific group on a note. - * @param {Note} note - the note - * @param {Group} permissionGroup - the group for which the permission should be set - * @param {boolean} canEdit - specifies if the group can edit the note - * @return {Note} the note with the new permission - */ - async setGroupPermission( - note: Note, - permissionGroup: Group, - canEdit: boolean, - ): Promise { - const permissions = await note.groupPermissions; - const permission = permissions.find( - (value: NoteGroupPermission, index: number) => { - if (value.group.id == permissionGroup.id) { - if (value.canEdit != canEdit) { - value.canEdit = canEdit; - permissions[index] = value; - } - return true; - } - }, - ); - if (permission == undefined) { - const noteGroupPermission = NoteGroupPermission.create( - permissionGroup, - note, - canEdit, - ); - (await note.groupPermissions).push(noteGroupPermission); - } - return await this.noteRepository.save(note); - } - - /** - * @async - * Remove permission for a specific group on a note. - * @param {Note} note - the note - * @param {Group} permissionGroup - the group for which the permission should be set - * @return {Note} the note with the new permission - */ - async removeGroupPermission( - note: Note, - permissionGroup: Group, - ): Promise { - const permissions = await note.groupPermissions; - const permissionsFiltered = permissions.filter( - (value: NoteGroupPermission) => { - return value.group.id != permissionGroup.id; - }, - ); - note.groupPermissions = Promise.resolve(permissionsFiltered); - return await this.noteRepository.save(note); - } - /** * @async * Calculate the updateUser (for the NoteDto) for a Note. @@ -529,18 +345,6 @@ export class NotesService { }; } - /** - * @async - * Updates the owner of a note. - * @param {Note} note - the note to use - * @param {User} owner - the new owner - * @return {Note} the updated note - */ - async changeOwner(note: Note, owner: User): Promise { - note.owner = Promise.resolve(owner); - return await this.noteRepository.save(note); - } - /** * @async * Build NoteDto from a note. diff --git a/src/permissions/permissions.module.ts b/src/permissions/permissions.module.ts index f0bcdb563..5bee49444 100644 --- a/src/permissions/permissions.module.ts +++ b/src/permissions/permissions.module.ts @@ -1,13 +1,26 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { GroupsModule } from '../groups/groups.module'; +import { LoggerModule } from '../logger/logger.module'; +import { Note } from '../notes/note.entity'; +import { NotesModule } from '../notes/notes.module'; +import { UsersModule } from '../users/users.module'; import { PermissionsService } from './permissions.service'; @Module({ + imports: [ + TypeOrmModule.forFeature([Note]), + NotesModule, + UsersModule, + GroupsModule, + LoggerModule, + ], exports: [PermissionsService], providers: [PermissionsService], }) diff --git a/src/permissions/permissions.service.spec.ts b/src/permissions/permissions.service.spec.ts index a76200c6c..2c6c3e267 100644 --- a/src/permissions/permissions.service.spec.ts +++ b/src/permissions/permissions.service.spec.ts @@ -6,16 +6,23 @@ import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; import { AuthToken } from '../auth/auth-token.entity'; import { Author } from '../authors/author.entity'; import appConfigMock from '../config/mock/app.config.mock'; import noteConfigMock from '../config/mock/note.config.mock'; +import { PermissionsUpdateInconsistentError } from '../errors/errors'; import { Group } from '../groups/group.entity'; +import { GroupsModule } from '../groups/groups.module'; import { SpecialGroup } from '../groups/groups.special'; import { Identity } from '../identity/identity.entity'; import { LoggerModule } from '../logger/logger.module'; import { Alias } from '../notes/alias.entity'; +import { + NoteGroupPermissionUpdateDto, + NoteUserPermissionUpdateDto, +} from '../notes/note-permissions.dto'; import { Note } from '../notes/note.entity'; import { NotesModule } from '../notes/notes.module'; import { Tag } from '../notes/tag.entity'; @@ -30,25 +37,54 @@ import { PermissionsModule } from './permissions.module'; import { GuestPermission, PermissionsService } from './permissions.service'; describe('PermissionsService', () => { - let permissionsService: PermissionsService; + let service: PermissionsService; let notes: Note[]; + let noteRepo: Repository; + let userRepo: Repository; + let groupRepo: Repository; beforeAll(async () => { + /** + * We need to have *one* userRepo and *one* noteRepo for both the providers + * array and the overrideProvider call, as otherwise we have two instances + * and the mock of createQueryBuilder replaces the wrong one + * **/ + userRepo = new Repository(); + noteRepo = new Repository(); const module: TestingModule = await Test.createTestingModule({ - providers: [PermissionsService], + providers: [ + PermissionsService, + { + provide: getRepositoryToken(Note), + useValue: noteRepo, + }, + { + provide: getRepositoryToken(Group), + useClass: Repository, + }, + { + provide: getRepositoryToken(User), + useValue: userRepo, + }, + ], imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [appConfigMock], + }), + LoggerModule, PermissionsModule, UsersModule, - LoggerModule, NotesModule, ConfigModule.forRoot({ isGlobal: true, load: [appConfigMock, noteConfigMock], }), + GroupsModule, ], }) .overrideProvider(getRepositoryToken(User)) - .useValue({}) + .useValue(userRepo) .overrideProvider(getRepositoryToken(AuthToken)) .useValue({}) .overrideProvider(getRepositoryToken(Identity)) @@ -58,7 +94,7 @@ describe('PermissionsService', () => { .overrideProvider(getRepositoryToken(Revision)) .useValue({}) .overrideProvider(getRepositoryToken(Note)) - .useValue({}) + .useValue(noteRepo) .overrideProvider(getRepositoryToken(Tag)) .useValue({}) .overrideProvider(getRepositoryToken(NoteGroupPermission)) @@ -66,7 +102,7 @@ describe('PermissionsService', () => { .overrideProvider(getRepositoryToken(NoteUserPermission)) .useValue({}) .overrideProvider(getRepositoryToken(Group)) - .useValue({}) + .useClass(Repository) .overrideProvider(getRepositoryToken(Session)) .useValue({}) .overrideProvider(getRepositoryToken(Author)) @@ -74,8 +110,10 @@ describe('PermissionsService', () => { .overrideProvider(getRepositoryToken(Alias)) .useValue({}) .compile(); - permissionsService = module.get(PermissionsService); + service = module.get(PermissionsService); notes = await createNoteUserPermissionNotes(); + groupRepo = module.get>(getRepositoryToken(Group)); + noteRepo = module.get>(getRepositoryToken(Note)); }); // The two users we test with: @@ -85,7 +123,7 @@ describe('PermissionsService', () => { user1.id = '1'; it('should be defined', () => { - expect(permissionsService).toBeDefined(); + expect(service).toBeDefined(); }); function createNote(owner: User): Note { @@ -176,78 +214,78 @@ describe('PermissionsService', () => { describe('mayRead works with', () => { it('Owner', async () => { - permissionsService.guestPermission = GuestPermission.DENY; - expect(await permissionsService.mayRead(user1, notes[0])).toBeTruthy(); - expect(await permissionsService.mayRead(user1, notes[7])).toBeFalsy(); + service.guestPermission = GuestPermission.DENY; + expect(await service.mayRead(user1, notes[0])).toBeTruthy(); + expect(await service.mayRead(user1, notes[7])).toBeFalsy(); }); it('userPermission read', async () => { - permissionsService.guestPermission = GuestPermission.DENY; - expect(await permissionsService.mayRead(user1, notes[1])).toBeTruthy(); - expect(await permissionsService.mayRead(user1, notes[2])).toBeTruthy(); - expect(await permissionsService.mayRead(user1, notes[3])).toBeTruthy(); + service.guestPermission = GuestPermission.DENY; + expect(await service.mayRead(user1, notes[1])).toBeTruthy(); + expect(await service.mayRead(user1, notes[2])).toBeTruthy(); + expect(await service.mayRead(user1, notes[3])).toBeTruthy(); }); it('userPermission write', async () => { - permissionsService.guestPermission = GuestPermission.DENY; - expect(await permissionsService.mayRead(user1, notes[4])).toBeTruthy(); - expect(await permissionsService.mayRead(user1, notes[5])).toBeTruthy(); - expect(await permissionsService.mayRead(user1, notes[6])).toBeTruthy(); - expect(await permissionsService.mayRead(user1, notes[7])).toBeFalsy(); + service.guestPermission = GuestPermission.DENY; + expect(await service.mayRead(user1, notes[4])).toBeTruthy(); + expect(await service.mayRead(user1, notes[5])).toBeTruthy(); + expect(await service.mayRead(user1, notes[6])).toBeTruthy(); + expect(await service.mayRead(user1, notes[7])).toBeFalsy(); }); describe('guest permission', () => { it('CREATE_ALIAS', async () => { - permissionsService.guestPermission = GuestPermission.CREATE_ALIAS; - expect(await permissionsService.mayRead(null, notes[8])).toBeTruthy(); + service.guestPermission = GuestPermission.CREATE_ALIAS; + expect(await service.mayRead(null, notes[8])).toBeTruthy(); }); it('CREATE', async () => { - permissionsService.guestPermission = GuestPermission.CREATE; - expect(await permissionsService.mayRead(null, notes[8])).toBeTruthy(); + service.guestPermission = GuestPermission.CREATE; + expect(await service.mayRead(null, notes[8])).toBeTruthy(); }); it('WRITE', async () => { - permissionsService.guestPermission = GuestPermission.WRITE; - expect(await permissionsService.mayRead(null, notes[8])).toBeTruthy(); + service.guestPermission = GuestPermission.WRITE; + expect(await service.mayRead(null, notes[8])).toBeTruthy(); }); it('READ', async () => { - permissionsService.guestPermission = GuestPermission.READ; - expect(await permissionsService.mayRead(null, notes[8])).toBeTruthy(); + service.guestPermission = GuestPermission.READ; + expect(await service.mayRead(null, notes[8])).toBeTruthy(); }); }); }); describe('mayWrite works with', () => { it('Owner', async () => { - permissionsService.guestPermission = GuestPermission.DENY; - expect(await permissionsService.mayWrite(user1, notes[0])).toBeTruthy(); - expect(await permissionsService.mayWrite(user1, notes[7])).toBeFalsy(); + service.guestPermission = GuestPermission.DENY; + expect(await service.mayWrite(user1, notes[0])).toBeTruthy(); + expect(await service.mayWrite(user1, notes[7])).toBeFalsy(); }); it('userPermission read', async () => { - permissionsService.guestPermission = GuestPermission.DENY; - expect(await permissionsService.mayWrite(user1, notes[1])).toBeFalsy(); - expect(await permissionsService.mayWrite(user1, notes[2])).toBeFalsy(); - expect(await permissionsService.mayWrite(user1, notes[3])).toBeFalsy(); + service.guestPermission = GuestPermission.DENY; + expect(await service.mayWrite(user1, notes[1])).toBeFalsy(); + expect(await service.mayWrite(user1, notes[2])).toBeFalsy(); + expect(await service.mayWrite(user1, notes[3])).toBeFalsy(); }); it('userPermission write', async () => { - permissionsService.guestPermission = GuestPermission.DENY; - expect(await permissionsService.mayWrite(user1, notes[4])).toBeTruthy(); - expect(await permissionsService.mayWrite(user1, notes[5])).toBeTruthy(); - expect(await permissionsService.mayWrite(user1, notes[6])).toBeTruthy(); - expect(await permissionsService.mayWrite(user1, notes[7])).toBeFalsy(); + service.guestPermission = GuestPermission.DENY; + expect(await service.mayWrite(user1, notes[4])).toBeTruthy(); + expect(await service.mayWrite(user1, notes[5])).toBeTruthy(); + expect(await service.mayWrite(user1, notes[6])).toBeTruthy(); + expect(await service.mayWrite(user1, notes[7])).toBeFalsy(); }); describe('guest permission', () => { it('CREATE_ALIAS', async () => { - permissionsService.guestPermission = GuestPermission.CREATE_ALIAS; - expect(await permissionsService.mayWrite(null, notes[9])).toBeTruthy(); + service.guestPermission = GuestPermission.CREATE_ALIAS; + expect(await service.mayWrite(null, notes[9])).toBeTruthy(); }); it('CREATE', async () => { - permissionsService.guestPermission = GuestPermission.CREATE; - expect(await permissionsService.mayWrite(null, notes[9])).toBeTruthy(); + service.guestPermission = GuestPermission.CREATE; + expect(await service.mayWrite(null, notes[9])).toBeTruthy(); }); it('WRITE', async () => { - permissionsService.guestPermission = GuestPermission.WRITE; - expect(await permissionsService.mayWrite(null, notes[9])).toBeTruthy(); + service.guestPermission = GuestPermission.WRITE; + expect(await service.mayWrite(null, notes[9])).toBeTruthy(); }); it('READ', async () => { - permissionsService.guestPermission = GuestPermission.READ; - expect(await permissionsService.mayWrite(null, notes[9])).toBeFalsy(); + service.guestPermission = GuestPermission.READ; + expect(await service.mayWrite(null, notes[9])).toBeFalsy(); }); }); }); @@ -512,14 +550,14 @@ describe('PermissionsService', () => { permissionString += ` ${perm.group.name}:${String(perm.canEdit)}`; } it(`mayWrite - test #${i}:${permissionString}`, async () => { - permissionsService.guestPermission = guestPermission; - expect(await permissionsService.mayWrite(user1, note)).toEqual( + service.guestPermission = guestPermission; + expect(await service.mayWrite(user1, note)).toEqual( permission.allowsWrite, ); }); it(`mayRead - test #${i}:${permissionString}`, async () => { - permissionsService.guestPermission = guestPermission; - expect(await permissionsService.mayRead(user1, note)).toEqual( + service.guestPermission = guestPermission; + expect(await service.mayRead(user1, note)).toEqual( permission.allowsRead, ); }); @@ -529,39 +567,569 @@ describe('PermissionsService', () => { describe('mayCreate works for', () => { it('logged in', () => { - permissionsService.guestPermission = GuestPermission.DENY; - expect(permissionsService.mayCreate(user1)).toBeTruthy(); + service.guestPermission = GuestPermission.DENY; + expect(service.mayCreate(user1)).toBeTruthy(); }); it('guest denied', () => { - permissionsService.guestPermission = GuestPermission.DENY; - expect(permissionsService.mayCreate(null)).toBeFalsy(); + service.guestPermission = GuestPermission.DENY; + expect(service.mayCreate(null)).toBeFalsy(); }); it('guest read', () => { - permissionsService.guestPermission = GuestPermission.READ; - expect(permissionsService.mayCreate(null)).toBeFalsy(); + service.guestPermission = GuestPermission.READ; + expect(service.mayCreate(null)).toBeFalsy(); }); it('guest write', () => { - permissionsService.guestPermission = GuestPermission.WRITE; - expect(permissionsService.mayCreate(null)).toBeFalsy(); + service.guestPermission = GuestPermission.WRITE; + expect(service.mayCreate(null)).toBeFalsy(); }); it('guest create', () => { - permissionsService.guestPermission = GuestPermission.CREATE; - expect(permissionsService.mayCreate(null)).toBeTruthy(); + service.guestPermission = GuestPermission.CREATE; + expect(service.mayCreate(null)).toBeTruthy(); }); it('guest create alias', () => { - permissionsService.guestPermission = GuestPermission.CREATE_ALIAS; - expect(permissionsService.mayCreate(null)).toBeTruthy(); + service.guestPermission = GuestPermission.CREATE_ALIAS; + expect(service.mayCreate(null)).toBeTruthy(); }); }); describe('isOwner works', () => { it('for positive case', async () => { - permissionsService.guestPermission = GuestPermission.DENY; - expect(await permissionsService.isOwner(user1, notes[0])).toBeTruthy(); + service.guestPermission = GuestPermission.DENY; + expect(await service.isOwner(user1, notes[0])).toBeTruthy(); }); it('for negative case', async () => { - permissionsService.guestPermission = GuestPermission.DENY; - expect(await permissionsService.isOwner(user1, notes[1])).toBeFalsy(); + service.guestPermission = GuestPermission.DENY; + expect(await service.isOwner(user1, notes[1])).toBeFalsy(); + }); + }); + + describe('updateNotePermissions', () => { + const userPermissionUpdate = new NoteUserPermissionUpdateDto(); + userPermissionUpdate.username = 'hardcoded'; + userPermissionUpdate.canEdit = true; + const groupPermissionUpdate = new NoteGroupPermissionUpdateDto(); + groupPermissionUpdate.groupName = 'testGroup'; + groupPermissionUpdate.canEdit = false; + const user = User.create(userPermissionUpdate.username, 'Testy') as User; + const group = Group.create( + groupPermissionUpdate.groupName, + groupPermissionUpdate.groupName, + false, + ) as Group; + const note = Note.create(user) as Note; + describe('works', () => { + it('with empty GroupPermissions and with empty UserPermissions', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const savedNote = await service.updateNotePermissions(note, { + sharedToUsers: [], + sharedToGroups: [], + }); + expect(await savedNote.userPermissions).toHaveLength(0); + expect(await savedNote.groupPermissions).toHaveLength(0); + }); + it('with empty GroupPermissions and with new UserPermissions', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user); + const savedNote = await service.updateNotePermissions(note, { + sharedToUsers: [userPermissionUpdate], + sharedToGroups: [], + }); + expect(await savedNote.userPermissions).toHaveLength(1); + expect((await savedNote.userPermissions)[0].user.username).toEqual( + userPermissionUpdate.username, + ); + expect((await savedNote.userPermissions)[0].canEdit).toEqual( + userPermissionUpdate.canEdit, + ); + expect(await savedNote.groupPermissions).toHaveLength(0); + }); + it('with empty GroupPermissions and with existing UserPermissions', async () => { + const noteWithPreexistingPermissions: Note = { ...note }; + noteWithPreexistingPermissions.userPermissions = Promise.resolve([ + { + note: noteWithPreexistingPermissions, + user: user, + canEdit: !userPermissionUpdate.canEdit, + }, + ]); + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user); + const savedNote = await service.updateNotePermissions(note, { + sharedToUsers: [userPermissionUpdate], + sharedToGroups: [], + }); + expect(await savedNote.userPermissions).toHaveLength(1); + expect((await savedNote.userPermissions)[0].user.username).toEqual( + userPermissionUpdate.username, + ); + expect((await savedNote.userPermissions)[0].canEdit).toEqual( + userPermissionUpdate.canEdit, + ); + expect(await savedNote.groupPermissions).toHaveLength(0); + }); + it('with new GroupPermissions and with empty UserPermissions', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group); + const savedNote = await service.updateNotePermissions(note, { + sharedToUsers: [], + sharedToGroups: [groupPermissionUpdate], + }); + expect(await savedNote.userPermissions).toHaveLength(0); + expect((await savedNote.groupPermissions)[0].group.name).toEqual( + groupPermissionUpdate.groupName, + ); + expect((await savedNote.groupPermissions)[0].canEdit).toEqual( + groupPermissionUpdate.canEdit, + ); + }); + it('with new GroupPermissions and with new UserPermissions', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user); + jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group); + const savedNote = await service.updateNotePermissions(note, { + sharedToUsers: [userPermissionUpdate], + sharedToGroups: [groupPermissionUpdate], + }); + expect((await savedNote.userPermissions)[0].user.username).toEqual( + userPermissionUpdate.username, + ); + expect((await savedNote.userPermissions)[0].canEdit).toEqual( + userPermissionUpdate.canEdit, + ); + expect((await savedNote.groupPermissions)[0].group.name).toEqual( + groupPermissionUpdate.groupName, + ); + expect((await savedNote.groupPermissions)[0].canEdit).toEqual( + groupPermissionUpdate.canEdit, + ); + }); + it('with new GroupPermissions and with existing UserPermissions', async () => { + const noteWithUserPermission: Note = { ...note }; + noteWithUserPermission.userPermissions = Promise.resolve([ + { + note: noteWithUserPermission, + user: user, + canEdit: !userPermissionUpdate.canEdit, + }, + ]); + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user); + jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group); + const savedNote = await service.updateNotePermissions( + noteWithUserPermission, + { + sharedToUsers: [userPermissionUpdate], + sharedToGroups: [groupPermissionUpdate], + }, + ); + expect((await savedNote.userPermissions)[0].user.username).toEqual( + userPermissionUpdate.username, + ); + expect((await savedNote.userPermissions)[0].canEdit).toEqual( + userPermissionUpdate.canEdit, + ); + expect((await savedNote.groupPermissions)[0].group.name).toEqual( + groupPermissionUpdate.groupName, + ); + expect((await savedNote.groupPermissions)[0].canEdit).toEqual( + groupPermissionUpdate.canEdit, + ); + }); + it('with existing GroupPermissions and with empty UserPermissions', async () => { + const noteWithPreexistingPermissions: Note = { ...note }; + noteWithPreexistingPermissions.groupPermissions = Promise.resolve([ + { + note: noteWithPreexistingPermissions, + group: group, + canEdit: !groupPermissionUpdate.canEdit, + }, + ]); + jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group); + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const savedNote = await service.updateNotePermissions( + noteWithPreexistingPermissions, + { + sharedToUsers: [], + sharedToGroups: [groupPermissionUpdate], + }, + ); + expect(await savedNote.userPermissions).toHaveLength(0); + expect((await savedNote.groupPermissions)[0].group.name).toEqual( + groupPermissionUpdate.groupName, + ); + expect((await savedNote.groupPermissions)[0].canEdit).toEqual( + groupPermissionUpdate.canEdit, + ); + }); + it('with existing GroupPermissions and with new UserPermissions', async () => { + const noteWithPreexistingPermissions: Note = { ...note }; + noteWithPreexistingPermissions.groupPermissions = Promise.resolve([ + { + note: noteWithPreexistingPermissions, + group: group, + canEdit: !groupPermissionUpdate.canEdit, + }, + ]); + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user); + jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group); + const savedNote = await service.updateNotePermissions( + noteWithPreexistingPermissions, + { + sharedToUsers: [userPermissionUpdate], + sharedToGroups: [groupPermissionUpdate], + }, + ); + expect((await savedNote.userPermissions)[0].user.username).toEqual( + userPermissionUpdate.username, + ); + expect((await savedNote.userPermissions)[0].canEdit).toEqual( + userPermissionUpdate.canEdit, + ); + expect((await savedNote.groupPermissions)[0].group.name).toEqual( + groupPermissionUpdate.groupName, + ); + expect((await savedNote.groupPermissions)[0].canEdit).toEqual( + groupPermissionUpdate.canEdit, + ); + }); + it('with existing GroupPermissions and with existing UserPermissions', async () => { + const noteWithPreexistingPermissions: Note = { ...note }; + noteWithPreexistingPermissions.groupPermissions = Promise.resolve([ + { + note: noteWithPreexistingPermissions, + group: group, + canEdit: !groupPermissionUpdate.canEdit, + }, + ]); + noteWithPreexistingPermissions.userPermissions = Promise.resolve([ + { + note: noteWithPreexistingPermissions, + user: user, + canEdit: !userPermissionUpdate.canEdit, + }, + ]); + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user); + jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group); + const savedNote = await service.updateNotePermissions( + noteWithPreexistingPermissions, + { + sharedToUsers: [userPermissionUpdate], + sharedToGroups: [groupPermissionUpdate], + }, + ); + expect((await savedNote.userPermissions)[0].user.username).toEqual( + userPermissionUpdate.username, + ); + expect((await savedNote.userPermissions)[0].canEdit).toEqual( + userPermissionUpdate.canEdit, + ); + expect((await savedNote.groupPermissions)[0].group.name).toEqual( + groupPermissionUpdate.groupName, + ); + expect((await savedNote.groupPermissions)[0].canEdit).toEqual( + groupPermissionUpdate.canEdit, + ); + }); + }); + describe('fails:', () => { + it('userPermissions has duplicate entries', async () => { + await expect( + service.updateNotePermissions(note, { + sharedToUsers: [userPermissionUpdate, userPermissionUpdate], + sharedToGroups: [], + }), + ).rejects.toThrow(PermissionsUpdateInconsistentError); + }); + + it('groupPermissions has duplicate entries', async () => { + await expect( + service.updateNotePermissions(note, { + sharedToUsers: [], + sharedToGroups: [groupPermissionUpdate, groupPermissionUpdate], + }), + ).rejects.toThrow(PermissionsUpdateInconsistentError); + }); + + it('userPermissions and groupPermissions have duplicate entries', async () => { + await expect( + service.updateNotePermissions(note, { + sharedToUsers: [userPermissionUpdate, userPermissionUpdate], + sharedToGroups: [groupPermissionUpdate, groupPermissionUpdate], + }), + ).rejects.toThrow(PermissionsUpdateInconsistentError); + }); + }); + }); + + describe('setUserPermission', () => { + describe('works', () => { + it('with user not added before and editable', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const note = Note.create(null) as Note; + const user = User.create('test', 'Testy') as User; + const resultNote = await service.setUserPermission(note, user, true); + const noteUserPermission = NoteUserPermission.create(user, note, true); + expect((await resultNote.userPermissions)[0]).toStrictEqual( + noteUserPermission, + ); + }); + it('with user not added before and not editable', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const note = Note.create(null) as Note; + const user = User.create('test', 'Testy') as User; + const resultNote = await service.setUserPermission(note, user, false); + const noteUserPermission = NoteUserPermission.create(user, note, false); + expect((await resultNote.userPermissions)[0]).toStrictEqual( + noteUserPermission, + ); + }); + it('with user added before and editable', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const note = Note.create(null) as Note; + const user = User.create('test', 'Testy') as User; + note.userPermissions = Promise.resolve([ + NoteUserPermission.create(user, note, false), + ]); + + const resultNote = await service.setUserPermission(note, user, true); + const noteUserPermission = NoteUserPermission.create(user, note, true); + expect((await resultNote.userPermissions)[0]).toStrictEqual( + noteUserPermission, + ); + }); + it('with user added before and not editable', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const note = Note.create(null) as Note; + const user = User.create('test', 'Testy') as User; + note.userPermissions = Promise.resolve([ + NoteUserPermission.create(user, note, true), + ]); + const resultNote = await service.setUserPermission(note, user, false); + const noteUserPermission = NoteUserPermission.create(user, note, false); + expect((await resultNote.userPermissions)[0]).toStrictEqual( + noteUserPermission, + ); + }); + }); + }); + + describe('removeUserPermission', () => { + describe('works', () => { + it('with user added before and editable', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const note = Note.create(null) as Note; + const user = User.create('test', 'Testy') as User; + note.userPermissions = Promise.resolve([ + NoteUserPermission.create(user, note, true), + ]); + + const resultNote = await service.removeUserPermission(note, user); + expect((await resultNote.userPermissions).length).toStrictEqual(0); + }); + it('with user not added before and not editable', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const note = Note.create(null) as Note; + const user = User.create('test', 'Testy') as User; + note.userPermissions = Promise.resolve([ + NoteUserPermission.create(user, note, false), + ]); + const resultNote = await service.removeUserPermission(note, user); + expect((await resultNote.userPermissions).length).toStrictEqual(0); + }); + }); + }); + + describe('setGroupPermission', () => { + describe('works', () => { + it('with group not added before and editable', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const note = Note.create(null) as Note; + const group = Group.create('test', 'Testy', false) as Group; + const resultNote = await service.setGroupPermission(note, group, true); + const noteGroupPermission = NoteGroupPermission.create( + group, + note, + true, + ); + expect((await resultNote.groupPermissions)[0]).toStrictEqual( + noteGroupPermission, + ); + }); + it('with group not added before and not editable', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const note = Note.create(null) as Note; + const group = Group.create('test', 'Testy', false) as Group; + const resultNote = await service.setGroupPermission(note, group, false); + const noteGroupPermission = NoteGroupPermission.create( + group, + note, + false, + ); + expect((await resultNote.groupPermissions)[0]).toStrictEqual( + noteGroupPermission, + ); + }); + it('with group added before and editable', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const note = Note.create(null) as Note; + const group = Group.create('test', 'Testy', false) as Group; + note.groupPermissions = Promise.resolve([ + NoteGroupPermission.create(group, note, false), + ]); + + const resultNote = await service.setGroupPermission(note, group, true); + const noteGroupPermission = NoteGroupPermission.create( + group, + note, + true, + ); + expect((await resultNote.groupPermissions)[0]).toStrictEqual( + noteGroupPermission, + ); + }); + it('with group added before and not editable', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const note = Note.create(null) as Note; + const group = Group.create('test', 'Testy', false) as Group; + note.groupPermissions = Promise.resolve([ + NoteGroupPermission.create(group, note, true), + ]); + const resultNote = await service.setGroupPermission(note, group, false); + const noteGroupPermission = NoteGroupPermission.create( + group, + note, + false, + ); + expect((await resultNote.groupPermissions)[0]).toStrictEqual( + noteGroupPermission, + ); + }); + }); + }); + + describe('removeGroupPermission', () => { + describe('works', () => { + it('with user added before and editable', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const note = Note.create(null) as Note; + const group = Group.create('test', 'Testy', false) as Group; + note.groupPermissions = Promise.resolve([ + NoteGroupPermission.create(group, note, true), + ]); + + const resultNote = await service.removeGroupPermission(note, group); + expect((await resultNote.groupPermissions).length).toStrictEqual(0); + }); + it('with user not added before and not editable', async () => { + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const note = Note.create(null) as Note; + const group = Group.create('test', 'Testy', false) as Group; + note.groupPermissions = Promise.resolve([ + NoteGroupPermission.create(group, note, false), + ]); + const resultNote = await service.removeGroupPermission(note, group); + expect((await resultNote.groupPermissions).length).toStrictEqual(0); + }); + }); + }); + + describe('changeOwner', () => { + it('works', async () => { + const note = Note.create(null) as Note; + const user = User.create('test', 'Testy') as User; + jest + .spyOn(noteRepo, 'save') + .mockImplementationOnce(async (entry: Note) => { + return entry; + }); + const resultNote = await service.changeOwner(note, user); + expect(await resultNote.owner).toStrictEqual(user); }); }); }); diff --git a/src/permissions/permissions.service.ts b/src/permissions/permissions.service.ts index 5f3cfd25e..574449f01 100644 --- a/src/permissions/permissions.service.ts +++ b/src/permissions/permissions.service.ts @@ -1,13 +1,25 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { PermissionsUpdateInconsistentError } from '../errors/errors'; +import { Group } from '../groups/group.entity'; +import { GroupsService } from '../groups/groups.service'; import { SpecialGroup } from '../groups/groups.special'; +import { ConsoleLoggerService } from '../logger/console-logger.service'; +import { NotePermissionsUpdateDto } from '../notes/note-permissions.dto'; import { Note } from '../notes/note.entity'; +import { NotesService } from '../notes/notes.service'; import { User } from '../users/user.entity'; +import { UsersService } from '../users/users.service'; +import { checkArrayForDuplicates } from '../utils/arrayDuplicatCheck'; +import { NoteGroupPermission } from './note-group-permission.entity'; +import { NoteUserPermission } from './note-user-permission.entity'; // TODO move to config or remove export enum GuestPermission { @@ -20,6 +32,14 @@ export enum GuestPermission { @Injectable() export class PermissionsService { + constructor( + public notesService: NotesService, + public usersService: UsersService, + public groupsService: GroupsService, + @InjectRepository(Note) private noteRepository: Repository, + private readonly logger: ConsoleLoggerService, + ) {} + public guestPermission: GuestPermission; // TODO change to configOption async mayRead(user: User | null, note: Note): Promise { if (await this.isOwner(user, note)) return true; @@ -126,4 +146,192 @@ export class PermissionsService { } return false; } + + /** + * @async + * Update a notes permissions. + * @param {Note} note - the note + * @param {NotePermissionsUpdateDto} newPermissions - the permissions the not should be set to + * @return {Note} the note with the new permissions + * @throws {NotInDBError} there is no note with this id or alias + * @throws {PermissionsUpdateInconsistentError} the new permissions specify a user or group twice. + */ + async updateNotePermissions( + note: Note, + newPermissions: NotePermissionsUpdateDto, + ): Promise { + const users = newPermissions.sharedToUsers.map( + (userPermission) => userPermission.username, + ); + + const groups = newPermissions.sharedToGroups.map( + (groupPermission) => groupPermission.groupName, + ); + + if (checkArrayForDuplicates(users) || checkArrayForDuplicates(groups)) { + this.logger.debug( + `The PermissionUpdate requested specifies the same user or group multiple times.`, + 'updateNotePermissions', + ); + throw new PermissionsUpdateInconsistentError( + 'The PermissionUpdate requested specifies the same user or group multiple times.', + ); + } + + note.userPermissions = Promise.resolve([]); + note.groupPermissions = Promise.resolve([]); + + // Create new userPermissions + for (const newUserPermission of newPermissions.sharedToUsers) { + const user = await this.usersService.getUserByUsername( + newUserPermission.username, + ); + const createdPermission = NoteUserPermission.create( + user, + note, + newUserPermission.canEdit, + ); + createdPermission.note = note; + (await note.userPermissions).push(createdPermission); + } + + // Create groupPermissions + for (const newGroupPermission of newPermissions.sharedToGroups) { + const group = await this.groupsService.getGroupByName( + newGroupPermission.groupName, + ); + const createdPermission = NoteGroupPermission.create( + group, + note, + newGroupPermission.canEdit, + ); + createdPermission.note = note; + (await note.groupPermissions).push(createdPermission); + } + + return await this.noteRepository.save(note); + } + + /** + * @async + * Set permission for a specific user on a note. + * @param {Note} note - the note + * @param {User} permissionUser - the user for which the permission should be set + * @param {boolean} canEdit - specifies if the user can edit the note + * @return {Note} the note with the new permission + */ + async setUserPermission( + note: Note, + permissionUser: User, + canEdit: boolean, + ): Promise { + const permissions = await note.userPermissions; + const permission = permissions.find( + (value: NoteUserPermission, index: number) => { + if (value.user.id == permissionUser.id) { + if (value.canEdit != canEdit) { + value.canEdit = canEdit; + permissions[index] = value; + } + return true; + } + }, + ); + if (permission == undefined) { + const noteUserPermission = NoteUserPermission.create( + permissionUser, + note, + canEdit, + ); + (await note.userPermissions).push(noteUserPermission); + } + return await this.noteRepository.save(note); + } + + /** + * @async + * Remove permission for a specific user on a note. + * @param {Note} note - the note + * @param {User} permissionUser - the user for which the permission should be set + * @return {Note} the note with the new permission + */ + async removeUserPermission(note: Note, permissionUser: User): Promise { + const permissions = await note.userPermissions; + const permissionsFiltered = permissions.filter( + (value: NoteUserPermission) => { + return value.user.id != permissionUser.id; + }, + ); + note.userPermissions = Promise.resolve(permissionsFiltered); + return await this.noteRepository.save(note); + } + + /** + * @async + * Set permission for a specific group on a note. + * @param {Note} note - the note + * @param {Group} permissionGroup - the group for which the permission should be set + * @param {boolean} canEdit - specifies if the group can edit the note + * @return {Note} the note with the new permission + */ + async setGroupPermission( + note: Note, + permissionGroup: Group, + canEdit: boolean, + ): Promise { + const permissions = await note.groupPermissions; + const permission = permissions.find( + (value: NoteGroupPermission, index: number) => { + if (value.group.id == permissionGroup.id) { + if (value.canEdit != canEdit) { + value.canEdit = canEdit; + permissions[index] = value; + } + return true; + } + }, + ); + if (permission == undefined) { + const noteGroupPermission = NoteGroupPermission.create( + permissionGroup, + note, + canEdit, + ); + (await note.groupPermissions).push(noteGroupPermission); + } + return await this.noteRepository.save(note); + } + + /** + * @async + * Remove permission for a specific group on a note. + * @param {Note} note - the note + * @param {Group} permissionGroup - the group for which the permission should be set + * @return {Note} the note with the new permission + */ + async removeGroupPermission( + note: Note, + permissionGroup: Group, + ): Promise { + const permissions = await note.groupPermissions; + const permissionsFiltered = permissions.filter( + (value: NoteGroupPermission) => { + return value.group.id != permissionGroup.id; + }, + ); + note.groupPermissions = Promise.resolve(permissionsFiltered); + return await this.noteRepository.save(note); + } + + /** + * @async + * Updates the owner of a note. + * @param {Note} note - the note to use + * @param {User} owner - the new owner + * @return {Note} the updated note + */ + async changeOwner(note: Note, owner: User): Promise { + note.owner = Promise.resolve(owner); + return await this.noteRepository.save(note); + } }