Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): add permissions limitation to document actions in releases #8603

Merged
merged 5 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/sanity/src/core/releases/i18n/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ const releasesLocaleStrings = {
/** Title for the release tool */
'overview.title': 'Releases',

/** Tooltip label when the user doesn't have permission for discarding a version */
'permissions.error.discard-version': 'You do not have permission to discard this version',
/** Tooltip label when the user doesn't have permission for unpublishing a document */
'permissions.error.unpublish': 'You do not have permission to unpublish this document',
/** Title for the dialog confirming the publish of a release */
'publish-dialog.confirm-publish.title':
'Are you sure you want to publish the release and all document versions?',
Expand Down Expand Up @@ -333,6 +337,10 @@ const releasesLocaleStrings = {
/** Link text for toast link to the generated revert release */
'toast.revert-stage.success-link': 'View revert release',

/** Text for when a document is unpublished */
'unpublish.already-unpublished': 'This document is already unpublished.',
/** Tooltip label for when a document is unpublished */
'unpublish.no-published-version': 'There is no published version of this document.',
/** Title for the dialog confirming the unpublish of a release */
'unpublish-dialog.header': 'Are you sure you want to unpublish this document when releasing?',
/** Text action in unpublish dialog to cancel */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const DiscardVersionAction = (
id,
type,
version: release,
permission: 'publish',
permission: 'discardVersion',
})

const [dialogOpen, setDialogOpen] = useState(false)
Expand All @@ -48,6 +48,7 @@ export const DiscardVersionAction = (
}

return {
disabled: isPermissionsLoading || !permissions?.granted,
dialog: dialogOpen && {
type: 'custom',
component: (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {CloseIcon, UnpublishIcon} from '@sanity/icons'
import {Box, Card, Label, Menu, MenuDivider} from '@sanity/ui'
import {memo, useState} from 'react'
import {memo, useMemo, useState} from 'react'

import {MenuButton, MenuItem} from '../../../../../ui-components'
import {ContextMenuButton} from '../../../../components/contextMenuButton'
import {useTranslation} from '../../../../i18n'
import {useDocumentPairPermissions} from '../../../../store/_legacy/grants/documentPairPermissions'
import {getPublishedId, getVersionFromId} from '../../../../util/draftUtils'
import {DiscardVersionDialog} from '../../../components'
import {UnpublishVersionDialog} from '../../../components/dialog/UnpublishVersionDialog'
import {releasesLocaleNamespace} from '../../../i18n'
Expand All @@ -25,6 +27,47 @@ export const DocumentActions = memo(
const {t} = useTranslation(releasesLocaleNamespace)
const isAlreadyUnpublished = isGoingToUnpublish(document.document)

const publishedId = getPublishedId(document.document._id)
const type = document.document._type
const version = getVersionFromId(document.document._id)

const [discardVersionPermission, isDiscardVersionPermissionsLoading] =
useDocumentPairPermissions({
id: publishedId,
type,
version,
permission: 'discardVersion',
})
const [unpublishPermission, isUnpublishPermissionsLoading] = useDocumentPairPermissions({
id: publishedId,
type,
version,
permission: 'unpublish',
})

const isDiscardVersionActionDisabled =
!discardVersionPermission?.granted || isDiscardVersionPermissionsLoading
const noPermissionToUnpublish = !unpublishPermission?.granted || isUnpublishPermissionsLoading

const unPublishTooltipContent = useMemo(() => {
if (noPermissionToUnpublish) {
return t('permissions.error.unpublish')
}
if (!document.document.publishedDocumentExists) {
return t('unpublish.no-published-version')
}
if (isAlreadyUnpublished) {
return t('unpublish.already-unpublished')
}

return null
}, [
document.document.publishedDocumentExists,
isAlreadyUnpublished,
noPermissionToUnpublish,
t,
])

return (
<>
<Card tone="default" display="flex">
Expand All @@ -37,6 +80,8 @@ export const DocumentActions = memo(
text={coreT('release.action.discard-version')}
icon={CloseIcon}
onClick={() => setShowDiscardDialog(true)}
disabled={isDiscardVersionActionDisabled}
tooltipProps={{content: t('permissions.error.discard-version')}}
/>
<MenuDivider />
<Box padding={3} paddingBottom={2}>
Expand All @@ -45,7 +90,12 @@ export const DocumentActions = memo(
<MenuItem
text={t('action.unpublish')}
icon={UnpublishIcon}
disabled={!document.document.publishedDocumentExists || isAlreadyUnpublished}
disabled={
noPermissionToUnpublish ||
!document.document.publishedDocumentExists ||
isAlreadyUnpublished
}
tooltipProps={{content: unPublishTooltipContent}}
onClick={() => setShowUnpublishDialog(true)}
/>
</Menu>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface PairPermissionsOptions {
grantsStore: GrantsStore
permission: DocumentPermission
draft: SanityDocument | null
version: SanityDocument | null
published: SanityDocument | null
liveEdit: boolean
}
Expand All @@ -41,6 +42,7 @@ function getPairPermissions({
grantsStore,
permission,
draft,
version,
published,
liveEdit,
}: PairPermissionsOptions): Array<[string, Observable<PermissionCheckResult>]> {
Expand All @@ -51,8 +53,10 @@ function getPairPermissions({
//
// note: this should _not_ be used if the draft and published versions should
// be considered separately/explicitly in the permissions.
const effectiveVersion = draft || published
const effectiveVersionType = effectiveVersion === draft ? 'draft' : 'published'
const effectiveVersion = version || draft || published
const effectiveVersionType =
// eslint-disable-next-line no-nested-ternary
effectiveVersion === version ? version : effectiveVersion === draft ? 'draft' : 'published'

const {checkDocumentPermission} = grantsStore

Expand All @@ -76,6 +80,12 @@ function getPairPermissions({
return [['delete draft document', checkDocumentPermission('update', draft)]]
}

case 'discardVersion': {
if (liveEdit) return []

return [['delete version', checkDocumentPermission('update', version || null)]]
}

case 'publish': {
if (liveEdit) return []

Expand Down Expand Up @@ -157,6 +167,7 @@ function getPairPermissions({
export type DocumentPermission =
| 'delete'
| 'discardDraft'
| 'discardVersion'
| 'publish'
| 'unpublish'
| 'update'
Expand Down Expand Up @@ -190,7 +201,7 @@ export function getDocumentPairPermissions({
permission,
type,
serverActionsEnabled,
version,
version: v,
pairListenerOptions,
}: DocumentPairPermissionsOptions): Observable<PermissionCheckResult> {
// this case was added to fix a crash that would occur if the `schemaType` was
Expand All @@ -206,21 +217,24 @@ export function getDocumentPairPermissions({

return snapshotPair(
client,
getIdPair(id, {version}),
getIdPair(id, {version: v}),
type,
serverActionsEnabled,
pairListenerOptions,
).pipe(
switchMap((pair) =>
combineLatest([pair.draft.snapshots$, pair.published.snapshots$]).pipe(
map(([draft, published]) => ({draft, published})),
),
combineLatest([
pair.draft.snapshots$,
pair.published.snapshots$,
pair.version?.snapshots$ || of(null),
]).pipe(map(([draft, published, version]) => ({draft, published, version}))),
),
switchMap(({draft, published}) => {
switchMap(({draft, published, version}) => {
const pairPermissions = getPairPermissions({
grantsStore,
permission,
draft,
version,
published,
liveEdit,
}).map(([label, observable]) =>
Expand Down
Loading