Skip to content

Commit

Permalink
Core & Multiple modules: activity controls (#9802)
Browse files Browse the repository at this point in the history
* 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
dgirardi authored Jun 1, 2023
1 parent 129f6f6 commit 0a7ca9f
Show file tree
Hide file tree
Showing 29 changed files with 2,343 additions and 804 deletions.
108 changes: 108 additions & 0 deletions libraries/objectGuard/objectGuard.js
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)
}
88 changes: 88 additions & 0 deletions libraries/objectGuard/ortbGuard.js
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();
74 changes: 74 additions & 0 deletions modules/allowActivities.js
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);
Loading

0 comments on commit 0a7ca9f

Please sign in to comment.