Skip to content

Commit

Permalink
#186531662 : Support matching on request headers for gov rule (#91)
Browse files Browse the repository at this point in the history
* support governance rule matching on request headers
  • Loading branch information
xinghengwang authored Feb 10, 2024
1 parent e1dc157 commit 0dccc2c
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 34 deletions.
10 changes: 10 additions & 0 deletions lib/dataUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,15 @@ function ensureToString(id) {
return id;
}

function getReqHeaders(req) {
if (req.headers) {
return req.headers;
} else if (req.getHeaders) {
return req.getHeaders() || {};
}
return {};
}

module.exports = {
getUrlFromRequestOptions: _getUrlFromRequestOptions,
getEventModelFromRequestAndResponse: _getEventModelFromRequestAndResponse,
Expand All @@ -405,4 +414,5 @@ module.exports = {
computeBodySize: computeBodySize,
totalChunkLength: totalChunkLength,
ensureToString: ensureToString,
getReqHeaders: getReqHeaders
};
57 changes: 34 additions & 23 deletions lib/governanceRulesManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var requestIp = require('request-ip');
var dataUtils = require('./dataUtils');

var safeJsonParse = dataUtils.safeJsonParse;
var getReqHeaders = dataUtils.getReqHeaders;

var moesifController = require('moesifapi').ApiController;

Expand Down Expand Up @@ -50,18 +51,20 @@ function prepareRequestBody(request) {
return null;
}

function getFieldValueForPath(path, requestFields, requestBody) {
function getFieldValueForPath(path, requestFields, requestBody, requestHeaders) {
if (path && path.indexOf('request.body.') === 0 && requestBody) {
const bodyKey = path.replace('request.body.', '');
return requestBody[bodyKey];
}
if (path && requestFields) {
} else if (path && path.indexOf('request.headers.') === 0 && requestHeaders) {
const headerKey = path.replace('request.headers.', '');
return requestHeaders[headerKey] || requestHeaders[headerKey.toLowerCase()];
} else if (path && requestFields) {
return requestFields[path];
}
return '';
}

function doesRegexConfigMatch(regexConfig, requestFields, requestBody) {
function doesRegexConfigMatch(regexConfig, requestFields, requestBody, requestHeaders) {
if (!regexConfig || regexConfig.length <= 0 || !Array.isArray(regexConfig)) {
// means customer do not care about regex match and only cohort match.
return true;
Expand All @@ -75,7 +78,7 @@ function doesRegexConfigMatch(regexConfig, requestFields, requestBody) {

const path = currentCondition.path;

const fieldValue = getFieldValueForPath(path, requestFields, requestBody);
const fieldValue = getFieldValueForPath(path, requestFields, requestBody, requestHeaders);

try {
const regex = new RegExp(currentCondition.value);
Expand Down Expand Up @@ -264,37 +267,39 @@ GovernanceRulesManager.prototype._cacheRules = function (rules) {
});
};

GovernanceRulesManager.prototype._getApplicableRegexRules = function (requestFields, requestBody) {
GovernanceRulesManager.prototype._getApplicableRegexRules = function (requestFields, requestBody, requestHeaders) {
if (this.regexRules) {
return this.regexRules.filter((rule) => {
const regexConfig = rule.regex_config;
return doesRegexConfigMatch(regexConfig, requestFields, requestBody);
return doesRegexConfigMatch(regexConfig, requestFields, requestBody, requestHeaders);
});
}
return [];
};

GovernanceRulesManager.prototype._getApplicableUnidentifiedUserRules = function (
requestFields,
requestBody
requestBody,
requestHeaders
) {
if (this.unidentifiedUserRules) {
return this.unidentifiedUserRules.filter((rule) => {
const regexConfig = rule.regex_config;
return doesRegexConfigMatch(regexConfig, requestFields, requestBody);
return doesRegexConfigMatch(regexConfig, requestFields, requestBody, requestHeaders);
});
}
return [];
};

GovernanceRulesManager.prototype._getApplicableUnidentifiedCompanyRules = function (
requestFields,
requestBody
requestBody,
requestHeaders
) {
if (this.unidentifiedCompanyRules) {
return this.unidentifiedCompanyRules.filter((rule) => {
const regexConfig = rule.regex_config;
return doesRegexConfigMatch(regexConfig, requestFields, requestBody);
return doesRegexConfigMatch(regexConfig, requestFields, requestBody, requestHeaders);
});
}
return [];
Expand All @@ -303,7 +308,8 @@ GovernanceRulesManager.prototype._getApplicableUnidentifiedCompanyRules = functi
GovernanceRulesManager.prototype._getApplicableUserRules = function (
configUserRulesValues,
requestFields,
requestBody
requestBody,
requestHeaders
) {
const self = this;

Expand All @@ -328,7 +334,7 @@ GovernanceRulesManager.prototype._getApplicableUserRules = function (
return;
}

const regexMatched = doesRegexConfigMatch(foundRule.regex_config, requestFields, requestBody);
const regexMatched = doesRegexConfigMatch(foundRule.regex_config, requestFields, requestBody, requestHeaders);

if (!regexMatched) {
// skipping because regex didn't not match.
Expand All @@ -347,7 +353,7 @@ GovernanceRulesManager.prototype._getApplicableUserRules = function (
// handle if rule is not matching and user is not in the cohort.
Object.values(userRulesHashByRuleId).forEach((rule) => {
if (rule.applied_to === 'not_matching' && !rulesThatUserIsInCohortHash[rule._id]) {
const regexMatched = doesRegexConfigMatch(rule.regex_config, requestFields, requestBody);
const regexMatched = doesRegexConfigMatch(rule.regex_config, requestFields, requestBody, requestHeaders);
if (regexMatched) {
applicableRules.push(rule);
}
Expand All @@ -360,7 +366,8 @@ GovernanceRulesManager.prototype._getApplicableUserRules = function (
GovernanceRulesManager.prototype._getApplicableCompanyRules = function (
configCompanyRulesValues,
requestFields,
requestBody
requestBody,
requestHeaders
) {
const applicableRules = [];
const rulesThatCompanyIsInCohortHash = {};
Expand All @@ -384,7 +391,7 @@ GovernanceRulesManager.prototype._getApplicableCompanyRules = function (
return;
}

const regexMatched = doesRegexConfigMatch(foundRule.regex_config, requestFields, requestBody);
const regexMatched = doesRegexConfigMatch(foundRule.regex_config, requestFields, requestBody, requestHeaders);

if (!regexMatched) {
// skipping because regex didn't not match.
Expand All @@ -403,7 +410,7 @@ GovernanceRulesManager.prototype._getApplicableCompanyRules = function (
// company is not in cohort, and if rule is not matching we apply the rule.
Object.values(rulesHashByRuleId).forEach((rule) => {
if (rule.applied_to === 'not_matching' && !rulesThatCompanyIsInCohortHash[rule._id]) {
const regexMatched = doesRegexConfigMatch(rule.regex_config, requestFields, requestBody);
const regexMatched = doesRegexConfigMatch(rule.regex_config, requestFields, requestBody, requestHeaders);
if (regexMatched) {
applicableRules.push(rule);
}
Expand Down Expand Up @@ -443,7 +450,8 @@ GovernanceRulesManager.prototype.applyRuleList = function (
GovernanceRulesManager.prototype.governRequest = function (config, userId, companyId, request) {
const requestBody = prepareRequestBody(request);
const requestFields = prepareFieldValues(request, requestBody);
this.log('preparing to govern', { requestBody, requestFields, userId, companyId });
const requestHeaders = getReqHeaders(request);
this.log('preparing to govern', { requestBody, requestFields, userId, companyId, requestHeaders });

// start with null for everything except for headers with empty hash that can accumulate values.
let responseHolder = {
Expand All @@ -456,34 +464,37 @@ GovernanceRulesManager.prototype.governRequest = function (config, userId, compa
try {
// apply in reverse order of priority will results in highest priority rules is final rule applied.
// highest to lowest priority are: user rules, company rules, and regex rules.
const applicableRegexRules = this._getApplicableRegexRules(requestFields, requestBody);
const applicableRegexRules = this._getApplicableRegexRules(requestFields, requestBody, requestHeaders);
responseHolder = this.applyRuleList(applicableRegexRules, responseHolder);

if (isNil(companyId)) {
const anonCompanyRules = this._getApplicableUnidentifiedCompanyRules(
requestFields,
requestBody
requestBody,
requestHeaders
);
responseHolder = this.applyRuleList(anonCompanyRules, responseHolder);
} else {
const configCompanyRulesValues = safeGet(safeGet(config, 'company_rules'), companyId);
const idCompanyRules = this._getApplicableCompanyRules(
configCompanyRulesValues,
requestFields,
requestBody
requestBody,
requestHeaders
);
responseHolder = this.applyRuleList(idCompanyRules, responseHolder, configCompanyRulesValues);
}

if (isNil(userId)) {
const anonUserRules = this._getApplicableUnidentifiedUserRules(requestFields, requestBody);
const anonUserRules = this._getApplicableUnidentifiedUserRules(requestFields, requestBody, requestHeaders);
responseHolder = this.applyRuleList(anonUserRules, responseHolder);
} else {
const configUserRulesValues = safeGet(safeGet(config, 'user_rules'), userId);
const idUserRules = this._getApplicableUserRules(
configUserRulesValues,
requestFields,
requestBody
requestBody,
requestHeaders,
);
responseHolder = this.applyRuleList(idUserRules, responseHolder, configUserRulesValues);
}
Expand Down
10 changes: 1 addition & 9 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var timeTookInSeconds = dataUtils.timeTookInSeconds;
var appendChunk = dataUtils.appendChunk;
var totalChunkLength = dataUtils.totalChunkLength;
var ensureToString = dataUtils.ensureToString;
var getReqHeaders = dataUtils.getReqHeaders;

var ensureValidOptions = ensureValidUtils.ensureValidOptions;
var ensureValidUserModel = ensureValidUtils.ensureValidUserModel;
Expand Down Expand Up @@ -58,15 +59,6 @@ var defaultIdentifyUser = function (req, res) {
return undefined;
};

var getReqHeaders = function (req) {
if (req.headers) {
return req.headers;
} else if (req.getHeaders) {
return req.getHeaders() || {};
}
return {};
}

/**
* @typedef {Object} MoesifOptions
* @property {string} applicationId
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "moesif-nodejs",
"version": "3.5.8",
"version": "3.6.1",
"description": "Monitoring agent to log API calls to Moesif for deep API analytics",
"main": "lib/index.js",
"typings": "dist/index.d.ts",
Expand Down

0 comments on commit 0dccc2c

Please sign in to comment.