/* * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { GuestAccess } from '../config/guest_access.enum'; import noteConfiguration, { NoteConfig } from '../config/note.config'; import { PermissionsUpdateInconsistentError } from '../errors/errors'; import { NoteEvent, NoteEventMap } from '../events'; import { Group } from '../groups/group.entity'; import { GroupsService } from '../groups/groups.service'; import { ConsoleLoggerService } from '../logger/console-logger.service'; import { MediaUpload } from '../media/media-upload.entity'; import { NotePermissionsUpdateDto } from '../notes/note-permissions.dto'; import { Note } from '../notes/note.entity'; 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 { NotePermission } from './note-permission.enum'; import { NoteUserPermission } from './note-user-permission.entity'; import { convertGuestAccessToNotePermission } from './utils/convert-guest-access-to-note-permission'; import { findHighestNotePermissionByGroup } from './utils/find-highest-note-permission-by-group'; import { findHighestNotePermissionByUser } from './utils/find-highest-note-permission-by-user'; @Injectable() export class PermissionsService { constructor( private usersService: UsersService, private groupsService: GroupsService, @InjectRepository(Note) private noteRepository: Repository, private readonly logger: ConsoleLoggerService, @Inject(noteConfiguration.KEY) private noteConfig: NoteConfig, private eventEmitter: EventEmitter2, ) {} public async checkMediaDeletePermission( user: User, mediaUpload: MediaUpload, ): Promise { const mediaUploadNote = await mediaUpload.note; const mediaUploadOwner = await mediaUpload.user; const owner = !!mediaUploadNote && (await this.isOwner(user, mediaUploadNote)); return mediaUploadOwner?.id === user.id || owner; } /** * Checks if the given {@link User} is allowed to create notes. * * @async * @param {User} user - The user whose permission should be checked. Value is null if guest access should be checked * @return if the user is allowed to create notes */ public mayCreate(user: User | null): boolean { return !!user || this.noteConfig.guestAccess === GuestAccess.CREATE; } async isOwner(user: User | null, note: Note): Promise { if (!user) { return false; } const owner = await note.owner; if (!owner) { return false; } return owner.id === user.id; } /** * Determines the {@link NotePermission permission} of the user on the given {@link Note}. * * @param {User | null} user The user whose permission should be checked * @param {Note} note The note that is accessed by the given user * @return {Promise} The determined permission */ public async determinePermission( user: User | null, note: Note, ): Promise { if (user === null) { return await this.findGuestNotePermission(await note.groupPermissions); } if (await this.isOwner(user, note)) { return NotePermission.OWNER; } const userPermission = await findHighestNotePermissionByUser( user, await note.userPermissions, ); if (userPermission === NotePermission.WRITE) { return userPermission; } const groupPermission = await findHighestNotePermissionByGroup( user, await note.groupPermissions, ); return groupPermission > userPermission ? groupPermission : userPermission; } private async findGuestNotePermission( groupPermissions: NoteGroupPermission[], ): Promise { if (this.noteConfig.guestAccess === GuestAccess.DENY) { return NotePermission.DENY; } const everyonePermission = await this.findPermissionForGroup( groupPermissions, await this.groupsService.getEveryoneGroup(), ); if (everyonePermission === undefined) { return NotePermission.DENY; } const notePermission = everyonePermission.canEdit ? NotePermission.WRITE : NotePermission.READ; return this.limitNotePermissionToGuestAccessLevel(notePermission); } private limitNotePermissionToGuestAccessLevel( notePermission: NotePermission, ): NotePermission { const configuredGuestNotePermission = convertGuestAccessToNotePermission( this.noteConfig.guestAccess, ); return configuredGuestNotePermission < notePermission ? configuredGuestNotePermission : notePermission; } private notifyOthers(note: Note): void { this.eventEmitter.emit(NoteEvent.PERMISSION_CHANGE, note); } /** * @async * Update a notes permissions. * @param {Note} note - the note * @param {NotePermissionsUpdateDto} newPermissions - the permissions that should be applied to the note * @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 = Promise.resolve(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 = Promise.resolve(note); (await note.groupPermissions).push(createdPermission); } this.notifyOthers(note); 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 { if (await this.isOwner(permissionUser, note)) { return note; } const permissions = await note.userPermissions; const permission = await this.findPermissionForUser( permissions, permissionUser, ); if (permission !== undefined) { permission.canEdit = canEdit; } else { const noteUserPermission = NoteUserPermission.create( permissionUser, note, canEdit, ); (await note.userPermissions).push(noteUserPermission); } this.notifyOthers(note); return await this.noteRepository.save(note); } private async findPermissionForUser( permissions: NoteUserPermission[], user: User, ): Promise { for (const permission of permissions) { if ((await permission.user).id == user.id) { return permission; } } return undefined; } /** * @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 newPermissions = []; for (const permission of permissions) { if ((await permission.user).id != permissionUser.id) { newPermissions.push(permission); } } note.userPermissions = Promise.resolve(newPermissions); this.notifyOthers(note); 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 { this.logger.debug( `Setting group permission for group ${permissionGroup.name} on note ${note.id}`, 'setGroupPermission', ); const permissions = await note.groupPermissions; const permission = await this.findPermissionForGroup( permissions, permissionGroup, ); if (permission !== undefined) { permission.canEdit = canEdit; } else { this.logger.debug( `Permission does not exist yet, creating new one.`, 'setGroupPermission', ); const noteGroupPermission = NoteGroupPermission.create( permissionGroup, note, canEdit, ); (await note.groupPermissions).push(noteGroupPermission); } this.notifyOthers(note); return await this.noteRepository.save(note); } private async findPermissionForGroup( permissions: NoteGroupPermission[], group: Group, ): Promise { for (const permission of permissions) { if ((await permission.group).id == group.id) { return permission; } } return undefined; } /** * @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 newPermissions = []; for (const permission of permissions) { if ((await permission.group).id != permissionGroup.id) { newPermissions.push(permission); } } note.groupPermissions = Promise.resolve(newPermissions); this.notifyOthers(note); 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); this.notifyOthers(note); return await this.noteRepository.save(note); } }