Skip to content

Commit

Permalink
Add support for serializers
Browse files Browse the repository at this point in the history
  • Loading branch information
Dakrs committed Jan 30, 2023
1 parent 7ab7ac8 commit 33aacbf
Show file tree
Hide file tree
Showing 6 changed files with 359 additions and 5 deletions.
51 changes: 49 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ Object redaction with whitelist and blacklist. Blacklist items have higher prior
3. `options` _(Object)_: An object with optional options.

`options.replacement` _(Function)_: A function that allows customizing the replacement value (default implementation is `--REDACTED--`).


`options.serializers` _(List[Object])_: A list with serializers to apply. Each serializers must contain two properties: `path` (path for the value to be serialized, must be a `string`) and `serializer` (function to be called on the path's value).

`options.trim` _(Boolean)_: A flag that enables trimming all redacted values, saving their keys to a `__redacted__` list (default value is `false`).

### Example

```js
const anonymizer = require('@uphold/anonymizer');
const { anonymizer } = require('@uphold/anonymizer');
const whitelist = ['foo.key', 'foo.depth.*', 'bar.*', 'toAnonymize.baz', 'toAnonymizeSuperString'];
const blacklist = ['foo.depth.innerBlacklist', 'toAnonymize.*'];
const anonymize = anonymizer({ blacklist, whitelist });
Expand All @@ -39,6 +41,51 @@ anonymize(data);
// }
```

#### Example using serializers

```js
const { anonymizer } = require('@uphold/anonymizer');
const whitelist = ['foo.key', 'foo.depth.*', 'bar.*', 'toAnonymize.baz'];
const blacklist = ['foo.depth.innerBlacklist'];
const serializers = [
{ path: 'foo.key', serializer: () => 'biz' },
{ path: 'toAnonymize', serializer: () => ({ baz: 'baz' }) }
]
const anonymize = anonymizer({ blacklist, whitelist });

const data = {
foo: { key: 'public', another: 'bar', depth: { bar: 10, innerBlacklist: 11 } },
bar: { foo: 1, bar: 2 },
toAnonymize: {}
};

// {
// foo: {
// key: 'biz',
// another: '--REDACTED--',
// depth: { bar: 10, innerBlacklist: '--REDACTED--' }
// },
// bar: { foo: 1, bar: 2 },
// toAnonymize: { baz: 'baz' }
// }
```

### Default serializers

The introduction of serializers also added the possibility of using serializer functions exported by our module. The list of default serializers is presented below:
- error

#### Example

```js
const { anonymizer, defaultSerializers } = require('@uphold/anonymizer');
const serializers = [
{ path: 'foo', serializer: defaultSerializers.error }
];

const anonymize = anonymizer({ whitelist }, { serializers });
```

## Releasing a new version

- Diff the current code with the latest tag and make sure the output is expected.
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"dependencies": {
"json-stringify-safe": "^5.0.1",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"serialize-error": "^5.0.0",
"traverse": "^0.6.6"
},
"devDependencies": {
Expand Down
75 changes: 73 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
* Module dependencies.
*/

const { serializeError } = require('serialize-error');
const get = require('lodash.get');
const set = require('lodash.set');
const stringify = require('json-stringify-safe');
const traverse = require('traverse');

Expand All @@ -14,27 +16,88 @@ const traverse = require('traverse');

const DEFAULT_REPLACEMENT = '--REDACTED--';

/**
* Validate serializers.
*/

function validateSerializers(serializers) {
serializers.map(({ path, serializer }) => {
if (typeof serializer !== 'function') {
throw new TypeError(`Invalid serializer for \`${path}\` path: must be a function`);
}
});
}

/**
* Compute Mutations
*/

function computeSerializedChanges(values, serializers) {
const changes = {};

for (const { path, serializer } of serializers) {
const value = get(values, path);

if (value === undefined) {
continue;
}

try {
changes[path] = serializer(value);
} catch (error) {
changes[path] = `Anonymize ERROR: Error while applying ${path} serializer`;
}
}

return changes;
}

/**
* Module exports.
*
* Example:
*
* anonymizer({
* whitelist: ['foo']
* }, {
* replacement,
* serializers: [
* { path: 'foo.bar', serializer: () => {} }
* ]
* })
*/

module.exports = (
module.exports.anonymizer = (
{ blacklist = [], whitelist = [] } = {},
{ replacement = () => DEFAULT_REPLACEMENT, trim = false } = {}
{ replacement = () => DEFAULT_REPLACEMENT, serializers = [], trim = false } = {}
) => {
const whitelistTerms = whitelist.join('|');
const whitelistPaths = new RegExp(`^(${whitelistTerms.replace(/\./g, '\\.').replace(/\*/g, '.*')})$`, 'i');
const blacklistTerms = blacklist.join('|');
const blacklistPaths = new RegExp(`^(${blacklistTerms.replace(/\./g, '\\.').replace(/\*/g, '.*')})$`, 'i');

validateSerializers(serializers);

return values => {
if (!(values instanceof Object)) {
return values;
}

const blacklistedKeys = new Set();
// JSON.parse(stringify(values)) builds an object copy that isn't an
// exact replication of the initial input. It destroys some relevant
// data that can't be lost. However, it can't be swapped for another
// solution due to its performance and because it can also handle
// classes correctly. Moreover, the `computeSerializedChanges()`
// also requires a copy to avoid updates by reference and in order
// to avoid two copies, we build an object with the result of applying
// the serializers to their respective paths. After we perform the copy,
// the serializers output is merged into the copy.
const changes = computeSerializedChanges(values, serializers);
const obj = JSON.parse(stringify(values));

Object.entries(changes).forEach(([path, change]) => set(obj, path, change));

traverse(obj).forEach(function() {
const path = this.path.join('.');
const isBuffer = Buffer.isBuffer(get(values, path));
Expand Down Expand Up @@ -78,3 +141,11 @@ module.exports = (
return obj;
};
};

/**
* Module exports defaultSerializers.
*/

module.exports.defaultSerializers = {
error: serializeError
};
65 changes: 65 additions & 0 deletions test/src/benchmark/samples.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict';

/**
* Function to generate a sample object.
*/

module.exports.generateObjectSample = ({ depth = 6, branches = 2, leafValue = () => 'foobar', leafs = 32 }) => {
const sample = {};

if (depth === 0) {
for (let leaf = 0; leaf < leafs; leaf++) {
sample[`leaf-${leaf}`] = leafValue();
}

return sample;
}

for (let branch = 0; branch < branches; branch++) {
sample[`branch-${branch}`] = this.generateObjectSample({ branches, depth: depth - 1, leafs });
}

return sample;
};

/**
* Function to generate all the paths contained in a sample generated with `generateObjectSample`.
*/

module.exports.generateObjectSamplesPaths = ({ depth = 6, branches = 2, leafs = 32, path = '' }) => {
let paths = [];

if (depth === 0) {
for (let leaf = 0; leaf < leafs; leaf++) {
paths.push(`${path}.leaf-${leaf}`);
}

return paths;
}

for (let branch = 0; branch < branches; branch++) {
const childPathString = path === '' ? `branch-${branch}` : `${path}.branch-${branch}`;
const childPaths = this.generateObjectSamplesPaths({ branches, depth: depth - 1, leafs, path: childPathString });

paths = paths.concat(childPaths);
}

return paths;
};

module.exports.samples = {
// Sample with 0 props.
SAMPLE_0x: {},
// Sample with 2048 props.
SAMPLE_1x: this.generateObjectSample({ depth: 6 }),
// Sample with 4096 props.
SAMPLE_2x: this.generateObjectSample({ depth: 7 }),
// Sample with 8192 props.
SAMPLE_4x: this.generateObjectSample({ depth: 8 }),
// Sample with 16384 props.
SAMPLE_8x: this.generateObjectSample({ depth: 9 }),
// Sample with 32768 props.
SAMPLE_16x: this.generateObjectSample({ depth: 10 }),
// Sample with 65536 props.
SAMPLE_32x: this.generateObjectSample({ depth: 11 })
};
Loading

0 comments on commit 33aacbf

Please sign in to comment.