-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
29 changed files
with
2,343 additions
and
804 deletions.
There are no files selected for viewing
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,108 @@ | ||
import {isData, objectTransformer} from '../../src/activities/redactor.js'; | ||
import {deepAccess, deepClone, deepEqual, deepSetValue} from '../../src/utils.js'; | ||
|
||
/** | ||
* @typedef {Object} ObjectGuard | ||
* @property {*} obj a view on the guarded object | ||
* @property {function(): void} verify a function that checks for and rolls back disallowed changes to the guarded object | ||
*/ | ||
|
||
/** | ||
* Create a factory function for object guards using the given rules. | ||
* | ||
* An object guard is a pair {obj, verify} where: | ||
* - `obj` is a view on the guarded object that applies "redact" rules (the same rules used in activites/redactor.js) | ||
* - `verify` is a function that, when called, will check that the guarded object was not modified | ||
* in a way that violates any "write protect" rules, and rolls back any offending changes. | ||
* | ||
* This is meant to provide sandboxed version of a privacy-sensitive object, where reads | ||
* are filtered through redaction rules and writes are checked against write protect rules. | ||
* | ||
* @param {Array[TransformationRule]} rules | ||
* @return {function(*, ...[*]): ObjectGuard} | ||
*/ | ||
export function objectGuard(rules) { | ||
const root = {}; | ||
const writeRules = []; | ||
|
||
rules.forEach(rule => { | ||
if (rule.wp) writeRules.push(rule); | ||
if (!rule.get) return; | ||
rule.paths.forEach(path => { | ||
let node = root; | ||
path.split('.').forEach(el => { | ||
node.children = node.children || {}; | ||
node.children[el] = node.children[el] || {}; | ||
node = node.children[el]; | ||
}) | ||
node.rule = rule; | ||
}); | ||
}); | ||
|
||
const wpTransformer = objectTransformer(writeRules); | ||
|
||
function mkApplies(session, args) { | ||
return function applies(rule) { | ||
if (!session.hasOwnProperty(rule.name)) { | ||
session[rule.name] = rule.applies(...args); | ||
} | ||
return session[rule.name]; | ||
} | ||
} | ||
|
||
function mkGuard(obj, tree, applies) { | ||
return new Proxy(obj, { | ||
get(target, prop, receiver) { | ||
const val = Reflect.get(target, prop, receiver); | ||
if (tree.hasOwnProperty(prop)) { | ||
const {children, rule} = tree[prop]; | ||
if (children && val != null && typeof val === 'object') { | ||
return mkGuard(val, children, applies); | ||
} else if (rule && isData(val) && applies(rule)) { | ||
return rule.get(val); | ||
} | ||
} | ||
return val; | ||
}, | ||
}); | ||
} | ||
|
||
function mkVerify(transformResult) { | ||
return function () { | ||
transformResult.forEach(fn => fn()); | ||
} | ||
} | ||
|
||
return function guard(obj, ...args) { | ||
const session = {}; | ||
return { | ||
obj: mkGuard(obj, root.children || {}, mkApplies(session, args)), | ||
verify: mkVerify(wpTransformer(session, obj, ...args)) | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
* @param {TransformationRuleDef} ruleDef | ||
* @return {TransformationRule} | ||
*/ | ||
export function writeProtectRule(ruleDef) { | ||
return Object.assign({ | ||
wp: true, | ||
run(root, path, object, property, applies) { | ||
const origHasProp = object && object.hasOwnProperty(property); | ||
const original = origHasProp ? object[property] : undefined; | ||
const origCopy = origHasProp && original != null && typeof original === 'object' ? deepClone(original) : original; | ||
return function () { | ||
const object = path == null ? root : deepAccess(root, path); | ||
const finalHasProp = object && isData(object[property]); | ||
const finalValue = finalHasProp ? object[property] : undefined; | ||
if (!origHasProp && finalHasProp && applies()) { | ||
delete object[property]; | ||
} else if ((origHasProp !== finalHasProp || finalValue !== original || !deepEqual(finalValue, origCopy)) && applies()) { | ||
deepSetValue(root, (path == null ? [] : [path]).concat(property).join('.'), origCopy); | ||
} | ||
} | ||
} | ||
}, ruleDef) | ||
} |
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,88 @@ | ||
import {isActivityAllowed} from '../../src/activities/rules.js'; | ||
import {ACTIVITY_ENRICH_EIDS, ACTIVITY_ENRICH_UFPD} from '../../src/activities/activities.js'; | ||
import { | ||
appliesWhenActivityDenied, | ||
ortb2TransmitRules, | ||
ORTB_EIDS_PATHS, | ||
ORTB_UFPD_PATHS | ||
} from '../../src/activities/redactor.js'; | ||
import {objectGuard, writeProtectRule} from './objectGuard.js'; | ||
import {mergeDeep} from '../../src/utils.js'; | ||
|
||
function ortb2EnrichRules(isAllowed = isActivityAllowed) { | ||
return [ | ||
{ | ||
name: ACTIVITY_ENRICH_EIDS, | ||
paths: ORTB_EIDS_PATHS, | ||
applies: appliesWhenActivityDenied(ACTIVITY_ENRICH_EIDS, isAllowed) | ||
}, | ||
{ | ||
name: ACTIVITY_ENRICH_UFPD, | ||
paths: ORTB_UFPD_PATHS, | ||
applies: appliesWhenActivityDenied(ACTIVITY_ENRICH_UFPD, isAllowed) | ||
} | ||
].map(writeProtectRule) | ||
} | ||
|
||
export function ortb2GuardFactory(isAllowed = isActivityAllowed) { | ||
return objectGuard(ortb2TransmitRules(isAllowed).concat(ortb2EnrichRules(isAllowed))); | ||
} | ||
|
||
/** | ||
* | ||
* | ||
* @typedef {Function} ortb2Guard | ||
* @param {{}} ortb2 ORTB object to guard | ||
* @param {{}} params activity params to use for activity checks | ||
* @returns {ObjectGuard} | ||
*/ | ||
|
||
/* | ||
* Get a guard for an ORTB object. Read access is restricted in the same way it'd be redacted (see activites/redactor.js); | ||
* and writes are checked against the enrich* activites. | ||
* | ||
* @type ortb2Guard | ||
*/ | ||
export const ortb2Guard = ortb2GuardFactory(); | ||
|
||
export function ortb2FragmentsGuardFactory(guardOrtb2 = ortb2Guard) { | ||
return function guardOrtb2Fragments(fragments, params) { | ||
fragments.global = fragments.global || {}; | ||
fragments.bidder = fragments.bidder || {}; | ||
const bidders = new Set(Object.keys(fragments.bidder)); | ||
const verifiers = []; | ||
|
||
function makeGuard(ortb2) { | ||
const guard = guardOrtb2(ortb2, params); | ||
verifiers.push(guard.verify); | ||
return guard.obj; | ||
} | ||
|
||
const obj = { | ||
global: makeGuard(fragments.global), | ||
bidder: Object.fromEntries(Object.entries(fragments.bidder).map(([bidder, ortb2]) => [bidder, makeGuard(ortb2)])) | ||
}; | ||
|
||
return { | ||
obj, | ||
verify() { | ||
Object.entries(obj.bidder) | ||
.filter(([bidder]) => !bidders.has(bidder)) | ||
.forEach(([bidder, ortb2]) => { | ||
const repl = {}; | ||
const guard = guardOrtb2(repl, params); | ||
mergeDeep(guard.obj, ortb2); | ||
guard.verify(); | ||
fragments.bidder[bidder] = repl; | ||
}) | ||
verifiers.forEach(fn => fn()); | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Get a guard for an ortb2Fragments object. | ||
* @type {function(*, *): ObjectGuard} | ||
*/ | ||
export const guardOrtb2Fragments = ortb2FragmentsGuardFactory(); |
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,74 @@ | ||
import {config} from '../src/config.js'; | ||
import {registerActivityControl} from '../src/activities/rules.js'; | ||
|
||
const CFG_NAME = 'allowActivities'; | ||
const RULE_NAME = `${CFG_NAME} config`; | ||
const DEFAULT_PRIORITY = 1; | ||
|
||
export function updateRulesFromConfig(registerRule) { | ||
const activeRuleHandles = new Map(); | ||
const defaultRuleHandles = new Map(); | ||
const rulesByActivity = new Map(); | ||
|
||
function clearAllRules() { | ||
rulesByActivity.clear(); | ||
Array.from(activeRuleHandles.values()) | ||
.flatMap(ruleset => Array.from(ruleset.values())) | ||
.forEach(fn => fn()); | ||
activeRuleHandles.clear(); | ||
Array.from(defaultRuleHandles.values()).forEach(fn => fn()); | ||
defaultRuleHandles.clear(); | ||
} | ||
|
||
function cleanParams(params) { | ||
// remove private parameters for publisher condition checks | ||
return Object.fromEntries(Object.entries(params).filter(([k]) => !k.startsWith('_'))) | ||
} | ||
|
||
function setupRule(activity, priority) { | ||
if (!activeRuleHandles.has(activity)) { | ||
activeRuleHandles.set(activity, new Map()) | ||
} | ||
const handles = activeRuleHandles.get(activity); | ||
if (!handles.has(priority)) { | ||
handles.set(priority, registerRule(activity, RULE_NAME, function (params) { | ||
for (const rule of rulesByActivity.get(activity).get(priority)) { | ||
if (!rule.condition || rule.condition(cleanParams(params))) { | ||
return {allow: rule.allow, reason: rule} | ||
} | ||
} | ||
}, priority)); | ||
} | ||
} | ||
|
||
function setupDefaultRule(activity) { | ||
if (!defaultRuleHandles.has(activity)) { | ||
defaultRuleHandles.set(activity, registerRule(activity, RULE_NAME, function () { | ||
return {allow: false, reason: 'activity denied by default'} | ||
}, Number.POSITIVE_INFINITY)) | ||
} | ||
} | ||
|
||
config.getConfig(CFG_NAME, (cfg) => { | ||
clearAllRules(); | ||
Object.entries(cfg[CFG_NAME]).forEach(([activity, activityCfg]) => { | ||
if (activityCfg.default === false) { | ||
setupDefaultRule(activity); | ||
} | ||
const rules = new Map(); | ||
rulesByActivity.set(activity, rules); | ||
|
||
(activityCfg.rules || []).forEach(rule => { | ||
const priority = rule.priority == null ? DEFAULT_PRIORITY : rule.priority; | ||
if (!rules.has(priority)) { | ||
rules.set(priority, []) | ||
} | ||
rules.get(priority).push(rule); | ||
}); | ||
|
||
Array.from(rules.keys()).forEach(priority => setupRule(activity, priority)); | ||
}); | ||
}) | ||
} | ||
|
||
updateRulesFromConfig(registerActivityControl); |
Oops, something went wrong.