diff --git a/backend/src/api/private/media/media.controller.ts b/backend/src/api/private/media/media.controller.ts index 7fcb27498..08d74a0b9 100644 --- a/backend/src/api/private/media/media.controller.ts +++ b/backend/src/api/private/media/media.controller.ts @@ -23,10 +23,10 @@ import { MediaUploadDto } from '../../../media/media-upload.dto'; import { MediaService } from '../../../media/media.service'; import { MulterFile } from '../../../media/multer-file.interface'; import { Note } from '../../../notes/note.entity'; -import { Permission } from '../../../permissions/permissions.enum'; import { PermissionsGuard } from '../../../permissions/permissions.guard'; import { PermissionsService } from '../../../permissions/permissions.service'; import { RequirePermission } from '../../../permissions/require-permission.decorator'; +import { RequiredPermission } from '../../../permissions/required-permission.enum'; import { User } from '../../../users/user.entity'; import { NoteHeaderInterceptor } from '../../utils/note-header.interceptor'; import { OpenApi } from '../../utils/openapi.decorator'; @@ -66,7 +66,7 @@ export class MediaController { @UseGuards(PermissionsGuard) @UseInterceptors(FileInterceptor('file')) @UseInterceptors(NoteHeaderInterceptor) - @RequirePermission(Permission.WRITE) + @RequirePermission(RequiredPermission.WRITE) @OpenApi( { code: 201, diff --git a/backend/src/api/private/notes/notes.controller.ts b/backend/src/api/private/notes/notes.controller.ts index 515e4aa71..63609375e 100644 --- a/backend/src/api/private/notes/notes.controller.ts +++ b/backend/src/api/private/notes/notes.controller.ts @@ -30,10 +30,10 @@ import { NoteDto } from '../../../notes/note.dto'; 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 { PermissionsGuard } from '../../../permissions/permissions.guard'; import { PermissionsService } from '../../../permissions/permissions.service'; import { RequirePermission } from '../../../permissions/require-permission.decorator'; +import { RequiredPermission } from '../../../permissions/required-permission.enum'; import { RevisionMetadataDto } from '../../../revisions/revision-metadata.dto'; import { RevisionDto } from '../../../revisions/revision.dto'; import { RevisionsService } from '../../../revisions/revisions.service'; @@ -65,7 +65,7 @@ export class NotesController { @Get(':noteIdOrAlias') @OpenApi(200) - @RequirePermission(Permission.READ) + @RequirePermission(RequiredPermission.READ) @UseInterceptors(GetNoteInterceptor) async getNote( @RequestUser({ guestsAllowed: true }) user: User | null, @@ -77,7 +77,7 @@ export class NotesController { @Get(':noteIdOrAlias/media') @OpenApi(200) - @RequirePermission(Permission.READ) + @RequirePermission(RequiredPermission.READ) @UseInterceptors(GetNoteInterceptor) async getNotesMedia(@RequestNote() note: Note): Promise { const media = await this.mediaService.listUploadsByNote(note); @@ -88,7 +88,7 @@ export class NotesController { @Post() @OpenApi(201, 413) - @RequirePermission(Permission.CREATE) + @RequirePermission(RequiredPermission.CREATE) async createNote( @RequestUser({ guestsAllowed: true }) user: User | null, @MarkdownBody() text: string, @@ -101,7 +101,7 @@ export class NotesController { @Post(':noteAlias') @OpenApi(201, 400, 404, 409, 413) - @RequirePermission(Permission.CREATE) + @RequirePermission(RequiredPermission.CREATE) async createNamedNote( @RequestUser({ guestsAllowed: true }) user: User | null, @Param('noteAlias') noteAlias: string, @@ -115,7 +115,7 @@ export class NotesController { @Delete(':noteIdOrAlias') @OpenApi(204, 404, 500) - @RequirePermission(Permission.OWNER) + @RequirePermission(RequiredPermission.OWNER) @UseInterceptors(GetNoteInterceptor) async deleteNote( @RequestUser() user: User, @@ -137,7 +137,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.READ) + @RequirePermission(RequiredPermission.READ) @Get(':noteIdOrAlias/metadata') async getNoteMetadata( @RequestUser({ guestsAllowed: true }) user: User | null, @@ -148,7 +148,7 @@ export class NotesController { @Get(':noteIdOrAlias/revisions') @OpenApi(200, 404) - @RequirePermission(Permission.READ) + @RequirePermission(RequiredPermission.READ) @UseInterceptors(GetNoteInterceptor) async getNoteRevisions( @RequestUser({ guestsAllowed: true }) user: User | null, @@ -164,7 +164,7 @@ export class NotesController { @Delete(':noteIdOrAlias/revisions') @OpenApi(204, 404) - @RequirePermission(Permission.OWNER) + @RequirePermission(RequiredPermission.OWNER) @UseInterceptors(GetNoteInterceptor) async purgeNoteRevisions( @RequestUser() user: User, @@ -184,7 +184,7 @@ export class NotesController { @Get(':noteIdOrAlias/revisions/:revisionId') @OpenApi(200, 404) - @RequirePermission(Permission.READ) + @RequirePermission(RequiredPermission.READ) @UseInterceptors(GetNoteInterceptor) async getNoteRevision( @RequestUser({ guestsAllowed: true }) user: User | null, @@ -199,7 +199,7 @@ export class NotesController { @Put(':noteIdOrAlias/metadata/permissions/users/:userName') @OpenApi(200, 403, 404) @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.OWNER) + @RequirePermission(RequiredPermission.OWNER) async setUserPermission( @RequestUser() user: User, @RequestNote() note: Note, @@ -216,7 +216,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.OWNER) + @RequirePermission(RequiredPermission.OWNER) @Delete(':noteIdOrAlias/metadata/permissions/users/:userName') async removeUserPermission( @RequestUser() user: User, @@ -241,7 +241,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.OWNER) + @RequirePermission(RequiredPermission.OWNER) @Put(':noteIdOrAlias/metadata/permissions/groups/:groupName') async setGroupPermission( @RequestUser() user: User, @@ -259,7 +259,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.OWNER) + @RequirePermission(RequiredPermission.OWNER) @UseGuards(PermissionsGuard) @Delete(':noteIdOrAlias/metadata/permissions/groups/:groupName') async removeGroupPermission( @@ -276,7 +276,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.OWNER) + @RequirePermission(RequiredPermission.OWNER) @Put(':noteIdOrAlias/metadata/permissions/owner') async changeOwner( @RequestUser() user: User, diff --git a/backend/src/api/public/media/media.controller.ts b/backend/src/api/public/media/media.controller.ts index daffab060..f11356c13 100644 --- a/backend/src/api/public/media/media.controller.ts +++ b/backend/src/api/public/media/media.controller.ts @@ -29,10 +29,10 @@ import { MediaUploadDto } from '../../../media/media-upload.dto'; import { MediaService } from '../../../media/media.service'; import { MulterFile } from '../../../media/multer-file.interface'; import { Note } from '../../../notes/note.entity'; -import { Permission } from '../../../permissions/permissions.enum'; import { PermissionsGuard } from '../../../permissions/permissions.guard'; import { PermissionsService } from '../../../permissions/permissions.service'; import { RequirePermission } from '../../../permissions/require-permission.decorator'; +import { RequiredPermission } from '../../../permissions/required-permission.enum'; import { User } from '../../../users/user.entity'; import { NoteHeaderInterceptor } from '../../utils/note-header.interceptor'; import { OpenApi } from '../../utils/openapi.decorator'; @@ -84,7 +84,7 @@ export class MediaController { @UseGuards(PermissionsGuard) @UseInterceptors(FileInterceptor('file')) @UseInterceptors(NoteHeaderInterceptor) - @RequirePermission(Permission.WRITE) + @RequirePermission(RequiredPermission.WRITE) async uploadMedia( @RequestUser() user: User, @UploadedFile() file: MulterFile, diff --git a/backend/src/api/public/notes/notes.controller.ts b/backend/src/api/public/notes/notes.controller.ts index b9b8a7880..802d10584 100644 --- a/backend/src/api/public/notes/notes.controller.ts +++ b/backend/src/api/public/notes/notes.controller.ts @@ -33,10 +33,10 @@ import { NoteDto } from '../../../notes/note.dto'; 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 { PermissionsGuard } from '../../../permissions/permissions.guard'; import { PermissionsService } from '../../../permissions/permissions.service'; import { RequirePermission } from '../../../permissions/require-permission.decorator'; +import { RequiredPermission } from '../../../permissions/required-permission.enum'; import { RevisionMetadataDto } from '../../../revisions/revision-metadata.dto'; import { RevisionDto } from '../../../revisions/revision.dto'; import { RevisionsService } from '../../../revisions/revisions.service'; @@ -67,7 +67,7 @@ export class NotesController { this.logger.setContext(NotesController.name); } - @RequirePermission(Permission.CREATE) + @RequirePermission(RequiredPermission.CREATE) @Post() @OpenApi(201, 403, 409, 413) async createNote( @@ -81,7 +81,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.READ) + @RequirePermission(RequiredPermission.READ) @Get(':noteIdOrAlias') @OpenApi( { @@ -100,7 +100,7 @@ export class NotesController { return await this.noteService.toNoteDto(note); } - @RequirePermission(Permission.CREATE) + @RequirePermission(RequiredPermission.CREATE) @Post(':noteAlias') @OpenApi( { @@ -125,7 +125,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.OWNER) + @RequirePermission(RequiredPermission.OWNER) @Delete(':noteIdOrAlias') @OpenApi(204, 403, 404, 500) async deleteNote( @@ -148,7 +148,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.WRITE) + @RequirePermission(RequiredPermission.WRITE) @Put(':noteIdOrAlias') @OpenApi( { @@ -171,7 +171,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.READ) + @RequirePermission(RequiredPermission.READ) @Get(':noteIdOrAlias/content') @OpenApi( { @@ -190,7 +190,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.READ) + @RequirePermission(RequiredPermission.READ) @Get(':noteIdOrAlias/metadata') @OpenApi( { @@ -209,7 +209,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.OWNER) + @RequirePermission(RequiredPermission.OWNER) @Put(':noteIdOrAlias/metadata/permissions') @OpenApi( { @@ -231,7 +231,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.READ) + @RequirePermission(RequiredPermission.READ) @Get(':noteIdOrAlias/metadata/permissions') @OpenApi( { @@ -250,7 +250,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.OWNER) + @RequirePermission(RequiredPermission.OWNER) @Put(':noteIdOrAlias/metadata/permissions/users/:userName') @OpenApi( { @@ -277,7 +277,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.OWNER) + @RequirePermission(RequiredPermission.OWNER) @Delete(':noteIdOrAlias/metadata/permissions/users/:userName') @OpenApi( { @@ -311,7 +311,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.OWNER) + @RequirePermission(RequiredPermission.OWNER) @Put(':noteIdOrAlias/metadata/permissions/groups/:groupName') @OpenApi( { @@ -338,7 +338,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.OWNER) + @RequirePermission(RequiredPermission.OWNER) @Delete(':noteIdOrAlias/metadata/permissions/groups/:groupName') @OpenApi( { @@ -363,7 +363,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.OWNER) + @RequirePermission(RequiredPermission.OWNER) @Put(':noteIdOrAlias/metadata/permissions/owner') @OpenApi( { @@ -386,7 +386,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.READ) + @RequirePermission(RequiredPermission.READ) @Get(':noteIdOrAlias/revisions') @OpenApi( { @@ -411,7 +411,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.READ) + @RequirePermission(RequiredPermission.READ) @Get(':noteIdOrAlias/revisions/:revisionId') @OpenApi( { @@ -433,7 +433,7 @@ export class NotesController { } @UseInterceptors(GetNoteInterceptor) - @RequirePermission(Permission.READ) + @RequirePermission(RequiredPermission.READ) @Get(':noteIdOrAlias/media') @OpenApi({ code: 200, diff --git a/backend/src/permissions/permissions.enum.ts b/backend/src/permissions/permissions.enum.ts deleted file mode 100644 index 6b09c8166..000000000 --- a/backend/src/permissions/permissions.enum.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/** - * Represents the Permissions a user may hold in a request - */ -export enum Permission { - READ = 'read', - WRITE = 'write', - CREATE = 'create', - OWNER = 'owner', -} diff --git a/backend/src/permissions/permissions.guard.ts b/backend/src/permissions/permissions.guard.ts index 721809f8f..ff821f62e 100644 --- a/backend/src/permissions/permissions.guard.ts +++ b/backend/src/permissions/permissions.guard.ts @@ -10,8 +10,8 @@ import { extractNoteFromRequest } from '../api/utils/extract-note-from-request'; import { CompleteRequest } from '../api/utils/request.type'; import { ConsoleLoggerService } from '../logger/console-logger.service'; import { NotesService } from '../notes/notes.service'; -import { Permission } from './permissions.enum'; import { PermissionsService } from './permissions.service'; +import { RequiredPermission } from './required-permission.enum'; /** * This guards controller methods from access, if the user has not the appropriate permissions. @@ -31,7 +31,7 @@ export class PermissionsGuard implements CanActivate { } async canActivate(context: ExecutionContext): Promise { - const permissions = this.reflector.get( + const permissions = this.reflector.get( 'permissions', context.getHandler(), ); @@ -45,7 +45,7 @@ export class PermissionsGuard implements CanActivate { const request: CompleteRequest = context.switchToHttp().getRequest(); const user = request.user ?? null; // handle CREATE permissions, as this does not need any note - if (permissions[0] === Permission.CREATE) { + if (permissions[0] === RequiredPermission.CREATE) { return this.permissionsService.mayCreate(user); } // Attention: This gets the note an additional time if used in conjunction with GetNoteInterceptor or NoteHeaderInterceptor diff --git a/backend/src/permissions/permissions.service.spec.ts b/backend/src/permissions/permissions.service.spec.ts index fc233e3ce..ba2ae2e0a 100644 --- a/backend/src/permissions/permissions.service.spec.ts +++ b/backend/src/permissions/permissions.service.spec.ts @@ -44,9 +44,9 @@ import { User } from '../users/user.entity'; import { UsersModule } from '../users/users.module'; import { NoteGroupPermission } from './note-group-permission.entity'; import { NoteUserPermission } from './note-user-permission.entity'; -import { Permission } from './permissions.enum'; import { PermissionsModule } from './permissions.module'; import { PermissionsService } from './permissions.service'; +import { RequiredPermission } from './required-permission.enum'; function mockedEventEmitter(eventEmitter: EventEmitter2) { return jest.spyOn(eventEmitter, 'emit').mockImplementationOnce((event) => { @@ -188,6 +188,7 @@ describe('PermissionsService', () => { return isOwner; }); } + beforeEach(() => { mockNoteRepo(noteRepo); eventEmitterEmitSpy = mockedEventEmitter(eventEmitter); @@ -793,14 +794,18 @@ describe('PermissionsService', () => { it('with mayRead', async () => { mockMayReadTrue(); expect( - await service.checkPermissionOnNote(Permission.READ, user1, notes[0]), + await service.checkPermissionOnNote( + RequiredPermission.READ, + user1, + notes[0], + ), ).toBeTruthy(); }); it('with mayWrite', async () => { mockMayWriteTrue(); expect( await service.checkPermissionOnNote( - Permission.WRITE, + RequiredPermission.WRITE, user1, notes[0], ), @@ -810,7 +815,7 @@ describe('PermissionsService', () => { mockIsOwner(true); expect( await service.checkPermissionOnNote( - Permission.OWNER, + RequiredPermission.OWNER, user1, notes[0], ), @@ -824,7 +829,7 @@ describe('PermissionsService', () => { mockIsOwner(false); expect( await service.checkPermissionOnNote( - Permission.OWNER, + RequiredPermission.OWNER, user1, notes[0], ), diff --git a/backend/src/permissions/permissions.service.ts b/backend/src/permissions/permissions.service.ts index 14c7d303a..84706500b 100644 --- a/backend/src/permissions/permissions.service.ts +++ b/backend/src/permissions/permissions.service.ts @@ -27,7 +27,7 @@ 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'; -import { Permission } from './permissions.enum'; +import { RequiredPermission } from './required-permission.enum'; @Injectable() export class PermissionsService { @@ -44,22 +44,22 @@ export class PermissionsService { * Checks if the given {@link User} is has the in {@link desiredPermission} specified permission on {@link Note}. * * @async - * @param {Permission} desiredPermission - permission level to check for + * @param {RequiredPermission} desiredPermission - permission level to check for * @param {User} user - The user whose permission should be checked. Value is null if guest access should be checked * @param {Note} note - The note for which the permission should be checked * @return if the user has the specified permission on the note */ public async checkPermissionOnNote( - desiredPermission: Exclude, + desiredPermission: Exclude, user: User | null, note: Note, ): Promise { switch (desiredPermission) { - case Permission.READ: + case RequiredPermission.READ: return await this.mayRead(user, note); - case Permission.WRITE: + case RequiredPermission.WRITE: return await this.mayWrite(user, note); - case Permission.OWNER: + case RequiredPermission.OWNER: return await this.isOwner(user, note); } } diff --git a/backend/src/permissions/require-permission.decorator.ts b/backend/src/permissions/require-permission.decorator.ts index f128e415e..1e5bd0444 100644 --- a/backend/src/permissions/require-permission.decorator.ts +++ b/backend/src/permissions/require-permission.decorator.ts @@ -5,7 +5,7 @@ */ import { CustomDecorator, SetMetadata } from '@nestjs/common'; -import { Permission } from './permissions.enum'; +import { RequiredPermission } from './required-permission.enum'; /** * This decorator gathers the {@link Permission Permission} a user must hold for the {@link PermissionsGuard} @@ -14,5 +14,5 @@ import { Permission } from './permissions.enum'; */ // eslint-disable-next-line func-style,@typescript-eslint/naming-convention export const RequirePermission = ( - ...permissions: Permission[] + ...permissions: RequiredPermission[] ): CustomDecorator => SetMetadata('permissions', permissions); diff --git a/backend/src/permissions/required-permission.enum.ts b/backend/src/permissions/required-permission.enum.ts new file mode 100644 index 000000000..8d35b1ded --- /dev/null +++ b/backend/src/permissions/required-permission.enum.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * Represents the required access level a user needs to use a specific API endpoint. + */ +export enum RequiredPermission { + READ = 'read', + WRITE = 'write', + OWNER = 'owner', + CREATE = 'create', +}