mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-18 09:04:44 -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
|
@ -1,9 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { Alias } from '../alias/types'
|
||||
import type { NotePermissions } from '@hedgedoc/commons'
|
||||
|
||||
export interface Note {
|
||||
content: string
|
||||
|
@ -35,22 +36,6 @@ export interface NoteEdit {
|
|||
updatedAt: string
|
||||
}
|
||||
|
||||
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 interface NoteDeletionOptions {
|
||||
keepMedia: boolean
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
|
||||
import { PutApiRequestBuilder } from '../common/api-request-builder/put-api-request-builder'
|
||||
import type { NotePermissions } from '../notes/types'
|
||||
import type { OwnerChangeDto, PermissionSetDto } from './types'
|
||||
import type { NotePermissions } from '@hedgedoc/commons'
|
||||
|
||||
/**
|
||||
* Sets the owner of a note.
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import * as AliasModule from '../../../../api/alias'
|
||||
import * as useApplicationStateModule from '../../../../hooks/common/use-application-state'
|
||||
import * as NoteDetailsReduxModule from '../../../../redux/note-details/methods'
|
||||
import type { NoteDetails } from '../../../../redux/note-details/types/note-details'
|
||||
import { mockNoteOwnership } from '../../../../test-utils/note-ownership'
|
||||
import { mockI18n } from '../../../markdown-renderer/test-utils/mock-i18n'
|
||||
import * as useUiNotificationsModule from '../../../notifications/ui-notification-boundary'
|
||||
import { AliasesAddForm } from './aliases-add-form'
|
||||
|
@ -25,12 +26,12 @@ describe('AliasesAddForm', () => {
|
|||
await mockI18n()
|
||||
jest.spyOn(AliasModule, 'addAlias').mockImplementation(() => addPromise)
|
||||
jest.spyOn(NoteDetailsReduxModule, 'updateMetadata').mockImplementation(() => Promise.resolve())
|
||||
jest.spyOn(useApplicationStateModule, 'useApplicationState').mockReturnValue('mock-note')
|
||||
jest.spyOn(useUiNotificationsModule, 'useUiNotifications').mockReturnValue({
|
||||
showErrorNotification: jest.fn(),
|
||||
dismissNotification: jest.fn(),
|
||||
dispatchUiNotification: jest.fn()
|
||||
})
|
||||
mockNoteOwnership('test', 'test', { noteDetails: { id: 'mock-note' } as NoteDetails })
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import { UiIcon } from '../../../common/icons/ui-icon'
|
||||
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
||||
import { AccessLevel } from './types'
|
||||
import { AccessLevel } from '@hedgedoc/commons'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Button, ToggleButtonGroup } from 'react-bootstrap'
|
||||
import { Eye as IconEye } from 'react-bootstrap-icons'
|
||||
|
|
|
@ -9,7 +9,7 @@ import { setNotePermissionsFromServer } from '../../../../redux/note-details/met
|
|||
import { IconButton } from '../../../common/icon-button/icon-button'
|
||||
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
||||
import { AccessLevel, SpecialGroup } from './types'
|
||||
import { AccessLevel, SpecialGroup } from '@hedgedoc/commons'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { ToggleButtonGroup } from 'react-bootstrap'
|
||||
import { Eye as IconEye } from 'react-bootstrap-icons'
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { NoteUserPermissionEntry } from '../../../../api/notes/types'
|
||||
import { removeUserPermission, setUserPermission } from '../../../../api/permissions'
|
||||
import { getUser } from '../../../../api/users'
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
|
@ -13,7 +12,8 @@ import { UserAvatarForUser } from '../../../common/user-avatar/user-avatar-for-u
|
|||
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
||||
import { PermissionEntryButtons, PermissionType } from './permission-entry-buttons'
|
||||
import { AccessLevel } from './types'
|
||||
import type { NoteUserPermissionEntry } from '@hedgedoc/commons'
|
||||
import { AccessLevel } from '@hedgedoc/commons'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useAsync } from 'react-use'
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useApplicationState } from '../../../../hooks/common/use-application-st
|
|||
import { useIsOwner } from '../../../../hooks/common/use-is-owner'
|
||||
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
||||
import { PermissionEntrySpecialGroup } from './permission-entry-special-group'
|
||||
import { AccessLevel, SpecialGroup } from './types'
|
||||
import { AccessLevel, SpecialGroup } from '@hedgedoc/commons'
|
||||
import React, { Fragment, useMemo } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
export enum AccessLevel {
|
||||
NONE,
|
||||
READ_ONLY,
|
||||
WRITEABLE
|
||||
}
|
||||
|
||||
export enum SpecialGroup {
|
||||
EVERYONE = '_EVERYONE',
|
||||
LOGGED_IN = '_LOGGED_IN'
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||
import { ORIGIN, useBaseUrl } from '../../../hooks/common/use-base-url'
|
||||
import { useDarkModeState } from '../../../hooks/common/use-dark-mode-state'
|
||||
import { useMayEdit } from '../../../hooks/common/use-may-edit'
|
||||
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
|
||||
import { findLanguageByCodeBlockName } from '../../markdown-renderer/extensions/base/code-block-markdown-extension/find-language-by-code-block-name'
|
||||
import type { ScrollProps } from '../synced-scroll/scroll-props'
|
||||
|
@ -130,6 +131,7 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
|
|||
const darkModeActivated = useDarkModeState()
|
||||
const editorOrigin = useBaseUrl(ORIGIN.EDITOR)
|
||||
const isSynced = useApplicationState((state) => state.realtimeStatus.isSynced)
|
||||
const mayEdit = useMayEdit()
|
||||
|
||||
useEffect(() => {
|
||||
const listener = messageTransporter.doAsSoonAsConnected(() => messageTransporter.sendReady())
|
||||
|
@ -144,11 +146,11 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
|
|||
onTouchStart={onMakeScrollSource}
|
||||
onMouseEnter={onMakeScrollSource}
|
||||
{...cypressId('editor-pane')}
|
||||
{...cypressAttribute('editor-ready', String(updateViewContextExtension !== null && isSynced))}>
|
||||
{...cypressAttribute('editor-ready', String(updateViewContextExtension !== null && isSynced && mayEdit))}>
|
||||
<MaxLengthWarning />
|
||||
<ToolBar />
|
||||
<ReactCodeMirror
|
||||
editable={updateViewContextExtension !== null && isSynced}
|
||||
editable={updateViewContextExtension !== null && isSynced && mayEdit}
|
||||
placeholder={t('editor.placeholder', { host: editorOrigin }) ?? ''}
|
||||
extensions={extensions}
|
||||
width={'100%'}
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useApplicationState } from './use-application-state'
|
||||
import type { NotePermissions } from '@hedgedoc/commons'
|
||||
import { userIsOwner } from '@hedgedoc/commons'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
/**
|
||||
|
@ -12,8 +14,8 @@ import { useMemo } from 'react'
|
|||
* @return True, if the current user is owner.
|
||||
*/
|
||||
export const useIsOwner = (): boolean => {
|
||||
const owner = useApplicationState((state) => state.noteDetails.permissions.owner)
|
||||
const me: string | undefined = useApplicationState((state) => state.user?.username)
|
||||
const permissions: NotePermissions = useApplicationState((state) => state.noteDetails.permissions)
|
||||
|
||||
return useMemo(() => !!me && owner === me, [owner, me])
|
||||
return useMemo(() => userIsOwner(permissions, me), [permissions, me])
|
||||
}
|
||||
|
|
21
frontend/src/hooks/common/use-may-edit.ts
Normal file
21
frontend/src/hooks/common/use-may-edit.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useApplicationState } from './use-application-state'
|
||||
import type { NotePermissions } from '@hedgedoc/commons'
|
||||
import { userCanEdit } from '@hedgedoc/commons'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
/**
|
||||
* Determines if the current user is allowed to write to this note.
|
||||
*
|
||||
* @return True, if the current user is allowed to write.
|
||||
*/
|
||||
export const useMayEdit = (): boolean => {
|
||||
const me: string | undefined = useApplicationState((state) => state.user?.username)
|
||||
const permissions: NotePermissions = useApplicationState((state) => state.noteDetails.permissions)
|
||||
|
||||
return useMemo(() => userCanEdit(permissions, me), [permissions, me])
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { store } from '..'
|
||||
import { getNoteMetadata } from '../../api/notes'
|
||||
import type { Note, NotePermissions } from '../../api/notes/types'
|
||||
import type { Note } from '../../api/notes/types'
|
||||
import type { CursorSelection } from '../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection'
|
||||
import type {
|
||||
SetNoteDetailsFromServerAction,
|
||||
|
@ -16,6 +16,7 @@ import type {
|
|||
UpdateNoteTitleByFirstHeadingAction
|
||||
} from './types'
|
||||
import { NoteDetailsActionType } from './types'
|
||||
import type { NotePermissions } from '@hedgedoc/commons'
|
||||
|
||||
/**
|
||||
* Sets the content of the current note, extracts and parses the frontmatter and extracts the markdown content part.
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { NotePermissions } from '../../../api/notes/types'
|
||||
import { initialState } from '../initial-state'
|
||||
import type { NoteDetails } from '../types/note-details'
|
||||
import { buildStateFromServerPermissions } from './build-state-from-server-permissions'
|
||||
import type { NotePermissions } from '@hedgedoc/commons'
|
||||
|
||||
describe('build state from server permissions', () => {
|
||||
it('creates a new state with the given permissions', () => {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { NotePermissions } from '../../../api/notes/types'
|
||||
import type { NoteDetails } from '../types/note-details'
|
||||
import type { NotePermissions } from '@hedgedoc/commons'
|
||||
|
||||
/**
|
||||
* Builds the updated state from a given previous state and updated NotePermissions data.
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { Note, NoteMetadata, NotePermissions } from '../../api/notes/types'
|
||||
import type { Note, NoteMetadata } from '../../api/notes/types'
|
||||
import type { CursorSelection } from '../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection'
|
||||
import type { NotePermissions } from '@hedgedoc/commons'
|
||||
import type { Action } from 'redux'
|
||||
|
||||
export enum NoteDetailsActionType {
|
||||
|
|
|
@ -7,15 +7,22 @@ import * as useApplicationStateModule from '../hooks/common/use-application-stat
|
|||
import type { ApplicationState } from '../redux/application-state'
|
||||
|
||||
jest.mock('../hooks/common/use-application-state')
|
||||
export const mockNoteOwnership = (ownUsername: string, noteOwner: string) => {
|
||||
export const mockNoteOwnership = (
|
||||
ownUsername: string,
|
||||
noteOwner: string,
|
||||
additionalState?: Partial<ApplicationState>
|
||||
) => {
|
||||
jest.spyOn(useApplicationStateModule, 'useApplicationState').mockImplementation((fn) => {
|
||||
return fn({
|
||||
...additionalState,
|
||||
noteDetails: {
|
||||
...additionalState?.noteDetails,
|
||||
permissions: {
|
||||
owner: noteOwner
|
||||
}
|
||||
},
|
||||
user: {
|
||||
...additionalState?.user,
|
||||
username: ownUsername
|
||||
}
|
||||
} as ApplicationState)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue