hedgedoc/backend/src/permissions/permissions.service.ts
Tilman Vatteroth a852c79947 refactor: replace permission check methods with ordered permission enum
This commit replaces the "mayWrite", "mayRead" and "checkPermissionOnNote"
functions with one that returns a sortable permission value.
This is done because many places in the code need to do actions based on the fact if
the user has no, read or write access. If done with the may-functions then the permission
data need to be looked through multiple times.

Also, the whole check code is split into more functions that are tested separately and make it easier
to understand the process.

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
2023-05-19 19:10:45 +02:00

364 lines
12 KiB
TypeScript

/*
* 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<Note>,
private readonly logger: ConsoleLoggerService,
@Inject(noteConfiguration.KEY)
private noteConfig: NoteConfig,
private eventEmitter: EventEmitter2<NoteEventMap>,
) {}
public async checkMediaDeletePermission(
user: User,
mediaUpload: MediaUpload,
): Promise<boolean> {
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<boolean> {
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<NotePermission>} The determined permission
*/
public async determinePermission(
user: User | null,
note: Note,
): Promise<NotePermission> {
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<NotePermission> {
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<Note> {
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<Note> {
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<NoteUserPermission | undefined> {
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<Note> {
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<Note> {
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<NoteGroupPermission | undefined> {
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<Note> {
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> {
note.owner = Promise.resolve(owner);
this.notifyOthers(note);
return await this.noteRepository.save(note);
}
}