mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-24 20:14:35 -04:00
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:
parent
24f1b2a361
commit
c2f41118b6
27 changed files with 287 additions and 66 deletions
|
@ -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'
|
||||
|
|
87
commons/src/utils/permissions.spec.ts
Normal file
87
commons/src/utils/permissions.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
51
commons/src/utils/permissions.ts
Normal file
51
commons/src/utils/permissions.ts
Normal 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
|
||||
)
|
||||
}
|
31
commons/src/utils/permissions.types.ts
Normal file
31
commons/src/utils/permissions.types.ts
Normal 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'
|
||||
}
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue