Skip to content

Commit

Permalink
Merge pull request #7 from Icenium/CHAT-1208-mask-authorization
Browse files Browse the repository at this point in the history
[CHAT-1208] Mask authorization
  • Loading branch information
mbektchiev authored Aug 21, 2020
2 parents b79a4ca + 9c0fa02 commit 5c6e5d1
Show file tree
Hide file tree
Showing 5 changed files with 373 additions and 359 deletions.
98 changes: 49 additions & 49 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,54 @@
declare module PersonalDataFilter {
interface IPersonalDataFilter {
filter(data: any): any;
}


interface IPersonalDataFilterConfig {
/**
* Regular expressions which will be added to the default ones.
*/
additionalRegularExpressions?: string[];

/**
* Regular expression which will be used instead of the default one.
*/
regularExpression?: RegExp;

/**
* String which will be used to replace the personal data.
*/
personalDataMask?: string;

/**
* Personal data properties which will be added to the default ones.
*/
additionalPersonalDataProperties?: string[];

/**
* Personal data properties which will be used instead of the default ones.
*/
personalDataProperties?: string[];

/**
* Defines if the filter should use the default match replacer to replace each regular
* expression match.
*/
useDefaultMatchReplacer?: boolean;

/**
* Sets the match replacer which will be used to replace each regular
* expression match.
*/
matchReplacer: MatchReplacer;
}

/**
* Function which will be used to replace each regular expression match.
*/
type MatchReplacer = (match: string) => string
interface IPersonalDataFilter {
filter(data: any): any;
}


interface IPersonalDataFilterConfig {
/**
* Regular expressions which will be added to the default ones.
*/
additionalRegularExpressions?: string[];

/**
* Regular expression which will be used instead of the default one.
*/
regularExpression?: RegExp;

/**
* String which will be used to replace the personal data.
*/
personalDataMask?: string;

/**
* Personal data properties which will be added to the default ones.
*/
additionalPersonalDataProperties?: string[];

/**
* Personal data properties which will be used instead of the default ones.
*/
personalDataProperties?: string[];

/**
* Defines if the filter should use the default match replacer to replace each regular
* expression match.
*/
useDefaultMatchReplacer?: boolean;

/**
* Sets the match replacer which will be used to replace each regular
* expression match.
*/
matchReplacer: MatchReplacer;
}

/**
* Function which will be used to replace each regular expression match.
*/
type MatchReplacer = (match: string) => string
}

declare module "node-personal-data-filter" {
export const newFilter: (cfg?: PersonalDataFilter.IPersonalDataFilterConfig) => PersonalDataFilter.IPersonalDataFilter;
export const newFilter: (cfg?: PersonalDataFilter.IPersonalDataFilterConfig) => PersonalDataFilter.IPersonalDataFilter;
}
266 changes: 140 additions & 126 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,21 @@
const _ = require("lodash");
const crypto = require("crypto");

const personalDataProperties = ["email", "useremail", "user", "username", "userid", "accountid", "account", "password", "pass", "pwd", "ip", "ipaddress"];
const personalDataProperties = [
"account",
"accountid",
"authorization",
"email",
"ip",
"ipaddress",
"pass",
"password",
"pwd",
"user",
"useremail",
"userid",
"username",
];

// Source: https://stackoverflow.com/a/46181/4922411
const emailRegExpTemplate = `(([^<>()\\[\\]\\\\.,;:\\s@"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))`;
Expand All @@ -17,131 +31,131 @@ const ipV6RegExpTemplate = "((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9
const defaultRegularExpressions = [emailRegExpTemplate, guidRegExpTemplate, ipV4RegExpTemplate, ipV6RegExpTemplate];

class PersonalDataFilter {
constructor(config) {
config = config || {};
this._setRegularExpressions(config);
this._setMask(config);
this._setPersonalDataProperties(config);
this._setMatchReplacer(config);
}

filter(data) {
const result = this._filterRecursively(data, []);

return result;
}

_filterRecursively(data, referencesCache) {
if (_.isString(data)) {
return this._filterString(data);
} else if (_.isArray(data)) {
return _.map(data, v => this._filterRecursively(v, referencesCache));
} else if (_.isObject(data)) { // isObject check should always be after the isArray check because isObject returns true for [].
// if the current reference has already been traversed this means we've reaced a circular reference
// simply mask it in order to avoid maximum callstack due to endless traversal
if (_.find(referencesCache, (obj) => obj === data)) {
return this._mask;
} else {
referencesCache.push(data)
return this._maskPersonalDataProperties(data, referencesCache);
}
}

return data;
}

_maskPersonalDataProperties(data, referencesCache) {
// It's important to use getOwnPropertyNames here
// because if someone were to pass something with non-enumerable properties (e.g. an instance of the Error class)
// then a simple _.reduce would simply skip over the non-enumerable properties ultimately removing them from the result
const propertyNames = Object.getOwnPropertyNames(data);
return _.reduce(propertyNames, (result, key) => {
let value;

try {
value = data[key];
} catch (err) {
// We can't get the value and we should not include it in the result because we can't filter it.
return result;
}

if (this._personalDataProperties.indexOf(key.toString().toLowerCase()) >= 0) {
result[key] = this._mask;
} else {
result[key] = this._filterRecursively(value, referencesCache);
}

return result;
}, {});
}

_filterString(input) {
if (this._matchReplacer) {
return input.replace(this._filterRegExp, this._matchReplacer);
}

return input.replace(this._filterRegExp, this._mask)
}

_setRegularExpressions(config) {
const additionalRegularExpressionsSize = _.size(config.additionalRegularExpressions);
if (additionalRegularExpressionsSize > 0 && config.regularExpression) {
throw new Error("You can't use additionalRegularExpressions and regularExpression at the same time.");
}

if (config.regularExpression) {
this._filterRegExp = config.regularExpression;
return;
}

let regExps = _.concat([], defaultRegularExpressions);
if (additionalRegularExpressionsSize > 0) {
regExps = _.concat(regExps, config.additionalRegularExpressions);
}

const regExpString = regExps.map(r => `(${r})`).join("|");
this._filterRegExp = new RegExp(regExpString, "gi");
}

_setMask(config) {
this._mask = config.personalDataMask || "";
}

_setPersonalDataProperties(config) {
const additionalPropertiesSize = _.size(config.additionalPersonalDataProperties);
const propertiesSize = _.size(config.personalDataProperties);
if (additionalPropertiesSize > 0 && propertiesSize > 0) {
throw new Error("You can't use additionalPersonalDataProperties and personalDataProperties at the same time.");
}

if (propertiesSize > 0) {
this._personalDataProperties = config.personalDataProperties;
return;
}

let props = _.concat([], personalDataProperties);
if (additionalPropertiesSize > 0) {
props = _.concat(props, config.additionalPersonalDataProperties);
}

this._personalDataProperties = props;
}

_setMatchReplacer(config) {
if (config.useDefaultMatchReplacer) {
if (config.matchReplacer) {
throw new Error("You can't use the default match replacer and a cutom one.");
}

this._matchReplacer = (match) => {
return crypto.createHash("sha256").update(match).digest("hex");
};
}

if (config.matchReplacer) {
this._matchReplacer = config.matchReplacer;
}
}
constructor(config) {
config = config || {};
this._setRegularExpressions(config);
this._setMask(config);
this._setPersonalDataProperties(config);
this._setMatchReplacer(config);
}

filter(data) {
const result = this._filterRecursively(data, []);

return result;
}

_filterRecursively(data, referencesCache) {
if (_.isString(data)) {
return this._filterString(data);
} else if (_.isArray(data)) {
return _.map(data, v => this._filterRecursively(v, referencesCache));
} else if (_.isObject(data)) { // isObject check should always be after the isArray check because isObject returns true for [].
// if the current reference has already been traversed this means we've reaced a circular reference
// simply mask it in order to avoid maximum callstack due to endless traversal
if (_.find(referencesCache, (obj) => obj === data)) {
return this._mask;
} else {
referencesCache.push(data)
return this._maskPersonalDataProperties(data, referencesCache);
}
}

return data;
}

_maskPersonalDataProperties(data, referencesCache) {
// It's important to use getOwnPropertyNames here
// because if someone were to pass something with non-enumerable properties (e.g. an instance of the Error class)
// then a simple _.reduce would simply skip over the non-enumerable properties ultimately removing them from the result
const propertyNames = Object.getOwnPropertyNames(data);
return _.reduce(propertyNames, (result, key) => {
let value;

try {
value = data[key];
} catch (err) {
// We can't get the value and we should not include it in the result because we can't filter it.
return result;
}

if (this._personalDataProperties.indexOf(key.toString().toLowerCase()) >= 0) {
result[key] = this._mask;
} else {
result[key] = this._filterRecursively(value, referencesCache);
}

return result;
}, {});
}

_filterString(input) {
if (this._matchReplacer) {
return input.replace(this._filterRegExp, this._matchReplacer);
}

return input.replace(this._filterRegExp, this._mask)
}

_setRegularExpressions(config) {
const additionalRegularExpressionsSize = _.size(config.additionalRegularExpressions);
if (additionalRegularExpressionsSize > 0 && config.regularExpression) {
throw new Error("You can't use additionalRegularExpressions and regularExpression at the same time.");
}

if (config.regularExpression) {
this._filterRegExp = config.regularExpression;
return;
}

let regExps = _.concat([], defaultRegularExpressions);
if (additionalRegularExpressionsSize > 0) {
regExps = _.concat(regExps, config.additionalRegularExpressions);
}

const regExpString = regExps.map(r => `(${r})`).join("|");
this._filterRegExp = new RegExp(regExpString, "gi");
}

_setMask(config) {
this._mask = config.personalDataMask || "";
}

_setPersonalDataProperties(config) {
const additionalPropertiesSize = _.size(config.additionalPersonalDataProperties);
const propertiesSize = _.size(config.personalDataProperties);
if (additionalPropertiesSize > 0 && propertiesSize > 0) {
throw new Error("You can't use additionalPersonalDataProperties and personalDataProperties at the same time.");
}

if (propertiesSize > 0) {
this._personalDataProperties = config.personalDataProperties;
return;
}

let props = _.concat([], personalDataProperties);
if (additionalPropertiesSize > 0) {
props = _.concat(props, config.additionalPersonalDataProperties);
}

this._personalDataProperties = props;
}

_setMatchReplacer(config) {
if (config.useDefaultMatchReplacer) {
if (config.matchReplacer) {
throw new Error("You can't use the default match replacer and a cutom one.");
}

this._matchReplacer = (match) => {
return crypto.createHash("sha256").update(match).digest("hex");
};
}

if (config.matchReplacer) {
this._matchReplacer = config.matchReplacer;
}
}
}

module.exports.newFilter = (cfg) => new PersonalDataFilter(cfg);
Loading

0 comments on commit 5c6e5d1

Please sign in to comment.