feat: check permissions in realtime code and frontend

Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Philip Molares 2023-03-26 14:51:18 +02:00 committed by Tilman Vatteroth
parent 24f1b2a361
commit c2f41118b6
27 changed files with 287 additions and 66 deletions

View file

@ -15,6 +15,8 @@ export {
MissingTrailingSlashError,
WrongProtocolError
} from './utils/errors.js'
export * from './utils/permissions.js'
export * from './utils/permissions.types.js'
export * from './y-doc-sync/y-doc-sync-client-adapter.js'
export * from './y-doc-sync/y-doc-sync-server-adapter.js'

View file

@ -0,0 +1,87 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { userCanEdit, userIsOwner } from './permissions.js'
import { NotePermissions, SpecialGroup } from './permissions.types.js'
import { describe, expect, it } from '@jest/globals'
describe('Permissions', () => {
const testPermissions: NotePermissions = {
owner: 'owner',
sharedToUsers: [
{
username: 'logged_in',
canEdit: true
}
],
sharedToGroups: [
{
groupName: SpecialGroup.EVERYONE,
canEdit: true
},
{
groupName: SpecialGroup.LOGGED_IN,
canEdit: true
}
]
}
describe('userIsOwner', () => {
it('returns true, if user is owner', () => {
expect(userIsOwner(testPermissions, 'owner')).toBeTruthy()
})
it('returns false, if user is not ownerr', () => {
expect(userIsOwner(testPermissions, 'not_owner')).toBeFalsy()
})
it('returns false, if user is undefined', () => {
expect(userIsOwner(testPermissions, undefined)).toBeFalsy()
})
})
describe('userCanEdit', () => {
it('returns true, if user is owner', () => {
expect(userCanEdit(testPermissions, 'owner')).toBeTruthy()
})
it('returns true, if user is logged in and this is user specifically may edit', () => {
expect(
userCanEdit({ ...testPermissions, sharedToGroups: [] }, 'logged_in')
).toBeTruthy()
})
it('returns true, if user is logged in and loggedIn users may edit', () => {
expect(
userCanEdit({ ...testPermissions, sharedToUsers: [] }, 'logged_in')
).toBeTruthy()
})
it('returns true, if user is guest and guests are allowed to edit', () => {
expect(
userCanEdit({ ...testPermissions, sharedToUsers: [] }, undefined)
).toBeTruthy()
})
it('returns false, if user is logged in and loggedIn users may not edit', () => {
expect(
userCanEdit(
{ ...testPermissions, sharedToUsers: [], sharedToGroups: [] },
'logged_in'
)
).toBeFalsy()
})
it('returns false, if user is guest and guests are not allowed to edit', () => {
expect(
userCanEdit(
{
...testPermissions,
sharedToUsers: [],
sharedToGroups: [
{
groupName: SpecialGroup.LOGGED_IN,
canEdit: true
}
]
},
undefined
)
).toBeFalsy()
})
})
})

View file

@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NotePermissions, SpecialGroup } from './permissions.types.js'
/**
* Checks if the given user is the owner of a note.
*
* @param permissions The permissions of the note to check
* @param user The username of the user
* @return True if the user is the owner of the note
*/
export const userIsOwner = (
permissions: NotePermissions,
user?: string
): boolean => {
return !!user && permissions.owner === user
}
/**
* Checks if the given user may edit a note.
*
* @param permissions The permissions of the note to check
* @param user The username of the user
* @return True if the user has the permission to edit the note
*/
export const userCanEdit = (
permissions: NotePermissions,
user?: string
): boolean => {
const isOwner = userIsOwner(permissions, user)
const mayWriteViaUserPermission = permissions.sharedToUsers.some(
(value) => value.canEdit && value.username === user
)
const mayWriteViaGroupPermission =
!!user &&
permissions.sharedToGroups.some(
(value) => value.groupName === SpecialGroup.LOGGED_IN && value.canEdit
)
const everyoneMayWriteViaGroupPermission = permissions.sharedToGroups.some(
(value) => value.groupName === SpecialGroup.EVERYONE && value.canEdit
)
return (
isOwner ||
mayWriteViaUserPermission ||
mayWriteViaGroupPermission ||
everyoneMayWriteViaGroupPermission
)
}

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface NotePermissions {
owner: string | null
sharedToUsers: NoteUserPermissionEntry[]
sharedToGroups: NoteGroupPermissionEntry[]
}
export interface NoteUserPermissionEntry {
username: string
canEdit: boolean
}
export interface NoteGroupPermissionEntry {
groupName: string
canEdit: boolean
}
export enum AccessLevel {
NONE,
READ_ONLY,
WRITEABLE
}
export enum SpecialGroup {
EVERYONE = '_EVERYONE',
LOGGED_IN = '_LOGGED_IN'
}

View file

@ -104,12 +104,14 @@ describe('message transporter', () => {
const yDocSyncAdapterServerTo1 = new YDocSyncServerAdapter(
transporterServerTo1,
docServer
docServer,
true
)
const yDocSyncAdapterServerTo2 = new YDocSyncServerAdapter(
transporterServerTo2,
docServer
docServer,
true
)
const waitForClient1Sync = new Promise<void>((resolve) => {

View file

@ -28,7 +28,7 @@ export abstract class YDocSyncAdapter {
this.yDocUpdateListener = doc.on(
'update',
(update, origin) => {
this.processDocUpdate(update, origin)
this.distributeDocUpdate(update, origin)
},
{
objectify: true
@ -92,7 +92,7 @@ export abstract class YDocSyncAdapter {
const noteContentUpdateListener = this.messageTransporter.on(
MessageType.NOTE_CONTENT_UPDATE,
(payload) => this.doc.applyUpdate(payload.payload, this),
(payload) => this.applyIncomingUpdatePayload(payload.payload),
{ objectify: true }
) as Listener
@ -103,7 +103,11 @@ export abstract class YDocSyncAdapter {
}
}
private processDocUpdate(update: number[], origin: unknown): void {
protected applyIncomingUpdatePayload(update: number[]): void {
this.doc.applyUpdate(update, this)
}
private distributeDocUpdate(update: number[], origin: unknown): void {
if (!this.isSynced() || origin === this) {
return
}

View file

@ -10,9 +10,17 @@ import { YDocSyncAdapter } from './y-doc-sync-adapter.js'
export class YDocSyncServerAdapter extends YDocSyncAdapter {
constructor(
readonly messageTransporter: MessageTransporter,
readonly doc: RealtimeDoc
readonly doc: RealtimeDoc,
readonly acceptEdits: boolean
) {
super(messageTransporter, doc)
this.markAsSynced()
}
protected applyIncomingUpdatePayload(update: number[]): void {
if (!this.acceptEdits) {
return
}
super.applyIncomingUpdatePayload(update)
}
}