-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
193 additions
and
172 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import lodash from 'lodash'; | ||
|
||
enum Tag { | ||
PII = 'PII', | ||
SECRET = 'SECRET' | ||
} | ||
|
||
type PlainObject = Record<string, unknown>; | ||
|
||
class Masker { | ||
public maskString(value: string, tag: Tag): string { | ||
return `[${tag}]${value}[/${tag}]`; | ||
} | ||
|
||
public maskNumber(value: number, tag: Tag): string { | ||
return `[${tag}]${value}[/${tag}]`; | ||
} | ||
|
||
public maskObject<T extends PlainObject | Error>(object: T, maskSettings: Array<[string, Tag]>): T { | ||
if (this.isError(object)) { | ||
return this.maskError(object, maskSettings); | ||
} else if (this.isPlainObject(object)) { | ||
return this.maskPlainObject(object, maskSettings); | ||
} | ||
|
||
throw new Error('Input must be plain object or a class inheriting from Error'); | ||
} | ||
|
||
private maskPlainObject<T extends PlainObject>(plainObject: T, maskSettings: Array<[string, Tag]>): T { | ||
const clonedObj = this.clone(plainObject); | ||
const pathToTagMap = this.constructPathToTagMap(maskSettings); | ||
|
||
const allPaths = this.collectPaths(plainObject); | ||
for (const path of allPaths) { | ||
const tag = pathToTagMap.get(path); | ||
if (!tag) { | ||
continue; | ||
} | ||
|
||
const rawValue = lodash.get(clonedObj, path); | ||
if (typeof rawValue === 'string') { | ||
lodash.set(clonedObj, path, this.maskString(rawValue, tag)); | ||
} | ||
|
||
if (typeof rawValue === 'number') { | ||
lodash.set(clonedObj, path, this.maskNumber(rawValue, tag)); | ||
} | ||
} | ||
|
||
return clonedObj; | ||
} | ||
|
||
private maskError<T extends Error>(err: T, maskSettings: Array<[string, Tag]>): T { | ||
const clonedErr = new Error(err.message); | ||
Object.setPrototypeOf(clonedErr, Object.getPrototypeOf(err)); | ||
|
||
const dataToAssign = {}; | ||
for (const prop of Object.getOwnPropertyNames(err)) { | ||
dataToAssign[prop] = this.clone(err[prop]); | ||
} | ||
|
||
Object.assign(clonedErr, this.maskPlainObject(dataToAssign, maskSettings)); | ||
|
||
return clonedErr as T; | ||
} | ||
|
||
private collectPaths(input: any, currentPath?: string) { | ||
const paths: Array<string> = []; | ||
|
||
if (lodash.isPlainObject(input)) { | ||
for (const key in input) { | ||
const fullPath = this.buildPath(key, currentPath); | ||
const value = input[key]; | ||
|
||
paths.push(fullPath, ...this.collectPaths(value).map((nestedPath) => this.buildPath(nestedPath, fullPath))); | ||
} | ||
} | ||
|
||
return paths; | ||
} | ||
|
||
private buildPath(propPath: string, basePath?: string) { | ||
return basePath === undefined ? String(propPath) : `${basePath}.${propPath}`; | ||
} | ||
|
||
private isPlainObject(value: unknown): value is PlainObject { | ||
return lodash.isPlainObject(value); | ||
} | ||
|
||
private isError(value: unknown): value is Error { | ||
return value instanceof Error; | ||
} | ||
|
||
private constructPathToTagMap(maskSettings: Array<[string, Tag]>) { | ||
const pathToTagMap = new Map(); | ||
for (const [path, tag] of maskSettings) { | ||
pathToTagMap.set(path, tag); | ||
} | ||
|
||
return pathToTagMap; | ||
} | ||
|
||
private clone<T extends any>(value: T): T { | ||
return lodash.cloneDeep(value); | ||
} | ||
} | ||
|
||
export { | ||
Masker, | ||
Tag, | ||
}; |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { Masker, Tag } from '../../Masker'; | ||
|
||
describe('Masker', () => { | ||
const masker = new Masker(); | ||
|
||
describe('maskString', () => { | ||
it('should wrap the provided tag around the string that needs to be maskd', () => { | ||
expect(masker.maskString('string', Tag.PII)).toEqual('[PII]string[/PII]'); | ||
}); | ||
}); | ||
|
||
describe('maskNumber', () => { | ||
it('should wrap the provided tag around the number that needs to be maskd', () => { | ||
expect(masker.maskNumber(42, Tag.PII)).toEqual('[PII]42[/PII]'); | ||
}); | ||
}); | ||
|
||
describe('maskObject', () => { | ||
it('should wrap the provided tag around root-level elements in object', () => { | ||
const originalObject = { name: 'Pencho' }; | ||
const maskedObject = { name: '[PII]Pencho[/PII]' }; | ||
expect(masker.maskObject(originalObject, [['name', Tag.PII]])).toEqual(maskedObject); | ||
}); | ||
|
||
it('should wrap the provided tag around nested elements in object', () => { | ||
const originalObject = { id: 1, data: { name: 'Gosho', email: 'email@example.com' } }; | ||
const maskedObject = { id: 1, data: { name: '[PII]Gosho[/PII]', email: '[PII]email@example.com[/PII]' } }; | ||
expect(masker.maskObject(originalObject, [['data.name', Tag.PII], ['data.email', Tag.PII]])).toEqual(maskedObject); | ||
}); | ||
|
||
it('should work with numbers', () => { | ||
const originalObject = { id: 1 }; | ||
|
||
expect(masker.maskObject(originalObject, [['id', Tag.PII]])).toEqual({ | ||
id: '[PII]1[/PII]', | ||
}); | ||
}); | ||
|
||
it('should NOT wrap the provided tag around elements that are not specified for obfuscating in object', () => { | ||
const originalObject = { favouriteColor: 'red', nested: { field: 'value' } }; | ||
expect(masker.maskObject(originalObject, [['name', Tag.PII]])).toEqual(originalObject); | ||
}); | ||
|
||
it('should return a copy of the error and not modify the original', () => { | ||
const originalError = new Error(); | ||
const maskedError = masker.maskObject(originalError, [['bar', Tag.PII]]); | ||
expect(maskedError).not.toBe(originalError); | ||
}); | ||
|
||
it('should preserve the prototype, name, message and stack of the error', () => { | ||
class CustomError extends Error {} | ||
const originalError = new CustomError(); | ||
const maskedError = masker.maskObject(originalError, [['bar', Tag.PII]]); | ||
|
||
expect(maskedError).toBeInstanceOf(CustomError); | ||
expect(maskedError.name).toEqual(originalError.name); | ||
expect(maskedError.message).toEqual(originalError.message); | ||
expect(maskedError.stack).toEqual(originalError.stack); | ||
}); | ||
|
||
it('should mask error specific props', () => { | ||
class CustomError extends Error { | ||
bar = 'foo' | ||
foo = { | ||
test: 'test', | ||
} | ||
} | ||
const originalError = new CustomError(); | ||
const maskedError = masker.maskObject(originalError, [ | ||
['bar', Tag.PII], | ||
['foo.test', Tag.PII], | ||
]); | ||
|
||
expect(maskedError.bar).toEqual('[PII]foo[/PII]'); | ||
expect(maskedError.foo.test).toEqual('[PII]test[/PII]'); | ||
}); | ||
}); | ||
}); |
This file was deleted.
Oops, something went wrong.