Skip to content

Commit 0a7ca9f

Browse files
authored
Core & Multiple modules: activity controls (#9802)
* Core: allow restriction of cookies / localStorage through `bidderSettings.*.storageAllowed` * Add test cases * Remove gvlid param from storage manager logic * Refactor every invocation of `getStorageManager` * GVL ID registry * Refactor gdprEnforcement gvlid lookup * fix lint * Remove empty file * Undo #9728 for realVu * Fix typo * Activity control rules * Rule un-registration * fetchBids enforcement * fetchBids rule for gdpr * enableAnalytics check * reportAnalytics TCF2 rule * Update logging condition for multiple GVL IDs * Change core to prebid * Refactor userID to use non-core storage manager when storing for submodules * enrichEids check * gdpr enforcement for enrichEids * syncUser activity check * gdpr enforcement for syncUser * refactor gdprEnforcement * storageManager activity checks * gdpr enforcement for accessDevice * move alias resolution logic to adapterManager * Refactor file structure to get around circular deps * transmit(Eids/Ufpd/PreciseGeo) enforcement for bid adapters * Object transformers and guards * transmit* and enrich* enforcement for RTD modules * allowActivities configuration * improve comments * do not pass private activity params to pub-defined rules * fix objectGuard edge case: null values * move config logic into a module * dedupe log messages
1 parent 129f6f6 commit 0a7ca9f

29 files changed

+2343
-804
lines changed

libraries/objectGuard/objectGuard.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import {isData, objectTransformer} from '../../src/activities/redactor.js';
2+
import {deepAccess, deepClone, deepEqual, deepSetValue} from '../../src/utils.js';
3+
4+
/**
5+
* @typedef {Object} ObjectGuard
6+
* @property {*} obj a view on the guarded object
7+
* @property {function(): void} verify a function that checks for and rolls back disallowed changes to the guarded object
8+
*/
9+
10+
/**
11+
* Create a factory function for object guards using the given rules.
12+
*
13+
* An object guard is a pair {obj, verify} where:
14+
* - `obj` is a view on the guarded object that applies "redact" rules (the same rules used in activites/redactor.js)
15+
* - `verify` is a function that, when called, will check that the guarded object was not modified
16+
* in a way that violates any "write protect" rules, and rolls back any offending changes.
17+
*
18+
* This is meant to provide sandboxed version of a privacy-sensitive object, where reads
19+
* are filtered through redaction rules and writes are checked against write protect rules.
20+
*
21+
* @param {Array[TransformationRule]} rules
22+
* @return {function(*, ...[*]): ObjectGuard}
23+
*/
24+
export function objectGuard(rules) {
25+
const root = {};
26+
const writeRules = [];
27+
28+
rules.forEach(rule => {
29+
if (rule.wp) writeRules.push(rule);
30+
if (!rule.get) return;
31+
rule.paths.forEach(path => {
32+
let node = root;
33+
path.split('.').forEach(el => {
34+
node.children = node.children || {};
35+
node.children[el] = node.children[el] || {};
36+
node = node.children[el];
37+
})
38+
node.rule = rule;
39+
});
40+
});
41+
42+
const wpTransformer = objectTransformer(writeRules);
43+
44+
function mkApplies(session, args) {
45+
return function applies(rule) {
46+
if (!session.hasOwnProperty(rule.name)) {
47+
session[rule.name] = rule.applies(...args);
48+
}
49+
return session[rule.name];
50+
}
51+
}
52+
53+
function mkGuard(obj, tree, applies) {
54+
return new Proxy(obj, {
55+
get(target, prop, receiver) {
56+
const val = Reflect.get(target, prop, receiver);
57+
if (tree.hasOwnProperty(prop)) {
58+
const {children, rule} = tree[prop];
59+
if (children && val != null && typeof val === 'object') {
60+
return mkGuard(val, children, applies);
61+
} else if (rule && isData(val) && applies(rule)) {
62+
return rule.get(val);
63+
}
64+
}
65+
return val;
66+
},
67+
});
68+
}
69+
70+
function mkVerify(transformResult) {
71+
return function () {
72+
transformResult.forEach(fn => fn());
73+
}
74+
}
75+
76+
return function guard(obj, ...args) {
77+
const session = {};
78+
return {
79+
obj: mkGuard(obj, root.children || {}, mkApplies(session, args)),
80+
verify: mkVerify(wpTransformer(session, obj, ...args))
81+
}
82+
};
83+
}
84+
85+
/**
86+
* @param {TransformationRuleDef} ruleDef
87+
* @return {TransformationRule}
88+
*/
89+
export function writeProtectRule(ruleDef) {
90+
return Object.assign({
91+
wp: true,
92+
run(root, path, object, property, applies) {
93+
const origHasProp = object && object.hasOwnProperty(property);
94+
const original = origHasProp ? object[property] : undefined;
95+
const origCopy = origHasProp && original != null && typeof original === 'object' ? deepClone(original) : original;
96+
return function () {
97+
const object = path == null ? root : deepAccess(root, path);
98+
const finalHasProp = object && isData(object[property]);
99+
const finalValue = finalHasProp ? object[property] : undefined;
100+
if (!origHasProp && finalHasProp && applies()) {
101+
delete object[property];
102+
} else if ((origHasProp !== finalHasProp || finalValue !== original || !deepEqual(finalValue, origCopy)) && applies()) {
103+
deepSetValue(root, (path == null ? [] : [path]).concat(property).join('.'), origCopy);
104+
}
105+
}
106+
}
107+
}, ruleDef)
108+
}

libraries/objectGuard/ortbGuard.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {isActivityAllowed} from '../../src/activities/rules.js';
2+
import {ACTIVITY_ENRICH_EIDS, ACTIVITY_ENRICH_UFPD} from '../../src/activities/activities.js';
3+
import {
4+
appliesWhenActivityDenied,
5+
ortb2TransmitRules,
6+
ORTB_EIDS_PATHS,
7+
ORTB_UFPD_PATHS
8+
} from '../../src/activities/redactor.js';
9+
import {objectGuard, writeProtectRule} from './objectGuard.js';
10+
import {mergeDeep} from '../../src/utils.js';
11+
12+
function ortb2EnrichRules(isAllowed = isActivityAllowed) {
13+
return [
14+
{
15+
name: ACTIVITY_ENRICH_EIDS,
16+
paths: ORTB_EIDS_PATHS,
17+
applies: appliesWhenActivityDenied(ACTIVITY_ENRICH_EIDS, isAllowed)
18+
},
19+
{
20+
name: ACTIVITY_ENRICH_UFPD,
21+
paths: ORTB_UFPD_PATHS,
22+
applies: appliesWhenActivityDenied(ACTIVITY_ENRICH_UFPD, isAllowed)
23+
}
24+
].map(writeProtectRule)
25+
}
26+
27+
export function ortb2GuardFactory(isAllowed = isActivityAllowed) {
28+
return objectGuard(ortb2TransmitRules(isAllowed).concat(ortb2EnrichRules(isAllowed)));
29+
}
30+
31+
/**
32+
*
33+
*
34+
* @typedef {Function} ortb2Guard
35+
* @param {{}} ortb2 ORTB object to guard
36+
* @param {{}} params activity params to use for activity checks
37+
* @returns {ObjectGuard}
38+
*/
39+
40+
/*
41+
* Get a guard for an ORTB object. Read access is restricted in the same way it'd be redacted (see activites/redactor.js);
42+
* and writes are checked against the enrich* activites.
43+
*
44+
* @type ortb2Guard
45+
*/
46+
export const ortb2Guard = ortb2GuardFactory();
47+
48+
export function ortb2FragmentsGuardFactory(guardOrtb2 = ortb2Guard) {
49+
return function guardOrtb2Fragments(fragments, params) {
50+
fragments.global = fragments.global || {};
51+
fragments.bidder = fragments.bidder || {};
52+
const bidders = new Set(Object.keys(fragments.bidder));
53+
const verifiers = [];
54+
55+
function makeGuard(ortb2) {
56+
const guard = guardOrtb2(ortb2, params);
57+
verifiers.push(guard.verify);
58+
return guard.obj;
59+
}
60+
61+
const obj = {
62+
global: makeGuard(fragments.global),
63+
bidder: Object.fromEntries(Object.entries(fragments.bidder).map(([bidder, ortb2]) => [bidder, makeGuard(ortb2)]))
64+
};
65+
66+
return {
67+
obj,
68+
verify() {
69+
Object.entries(obj.bidder)
70+
.filter(([bidder]) => !bidders.has(bidder))
71+
.forEach(([bidder, ortb2]) => {
72+
const repl = {};
73+
const guard = guardOrtb2(repl, params);
74+
mergeDeep(guard.obj, ortb2);
75+
guard.verify();
76+
fragments.bidder[bidder] = repl;
77+
})
78+
verifiers.forEach(fn => fn());
79+
}
80+
}
81+
}
82+
}
83+
84+
/**
85+
* Get a guard for an ortb2Fragments object.
86+
* @type {function(*, *): ObjectGuard}
87+
*/
88+
export const guardOrtb2Fragments = ortb2FragmentsGuardFactory();

modules/allowActivities.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {config} from '../src/config.js';
2+
import {registerActivityControl} from '../src/activities/rules.js';
3+
4+
const CFG_NAME = 'allowActivities';
5+
const RULE_NAME = `${CFG_NAME} config`;
6+
const DEFAULT_PRIORITY = 1;
7+
8+
export function updateRulesFromConfig(registerRule) {
9+
const activeRuleHandles = new Map();
10+
const defaultRuleHandles = new Map();
11+
const rulesByActivity = new Map();
12+
13+
function clearAllRules() {
14+
rulesByActivity.clear();
15+
Array.from(activeRuleHandles.values())
16+
.flatMap(ruleset => Array.from(ruleset.values()))
17+
.forEach(fn => fn());
18+
activeRuleHandles.clear();
19+
Array.from(defaultRuleHandles.values()).forEach(fn => fn());
20+
defaultRuleHandles.clear();
21+
}
22+
23+
function cleanParams(params) {
24+
// remove private parameters for publisher condition checks
25+
return Object.fromEntries(Object.entries(params).filter(([k]) => !k.startsWith('_')))
26+
}
27+
28+
function setupRule(activity, priority) {
29+
if (!activeRuleHandles.has(activity)) {
30+
activeRuleHandles.set(activity, new Map())
31+
}
32+
const handles = activeRuleHandles.get(activity);
33+
if (!handles.has(priority)) {
34+
handles.set(priority, registerRule(activity, RULE_NAME, function (params) {
35+
for (const rule of rulesByActivity.get(activity).get(priority)) {
36+
if (!rule.condition || rule.condition(cleanParams(params))) {
37+
return {allow: rule.allow, reason: rule}
38+
}
39+
}
40+
}, priority));
41+
}
42+
}
43+
44+
function setupDefaultRule(activity) {
45+
if (!defaultRuleHandles.has(activity)) {
46+
defaultRuleHandles.set(activity, registerRule(activity, RULE_NAME, function () {
47+
return {allow: false, reason: 'activity denied by default'}
48+
}, Number.POSITIVE_INFINITY))
49+
}
50+
}
51+
52+
config.getConfig(CFG_NAME, (cfg) => {
53+
clearAllRules();
54+
Object.entries(cfg[CFG_NAME]).forEach(([activity, activityCfg]) => {
55+
if (activityCfg.default === false) {
56+
setupDefaultRule(activity);
57+
}
58+
const rules = new Map();
59+
rulesByActivity.set(activity, rules);
60+
61+
(activityCfg.rules || []).forEach(rule => {
62+
const priority = rule.priority == null ? DEFAULT_PRIORITY : rule.priority;
63+
if (!rules.has(priority)) {
64+
rules.set(priority, [])
65+
}
66+
rules.get(priority).push(rule);
67+
});
68+
69+
Array.from(rules.keys()).forEach(priority => setupRule(activity, priority));
70+
});
71+
})
72+
}
73+
74+
updateRulesFromConfig(registerActivityControl);

0 commit comments

Comments
 (0)