mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-04-21 13:51:59 +02:00
implement scrubber for end user data in exceptions (#4999)
This commit is contained in:
parent
25be3b7031
commit
fe13a8f9e7
7
.changeset/flat-parrots-crash.md
Normal file
7
.changeset/flat-parrots-crash.md
Normal file
@ -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.
|
@ -48,6 +48,7 @@
|
||||
- [RangeRef](api/locations/range-ref.md)
|
||||
- [Span](api/locations/span.md)
|
||||
- [Operation](api/operation.md)
|
||||
- [Scrubber](api/scrubber.md)
|
||||
|
||||
## Libraries
|
||||
|
||||
|
78
docs/api/scrubber.md
Normal file
78
docs/api/scrubber.md
Normal file
@ -0,0 +1,78 @@
|
||||
# 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 ..."}
|
||||
```
|
||||
|
||||
## Text Randomizer Example
|
||||
|
||||
Here's an example "textRandomizer" scrubber, which randomizes particular fields
|
||||
of Nodes, preserving their length, but replacing their contents with randomly
|
||||
chosen alphanumeric characters.
|
||||
|
||||
```typescript
|
||||
import { Scrubber } from 'slate'
|
||||
|
||||
const textRandomizer = (fieldNames: string[]) => (key, value) => {
|
||||
if (fieldNames.includes(key)) {
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
.split('')
|
||||
.map(generateRandomCharacter)
|
||||
.join('')
|
||||
} else {
|
||||
return '... scrubbed ...'
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const generateRandomCharacter = (): string => {
|
||||
const chars =
|
||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLKMNOPQRSTUVWXYZ1234567890'
|
||||
return chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
|
||||
// randomize the 'text' and 'src' fields of any Node that is included in an
|
||||
// exception thrown by Slate
|
||||
Scrubber.setScrubber(Scrubber.textRandomizer(['text', 'src']))
|
||||
```
|
||||
|
||||
In this example, a Node that looked like
|
||||
|
||||
```json
|
||||
{ "text": "My test input string", "count": 5 }
|
||||
```
|
||||
|
||||
will be logged by Slate in an exception as (the random string will differ):
|
||||
|
||||
```json
|
||||
{ "text": "rSIvEzKe39l6rqQSCfyv", "count": 5 }
|
||||
```
|
@ -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'
|
||||
@ -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
|
||||
|
@ -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 {
|
||||
@ -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)}`
|
||||
)
|
||||
},
|
||||
|
||||
@ -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)}`
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
@ -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'
|
||||
|
@ -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'
|
||||
|
||||
/**
|
||||
@ -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
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
@ -145,7 +147,7 @@ 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)}`
|
||||
)
|
||||
}
|
||||
|
||||
@ -153,7 +155,7 @@ export const Node: NodeInterface = {
|
||||
|
||||
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
|
||||
)}`
|
||||
)
|
||||
@ -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
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
)}`
|
||||
)
|
||||
@ -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
|
||||
)}`
|
||||
)
|
||||
@ -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
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
|
33
packages/slate/src/interfaces/scrubber.ts
Normal file
33
packages/slate/src/interfaces/scrubber.ts
Normal file
@ -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)
|
||||
},
|
||||
}
|
@ -1,17 +1,18 @@
|
||||
import { createDraft, finishDraft, isDraft } from 'immer'
|
||||
import {
|
||||
Node,
|
||||
Editor,
|
||||
Selection,
|
||||
Range,
|
||||
Point,
|
||||
Text,
|
||||
Element,
|
||||
Operation,
|
||||
Descendant,
|
||||
NodeEntry,
|
||||
Path,
|
||||
Ancestor,
|
||||
Descendant,
|
||||
Editor,
|
||||
Element,
|
||||
Node,
|
||||
NodeEntry,
|
||||
Operation,
|
||||
Path,
|
||||
Point,
|
||||
Range,
|
||||
Scrubber,
|
||||
Selection,
|
||||
Text,
|
||||
} from '..'
|
||||
|
||||
export interface GeneralTransforms {
|
||||
@ -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)}`
|
||||
)
|
||||
}
|
||||
|
||||
@ -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.`
|
||||
)
|
||||
|
@ -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'
|
||||
@ -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)}`
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
@ -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
|
||||
)}`
|
||||
)
|
||||
|
24
packages/slate/test/interfaces/Scrubber/scrubber.ts
Normal file
24
packages/slate/test/interfaces/Scrubber/scrubber.ts
Normal file
@ -0,0 +1,24 @@
|
||||
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
|
Loading…
x
Reference in New Issue
Block a user