Skip to content

Commit

Permalink
implement scrubber for end user data in exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandercampbell committed May 25, 2022
1 parent 92c5730 commit 7d46bd3
Show file tree
Hide file tree
Showing 12 changed files with 150 additions and 33 deletions.
7 changes: 7 additions & 0 deletions .changeset/flat-parrots-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'slate': minor
'slate-react': minor
---

Add new Slate.Scrubber interface to allow scrubbing end user data from exception
text. The default behavior remains unchanged.
1 change: 1 addition & 0 deletions docs/Summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
- [RangeRef](api/locations/range-ref.md)
- [Span](api/locations/span.md)
- [Operation](api/operation.md)
- [Scrubber](api/scrubber.md)

## Libraries

Expand Down
31 changes: 31 additions & 0 deletions docs/api/scrubber.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Scrubber API

When Slate throws an exception, it includes a stringified representation of the
relevant data. For example, if your application makes an API call to access the
child of a text Node (an impossible operation), Slate will throw an exception
like this:

```
Cannot get the child of a text node: {"text": "This is my text node."}
```

If your rich text editor can include sensitive customer data, you may want to
scrub or obfuscate that text. To help with that, you can use the Scrubber API.
Here's an example of recursively scrubbing the `'text'` fields of any entity
that gets logged.

```typescript
import { Scrubber } from 'slate'

Scrubber.setScrubber((key, value) => {
if (key === 'text') return '... scrubbed ...'
return value
})
```

By setting the scrubber in this way, the error example given above will be
printed as

```
Cannot get the child of a text node: {"text": "... scrubbed ..."}
```
9 changes: 5 additions & 4 deletions packages/slate-react/src/components/slate.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useMemo, useState, useCallback, useEffect, useRef } from 'react'
import { Editor, Node, Element, Descendant } from 'slate'
import React, { useState, useCallback, useEffect, useRef } from 'react'
import { Editor, Node, Descendant, Scrubber } from 'slate'
import { ReactEditor } from '../plugin/react-editor'
import { FocusedContext } from '../hooks/use-focused'
import { EditorContext } from '../hooks/use-slate-static'
Expand Down Expand Up @@ -30,12 +30,13 @@ export const Slate = (props: {
if (!Node.isNodeList(value)) {
throw new Error(
`[Slate] value is invalid! Expected a list of elements` +
`but got: ${JSON.stringify(value)}`
`but got: ${Scrubber.stringify(value)}`
)
}
if (!Editor.isEditor(editor)) {
throw new Error(
`[Slate] editor is invalid! you passed:` + `${JSON.stringify(editor)}`
`[Slate] editor is invalid! you passed:` +
`${Scrubber.stringify(editor)}`
)
}
editor.children = value
Expand Down
19 changes: 15 additions & 4 deletions packages/slate-react/src/plugin/react-editor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { Editor, Node, Path, Point, Range, Transforms, BaseEditor } from 'slate'
import {
BaseEditor,
Editor,
Node,
Path,
Point,
Range,
Scrubber,
Transforms,
} from 'slate'

import { Key } from '../utils/key'
import {
Expand Down Expand Up @@ -108,7 +117,7 @@ export const ReactEditor = {
}

throw new Error(
`Unable to find the path for Slate node: ${JSON.stringify(node)}`
`Unable to find the path for Slate node: ${Scrubber.stringify(node)}`
)
},

Expand Down Expand Up @@ -285,7 +294,7 @@ export const ReactEditor = {

if (!domNode) {
throw new Error(
`Cannot resolve a DOM node from Slate node: ${JSON.stringify(node)}`
`Cannot resolve a DOM node from Slate node: ${Scrubber.stringify(node)}`
)
}

Expand Down Expand Up @@ -337,7 +346,9 @@ export const ReactEditor = {

if (!domPoint) {
throw new Error(
`Cannot resolve a DOM point from Slate point: ${JSON.stringify(point)}`
`Cannot resolve a DOM point from Slate point: ${Scrubber.stringify(
point
)}`
)
}

Expand Down
3 changes: 2 additions & 1 deletion packages/slate/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './create-editor'
export * from './interfaces/custom-types'
export * from './interfaces/editor'
export * from './interfaces/element'
export * from './interfaces/location'
Expand All @@ -10,6 +11,6 @@ export * from './interfaces/point'
export * from './interfaces/point-ref'
export * from './interfaces/range'
export * from './interfaces/range-ref'
export * from './interfaces/scrubber'
export * from './interfaces/text'
export * from './interfaces/custom-types'
export * from './transforms'
22 changes: 14 additions & 8 deletions packages/slate/src/interfaces/node.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { produce } from 'immer'
import { Editor, Path, Range, Text } from '..'
import { Editor, Path, Range, Text, Scrubber } from '..'
import { Element, ElementEntry } from './element'

/**
Expand Down Expand Up @@ -112,7 +112,9 @@ export const Node: NodeInterface = {

if (Text.isText(node)) {
throw new Error(
`Cannot get the ancestor node at path [${path}] because it refers to a text node instead: ${node}`
`Cannot get the ancestor node at path [${path}] because it refers to a text node instead: ${Scrubber.stringify(
node
)}`
)
}

Expand Down Expand Up @@ -145,15 +147,15 @@ export const Node: NodeInterface = {
child(root: Node, index: number): Descendant {
if (Text.isText(root)) {
throw new Error(
`Cannot get the child of a text node: ${JSON.stringify(root)}`
`Cannot get the child of a text node: ${Scrubber.stringify(root)}`
)
}

const c = root.children[index] as Descendant

if (c == null) {
throw new Error(
`Cannot get child at index \`${index}\` in node: ${JSON.stringify(
`Cannot get child at index \`${index}\` in node: ${Scrubber.stringify(
root
)}`
)
Expand Down Expand Up @@ -203,7 +205,9 @@ export const Node: NodeInterface = {

if (Editor.isEditor(node)) {
throw new Error(
`Cannot get the descendant node at path [${path}] because it refers to the root editor node instead: ${node}`
`Cannot get the descendant node at path [${path}] because it refers to the root editor node instead: ${Scrubber.stringify(
node
)}`
)
}

Expand Down Expand Up @@ -287,7 +291,7 @@ export const Node: NodeInterface = {
fragment(root: Node, range: Range): Descendant[] {
if (Text.isText(root)) {
throw new Error(
`Cannot get a fragment starting from a root text node: ${JSON.stringify(
`Cannot get a fragment starting from a root text node: ${Scrubber.stringify(
root
)}`
)
Expand Down Expand Up @@ -339,7 +343,7 @@ export const Node: NodeInterface = {

if (Text.isText(node) || !node.children[p]) {
throw new Error(
`Cannot find a descendant at path [${path}] in node: ${JSON.stringify(
`Cannot find a descendant at path [${path}] in node: ${Scrubber.stringify(
root
)}`
)
Expand Down Expand Up @@ -428,7 +432,9 @@ export const Node: NodeInterface = {

if (!Text.isText(node)) {
throw new Error(
`Cannot get the leaf node at path [${path}] because it refers to a non-leaf node: ${node}`
`Cannot get the leaf node at path [${path}] because it refers to a non-leaf node: ${Scrubber.stringify(
node
)}`
)
}

Expand Down
33 changes: 33 additions & 0 deletions packages/slate/src/interfaces/scrubber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export type Scrubber = (key: string, value: unknown) => unknown

export interface ScrubberInterface {
setScrubber(scrubber: Scrubber | undefined): void
stringify(value: any): string
}

let _scrubber: Scrubber | undefined = undefined

/**
* This interface implements a stringify() function, which is used by Slate
* internally when generating exceptions containing end user data. Developers
* using Slate may call Scrubber.setScrubber() to alter the behavior of this
* stringify() function.
*
* For example, to prevent the cleartext logging of 'text' fields within Nodes:
*
* import { Scrubber } from 'slate';
* Scrubber.setScrubber((key, val) => {
* if (key === 'text') return '...scrubbed...'
* return val
* });
*
*/
export const Scrubber: ScrubberInterface = {
setScrubber(scrubber: Scrubber | undefined): void {
_scrubber = scrubber
},

stringify(value: any): string {
return JSON.stringify(value, _scrubber)
},
}
23 changes: 13 additions & 10 deletions packages/slate/src/transforms/general.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { createDraft, finishDraft, isDraft } from 'immer'
import {
Node,
Ancestor,
Descendant,
Editor,
Selection,
Range,
Point,
Text,
Element,
Operation,
Descendant,
Node,
NodeEntry,
Operation,
Path,
Ancestor,
Point,
Range,
Scrubber,
Selection,
Text,
} from '..'

export interface GeneralTransforms {
Expand Down Expand Up @@ -73,7 +74,9 @@ const applyToDraft = (editor: Editor, selection: Selection, op: Operation) => {
prev.children.push(...node.children)
} else {
throw new Error(
`Cannot apply a "merge_node" operation at path [${path}] to nodes of different interfaces: ${node} ${prev}`
`Cannot apply a "merge_node" operation at path [${path}] to nodes of different interfaces: ${Scrubber.stringify(
node
)} ${Scrubber.stringify(prev)}`
)
}

Expand Down Expand Up @@ -236,7 +239,7 @@ const applyToDraft = (editor: Editor, selection: Selection, op: Operation) => {
if (selection == null) {
if (!Range.isRange(newProperties)) {
throw new Error(
`Cannot apply an incomplete "set_selection" operation properties ${JSON.stringify(
`Cannot apply an incomplete "set_selection" operation properties ${Scrubber.stringify(
newProperties
)} when there is no current selection.`
)
Expand Down
9 changes: 5 additions & 4 deletions packages/slate/src/transforms/node.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import {
Ancestor,
Editor,
Element,
Location,
Node,
NodeEntry,
Path,
Point,
Range,
Scrubber,
Text,
Transforms,
NodeEntry,
Ancestor,
} from '..'
import { NodeMatch, PropsCompare, PropsMerge } from '../interfaces/editor'
import { PointRef } from '../interfaces/point-ref'
Expand Down Expand Up @@ -406,9 +407,9 @@ export const NodeTransforms: NodeTransforms = {
properties = rest as Partial<Element>
} else {
throw new Error(
`Cannot merge the node at path [${path}] with the previous sibling because it is not the same kind: ${JSON.stringify(
`Cannot merge the node at path [${path}] with the previous sibling because it is not the same kind: ${Scrubber.stringify(
node
)} ${JSON.stringify(prevNode)}`
)} ${Scrubber.stringify(prevNode)}`
)
}

Expand Down
4 changes: 2 additions & 2 deletions packages/slate/src/transforms/selection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Editor, Location, Point, Range, Transforms } from '..'
import { Editor, Location, Point, Range, Scrubber, Transforms } from '..'
import { SelectionEdge, MoveUnit } from '../interfaces/types'

export interface SelectionCollapseOptions {
Expand Down Expand Up @@ -132,7 +132,7 @@ export const SelectionTransforms: SelectionTransforms = {

if (!Range.isRange(target)) {
throw new Error(
`When setting the selection and the current selection is \`null\` you must provide at least an \`anchor\` and \`focus\`, but you passed: ${JSON.stringify(
`When setting the selection and the current selection is \`null\` you must provide at least an \`anchor\` and \`focus\`, but you passed: ${Scrubber.stringify(
target
)}`
)
Expand Down
22 changes: 22 additions & 0 deletions packages/slate/test/interfaces/Scrubber/scrubber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Node, Scrubber } from 'slate'

export const input = {
customField: 'some very long custom field value that will get scrubbed',
anotherField: 'this field should not get scrambled',
}

export const test = (value: Node) => {
Scrubber.setScrubber((key, value) => key == 'customField' ? '... scrubbed ...' : value);
const stringified = Scrubber.stringify(value)
Scrubber.setScrubber(undefined)

const unmarshaled = JSON.parse(stringified)
return (
// ensure that first field has been scrubbed
unmarshaled.customField === '... scrubbed ...' &&
// ensure that second field is unaltered
unmarshaled.anotherField === input.anotherField
)
}

export const output = true

0 comments on commit 7d46bd3

Please sign in to comment.