UNPKG

sanity

Version:

Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches

302 lines (267 loc) • 9.17 kB
import {type SanityClient} from '@sanity/client' import {type SanityDocument, type Schema, type SchemaType} from '@sanity/types' import {useMemo} from 'react' import {combineLatest, type Observable, of} from 'rxjs' import {map, switchMap} from 'rxjs/operators' import {useClient, useSchema} from '../../../hooks' import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../../studioClient' import { createHookFromObservableFactory, getDraftId, getPublishedId, type PartialExcept, } from '../../../util' import {useGrantsStore} from '../datastores' import {snapshotPair} from '../document' import {type GrantsStore, type PermissionCheckResult} from './types' function getSchemaType(schema: Schema, typeName: string): SchemaType { const type = schema.get(typeName) if (!type) { throw new Error(`No such schema type: ${typeName}`) } return type } interface PairPermissionsOptions { grantsStore: GrantsStore permission: DocumentPermission draft: SanityDocument | null published: SanityDocument | null liveEdit: boolean } function getPairPermissions({ grantsStore, permission, draft, published, liveEdit, }: PairPermissionsOptions): Array<[string, Observable<PermissionCheckResult>] | null> { // this was introduced because we ran into a bug where a user with publish // access was marked as not allowed to duplicate a document unless it had a // draft variant. this would happen in non-live edit cases where the document // pair only had a published variant with the draft variant being null. // // 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 {checkDocumentPermission} = grantsStore switch (permission) { case 'delete': { if (liveEdit) { return [ ['delete published document (live-edit)', checkDocumentPermission('update', published)], ] } return [ ['delete draft document', checkDocumentPermission('update', draft)], ['delete published document', checkDocumentPermission('update', published)], ] } case 'discardDraft': { if (liveEdit) return [] return [['delete draft document', checkDocumentPermission('update', draft)]] } case 'publish': { if (liveEdit) return [] return [ // precondition [ 'update published document at its current state', checkDocumentPermission('update', published), ], // post condition ['delete draft document', checkDocumentPermission('update', draft)], [ 'create published document from draft', checkDocumentPermission('create', draft && {...draft, _id: getPublishedId(draft._id)}), ], ] } case 'unpublish': { if (liveEdit) return [] return [ // precondition ['update draft document at its current state', checkDocumentPermission('create', draft)], // post condition ['delete published document', checkDocumentPermission('update', published)], [ 'create draft document from published version', checkDocumentPermission( 'create', published && {...published, _id: getDraftId(published._id)}, ), ], ] } case 'update': { if (liveEdit) { return [ ['update published document (live-edit)', checkDocumentPermission('update', published)], ] } return [ [ `update ${effectiveVersionType} document`, checkDocumentPermission('update', effectiveVersion), ], ] } case 'duplicate': { if (liveEdit) { return [ [ 'create new published document from existing document (live-edit)', checkDocumentPermission('create', {...published, _id: 'dummy-id'}), ], ] } return [ [ `create new draft document from existing ${effectiveVersionType} document`, checkDocumentPermission('create', {...effectiveVersion, _id: getDraftId('dummy-id')}), ], ] } default: { throw new Error(`Could not match permission: ${permission}`) } } } /** @internal */ export type DocumentPermission = | 'delete' | 'discardDraft' | 'publish' | 'unpublish' | 'update' | 'duplicate' /** @internal */ export interface DocumentPairPermissionsOptions { client: SanityClient schema: Schema grantsStore: GrantsStore id: string type: string permission: DocumentPermission } /** * The observable version of `useDocumentPairPermissions` * * @see useDocumentPairPermissions * * @internal */ export function getDocumentPairPermissions({ client, grantsStore, schema, id, permission, type, }: DocumentPairPermissionsOptions): Observable<PermissionCheckResult> { // this case was added to fix a crash that would occur if the `schemaType` was // omitted from `S.documentList()` // // see `resolveTypeForDocument` which returns `'*'` if no type is provided // https://github.com/sanity-io/sanity/blob/4d49b83a987d5097064d567f75d21b268a410cbf/packages/%40sanity/base/src/datastores/document/resolveTypeForDocument.ts#L7 if (type === '*') { return of({granted: false, reason: 'Type specified was `*`'}) } const liveEdit = Boolean(getSchemaType(schema, type).liveEdit) return snapshotPair( client, {draftId: getDraftId(id), publishedId: getPublishedId(id)}, type, ).pipe( switchMap((pair) => combineLatest([pair.draft.snapshots$, pair.published.snapshots$]).pipe( map(([draft, published]) => ({draft, published})), ), ), switchMap(({draft, published}) => { const pairPermissions = getPairPermissions({ grantsStore, permission, draft, published, liveEdit, }).map(([label, observable]: any) => observable.pipe( map(({granted, reason}: any) => ({ granted, reason: granted ? '' : `not allowed to ${label}: ${reason}`, label, permission, })), ), ) if (!pairPermissions.length) return of({granted: true, reason: ''}) return combineLatest(pairPermissions).pipe( map((permissionResults: any[]) => { const granted = permissionResults.every((permissionResult) => permissionResult.granted) const reason = granted ? '' : `Unable to ${permission}:\n\t${permissionResults .filter((permissionResult) => !permissionResult.granted) .map((permissionResult) => permissionResult.reason) .join('\n\t')}` return {granted, reason} }), ) }), ) } /** * Gets document pair permissions based on a document ID and a type. * * This permissions API is a high-level permissions API that is draft-model * aware. In order to determine whether or not the user has the given * permission, both the draft and published documents are pulled and run through * all of the user's grants. If any pre or post conditions fail a permissions * checks, the operations will not be granted. * * The operations this hook accepts are only relevant to document pairs. E.g. * `'create'` is not included as an operation because it's not possible to tell * if a document can be created by only using the initial ID and type because an * initial template value may not have a matching grant (e.g. locked-document * pattern `!locked`). In contrast, the operation `'duplicate'` is supported * because the draft value of the document can be live queried and checked for * matching grants. * * Note: for live-edit documents, non-applicable operations (e.g. publish) will * return as true. * * @see useDocumentValuePermissions * * @internal */ export const useDocumentPairPermissionsFromHookFactory = createHookFromObservableFactory( getDocumentPairPermissions, ) /** @internal */ export function useDocumentPairPermissions({ id, type, permission, client: overrideClient, schema: overrideSchema, grantsStore: overrideGrantsStore, }: PartialExcept<DocumentPairPermissionsOptions, 'id' | 'type' | 'permission'>): ReturnType< typeof useDocumentPairPermissionsFromHookFactory > { const defaultClient = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS) const defaultSchema = useSchema() const defaultGrantsStore = useGrantsStore() const client = useMemo(() => overrideClient || defaultClient, [defaultClient, overrideClient]) const schema = useMemo(() => overrideSchema || defaultSchema, [defaultSchema, overrideSchema]) const grantsStore = useMemo( () => overrideGrantsStore || defaultGrantsStore, [defaultGrantsStore, overrideGrantsStore], ) return useDocumentPairPermissionsFromHookFactory( useMemo( () => ({client, schema, grantsStore, id, permission, type}), [client, grantsStore, id, permission, schema, type], ), ) }