Skip to content

Commit f3c64d0

Browse files
governance apis for getApplicableRulesForUserId (#110)
* add getApplicationUserRules * refactored code to be more concise. * expose the methods on top level. * added unit test for governance check * bump version * bump version. * Update lib/governanceRulesManager.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 9c01ae0 commit f3c64d0

File tree

6 files changed

+360
-133
lines changed

6 files changed

+360
-133
lines changed

lib/governanceRulesManager.js

Lines changed: 107 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,40 @@ function modifyResponseForOneRule(rule, responseHolder, mergeTagValues) {
200200
return responseHolder;
201201
}
202202

203+
// Internal helper to compute applicable rules based only on cohort membership and applied_to.
204+
// Logic (ignoring regex):
205+
// - If entity (user/company) is in rule cohort and rule.applied_to !== 'not_matching' -> include.
206+
// - If entity NOT in rule cohort and rule.applied_to === 'not_matching' -> include.
207+
function getApplicableRulesByCohortOnly(configRuleValues, rulesHashByRuleId, logger) {
208+
const rulesEntityInCohortHash = {};
209+
const applicable = [];
210+
211+
if (Array.isArray(configRuleValues) && configRuleValues.length > 0) {
212+
configRuleValues.forEach(function (entry) {
213+
const ruleId = entry.rules;
214+
rulesEntityInCohortHash[ruleId] = true;
215+
const rule = rulesHashByRuleId[ruleId];
216+
if (!rule) {
217+
if (logger) logger('rule not found for rule id from config: ' + ruleId + '. It could be deleted.');
218+
return;
219+
}
220+
if (rule.applied_to === 'not_matching') {
221+
// entity is in cohort; rule applies only to those NOT in cohort -> skip
222+
return;
223+
}
224+
applicable.push(rule);
225+
});
226+
}
227+
228+
Object.values(rulesHashByRuleId).forEach(function (rule) {
229+
if (rule.applied_to === 'not_matching' && !rulesEntityInCohortHash[rule._id]) {
230+
applicable.push(rule);
231+
}
232+
});
233+
234+
return applicable;
235+
}
236+
203237
/**
204238
*
205239
* @type Class
@@ -352,66 +386,16 @@ GovernanceRulesManager.prototype._getApplicableUserRules = function (
352386
requestBody,
353387
requestHeaders
354388
) {
355-
const self = this;
356-
357-
const applicableRules = [];
358-
const rulesThatUserIsInCohortHash = {};
359-
360-
const userRulesHashByRuleId = this.userRulesHashByRuleId;
361-
362-
// handle if user is in cohort.
363-
// if user is in a rule's cohort, the data is from config_rule_rules_values
364-
if (Array.isArray(configUserRulesValues) && configUserRulesValues.length > 0) {
365-
configUserRulesValues.forEach(function (entry) {
366-
const ruleId = entry.rules;
367-
368-
// cache the fact current user is in the cohort of this rule.
369-
rulesThatUserIsInCohortHash[ruleId] = true;
370-
371-
const foundRule = userRulesHashByRuleId[ruleId];
372-
if (!foundRule) {
373-
// skip not found, but shouldn't be the case here.
374-
self.log('rule not found for rule id from config' + ruleId);
375-
return;
376-
}
377-
378-
const regexMatched = doesRegexConfigMatch(
379-
foundRule.regex_config,
380-
requestFields,
381-
requestBody,
382-
requestHeaders
383-
);
384-
385-
if (!regexMatched) {
386-
// skipping because regex didn't not match.
387-
return;
388-
}
389-
390-
if (foundRule.applied_to === 'not_matching') {
391-
// skipping because rule is apply to those not in cohort.
392-
return;
393-
} else {
394-
applicableRules.push(foundRule);
395-
}
396-
});
397-
}
398-
399-
// handle if rule is not matching and user is not in the cohort.
400-
Object.values(userRulesHashByRuleId).forEach((rule) => {
401-
if (rule.applied_to === 'not_matching' && !rulesThatUserIsInCohortHash[rule._id]) {
402-
const regexMatched = doesRegexConfigMatch(
403-
rule.regex_config,
404-
requestFields,
405-
requestBody,
406-
requestHeaders
407-
);
408-
if (regexMatched) {
409-
applicableRules.push(rule);
410-
}
411-
}
412-
});
413-
414-
return applicableRules;
389+
// Use cohort-only helper then apply regex filtering identical to original behavior.
390+
const userRulesHashByRuleId = this.userRulesHashByRuleId || {};
391+
const cohortCandidates = getApplicableRulesByCohortOnly(
392+
configUserRulesValues,
393+
userRulesHashByRuleId,
394+
(msg) => this.log(msg)
395+
);
396+
return cohortCandidates.filter((rule) =>
397+
doesRegexConfigMatch(rule.regex_config, requestFields, requestBody, requestHeaders)
398+
);
415399
};
416400

417401
GovernanceRulesManager.prototype._getApplicableCompanyRules = function (
@@ -420,65 +404,15 @@ GovernanceRulesManager.prototype._getApplicableCompanyRules = function (
420404
requestBody,
421405
requestHeaders
422406
) {
423-
const applicableRules = [];
424-
const rulesThatCompanyIsInCohortHash = {};
425-
const self = this;
426-
427-
const rulesHashByRuleId = this.companyRulesHashByRuleId;
428-
429-
// handle if company is in cohort.
430-
// if company is in a rule's cohort, the data is from config_rules_values
431-
if (Array.isArray(configCompanyRulesValues) && configCompanyRulesValues.length > 0) {
432-
configCompanyRulesValues.forEach(function (entry) {
433-
const ruleId = entry.rules;
434-
435-
// cache the fact current company is in the cohort of this rule.
436-
rulesThatCompanyIsInCohortHash[ruleId] = true;
437-
438-
const foundRule = rulesHashByRuleId[ruleId];
439-
if (!foundRule) {
440-
// skip not found, but shouldn't be the case here.
441-
self.log('rule not found for rule id from config' + ruleId);
442-
return;
443-
}
444-
445-
const regexMatched = doesRegexConfigMatch(
446-
foundRule.regex_config,
447-
requestFields,
448-
requestBody,
449-
requestHeaders
450-
);
451-
452-
if (!regexMatched) {
453-
// skipping because regex didn't not match.
454-
return;
455-
}
456-
457-
if (foundRule.applied_to === 'not_matching') {
458-
// skipping because rule is apply to those not in cohort.
459-
return;
460-
} else {
461-
applicableRules.push(foundRule);
462-
}
463-
});
464-
}
465-
466-
// company is not in cohort, and if rule is not matching we apply the rule.
467-
Object.values(rulesHashByRuleId).forEach((rule) => {
468-
if (rule.applied_to === 'not_matching' && !rulesThatCompanyIsInCohortHash[rule._id]) {
469-
const regexMatched = doesRegexConfigMatch(
470-
rule.regex_config,
471-
requestFields,
472-
requestBody,
473-
requestHeaders
474-
);
475-
if (regexMatched) {
476-
applicableRules.push(rule);
477-
}
478-
}
479-
});
480-
481-
return applicableRules;
407+
const companyRulesHashByRuleId = this.companyRulesHashByRuleId || {};
408+
const cohortCandidates = getApplicableRulesByCohortOnly(
409+
configCompanyRulesValues,
410+
companyRulesHashByRuleId,
411+
(msg) => this.log(msg)
412+
);
413+
return cohortCandidates.filter((rule) =>
414+
doesRegexConfigMatch(rule.regex_config, requestFields, requestBody, requestHeaders)
415+
);
482416
};
483417

484418
GovernanceRulesManager.prototype.applyRuleList = function (
@@ -552,11 +486,10 @@ GovernanceRulesManager.prototype.governInternal = function (
552486
responseHolder = this.applyRuleList(anonCompanyRules, responseHolder);
553487
} else {
554488
const configCompanyRulesValues = safeGet(safeGet(config, 'company_rules'), companyId);
555-
const idCompanyRules = this._getApplicableCompanyRules(
556-
configCompanyRulesValues,
557-
requestFields,
558-
requestBody,
559-
requestHeaders
489+
// Get cohort-based list using new public helper then filter by regex for current request context.
490+
const companyCohortCandidates = this.getApplicableRulesForCompanyId(companyId, config);
491+
const idCompanyRules = companyCohortCandidates.filter((rule) =>
492+
doesRegexConfigMatch(rule.regex_config, requestFields, requestBody, requestHeaders)
560493
);
561494
responseHolder = this.applyRuleList(idCompanyRules, responseHolder, configCompanyRulesValues);
562495
}
@@ -570,11 +503,10 @@ GovernanceRulesManager.prototype.governInternal = function (
570503
responseHolder = this.applyRuleList(anonUserRules, responseHolder);
571504
} else {
572505
const configUserRulesValues = safeGet(safeGet(config, 'user_rules'), userId);
573-
const idUserRules = this._getApplicableUserRules(
574-
configUserRulesValues,
575-
requestFields,
576-
requestBody,
577-
requestHeaders
506+
// Cohort-only then regex filter for current request context.
507+
const userCohortCandidates = this.getApplicableRulesForUserId(userId, config);
508+
const idUserRules = userCohortCandidates.filter((rule) =>
509+
doesRegexConfigMatch(rule.regex_config, requestFields, requestBody, requestHeaders)
578510
);
579511
responseHolder = this.applyRuleList(idUserRules, responseHolder, configUserRulesValues);
580512
}
@@ -641,4 +573,50 @@ GovernanceRulesManager.prototype.governRequest = function (config, userId, compa
641573
);
642574
};
643575

576+
/**
577+
* Return all user rules applicable for the given userId based solely on cohort membership
578+
* (config user_rules values) and the rule's applied_to property. Ignores request fields,
579+
* body, headers, and regex matching. Logic:
580+
* - If user is in a rule cohort and rule.applied_to !== 'not_matching' -> include.
581+
* - If user is NOT in a rule cohort and rule.applied_to === 'not_matching' -> include.
582+
* @param {String} userId
583+
* @param {Object} config Full config object containing user_rules hash
584+
* @returns {Array<Object>} array of rule objects applicable to the userId
585+
*/
586+
GovernanceRulesManager.prototype.getApplicableRulesForUserId = function (userId, config) {
587+
if (isNil(userId)) {
588+
return [];
589+
}
590+
const userRulesHashByRuleId = this.userRulesHashByRuleId || {};
591+
const configUserRulesValues = safeGet(safeGet(config, 'user_rules'), userId);
592+
return getApplicableRulesByCohortOnly(
593+
configUserRulesValues,
594+
userRulesHashByRuleId,
595+
(msg) => this.log(msg)
596+
);
597+
};
598+
599+
/**
600+
* Return all company rules applicable for the given companyId based solely on cohort membership
601+
* (config company_rules values) and the rule's applied_to property. Ignores request fields,
602+
* body, headers, and regex matching. Logic:
603+
* - If company is in a rule cohort and rule.applied_to !== 'not_matching' -> include.
604+
* - If company is NOT in a rule cohort and rule.applied_to === 'not_matching' -> include.
605+
* @param {String} companyId
606+
* @param {Object} config Full config object containing company_rules hash
607+
* @returns {Array<Object>} array of rule objects applicable to the companyId
608+
*/
609+
GovernanceRulesManager.prototype.getApplicableRulesForCompanyId = function (companyId, config) {
610+
if (isNil(companyId)) {
611+
return [];
612+
}
613+
const companyRulesHashByRuleId = this.companyRulesHashByRuleId || {};
614+
const configCompanyRulesValues = safeGet(safeGet(config, 'company_rules'), companyId);
615+
return getApplicableRulesByCohortOnly(
616+
configCompanyRulesValues,
617+
companyRulesHashByRuleId,
618+
(msg) => this.log(msg)
619+
);
620+
};
621+
644622
module.exports = new GovernanceRulesManager();

lib/index.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ function makeMoesifMiddleware(options) {
111111
* @type {string}
112112
*/
113113
config.ApplicationId = options.applicationId || options.ApplicationId;
114-
config.UserAgent = 'moesif-nodejs/' + '3.10.0';
114+
config.UserAgent = 'moesif-nodejs/' + '3.11.0';
115115
config.BaseUri = options.baseUri || options.BaseUri || config.BaseUri;
116116
// default retry to 1.
117117
config.retry = isNil(options.retry) ? 1 : options.retry;
@@ -878,6 +878,32 @@ function makeMoesifMiddleware(options) {
878878
}
879879
};
880880

881+
// Expose governance rule helpers using internally cached config
882+
moesifMiddleware.getApplicableRulesForUserId = function (userId) {
883+
// refresh config/rules opportunistically
884+
moesifConfigManager.tryGetConfig();
885+
governanceRulesManager.tryGetRules();
886+
if (!governanceRulesManager.hasRules()) {
887+
return [];
888+
}
889+
return governanceRulesManager.getApplicableRulesForUserId(
890+
ensureToString(userId),
891+
moesifConfigManager._config
892+
);
893+
};
894+
895+
moesifMiddleware.getApplicableRulesForCompanyId = function (companyId) {
896+
moesifConfigManager.tryGetConfig();
897+
governanceRulesManager.tryGetRules();
898+
if (!governanceRulesManager.hasRules()) {
899+
return [];
900+
}
901+
return governanceRulesManager.getApplicableRulesForCompanyId(
902+
ensureToString(companyId),
903+
moesifConfigManager._config
904+
);
905+
};
906+
881907
logMessage(options.debug, 'moesifInitiator', 'returning moesifMiddleware Function');
882908
return moesifMiddleware;
883909
}

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "moesif-nodejs",
3-
"version": "3.10.0",
3+
"version": "3.11.0",
44
"description": "Monitoring agent to log API calls to Moesif for deep API analytics",
55
"main": "lib/index.js",
66
"typings": "dist/index.d.ts",

0 commit comments

Comments
 (0)