From ea448387235f2f6f7a50e2715bf0b7a4f81ace90 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Mon, 13 Mar 2023 14:03:14 -0700 Subject: [PATCH 01/37] Core: allow restriction of cookies / localStorage through `bidderSettings.*.storageAllowed` --- modules/userId/index.js | 11 ++- src/storageManager.js | 105 ++++++-------------- test/spec/unit/core/storageManager_spec.js | 108 ++++++++++++++------- 3 files changed, 109 insertions(+), 115 deletions(-) diff --git a/modules/userId/index.js b/modules/userId/index.js index 82ebaf2b14e..4c8d4c0be38 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -133,13 +133,15 @@ import adapterManager, {gdprDataHandler} from '../../src/adapterManager.js'; import CONSTANTS from '../../src/constants.json'; import {hook, module, ready as hooksReady} from '../../src/hook.js'; import {buildEidPermissions, createEidsArray, USER_IDS_CONFIG} from './eids.js'; -import {getCoreStorageManager} from '../../src/storageManager.js'; +import {getCoreStorageManager, STORAGE_TYPE_COOKIES, STORAGE_TYPE_LOCALSTORAGE} from '../../src/storageManager.js'; import { cyrb53Hash, deepAccess, + deepSetValue, delayExecution, getPrebidInternal, isArray, + isEmpty, isEmptyStr, isFn, isGptPubadsDefined, @@ -147,8 +149,7 @@ import { isPlainObject, logError, logInfo, - logWarn, - isEmpty, deepSetValue + logWarn } from '../../src/utils.js'; import {getPPID as coreGetPPID} from '../../src/adserver.js'; import {defer, GreedyPromise} from '../../src/utils/promise.js'; @@ -158,8 +159,8 @@ import {newMetrics, timedAuctionHook, useMetrics} from '../../src/utils/perfMetr import {findRootDomain} from '../../src/fpd/rootDomain.js'; const MODULE_NAME = 'User ID'; -const COOKIE = 'cookie'; -const LOCAL_STORAGE = 'html5'; +const COOKIE = STORAGE_TYPE_COOKIES; +const LOCAL_STORAGE = STORAGE_TYPE_LOCALSTORAGE; const DEFAULT_SYNC_DELAY = 500; const NO_AUCTION_DELAY = 0; const CONSENT_DATA_COOKIE_STORAGE_CONFIG = { diff --git a/src/storageManager.js b/src/storageManager.js index eaf35603c60..b8906b131e4 100644 --- a/src/storageManager.js +++ b/src/storageManager.js @@ -7,6 +7,9 @@ const moduleTypeWhiteList = ['core', 'prebid-module']; export let storageCallbacks = []; +export const STORAGE_TYPE_LOCALSTORAGE = 'html5'; +export const STORAGE_TYPE_COOKIES = 'cookie'; + /** * Storage options * @typedef {Object} storageOptions @@ -26,20 +29,22 @@ export let storageCallbacks = []; * @param {storageOptions} options */ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = {}, {bidderSettings = defaultBidderSettings} = {}) { - function isBidderAllowed() { + function isBidderAllowed(storageType) { if (bidderCode == null) { return true; } const storageAllowed = bidderSettings.get(bidderCode, 'storageAllowed'); - return storageAllowed == null ? false : storageAllowed; + if (!storageAllowed || storageAllowed === true) return !!storageAllowed; + if (Array.isArray(storageAllowed)) return storageAllowed.some((e) => e === storageType); + return storageAllowed === storageType; } if (moduleTypeWhiteList.includes(moduleType)) { gvlid = gvlid || VENDORLESS_GVLID; } - function isValid(cb) { - if (!isBidderAllowed()) { + function isValid(cb, storageType) { + if (!isBidderAllowed(storageType)) { logInfo(`bidderSettings denied access to device storage for bidder '${bidderCode}'`); const result = {valid: false}; return cb(result); @@ -63,6 +68,17 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = } } + function schedule(operation, storageType, done) { + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(operation, storageType); + done(result); + }); + } else { + return isValid(operation, storageType); + } + } + /** * @param {string} key * @param {string} value @@ -83,14 +99,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = document.cookie = `${key}=${encodeURIComponent(value)}${expiresPortion}; path=/${domainPortion}${sameSite ? `; SameSite=${sameSite}` : ''}${secure}`; } } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_COOKIES, done); }; /** @@ -105,14 +114,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = } return null; } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_COOKIES, done); }; /** @@ -133,14 +135,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = } return false; } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); } /** @@ -153,14 +148,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = } return false; } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_COOKIES, done); } /** @@ -173,14 +161,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = window.localStorage.setItem(key, value); } } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); } /** @@ -194,14 +175,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = } return null; } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); } /** @@ -213,14 +187,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = window.localStorage.removeItem(key); } } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); } /** @@ -237,14 +204,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = } return false; } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); } /** @@ -273,14 +233,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = } } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_COOKIES, done); } return { diff --git a/test/spec/unit/core/storageManager_spec.js b/test/spec/unit/core/storageManager_spec.js index 58bf8c9eb25..de05d2b3e58 100644 --- a/test/spec/unit/core/storageManager_spec.js +++ b/test/spec/unit/core/storageManager_spec.js @@ -142,11 +142,11 @@ describe('storage manager', function() { const COOKIE = 'test-cookie'; const LS_KEY = 'test-localstorage'; - function mockBidderSettings() { + function mockBidderSettings(val) { return { get(bidder, key) { if (bidder === ALLOWED_BIDDER && key === ALLOW_KEY) { - return true; + return val; } else { return undefined; } @@ -157,39 +157,79 @@ describe('storage manager', function() { Object.entries({ disallowed: ['denied_bidder', false], allowed: [ALLOWED_BIDDER, true] - }).forEach(([test, [bidderCode, shouldWork]]) => { - describe(`for ${test} bidders`, () => { - let mgr; - - beforeEach(() => { - mgr = newStorageManager({bidderCode: bidderCode}, {bidderSettings: mockBidderSettings()}); - }) - - afterEach(() => { - mgr.setCookie(COOKIE, 'delete', new Date().toUTCString()); - mgr.removeDataFromLocalStorage(LS_KEY); - }) - - const testDesc = (desc) => `should ${shouldWork ? '' : 'not'} ${desc}`; - - it(testDesc('allow cookies'), () => { - mgr.setCookie(COOKIE, 'value'); - expect(mgr.getCookie(COOKIE)).to.equal(shouldWork ? 'value' : null); - }); - - it(testDesc('allow localStorage'), () => { - mgr.setDataInLocalStorage(LS_KEY, 'value'); - expect(mgr.getDataFromLocalStorage(LS_KEY)).to.equal(shouldWork ? 'value' : null); - }); - - it(testDesc('report localStorage as available'), () => { - expect(mgr.hasLocalStorage()).to.equal(shouldWork); - }); - - it(testDesc('report cookies as available'), () => { - expect(mgr.cookiesAreEnabled()).to.equal(shouldWork); + }).forEach(([t, [bidderCode, isBidderAllowed]]) => { + describe(`for ${t} bidders`, () => { + Object.entries({ + 'all': { + configValues: [ + true, + ['html5', 'cookie'] + ], + shouldWork: { + html5: true, + cookie: true + } + }, + 'localStorage': { + configValues: [ + 'html5', + ['html5'] + ], + shouldWork: { + html5: true, + cookie: false + } + }, + 'cookies': { + configValues: [ + 'cookie', + ['cookie'] + ], + shouldWork: { + html5: false, + cookie: true + } + } + }).forEach(([t, {configValues, shouldWork: {cookie, html5}}]) => { + describe(`when ${t} is allowed`, () => { + configValues.forEach(configValue => describe(`storageAllowed = ${configValue}`, () => { + let mgr; + + beforeEach(() => { + mgr = newStorageManager({bidderCode: bidderCode}, {bidderSettings: mockBidderSettings(configValue)}); + }) + + afterEach(() => { + mgr.setCookie(COOKIE, 'delete', new Date().toUTCString()); + mgr.removeDataFromLocalStorage(LS_KEY); + }) + + function scenario(type, desc, fn) { + const shouldWork = isBidderAllowed && ({html5, cookie})[type]; + it(`${shouldWork ? '' : 'NOT'} ${desc}`, () => fn(shouldWork)); + } + + scenario('cookie', 'allow cookies', (shouldWork) => { + mgr.setCookie(COOKIE, 'value'); + expect(mgr.getCookie(COOKIE)).to.equal(shouldWork ? 'value' : null); + }); + + scenario('html5', 'allow localStorage', (shouldWork) => { + mgr.setDataInLocalStorage(LS_KEY, 'value'); + expect(mgr.getDataFromLocalStorage(LS_KEY)).to.equal(shouldWork ? 'value' : null); + }); + + scenario('html5', 'report localStorage as available', (shouldWork) => { + expect(mgr.hasLocalStorage()).to.equal(shouldWork); + }); + + scenario('cookie', 'report cookies as available', (shouldWork) => { + expect(mgr.cookiesAreEnabled()).to.equal(shouldWork); + }); + })); + }); }); }); }); - }) + }); }); From d7c3f2708cef2daa2bd06685bb5014c13acc12a2 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Mon, 13 Mar 2023 14:14:49 -0700 Subject: [PATCH 02/37] Add test cases --- test/spec/unit/core/storageManager_spec.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/spec/unit/core/storageManager_spec.js b/test/spec/unit/core/storageManager_spec.js index de05d2b3e58..d4b3c8e583e 100644 --- a/test/spec/unit/core/storageManager_spec.js +++ b/test/spec/unit/core/storageManager_spec.js @@ -170,6 +170,16 @@ describe('storage manager', function() { cookie: true } }, + 'none': { + configValues: [ + false, + [] + ], + shouldWork: { + html5: false, + cookie: false + } + }, 'localStorage': { configValues: [ 'html5', From 05759cce31de07f67f9ae317e8711a19d8317598 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 29 Mar 2023 07:22:45 -0700 Subject: [PATCH 03/37] Remove gvlid param from storage manager logic --- src/modules.js | 1 + src/storageManager.js | 83 ++++++++++------------ test/spec/unit/core/storageManager_spec.js | 52 ++++++++------ 3 files changed, 69 insertions(+), 67 deletions(-) create mode 100644 src/modules.js diff --git a/src/modules.js b/src/modules.js new file mode 100644 index 00000000000..1c33e8f40aa --- /dev/null +++ b/src/modules.js @@ -0,0 +1 @@ +// module type definitions - for storageManager / activity controls diff --git a/src/storageManager.js b/src/storageManager.js index b8906b131e4..3bc70e97052 100644 --- a/src/storageManager.js +++ b/src/storageManager.js @@ -1,51 +1,35 @@ import {hook} from './hook.js'; -import {hasDeviceAccess, checkCookieSupport, logError, logInfo, isPlainObject} from './utils.js'; +import {checkCookieSupport, hasDeviceAccess, logError, logInfo} from './utils.js'; import {bidderSettings as defaultBidderSettings} from './bidderSettings.js'; -import {VENDORLESS_GVLID} from './consentHandler.js'; -const moduleTypeWhiteList = ['core', 'prebid-module']; - -export let storageCallbacks = []; +export const MODULE_TYPE_CORE = 'core'; +export const MODULE_TYPE_BIDDER = 'bidder'; +export const MODULE_TYPE_UID = 'userId'; +export const MODULE_TYPE_RTD = 'rtd'; +export const MODULE_TYPE_ANALYTICS = 'analytics'; export const STORAGE_TYPE_LOCALSTORAGE = 'html5'; export const STORAGE_TYPE_COOKIES = 'cookie'; -/** - * Storage options - * @typedef {Object} storageOptions - * @property {Number=} gvlid - Vendor id - * @property {string} moduleName? - Module name - * @property {string=} bidderCode? - Bidder code - * @property {string=} moduleType - Module type, value can be anyone of core or prebid-module - */ +export let storageCallbacks = []; -/** - * Returns list of storage related functions with gvlid, module name and module type in its scope. - * All three argument are optional here. Below shows the usage of of these - * - GVL Id: Pass GVL id if you are a vendor - * - Bidder code: All bid adapters need to pass bidderCode - * - Module name: All other modules need to pass module name - * - Module type: Some modules may need these functions but are not vendor. e.g prebid core files in src and modules like currency. - * @param {storageOptions} options +/* + * Storage manager constructor. Consumers should prefer one of `getStorageManager` or `getCoreStorageManager`. */ -export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = {}, {bidderSettings = defaultBidderSettings} = {}) { +export function newStorageManager({moduleName, moduleType} = {}, {bidderSettings = defaultBidderSettings} = {}) { function isBidderAllowed(storageType) { - if (bidderCode == null) { + if (moduleType !== MODULE_TYPE_BIDDER) { return true; } - const storageAllowed = bidderSettings.get(bidderCode, 'storageAllowed'); + const storageAllowed = bidderSettings.get(moduleName, 'storageAllowed'); if (!storageAllowed || storageAllowed === true) return !!storageAllowed; if (Array.isArray(storageAllowed)) return storageAllowed.some((e) => e === storageType); return storageAllowed === storageType; } - if (moduleTypeWhiteList.includes(moduleType)) { - gvlid = gvlid || VENDORLESS_GVLID; - } - function isValid(cb, storageType) { if (!isBidderAllowed(storageType)) { - logInfo(`bidderSettings denied access to device storage for bidder '${bidderCode}'`); + logInfo(`bidderSettings denied access to device storage for bidder '${moduleName}'`); const result = {valid: false}; return cb(result); } else { @@ -53,7 +37,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = let hookDetails = { hasEnforcementHook: false } - validateStorageEnforcement(gvlid, bidderCode || moduleName, hookDetails, function(result) { + validateStorageEnforcement(moduleType, moduleName, hookDetails, function(result) { if (result && result.hasEnforcementHook) { value = cb(result); } else { @@ -252,31 +236,38 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = /** * This hook validates the storage enforcement if gdprEnforcement module is included */ -export const validateStorageEnforcement = hook('async', function(gvlid, moduleName, hookDetails, callback) { +export const validateStorageEnforcement = hook('async', function(moduleType, moduleName, hookDetails, callback) { callback(hookDetails); }, 'validateStorageEnforcement'); /** - * This function returns storage functions to access cookies and localstorage. This function will bypass the gdpr enforcement requirement. Prebid as a software needs to use storage in some scenarios and is not a vendor so GDPR enforcement rules does not apply on Prebid. - * @param {string} moduleName Module name + * Get a storage manager for a particular module. + * + * Either bidderCode or a combination of moduleType + moduleName must be provided. The former is a shorthand + * for `{moduleType: 'bidder', moduleName: bidderCode}`. + * */ -export function getCoreStorageManager(moduleName) { - return newStorageManager({moduleName: moduleName, moduleType: 'core'}); +export function getStorageManager({moduleType, moduleName, bidderCode} = {}) { + function err() { + throw new Error(`Invalid invocation for getStorageManager: must set either bidderCode, or moduleType + moduleName`) + } + if (bidderCode) { + if ((moduleType && moduleType !== MODULE_TYPE_BIDDER) || moduleName) err() + moduleType = MODULE_TYPE_BIDDER; + moduleName = bidderCode; + } else if (!moduleName || !moduleType) { + err() + } + return newStorageManager({moduleType, moduleName}); } /** - * Note: Core modules or Prebid modules like Currency, SizeMapping should use getCoreStorageManager - * This function returns storage functions to access cookies and localstorage. Bidders and User id modules should import this and use it in their module if needed. - * Bid adapters should always provide `bidderCode`. GVL ID and Module name are optional param but gvl id is needed for when gdpr enforcement module is used. - * @param {Number=} gvlid? Vendor id - required for proper GDPR integration - * @param {string=} bidderCode? - required for bid adapters - * @param {string=} moduleName? module name + * Get a storage manager for "core" (vendorless, or first-party) modules. Shorthand for `getStorageManager({moduleName, moduleType: 'core'})`. + * + * @param {string} moduleName Module name */ -export function getStorageManager({gvlid, moduleName, bidderCode} = {}) { - if (arguments.length > 1 || (arguments.length > 0 && !isPlainObject(arguments[0]))) { - throw new Error('Invalid invocation for getStorageManager') - } - return newStorageManager({gvlid, moduleName, bidderCode}); +export function getCoreStorageManager(moduleName) { + return newStorageManager({moduleName: moduleName, moduleType: MODULE_TYPE_CORE}); } export function resetData() { diff --git a/test/spec/unit/core/storageManager_spec.js b/test/spec/unit/core/storageManager_spec.js index d4b3c8e583e..21c4bbf41b7 100644 --- a/test/spec/unit/core/storageManager_spec.js +++ b/test/spec/unit/core/storageManager_spec.js @@ -1,14 +1,14 @@ import { + getCoreStorageManager, getStorageManager, + MODULE_TYPE_BIDDER, + newStorageManager, resetData, - getCoreStorageManager, storageCallbacks, - getStorageManager, - newStorageManager, validateStorageEnforcement + validateStorageEnforcement } from 'src/storageManager.js'; -import { config } from 'src/config.js'; +import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import {hook} from '../../../../src/hook.js'; -import {VENDORLESS_GVLID} from '../../../../src/consentHandler.js'; describe('storage manager', function() { before(() => { @@ -34,7 +34,7 @@ describe('storage manager', function() { it('should add done callbacks to storageCallbacks array', function() { let noop = sinon.spy(); - const coreStorage = getStorageManager(); + const coreStorage = newStorageManager(); coreStorage.setCookie('foo', 'bar', null, null, null, noop); coreStorage.getCookie('foo', noop); @@ -50,17 +50,16 @@ describe('storage manager', function() { it('should allow bidder to access device if gdpr enforcement module is not included', function() { let deviceAccessSpy = sinon.spy(utils, 'hasDeviceAccess'); - const storage = getStorageManager(); + const storage = newStorageManager(); storage.setCookie('foo1', 'baz1'); expect(deviceAccessSpy.calledOnce).to.equal(true); deviceAccessSpy.restore(); }); - describe(`core storage`, () => { - let storage, validateHook; + describe(`enforcement`, () => { + let validateHook; beforeEach(() => { - storage = getCoreStorageManager(); validateHook = sinon.stub().callsFake(function (next, ...args) { next.apply(this, args); }); @@ -72,15 +71,26 @@ describe('storage manager', function() { config.resetConfig(); }) - it('should respect (vendorless) consent enforcement', () => { - storage.localStorageIsEnabled(); - expect(validateHook.args[0][1]).to.equal(VENDORLESS_GVLID); // gvlid should be set to VENDORLESS_GVLID - }); + Object.entries({ + 'core': () => getCoreStorageManager('mock'), + 'other': () => getStorageManager({moduleType: 'other', moduleName: 'mock'}) + }).forEach(([moduleType, getMgr]) => { + describe(`for ${moduleType} modules`, () => { + let storage; + beforeEach(() => { + storage = getMgr(); + }); + it(`should pass '${moduleType}' module type to consent enforcement`, () => { + storage.localStorageIsEnabled(); + expect(validateHook.args[0][1]).to.equal(moduleType); + }); - it('should respect the deviceAccess flag', () => { - config.setConfig({deviceAccess: false}); - expect(storage.localStorageIsEnabled()).to.be.false - }) + it('should respect the deviceAccess flag', () => { + config.setConfig({deviceAccess: false}); + expect(storage.localStorageIsEnabled()).to.be.false + }); + }); + }); }) describe('localstorage forbidden access in 3rd-party context', function() { @@ -100,7 +110,7 @@ describe('storage manager', function() { }) it('should not throw if the localstorage is not accessible when setting/getting/removing from localstorage', function() { - const coreStorage = getStorageManager(); + const coreStorage = newStorageManager(); coreStorage.setDataInLocalStorage('key', 'value'); const val = coreStorage.getDataFromLocalStorage('key'); @@ -124,7 +134,7 @@ describe('storage manager', function() { }) it('should remove side-effect after checking', function () { - const storage = getStorageManager(); + const storage = newStorageManager(); localStorage.setItem('unrelated', 'dummy'); const val = storage.localStorageIsEnabled(); @@ -206,7 +216,7 @@ describe('storage manager', function() { let mgr; beforeEach(() => { - mgr = newStorageManager({bidderCode: bidderCode}, {bidderSettings: mockBidderSettings(configValue)}); + mgr = newStorageManager({moduleType: MODULE_TYPE_BIDDER, moduleName: bidderCode}, {bidderSettings: mockBidderSettings(configValue)}); }) afterEach(() => { From 7228769eae9cc396a23a0813c5cb3e391071fd91 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 29 Mar 2023 11:18:37 -0700 Subject: [PATCH 04/37] Refactor every invocation of `getStorageManager` --- modules/adagioBidAdapter.js | 2 +- modules/adkernelAdnAnalyticsAdapter.js | 7 +++-- modules/admixerIdSystem.js | 7 +++-- modules/adnuntiusBidAdapter.js | 2 +- modules/adqueryBidAdapter.js | 2 +- modules/adqueryIdSystem.js | 4 +-- modules/adriverIdSystem.js | 4 +-- modules/airgridRtdProvider.js | 5 ++-- modules/akamaiDapRtdProvider.js | 4 +-- modules/amxBidAdapter.js | 2 +- modules/appnexusBidAdapter.js | 2 +- modules/apstreamBidAdapter.js | 2 +- modules/atsAnalyticsAdapter.js | 8 ++--- modules/blueconicRtdProvider.js | 4 +-- modules/browsiRtdProvider.js | 7 +++-- modules/byDataAnalyticsAdapter.js | 8 +++-- modules/criteoBidAdapter.js | 2 +- modules/criteoIdSystem.js | 4 +-- modules/czechAdIdSystem.js | 4 +-- modules/dacIdSystem.js | 8 ++--- modules/deepintentDpesIdSystem.js | 4 +-- modules/discoveryBidAdapter.js | 2 +- modules/fintezaAnalyticsAdapter.js | 7 +++-- modules/ftrackIdSystem.js | 4 +-- modules/glimpseBidAdapter.js | 5 +--- modules/gnetBidAdapter.js | 3 +- modules/gravitoIdSystem.js | 7 +++-- modules/gridBidAdapter.js | 2 +- modules/growthCodeAnalyticsAdapter.js | 4 +-- modules/growthCodeIdSystem.js | 4 +-- modules/hadronAnalyticsAdapter.js | 7 +++-- modules/hadronIdSystem.js | 4 +-- modules/hadronRtdProvider.js | 7 +++-- modules/id5IdSystem.js | 4 +-- modules/idWardRtdProvider.js | 4 +-- modules/identityLinkIdSystem.js | 8 +++-- modules/idxIdSystem.js | 4 +-- modules/imRtdProvider.js | 5 ++-- modules/imuIdSystem.js | 8 +++-- modules/insticatorBidAdapter.js | 2 +- modules/intentIqIdSystem.js | 4 +-- modules/invibesBidAdapter.js | 2 +- modules/ixBidAdapter.js | 2 +- modules/kargoBidAdapter.js | 2 +- modules/kueezRtbBidAdapter.js | 2 +- modules/liveIntentIdSystem.js | 4 +-- modules/lotamePanoramaIdSystem.js | 4 +-- modules/magniteAnalyticsAdapter.js | 30 +++++++++++++++---- modules/mediafuseBidAdapter.js | 2 +- modules/mediagoBidAdapter.js | 2 +- modules/merkleIdSystem.js | 4 +-- modules/mgidBidAdapter.js | 2 +- modules/mgidRtdProvider.js | 5 ++-- modules/minutemediaplusBidAdapter.js | 2 +- modules/mwOpenLinkIdSystem.js | 4 +-- modules/naveggIdSystem.js | 4 +-- modules/nexx360BidAdapter.js | 1 - modules/nobidBidAdapter.js | 2 +- modules/novatiqIdSystem.js | 8 +++-- modules/onetagBidAdapter.js | 2 +- modules/parrableIdSystem.js | 7 +++-- modules/permutiveRtdProvider.js | 4 +-- modules/prebidmanagerAnalyticsAdapter.js | 4 +-- modules/pubCommonId.js | 4 +-- modules/publinkIdSystem.js | 4 +-- modules/pubwiseAnalyticsAdapter.js | 7 +++-- modules/quantcastBidAdapter.js | 2 +- modules/quantcastIdSystem.js | 7 +++-- modules/realvuAnalyticsAdapter.js | 8 +++-- modules/roxotAnalyticsAdapter.js | 8 +++-- modules/rubiconAnalyticsAdapter.js | 4 +-- modules/sharedIdSystem.js | 4 +-- modules/sigmoidAnalyticsAdapter.js | 7 +++-- modules/staqAnalyticsAdapter.js | 7 +++-- modules/taboolaBidAdapter.js | 2 +- modules/teadsBidAdapter.js | 2 +- modules/teadsIdSystem.js | 4 +-- modules/tripleliftBidAdapter.js | 2 +- modules/trustpidSystem.js | 4 +-- modules/uid2IdSystem.js | 4 +-- modules/validationFpdModule/index.js | 5 ++-- modules/vidazooBidAdapter.js | 2 +- modules/weboramaRtdProvider.js | 5 ++-- modules/yuktamediaAnalyticsAdapter.js | 7 +++-- modules/zeotapIdPlusIdSystem.js | 4 +-- test/spec/modules/admixerIdSystem_spec.js | 3 -- test/spec/modules/adnuntiusBidAdapter_spec.js | 12 ++++---- test/spec/modules/atsAnalyticsAdapter_spec.js | 4 +-- .../spec/modules/datablocksBidAdapter_spec.js | 1 - test/spec/modules/growthCodeIdSystem_spec.js | 4 +-- .../spec/modules/identityLinkIdSystem_spec.js | 4 +-- test/spec/modules/publinkIdSystem_spec.js | 5 ++-- test/spec/modules/relaidoBidAdapter_spec.js | 10 +++---- .../spec/modules/zeotapIdPlusIdSystem_spec.js | 5 ++-- 94 files changed, 239 insertions(+), 198 deletions(-) diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index 798ff499206..8b4bcbfa1f4 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -43,7 +43,7 @@ const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; const ADAGIO_TAG_URL = 'https://script.4dex.io/localstore.js'; const ADAGIO_LOCALSTORAGE_KEY = 'adagioScript'; const GVLID = 617; -export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const RENDERER_URL = 'https://script.4dex.io/outstream-player.js'; const MAX_SESS_DURATION = 30 * 60 * 1000; const ADAGIO_PUBKEY = 'AL16XT44Sfp+8SHVF1UdC7hydPSMVLMhsYknKDdwqq+0ToDSJrP0+Qh0ki9JJI2uYm/6VEYo8TJED9WfMkiJ4vf02CW3RvSWwc35bif2SK1L8Nn/GfFYr/2/GG/Rm0vUsv+vBHky6nuuYls20Og0HDhMgaOlXoQ/cxMuiy5QSktp'; diff --git a/modules/adkernelAdnAnalyticsAdapter.js b/modules/adkernelAdnAnalyticsAdapter.js index 901c0d2fd98..cb5a8cf837c 100644 --- a/modules/adkernelAdnAnalyticsAdapter.js +++ b/modules/adkernelAdnAnalyticsAdapter.js @@ -3,14 +3,15 @@ import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; import { logError, parseUrl, _each } from '../src/utils.js'; import {ajax} from '../src/ajax.js'; -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; import {config} from '../src/config.js'; +const MODULE_CODE = 'adkernelAdn'; const GVLID = 14; const ANALYTICS_VERSION = '1.0.2'; const DEFAULT_QUEUE_TIMEOUT = 4000; const DEFAULT_HOST = 'tag.adkernel.com'; -const storageObj = getStorageManager({gvlid: GVLID}); +const storageObj = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); const ADK_HB_EVENTS = { AUCTION_INIT: 'auctionInit', @@ -104,7 +105,7 @@ analyticsAdapter.enableAnalytics = (config) => { adapterManager.registerAnalyticsAdapter({ adapter: analyticsAdapter, - code: 'adkernelAdn', + code: MODULE_CODE, gvlid: GVLID }); diff --git a/modules/admixerIdSystem.js b/modules/admixerIdSystem.js index 49ffe4f4680..30363247784 100644 --- a/modules/admixerIdSystem.js +++ b/modules/admixerIdSystem.js @@ -8,9 +8,10 @@ import { logError, logInfo } from '../src/utils.js' import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; -export const storage = getStorageManager(); +const NAME = 'admixerId'; +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: NAME}); /** @type {Submodule} */ export const admixerIdSubmodule = { @@ -18,7 +19,7 @@ export const admixerIdSubmodule = { * used to link submodule with config * @type {string} */ - name: 'admixerId', + name: NAME, /** * used to specify vendor id * @type {number} diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js index ea3b723b316..c3cd1963248 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -55,7 +55,7 @@ const getSegmentsFromOrtb = function (ortb2) { // } const handleMeta = function () { - const storage = getStorageManager({ gvlid: GVLID, bidderCode: BIDDER_CODE }) + const storage = getStorageManager({ bidderCode: BIDDER_CODE }) let adnMeta = null if (storage.localStorageIsEnabled()) { adnMeta = JSON.parse(storage.getDataFromLocalStorage('adn.metaData')) diff --git a/modules/adqueryBidAdapter.js b/modules/adqueryBidAdapter.js index be5ca4b1057..63e0c7dbe22 100644 --- a/modules/adqueryBidAdapter.js +++ b/modules/adqueryBidAdapter.js @@ -11,7 +11,7 @@ const ADQUERY_USER_SYNC_DOMAIN = ADQUERY_BIDDER_DOMAIN_PROTOCOL + '://' + ADQUER const ADQUERY_DEFAULT_CURRENCY = 'PLN'; const ADQUERY_NET_REVENUE = true; const ADQUERY_TTL = 360; -const storage = getStorageManager({gvlid: ADQUERY_GVLID, bidderCode: ADQUERY_BIDDER_CODE}); +const storage = getStorageManager({bidderCode: ADQUERY_BIDDER_CODE}); /** @type {BidderSpec} */ export const spec = { diff --git a/modules/adqueryIdSystem.js b/modules/adqueryIdSystem.js index d9552a470d0..24902c7470c 100644 --- a/modules/adqueryIdSystem.js +++ b/modules/adqueryIdSystem.js @@ -6,14 +6,14 @@ */ import {ajax} from '../src/ajax.js'; -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import { isFn, isStr, isPlainObject, logError } from '../src/utils.js'; const MODULE_NAME = 'qid'; const AU_GVLID = 902; -export const storage = getStorageManager({gvlid: AU_GVLID, moduleName: 'qid'}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: 'qid'}); /** * Param or default. diff --git a/modules/adriverIdSystem.js b/modules/adriverIdSystem.js index fb8ce99ec16..c984ab180de 100644 --- a/modules/adriverIdSystem.js +++ b/modules/adriverIdSystem.js @@ -8,11 +8,11 @@ import { logError, isPlainObject } from '../src/utils.js' import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; const MODULE_NAME = 'adriverId'; -export const storage = getStorageManager(); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** @type {Submodule} */ export const adriverIdSubmodule = { diff --git a/modules/airgridRtdProvider.js b/modules/airgridRtdProvider.js index 3578cc4b87e..d34e9fb524e 100644 --- a/modules/airgridRtdProvider.js +++ b/modules/airgridRtdProvider.js @@ -13,7 +13,7 @@ import { deepAccess, } from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_RTD} from '../src/storageManager.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'airgrid'; @@ -21,7 +21,7 @@ const AG_TCF_ID = 782; export const AG_AUDIENCE_IDS_KEY = 'edkt_matched_audience_ids'; export const storage = getStorageManager({ - gvlid: AG_TCF_ID, + moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME, }); @@ -144,6 +144,7 @@ export const airgridSubmodule = { name: SUBMODULE_NAME, init: init, getBidRequestData: passAudiencesToBidders, + gvlid: AG_TCF_ID }; submodule(MODULE_NAME, airgridSubmodule); diff --git a/modules/akamaiDapRtdProvider.js b/modules/akamaiDapRtdProvider.js index 1c2af70d737..f20c329899a 100644 --- a/modules/akamaiDapRtdProvider.js +++ b/modules/akamaiDapRtdProvider.js @@ -6,7 +6,7 @@ * @requires module:modules/realTimeData */ import {ajax} from '../src/ajax.js'; -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_RTD} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isPlainObject, mergeDeep, logMessage, logInfo, logError} from '../src/utils.js'; import { loadExternalScript } from '../src/adloader.js'; @@ -23,7 +23,7 @@ export const DAP_DEFAULT_TOKEN_TTL = 3600; // in seconds export const DAP_MAX_RETRY_TOKENIZE = 1; export const DAP_CLIENT_ENTROPY = 'dap_client_entropy' -export const storage = getStorageManager({gvlid: null, moduleName: SUBMODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME}); let dapRetryTokenize = 0; /** diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index 2a3716589b8..22ca49838ee 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -15,7 +15,7 @@ import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'amx'; -const storage = getStorageManager({ gvlid: 737, bidderCode: BIDDER_CODE }); +const storage = getStorageManager({bidderCode: BIDDER_CODE }); const SIMPLE_TLD_TEST = /\.com?\.\w{2,4}$/; const DEFAULT_ENDPOINT = 'https://prebid.a-mo.net/a/c'; const VERSION = 'pba1.3.2'; diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index f354eb053b1..11b623a1d2a 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -94,7 +94,7 @@ const SCRIPT_TAG_START = ' { return storage.getCookie('czaid') || storage.getDataFromLocalStorage('czaid') } diff --git a/modules/dacIdSystem.js b/modules/dacIdSystem.js index 856e1976bb1..e5a02f69c8f 100644 --- a/modules/dacIdSystem.js +++ b/modules/dacIdSystem.js @@ -17,10 +17,10 @@ import { submodule } from '../src/hook.js'; import { - getStorageManager + getStorageManager, MODULE_TYPE_UID } from '../src/storageManager.js'; - -export const storage = getStorageManager(); +const MODULE_NAME = 'dacId'; +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); export const FUUID_COOKIE_NAME = '_a1_f'; export const AONEID_COOKIE_NAME = '_a1_d'; @@ -116,7 +116,7 @@ export const dacIdSystemSubmodule = { * used to link submodule with config * @type {string} */ - name: 'dacId', + name: MODULE_NAME, /** * decode the stored id value for passing to bid requests diff --git a/modules/deepintentDpesIdSystem.js b/modules/deepintentDpesIdSystem.js index 43c7af1b3cc..4d73d2aabf1 100644 --- a/modules/deepintentDpesIdSystem.js +++ b/modules/deepintentDpesIdSystem.js @@ -6,10 +6,10 @@ */ import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; const MODULE_NAME = 'deepintentId'; -export const storage = getStorageManager({gvlid: null, moduleName: MODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** @type {Submodule} */ export const deepintentDpesSubmodule = { diff --git a/modules/discoveryBidAdapter.js b/modules/discoveryBidAdapter.js index 7930efc4fa8..a0f864d529f 100644 --- a/modules/discoveryBidAdapter.js +++ b/modules/discoveryBidAdapter.js @@ -6,7 +6,7 @@ import { BANNER, NATIVE } from '../src/mediaTypes.js'; const BIDDER_CODE = 'discovery'; const ENDPOINT_URL = 'https://rtb-jp.mediago.io/api/bid?tn='; const TIME_TO_LIVE = 500; -const storage = getStorageManager(); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); let globals = {}; let itemMaps = {}; const MEDIATYPE = [BANNER, NATIVE]; diff --git a/modules/fintezaAnalyticsAdapter.js b/modules/fintezaAnalyticsAdapter.js index 88c5f85f15d..447fce24d2f 100644 --- a/modules/fintezaAnalyticsAdapter.js +++ b/modules/fintezaAnalyticsAdapter.js @@ -2,10 +2,11 @@ import { parseUrl, logError } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; import CONSTANTS from '../src/constants.json'; -const storage = getStorageManager(); +const MODULE_CODE = 'finteza'; +const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); const ANALYTICS_TYPE = 'endpoint'; const FINTEZA_HOST = 'https://content.mql5.com/tr'; @@ -439,7 +440,7 @@ fntzAnalyticsAdapter.enableAnalytics = function (config) { adapterManager.registerAnalyticsAdapter({ adapter: fntzAnalyticsAdapter, - code: 'finteza' + code: MODULE_CODE, }); export default fntzAnalyticsAdapter; diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js index 244807a3164..569a4a7d662 100644 --- a/modules/ftrackIdSystem.js +++ b/modules/ftrackIdSystem.js @@ -7,7 +7,7 @@ import * as utils from '../src/utils.js'; import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; import { uspDataHandler } from '../src/adapterManager.js'; import { loadExternalScript } from '../src/adloader.js'; @@ -18,7 +18,7 @@ const VENDOR_ID = null; const LOCAL_STORAGE = 'html5'; const FTRACK_STORAGE_NAME = 'ftrackId'; const FTRACK_PRIVACY_STORAGE_NAME = `${FTRACK_STORAGE_NAME}_privacy`; -const storage = getStorageManager({gvlid: VENDOR_ID, moduleName: MODULE_NAME}); +const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); let consentInfo = { gdpr: { diff --git a/modules/glimpseBidAdapter.js b/modules/glimpseBidAdapter.js index bbb4dbb30cd..3847a72368a 100644 --- a/modules/glimpseBidAdapter.js +++ b/modules/glimpseBidAdapter.js @@ -12,10 +12,7 @@ import { const GVLID = 1012; const BIDDER_CODE = 'glimpse'; -const storageManager = getStorageManager({ - gvlid: GVLID, - bidderCode: BIDDER_CODE, -}); +const storageManager = getStorageManager({bidderCode: BIDDER_CODE}); const ENDPOINT = 'https://market.glimpsevault.io/public/v1/prebid'; const LOCAL_STORAGE_KEY = { vault: { diff --git a/modules/gnetBidAdapter.js b/modules/gnetBidAdapter.js index 8bab043d0db..0b02a29c0d4 100644 --- a/modules/gnetBidAdapter.js +++ b/modules/gnetBidAdapter.js @@ -4,10 +4,9 @@ import { BANNER } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; -const storage = getStorageManager(); - const BIDDER_CODE = 'gnet'; const ENDPOINT = 'https://service.gnetrtb.com/api'; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, diff --git a/modules/gravitoIdSystem.js b/modules/gravitoIdSystem.js index 809263a1c68..00f3408a4cc 100644 --- a/modules/gravitoIdSystem.js +++ b/modules/gravitoIdSystem.js @@ -6,9 +6,10 @@ */ import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; -export const storage = getStorageManager(); +const MODULE_NAME = 'gravitompId'; +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); export const cookieKey = 'gravitompId'; @@ -17,7 +18,7 @@ export const gravitoIdSystemSubmodule = { * used to link submodule with config * @type {string} */ - name: 'gravitompId', + name: MODULE_NAME, /** * performs action to obtain id diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index 31bb350aaa6..1adc51dbbe7 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -31,7 +31,7 @@ const TIME_TO_LIVE = 360; const USER_ID_KEY = 'tmguid'; const GVLID = 686; const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; -export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); const LOG_ERROR_MESS = { noAuid: 'Bid from response has no auid parameter - ', diff --git a/modules/growthCodeAnalyticsAdapter.js b/modules/growthCodeAnalyticsAdapter.js index 1f11b891139..c9b5ef4ca84 100644 --- a/modules/growthCodeAnalyticsAdapter.js +++ b/modules/growthCodeAnalyticsAdapter.js @@ -6,7 +6,7 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import * as utils from '../src/utils.js'; import CONSTANTS from '../src/constants.json'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {logError, logInfo} from '../src/utils.js'; @@ -14,7 +14,7 @@ const MODULE_NAME = 'growthCodeAnalytics'; const DEFAULT_PID = 'INVALID_PID' const ENDPOINT_URL = 'https://p2.gcprivacy.com/v1/pb/analytics' -export const storage = getStorageManager(); +export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_NAME}); let sessionId = utils.generateUUID(); diff --git a/modules/growthCodeIdSystem.js b/modules/growthCodeIdSystem.js index edd7cd33012..f49abbd3e44 100644 --- a/modules/growthCodeIdSystem.js +++ b/modules/growthCodeIdSystem.js @@ -8,14 +8,14 @@ import {logError, logInfo, tryAppendQueryString} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import { submodule } from '../src/hook.js' -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; const GCID_EXPIRY = 7; const MODULE_NAME = 'growthCodeId'; const GC_DATA_KEY = '_gc_data'; const ENDPOINT_URL = 'https://p2.gcprivacy.com/v1/pb?' -export const storage = getStorageManager({ gvlid: undefined, moduleName: MODULE_NAME }); +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); /** * Read GrowthCode data from cookie or local storage diff --git a/modules/hadronAnalyticsAdapter.js b/modules/hadronAnalyticsAdapter.js index 52829cf754d..0f91f938be6 100644 --- a/modules/hadronAnalyticsAdapter.js +++ b/modules/hadronAnalyticsAdapter.js @@ -3,7 +3,7 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import * as utils from '../src/utils.js'; import CONSTANTS from '../src/constants.json'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; /** @@ -14,8 +14,9 @@ const HADRON_ANALYTICS_URL = 'https://analytics.hadron.ad.gt/api/v1/analytics'; const HADRONID_ANALYTICS_VER = 'pbadgt0'; const DEFAULT_PARTNER_ID = 0; const AU_GVLID = 561; +const MODULE_CODE = 'hadronAnalytics'; -export const storage = getStorageManager(); +export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); var viewId = utils.generateUUID(); @@ -191,7 +192,7 @@ function sendEvent(event) { adapterManager.registerAnalyticsAdapter({ adapter: hadronAnalyticsAdapter, - code: 'hadronAnalytics', + code: MODULE_CODE, gvlid: AU_GVLID }); diff --git a/modules/hadronIdSystem.js b/modules/hadronIdSystem.js index a75c03ee1c4..442565bcd5b 100644 --- a/modules/hadronIdSystem.js +++ b/modules/hadronIdSystem.js @@ -6,7 +6,7 @@ */ import {ajax} from '../src/ajax.js'; -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isFn, isStr, isPlainObject, logError, logInfo} from '../src/utils.js'; @@ -15,7 +15,7 @@ const MODULE_NAME = 'hadronId'; const AU_GVLID = 561; const DEFAULT_HADRON_URL_ENDPOINT = 'https://id.hadron.ad.gt/api/v1/pbhid'; -export const storage = getStorageManager({gvlid: AU_GVLID, moduleName: 'hadron'}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: 'hadron'}); /** * Param or default. diff --git a/modules/hadronRtdProvider.js b/modules/hadronRtdProvider.js index 0bd4e6f8344..4e4a503ead0 100644 --- a/modules/hadronRtdProvider.js +++ b/modules/hadronRtdProvider.js @@ -8,7 +8,7 @@ import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_RTD} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isFn, isStr, isArray, deepEqual, isPlainObject, logError, logInfo} from '../src/utils.js'; import {loadExternalScript} from '../src/adloader.js'; @@ -21,7 +21,7 @@ const HADRON_ID_DEFAULT_URL = 'https://id.hadron.ad.gt/api/v1/hadronid?_it=prebi const HADRON_SEGMENT_URL = 'https://seg.hadron.ad.gt/api/v1/rtd'; export const HALOID_LOCAL_NAME = 'auHadronId'; export const RTD_LOCAL_NAME = 'auHadronRtd'; -export const storage = getStorageManager({gvlid: AU_GVLID, moduleName: SUBMODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME}); /** * @param {string} url @@ -251,7 +251,8 @@ function init(provider, userConsent) { export const hadronSubmodule = { name: SUBMODULE_NAME, getBidRequestData: getRealTimeData, - init: init + init: init, + gvlid: AU_GVLID, }; submodule(MODULE_NAME, hadronSubmodule); diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 488df984913..9b75d0e1812 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -18,7 +18,7 @@ import { import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {getRefererInfo} from '../src/refererDetection.js'; -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; import {uspDataHandler} from '../src/adapterManager.js'; const MODULE_NAME = 'id5Id'; @@ -34,7 +34,7 @@ const ID5_API_CONFIG_URL = 'https://id5-sync.com/api/config/prebid'; // cookie in the array is the most preferred to use const LEGACY_COOKIE_NAMES = ['pbjs-id5id', 'id5id.1st', 'id5id']; -export const storage = getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** @type {Submodule} */ export const id5IdSubmodule = { diff --git a/modules/idWardRtdProvider.js b/modules/idWardRtdProvider.js index 9678739672d..c714832c6e9 100644 --- a/modules/idWardRtdProvider.js +++ b/modules/idWardRtdProvider.js @@ -5,14 +5,14 @@ * @module modules/idWardRtdProvider * @requires module:modules/realTimeData */ -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_RTD} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isPlainObject, mergeDeep, logMessage, logError} from '../src/utils.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'idWard'; -export const storage = getStorageManager({moduleName: SUBMODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME}); /** * Add real-time data & merge segments. * @param ortb2 object to merge into diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index df7b03b4e6e..4e266e0898d 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -8,9 +8,11 @@ import * as utils from '../src/utils.js' import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; -export const storage = getStorageManager(); +const MODULE_NAME = 'identityLink'; + +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** @type {Submodule} */ export const identityLinkSubmodule = { @@ -18,7 +20,7 @@ export const identityLinkSubmodule = { * used to link submodule with config * @type {string} */ - name: 'identityLink', + name: MODULE_NAME, /** * used to specify vendor id * @type {number} diff --git a/modules/idxIdSystem.js b/modules/idxIdSystem.js index 908edad4c04..5800b7d5cd2 100644 --- a/modules/idxIdSystem.js +++ b/modules/idxIdSystem.js @@ -6,11 +6,11 @@ */ import { isStr, isPlainObject, logError } from '../src/utils.js'; import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; const IDX_MODULE_NAME = 'idx'; const IDX_COOKIE_NAME = '_idx'; -export const storage = getStorageManager(); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: IDX_MODULE_NAME}); function readIDxFromCookie() { return storage.cookiesAreEnabled ? storage.getCookie(IDX_COOKIE_NAME) : null; diff --git a/modules/imRtdProvider.js b/modules/imRtdProvider.js index bc01896d062..29b8d544865 100644 --- a/modules/imRtdProvider.js +++ b/modules/imRtdProvider.js @@ -7,7 +7,7 @@ import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {getGlobal} from '../src/prebidGlobal.js' -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_RTD} from '../src/storageManager.js'; import { deepSetValue, deepAccess, @@ -22,12 +22,13 @@ import {submodule} from '../src/hook.js'; export const imUidLocalName = '__im_uid'; export const imVidCookieName = '_im_vid'; export const imRtdLocalName = '__im_sids'; -export const storage = getStorageManager(); const submoduleName = 'im'; const segmentsMaxAge = 3600000; // 1 hour (30 * 60 * 1000) const uidMaxAge = 1800000; // 30 minites (30 * 60 * 1000) const vidMaxAge = 97200000000; // 37 months ((365 * 3 + 30) * 24 * 60 * 60 * 1000) +export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: submoduleName}); + function setImDataInCookie(value) { storage.setCookie( imVidCookieName, diff --git a/modules/imuIdSystem.js b/modules/imuIdSystem.js index 41ff95b6702..5a8685e0b8c 100644 --- a/modules/imuIdSystem.js +++ b/modules/imuIdSystem.js @@ -8,9 +8,11 @@ import { timestamp, logError } from '../src/utils.js'; import { ajax } from '../src/ajax.js' import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; -export const storage = getStorageManager(); +const MODULE_NAME = 'imuid'; + +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); export const storageKey = '__im_uid'; export const storagePpKey = '__im_ppid'; @@ -112,7 +114,7 @@ export const imuIdSubmodule = { * used to link submodule with config * @type {string} */ - name: 'imuid', + name: MODULE_NAME, /** * decode the stored id value for passing to bid requests * @function diff --git a/modules/insticatorBidAdapter.js b/modules/insticatorBidAdapter.js index 150e9d3c5c2..46ff17d2a5a 100644 --- a/modules/insticatorBidAdapter.js +++ b/modules/insticatorBidAdapter.js @@ -12,7 +12,7 @@ const USER_ID_COOKIE_EXP = 2592000000; // 30 days const BID_TTL = 300; // 5 minutes const GVLID = 910; -export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); config.setDefaults({ insticator: { diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 563435dee65..cac4cab16f1 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -8,7 +8,7 @@ import { logError, logInfo } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js' -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; const PCID_EXPIRY = 365; @@ -16,7 +16,7 @@ const MODULE_NAME = 'intentIqId'; export const FIRST_PARTY_KEY = '_iiq_fdata'; export var FIRST_PARTY_DATA_KEY = '_iiq_fdata'; -export const storage = getStorageManager({ gvlid: undefined, moduleName: MODULE_NAME }); +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); const INVALID_ID = 'INVALID_ID'; diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index c3e5cf6cca8..b2444043c22 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -17,7 +17,7 @@ const CONSTANTS = { DISABLE_USER_SYNC: true }; -const storage = getStorageManager({gvlid: CONSTANTS.INVIBES_VENDOR_ID, bidderCode: CONSTANTS.BIDDER_CODE}); +const storage = getStorageManager({bidderCode: CONSTANTS.BIDDER_CODE}); export const spec = { code: CONSTANTS.BIDDER_CODE, diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index a00fd90506a..637c512aee1 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -102,7 +102,7 @@ const VIDEO_PARAMS_ALLOW_LIST = [ const LOCAL_STORAGE_KEY = 'ixdiag'; export const LOCAL_STORAGE_FEATURE_TOGGLES_KEY = `${BIDDER_CODE}_features`; let hasRegisteredHandler = false; -export const storage = getStorageManager({ gvlid: GLOBAL_VENDOR_ID, bidderCode: BIDDER_CODE }); +export const storage = getStorageManager({bidderCode: BIDDER_CODE }); export const FEATURE_TOGGLES = { featureToggles: {}, isFeatureEnabled: function (ft) { diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index b3d5bc2af64..0c647095e24 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -11,7 +11,7 @@ const PREBID_VERSION = '$prebid.version$'; const SYNC_COUNT = 5; const GVLID = 972; const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; -const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); let sessionId, lastPageUrl, diff --git a/modules/kueezRtbBidAdapter.js b/modules/kueezRtbBidAdapter.js index 26c0d871a12..3bd468a581f 100644 --- a/modules/kueezRtbBidAdapter.js +++ b/modules/kueezRtbBidAdapter.js @@ -23,7 +23,7 @@ export const SUPPORTED_ID_SYSTEMS = { 'tdid': 1, 'pubProvidedId': 1 }; -const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); function getTopWindowQueryParams() { try { diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index 9f45daeea29..24618f2cfe3 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -9,10 +9,10 @@ import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { LiveConnect } from 'live-connect-js'; // eslint-disable-line prebid/validate-imports import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; const MODULE_NAME = 'liveIntentId'; -export const storage = getStorageManager({gvlid: null, moduleName: MODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); const defaultRequestedAttributes = {'nonId': true} const calls = { ajaxGet: (url, onSuccess, onError, timeout) => { diff --git a/modules/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js index 883c931824b..012f1764171 100644 --- a/modules/lotamePanoramaIdSystem.js +++ b/modules/lotamePanoramaIdSystem.js @@ -16,7 +16,7 @@ import { } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; import { uspDataHandler } from '../src/adapterManager.js'; const KEY_ID = 'panoramaId'; @@ -31,7 +31,7 @@ const GVLID = 95; const ID_HOST = 'id.crwdcntrl.net'; const ID_HOST_COOKIELESS = 'c.ltmsphrcl.net'; -export const storage = getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); let cookieDomain; /** diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 9d437c0b246..56a5ff58304 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -1,14 +1,32 @@ -import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, deepSetValue, deepClone, logInfo, isGptPubadsDefined } from '../src/utils.js'; +import { + debugTurnedOn, + deepAccess, + deepClone, + deepSetValue, + generateUUID, + getWindowLocation, + isAdUnitCodeMatchingSlot, + isEmpty, + isGptPubadsDefined, + isNumber, + logError, + logInfo, + logWarn, + mergeDeep, + parseQS, + parseUrl, + pick +} from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import { ajax } from '../src/ajax.js'; -import { config } from '../src/config.js'; -import { getGlobal } from '../src/prebidGlobal.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {ajax} from '../src/ajax.js'; +import {config} from '../src/config.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; const RUBICON_GVL_ID = 52; -export const storage = getStorageManager({ gvlid: RUBICON_GVL_ID, moduleName: 'magnite' }); +export const storage = getStorageManager({ moduleType: MODULE_TYPE_ANALYTICS, moduleName: 'magnite' }); const COOKIE_NAME = 'mgniSession'; const LAST_SEEN_EXPIRE_TIME = 1800000; // 30 mins const END_EXPIRE_TIME = 21600000; // 6 hours diff --git a/modules/mediafuseBidAdapter.js b/modules/mediafuseBidAdapter.js index e61c2e65c39..87347ca8d27 100644 --- a/modules/mediafuseBidAdapter.js +++ b/modules/mediafuseBidAdapter.js @@ -62,7 +62,7 @@ const SCRIPT_TAG_START = ' { adapterManager.registerAnalyticsAdapter({ adapter: analyticsAdapter, - code: 'staq' + code: MODULE_CODE, }); export default analyticsAdapter; diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index 026d5a098df..ab04c4e7390 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -24,7 +24,7 @@ const COOKIE_KEY = 'trc_cookie_storage'; * 4. new user set it to 0 */ export const userData = { - storageManager: getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}), + storageManager: getStorageManager({bidderCode: BIDDER_CODE}), getUserId: () => { const {getFromLocalStorage, getFromCookie, getFromTRC} = userData; diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index 56b21d0d4cf..f750af0f64d 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -12,7 +12,7 @@ const gdprStatus = { CMP_NOT_FOUND_OR_ERROR: 22 }; const FP_TEADS_ID_COOKIE_NAME = '_tfpvi'; -export const storage = getStorageManager({gvlid: GVL_ID, bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, diff --git a/modules/teadsIdSystem.js b/modules/teadsIdSystem.js index c18f8fb76ac..3cccd80b4a4 100644 --- a/modules/teadsIdSystem.js +++ b/modules/teadsIdSystem.js @@ -8,7 +8,7 @@ import {isStr, isNumber, logError, logInfo, isEmpty, timestamp} from '../src/utils.js' import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; import {uspDataHandler} from '../src/adapterManager.js'; const MODULE_NAME = 'teadsId'; @@ -28,7 +28,7 @@ export const gdprReason = { GDPR_APPLIES_PUBLISHER_CLASSIC: 120, }; -export const storage = getStorageManager({gvlid: GVL_ID, moduleName: MODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** @type {Submodule} */ export const teadsIdSubmodule = { diff --git a/modules/tripleliftBidAdapter.js b/modules/tripleliftBidAdapter.js index 1f654d34c6a..038cd7d757d 100644 --- a/modules/tripleliftBidAdapter.js +++ b/modules/tripleliftBidAdapter.js @@ -11,7 +11,7 @@ const BANNER_TIME_TO_LIVE = 300; const VIDEO_TIME_TO_LIVE = 3600; let gdprApplies = true; let consentString = null; -export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const tripleliftAdapterSpec = { gvlid: GVLID, diff --git a/modules/trustpidSystem.js b/modules/trustpidSystem.js index cb61ffcc8b5..d5e61d7dfac 100644 --- a/modules/trustpidSystem.js +++ b/modules/trustpidSystem.js @@ -6,13 +6,13 @@ */ import { logInfo } from '../src/utils.js'; import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; const MODULE_NAME = 'trustpid'; const LOG_PREFIX = 'Trustpid module' let mnoDomain = ''; -export const storage = getStorageManager({gvlid: null, moduleName: MODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** * Handle an event for an iframe. diff --git a/modules/uid2IdSystem.js b/modules/uid2IdSystem.js index 9fd75f12591..3dee7b2038c 100644 --- a/modules/uid2IdSystem.js +++ b/modules/uid2IdSystem.js @@ -8,7 +8,7 @@ import { logInfo } from '../src/utils.js'; import {submodule} from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; const MODULE_NAME = 'uid2'; const MODULE_REVISION = `1.0`; @@ -24,7 +24,7 @@ const UID2_PROD_URL = 'https://prod.uidapi.com'; const UID2_BASE_URL = UID2_PROD_URL; function getStorage() { - return getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME}); + return getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); } function createLogInfo(prefix) { diff --git a/modules/validationFpdModule/index.js b/modules/validationFpdModule/index.js index 8771e50b156..70af9d30ec3 100644 --- a/modules/validationFpdModule/index.js +++ b/modules/validationFpdModule/index.js @@ -5,9 +5,10 @@ import {deepAccess, isEmpty, isNumber, logWarn} from '../../src/utils.js'; import {ORTB_MAP} from './config.js'; import {submodule} from '../../src/hook.js'; -import {getStorageManager} from '../../src/storageManager.js'; +import {getCoreStorageManager} from '../../src/storageManager.js'; -const STORAGE = getStorageManager(); +// TODO: do FPD modules need their own namespace? +const STORAGE = getCoreStorageManager('FPDValidation'); let optout; /** diff --git a/modules/vidazooBidAdapter.js b/modules/vidazooBidAdapter.js index 44538e30921..ca157ed3694 100644 --- a/modules/vidazooBidAdapter.js +++ b/modules/vidazooBidAdapter.js @@ -28,7 +28,7 @@ export const SUPPORTED_ID_SYSTEMS = { 'pubProvidedId': 1 }; export const webSessionId = 'wsid_' + parseInt(Date.now() * Math.random()); -const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); function getTopWindowQueryParams() { try { diff --git a/modules/weboramaRtdProvider.js b/modules/weboramaRtdProvider.js index 12e8d4b23c8..894d86d47a0 100644 --- a/modules/weboramaRtdProvider.js +++ b/modules/weboramaRtdProvider.js @@ -119,7 +119,7 @@ import { ajax } from '../src/ajax.js'; import { - getStorageManager + getStorageManager, MODULE_TYPE_RTD } from '../src/storageManager.js'; import adapterManager from '../src/adapterManager.js'; @@ -153,7 +153,7 @@ const SFBX_LITE_DATA_SOURCE_LABEL = 'lite'; const GVLID = 284; export const storage = getStorageManager({ - gvlid: GVLID, + moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }); @@ -180,6 +180,7 @@ export const storage = getStorageManager({ class WeboramaRtdProvider { #components; name = SUBMODULE_NAME; + gvlid = GVLID; /** * @param {Components} components */ diff --git a/modules/yuktamediaAnalyticsAdapter.js b/modules/yuktamediaAnalyticsAdapter.js index 31c6daae7f6..bc8a90c5bb6 100644 --- a/modules/yuktamediaAnalyticsAdapter.js +++ b/modules/yuktamediaAnalyticsAdapter.js @@ -3,11 +3,12 @@ import {ajax} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {includes as strIncludes} from '../src/polyfill.js'; -const storage = getStorageManager(); +const MODULE_CODE = 'yuktamedia'; +const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); const yuktamediaAnalyticsVersion = 'v3.1.0'; let initOptions; @@ -260,7 +261,7 @@ yuktamediaAnalyticsAdapter.enableAnalytics = function (config) { adapterManager.registerAnalyticsAdapter({ adapter: yuktamediaAnalyticsAdapter, - code: 'yuktamedia' + code: MODULE_CODE, }); export default yuktamediaAnalyticsAdapter; diff --git a/modules/zeotapIdPlusIdSystem.js b/modules/zeotapIdPlusIdSystem.js index 3437928df4b..270f55c6651 100644 --- a/modules/zeotapIdPlusIdSystem.js +++ b/modules/zeotapIdPlusIdSystem.js @@ -6,7 +6,7 @@ */ import { isStr, isPlainObject } from '../src/utils.js'; import {submodule} from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; const ZEOTAP_COOKIE_NAME = 'IDP'; const ZEOTAP_VENDOR_ID = 301; @@ -21,7 +21,7 @@ function readFromLocalStorage() { } export function getStorage() { - return getStorageManager({gvlid: ZEOTAP_VENDOR_ID, moduleName: ZEOTAP_MODULE_NAME}); + return getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: ZEOTAP_MODULE_NAME}); } export const storage = getStorage(); diff --git a/test/spec/modules/admixerIdSystem_spec.js b/test/spec/modules/admixerIdSystem_spec.js index 18107b780db..753b1e3c2d5 100644 --- a/test/spec/modules/admixerIdSystem_spec.js +++ b/test/spec/modules/admixerIdSystem_spec.js @@ -1,9 +1,6 @@ import {admixerIdSubmodule} from 'modules/admixerIdSystem.js'; import * as utils from 'src/utils.js'; import {server} from 'test/mocks/xhr.js'; -import {getStorageManager} from '../../../src/storageManager.js'; - -export const storage = getStorageManager(); const pid = '4D393FAC-B6BB-4E19-8396-0A4813607316'; const getIdParams = {params: {pid: pid}}; diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index b787a52d6f2..150e013af98 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -13,21 +13,21 @@ describe('adnuntiusBidAdapter', function () { const meta = [{ key: 'usi', value: usi }] before(() => { - const storage = getStorageManager({ gvlid: GVLID, moduleName: 'adnuntius' }) - storage.setDataInLocalStorage('adn.metaData', JSON.stringify(meta)) - }); - - beforeEach(function () { $$PREBID_GLOBAL$$.bidderSettings = { adnuntius: { storageAllowed: true } }; + const storage = getStorageManager({ bidderCode: 'adnuntius' }) + storage.setDataInLocalStorage('adn.metaData', JSON.stringify(meta)) + }); + + after(() => { + $$PREBID_GLOBAL$$.bidderSettings = {}; }); afterEach(function () { config.resetConfig(); - $$PREBID_GLOBAL$$.bidderSettings = {}; }); const tzo = new Date().getTimezoneOffset(); diff --git a/test/spec/modules/atsAnalyticsAdapter_spec.js b/test/spec/modules/atsAnalyticsAdapter_spec.js index cae90a19223..2316f96ec8e 100644 --- a/test/spec/modules/atsAnalyticsAdapter_spec.js +++ b/test/spec/modules/atsAnalyticsAdapter_spec.js @@ -3,14 +3,14 @@ import { expect } from 'chai'; import adapterManager from 'src/adapterManager.js'; import {server} from '../../mocks/xhr.js'; import {parseBrowser} from '../../../modules/atsAnalyticsAdapter.js'; -import {getStorageManager} from '../../../src/storageManager.js'; +import {getCoreStorageManager, getStorageManager} from '../../../src/storageManager.js'; import {analyticsUrl} from '../../../modules/atsAnalyticsAdapter.js'; let utils = require('src/utils'); let events = require('src/events'); let constants = require('src/constants.json'); -export const storage = getStorageManager(); +const storage = getCoreStorageManager(); let sandbox; let clock; let now = new Date(); diff --git a/test/spec/modules/datablocksBidAdapter_spec.js b/test/spec/modules/datablocksBidAdapter_spec.js index 8e203510b10..537b82b0e2e 100644 --- a/test/spec/modules/datablocksBidAdapter_spec.js +++ b/test/spec/modules/datablocksBidAdapter_spec.js @@ -2,7 +2,6 @@ import { expect } from 'chai'; import { spec } from '../../../modules/datablocksBidAdapter.js'; import { BotClientTests } from '../../../modules/datablocksBidAdapter.js'; import { getStorageManager } from '../../../src/storageManager.js'; -export let storage = getStorageManager(); const bid = { bidId: '2dd581a2b6281d', diff --git a/test/spec/modules/growthCodeIdSystem_spec.js b/test/spec/modules/growthCodeIdSystem_spec.js index dce995d25e0..58a9d12b6f8 100644 --- a/test/spec/modules/growthCodeIdSystem_spec.js +++ b/test/spec/modules/growthCodeIdSystem_spec.js @@ -3,13 +3,13 @@ import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; import { uspDataHandler } from 'src/adapterManager.js'; import {expect} from 'chai'; -import {getStorageManager} from '../../../src/storageManager.js'; +import {getStorageManager, MODULE_TYPE_UID} from '../../../src/storageManager.js'; const GCID_EXPIRY = 45; const MODULE_NAME = 'growthCodeId'; const SHAREDID = 'fe9c5c89-7d56-4666-976d-e07e73b3b664'; -export const storage = getStorageManager({ gvlid: undefined, moduleName: MODULE_NAME }); +const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); const getIdParams = {params: { pid: 'TEST01', diff --git a/test/spec/modules/identityLinkIdSystem_spec.js b/test/spec/modules/identityLinkIdSystem_spec.js index a31270c86c7..52e9f9171d6 100644 --- a/test/spec/modules/identityLinkIdSystem_spec.js +++ b/test/spec/modules/identityLinkIdSystem_spec.js @@ -1,9 +1,9 @@ import {identityLinkSubmodule} from 'modules/identityLinkIdSystem.js'; import * as utils from 'src/utils.js'; import {server} from 'test/mocks/xhr.js'; -import {getStorageManager} from '../../../src/storageManager.js'; +import {getCoreStorageManager} from '../../../src/storageManager.js'; -export const storage = getStorageManager(); +const storage = getCoreStorageManager(); const pid = '14'; let defaultConfigParams; diff --git a/test/spec/modules/publinkIdSystem_spec.js b/test/spec/modules/publinkIdSystem_spec.js index 4656afe1585..7d98b724bd8 100644 --- a/test/spec/modules/publinkIdSystem_spec.js +++ b/test/spec/modules/publinkIdSystem_spec.js @@ -1,11 +1,12 @@ import {publinkIdSubmodule} from 'modules/publinkIdSystem.js'; -import {getStorageManager} from '../../../src/storageManager'; +import {getCoreStorageManager, getStorageManager} from '../../../src/storageManager'; import {server} from 'test/mocks/xhr.js'; import sinon from 'sinon'; import {uspDataHandler} from '../../../src/adapterManager'; import {parseUrl} from '../../../src/utils'; -export const storage = getStorageManager({gvlid: 24}); +const storage = getCoreStorageManager(); + const TEST_COOKIE_VALUE = 'cookievalue'; describe('PublinkIdSystem', () => { describe('decode', () => { diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index 6a6e79c633d..0f2f9abd583 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -1,13 +1,13 @@ -import { expect } from 'chai'; -import { spec } from 'modules/relaidoBidAdapter.js'; +import {expect} from 'chai'; +import {spec} from 'modules/relaidoBidAdapter.js'; import * as utils from 'src/utils.js'; -import { BANNER, VIDEO } from 'src/mediaTypes.js'; -import { getStorageManager } from '../../../src/storageManager.js'; +import {VIDEO} from 'src/mediaTypes.js'; +import {getCoreStorageManager} from '../../../src/storageManager.js'; const UUID_KEY = 'relaido_uuid'; const relaido_uuid = 'hogehoge'; -const storage = getStorageManager(); +const storage = getCoreStorageManager(); storage.setCookie(UUID_KEY, relaido_uuid); describe('RelaidoAdapter', function () { diff --git a/test/spec/modules/zeotapIdPlusIdSystem_spec.js b/test/spec/modules/zeotapIdPlusIdSystem_spec.js index 6494a7cbfef..be9294e38c9 100644 --- a/test/spec/modules/zeotapIdPlusIdSystem_spec.js +++ b/test/spec/modules/zeotapIdPlusIdSystem_spec.js @@ -4,6 +4,7 @@ import { config } from 'src/config.js'; import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; import { storage, getStorage, zeotapIdPlusSubmodule } from 'modules/zeotapIdPlusIdSystem.js'; import * as storageManager from 'src/storageManager.js'; +import {MODULE_TYPE_UID} from 'src/storageManager.js'; const ZEOTAP_COOKIE_NAME = 'IDP'; const ZEOTAP_COOKIE = 'THIS-IS-A-DUMMY-COOKIE'; @@ -52,9 +53,9 @@ describe('Zeotap ID System', function() { }); it('when a stored Zeotap ID exists it is added to bids', function() { - let store = getStorage(); + getStorage(); expect(getStorageManagerSpy.calledOnce).to.be.true; - sinon.assert.calledWith(getStorageManagerSpy, {gvlid: 301, moduleName: 'zeotapIdPlus'}); + sinon.assert.calledWith(getStorageManagerSpy, {moduleType: MODULE_TYPE_UID, moduleName: 'zeotapIdPlus'}); }); }); From 8ee280c0503788395e11c194e2d030de699b3d38 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 29 Mar 2023 11:57:58 -0700 Subject: [PATCH 05/37] GVL ID registry --- modules/adkernelAdnAnalyticsAdapter.js | 3 +- modules/admixerIdSystem.js | 3 +- modules/adqueryIdSystem.js | 3 +- modules/adriverIdSystem.js | 3 +- modules/airgridRtdProvider.js | 3 +- modules/akamaiDapRtdProvider.js | 3 +- modules/atsAnalyticsAdapter.js | 3 +- modules/blueconicRtdProvider.js | 3 +- modules/browsiRtdProvider.js | 3 +- modules/byDataAnalyticsAdapter.js | 3 +- modules/criteoIdSystem.js | 3 +- modules/czechAdIdSystem.js | 3 +- modules/dacIdSystem.js | 3 +- modules/deepintentDpesIdSystem.js | 3 +- modules/fintezaAnalyticsAdapter.js | 3 +- modules/ftrackIdSystem.js | 3 +- modules/gravitoIdSystem.js | 3 +- modules/growthCodeAnalyticsAdapter.js | 3 +- modules/growthCodeIdSystem.js | 3 +- modules/hadronAnalyticsAdapter.js | 3 +- modules/hadronIdSystem.js | 3 +- modules/hadronRtdProvider.js | 3 +- modules/id5IdSystem.js | 3 +- modules/idWardRtdProvider.js | 3 +- modules/identityLinkIdSystem.js | 3 +- modules/idxIdSystem.js | 3 +- modules/imRtdProvider.js | 3 +- modules/imuIdSystem.js | 3 +- modules/intentIqIdSystem.js | 3 +- modules/liveIntentIdSystem.js | 3 +- modules/lotamePanoramaIdSystem.js | 3 +- modules/magniteAnalyticsAdapter.js | 3 +- modules/merkleIdSystem.js | 3 +- modules/mgidRtdProvider.js | 3 +- modules/mwOpenLinkIdSystem.js | 3 +- modules/naveggIdSystem.js | 3 +- modules/novatiqIdSystem.js | 3 +- modules/parrableIdSystem.js | 3 +- modules/permutiveRtdProvider.js | 3 +- modules/prebidmanagerAnalyticsAdapter.js | 3 +- modules/pubCommonId.js | 3 +- modules/publinkIdSystem.js | 3 +- modules/pubwiseAnalyticsAdapter.js | 3 +- modules/quantcastIdSystem.js | 3 +- modules/realvuAnalyticsAdapter.js | 3 +- modules/roxotAnalyticsAdapter.js | 3 +- modules/rtdModule/index.js | 3 ++ modules/rubiconAnalyticsAdapter.js | 3 +- modules/sharedIdSystem.js | 3 +- modules/sigmoidAnalyticsAdapter.js | 3 +- modules/staqAnalyticsAdapter.js | 3 +- modules/teadsIdSystem.js | 3 +- modules/trustpidSystem.js | 3 +- modules/uid2IdSystem.js | 3 +- modules/userId/index.js | 3 ++ modules/weboramaRtdProvider.js | 3 +- modules/yuktamediaAnalyticsAdapter.js | 3 +- modules/zeotapIdPlusIdSystem.js | 3 +- src/activities/modules.js | 5 ++++ src/adapterManager.js | 5 +++- src/adapters/bidderFactory.js | 2 +- src/consentHandler.js | 16 +++++++++++ src/storageManager.js | 7 +---- test/spec/modules/growthCodeIdSystem_spec.js | 3 +- test/spec/modules/realTimeDataModule_spec.js | 22 +++++++++++++++ test/spec/modules/userId_spec.js | 16 +++++++++++ .../spec/modules/zeotapIdPlusIdSystem_spec.js | 2 +- test/spec/unit/core/adapterManager_spec.js | 21 ++++++++++++++ test/spec/unit/core/consentHandler_spec.js | 28 ++++++++++++++++++- test/spec/unit/core/storageManager_spec.js | 2 +- 70 files changed, 235 insertions(+), 68 deletions(-) create mode 100644 src/activities/modules.js diff --git a/modules/adkernelAdnAnalyticsAdapter.js b/modules/adkernelAdnAnalyticsAdapter.js index cb5a8cf837c..48897f8516b 100644 --- a/modules/adkernelAdnAnalyticsAdapter.js +++ b/modules/adkernelAdnAnalyticsAdapter.js @@ -3,8 +3,9 @@ import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; import { logError, parseUrl, _each } from '../src/utils.js'; import {ajax} from '../src/ajax.js'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {config} from '../src/config.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_CODE = 'adkernelAdn'; const GVLID = 14; diff --git a/modules/admixerIdSystem.js b/modules/admixerIdSystem.js index 30363247784..7fbebecfc12 100644 --- a/modules/admixerIdSystem.js +++ b/modules/admixerIdSystem.js @@ -8,7 +8,8 @@ import { logError, logInfo } from '../src/utils.js' import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const NAME = 'admixerId'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: NAME}); diff --git a/modules/adqueryIdSystem.js b/modules/adqueryIdSystem.js index 24902c7470c..5171802caba 100644 --- a/modules/adqueryIdSystem.js +++ b/modules/adqueryIdSystem.js @@ -6,9 +6,10 @@ */ import {ajax} from '../src/ajax.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import { isFn, isStr, isPlainObject, logError } from '../src/utils.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'qid'; const AU_GVLID = 902; diff --git a/modules/adriverIdSystem.js b/modules/adriverIdSystem.js index c984ab180de..b3ab00350ea 100644 --- a/modules/adriverIdSystem.js +++ b/modules/adriverIdSystem.js @@ -8,7 +8,8 @@ import { logError, isPlainObject } from '../src/utils.js' import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'adriverId'; diff --git a/modules/airgridRtdProvider.js b/modules/airgridRtdProvider.js index d34e9fb524e..3119e624d82 100644 --- a/modules/airgridRtdProvider.js +++ b/modules/airgridRtdProvider.js @@ -13,7 +13,8 @@ import { deepAccess, } from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; -import {getStorageManager, MODULE_TYPE_RTD} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'airgrid'; diff --git a/modules/akamaiDapRtdProvider.js b/modules/akamaiDapRtdProvider.js index f20c329899a..f0bb7eb3a6c 100644 --- a/modules/akamaiDapRtdProvider.js +++ b/modules/akamaiDapRtdProvider.js @@ -6,10 +6,11 @@ * @requires module:modules/realTimeData */ import {ajax} from '../src/ajax.js'; -import {getStorageManager, MODULE_TYPE_RTD} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isPlainObject, mergeDeep, logMessage, logInfo, logError} from '../src/utils.js'; import { loadExternalScript } from '../src/adloader.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'dap'; diff --git a/modules/atsAnalyticsAdapter.js b/modules/atsAnalyticsAdapter.js index 15a3fdbbbdb..8c1d1cfc1da 100644 --- a/modules/atsAnalyticsAdapter.js +++ b/modules/atsAnalyticsAdapter.js @@ -3,7 +3,8 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adaptermanager from '../src/adapterManager.js'; import {ajax} from '../src/ajax.js'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_CODE = 'atsAnalytics'; export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); diff --git a/modules/blueconicRtdProvider.js b/modules/blueconicRtdProvider.js index 9c90ed3a933..b6eb9374671 100644 --- a/modules/blueconicRtdProvider.js +++ b/modules/blueconicRtdProvider.js @@ -6,9 +6,10 @@ * @requires module:modules/realTimeData */ -import {getStorageManager, MODULE_TYPE_RTD} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {mergeDeep, isPlainObject, logMessage, logError} from '../src/utils.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'blueconic'; diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index c8373127579..4a61f40600d 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -19,11 +19,12 @@ import {deepClone, deepSetValue, isFn, isGptPubadsDefined, isNumber, logError, l import {submodule} from '../src/hook.js'; import {ajaxBuilder} from '../src/ajax.js'; import {loadExternalScript} from '../src/adloader.js'; -import {getStorageManager, MODULE_TYPE_RTD} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {find, includes} from '../src/polyfill.js'; import {getGlobal} from '../src/prebidGlobal.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'browsi'; const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: MODULE_NAME}); diff --git a/modules/byDataAnalyticsAdapter.js b/modules/byDataAnalyticsAdapter.js index fc6809fc345..81fd4388c7d 100644 --- a/modules/byDataAnalyticsAdapter.js +++ b/modules/byDataAnalyticsAdapter.js @@ -5,9 +5,10 @@ import enc from 'crypto-js/enc-utf8'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import { auctionManager } from '../src/auctionManager.js'; import { ajax } from '../src/ajax.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const versionCode = '4.4.1' const secretKey = 'bydata@123456' diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index 27e4ddf60de..f1d4e8be064 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -9,7 +9,8 @@ import { timestamp, parseUrl, triggerPixel, logError } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { getRefererInfo } from '../src/refererDetection.js'; import { submodule } from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const gvlid = 91; const bidderCode = 'criteo'; diff --git a/modules/czechAdIdSystem.js b/modules/czechAdIdSystem.js index 20f8be3ea57..957b3ed30bd 100644 --- a/modules/czechAdIdSystem.js +++ b/modules/czechAdIdSystem.js @@ -6,7 +6,8 @@ */ import { submodule } from '../src/hook.js' -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; // Returns StorageManager export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: 'czechAdId' }) diff --git a/modules/dacIdSystem.js b/modules/dacIdSystem.js index e5a02f69c8f..5adca074c87 100644 --- a/modules/dacIdSystem.js +++ b/modules/dacIdSystem.js @@ -17,8 +17,9 @@ import { submodule } from '../src/hook.js'; import { - getStorageManager, MODULE_TYPE_UID + getStorageManager } from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'dacId'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); diff --git a/modules/deepintentDpesIdSystem.js b/modules/deepintentDpesIdSystem.js index 4d73d2aabf1..f2b50d535eb 100644 --- a/modules/deepintentDpesIdSystem.js +++ b/modules/deepintentDpesIdSystem.js @@ -6,7 +6,8 @@ */ import { submodule } from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'deepintentId'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); diff --git a/modules/fintezaAnalyticsAdapter.js b/modules/fintezaAnalyticsAdapter.js index 447fce24d2f..be661c96061 100644 --- a/modules/fintezaAnalyticsAdapter.js +++ b/modules/fintezaAnalyticsAdapter.js @@ -2,8 +2,9 @@ import { parseUrl, logError } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import CONSTANTS from '../src/constants.json'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_CODE = 'finteza'; const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js index 569a4a7d662..9777883683e 100644 --- a/modules/ftrackIdSystem.js +++ b/modules/ftrackIdSystem.js @@ -7,9 +7,10 @@ import * as utils from '../src/utils.js'; import { submodule } from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import { uspDataHandler } from '../src/adapterManager.js'; import { loadExternalScript } from '../src/adloader.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'ftrackId'; const LOG_PREFIX = 'FTRACK - '; diff --git a/modules/gravitoIdSystem.js b/modules/gravitoIdSystem.js index 00f3408a4cc..aa25ea7db2c 100644 --- a/modules/gravitoIdSystem.js +++ b/modules/gravitoIdSystem.js @@ -6,7 +6,8 @@ */ import { submodule } from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'gravitompId'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); diff --git a/modules/growthCodeAnalyticsAdapter.js b/modules/growthCodeAnalyticsAdapter.js index c9b5ef4ca84..a2ab4ddbfac 100644 --- a/modules/growthCodeAnalyticsAdapter.js +++ b/modules/growthCodeAnalyticsAdapter.js @@ -6,9 +6,10 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import * as utils from '../src/utils.js'; import CONSTANTS from '../src/constants.json'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {logError, logInfo} from '../src/utils.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_NAME = 'growthCodeAnalytics'; const DEFAULT_PID = 'INVALID_PID' diff --git a/modules/growthCodeIdSystem.js b/modules/growthCodeIdSystem.js index f49abbd3e44..739cea07489 100644 --- a/modules/growthCodeIdSystem.js +++ b/modules/growthCodeIdSystem.js @@ -8,7 +8,8 @@ import {logError, logInfo, tryAppendQueryString} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import { submodule } from '../src/hook.js' -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const GCID_EXPIRY = 7; const MODULE_NAME = 'growthCodeId'; diff --git a/modules/hadronAnalyticsAdapter.js b/modules/hadronAnalyticsAdapter.js index 0f91f938be6..e4c09c5b6c9 100644 --- a/modules/hadronAnalyticsAdapter.js +++ b/modules/hadronAnalyticsAdapter.js @@ -3,8 +3,9 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import * as utils from '../src/utils.js'; import CONSTANTS from '../src/constants.json'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; /** * hadronAnalyticsAdapter.js - Audigent Hadron Analytics Adapter diff --git a/modules/hadronIdSystem.js b/modules/hadronIdSystem.js index 442565bcd5b..a42eed8a6e0 100644 --- a/modules/hadronIdSystem.js +++ b/modules/hadronIdSystem.js @@ -6,9 +6,10 @@ */ import {ajax} from '../src/ajax.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isFn, isStr, isPlainObject, logError, logInfo} from '../src/utils.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const HADRONID_LOCAL_NAME = 'auHadronId'; const MODULE_NAME = 'hadronId'; diff --git a/modules/hadronRtdProvider.js b/modules/hadronRtdProvider.js index 4e4a503ead0..c023f6c0c41 100644 --- a/modules/hadronRtdProvider.js +++ b/modules/hadronRtdProvider.js @@ -8,10 +8,11 @@ import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import {getStorageManager, MODULE_TYPE_RTD} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isFn, isStr, isArray, deepEqual, isPlainObject, logError, logInfo} from '../src/utils.js'; import {loadExternalScript} from '../src/adloader.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const LOG_PREFIX = 'User ID - HadronRtdProvider submodule: '; const MODULE_NAME = 'realTimeData'; diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 9b75d0e1812..970a8bedefa 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -18,8 +18,9 @@ import { import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {getRefererInfo} from '../src/refererDetection.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {uspDataHandler} from '../src/adapterManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'id5Id'; const GVLID = 131; diff --git a/modules/idWardRtdProvider.js b/modules/idWardRtdProvider.js index c714832c6e9..29dda216fdc 100644 --- a/modules/idWardRtdProvider.js +++ b/modules/idWardRtdProvider.js @@ -5,9 +5,10 @@ * @module modules/idWardRtdProvider * @requires module:modules/realTimeData */ -import {getStorageManager, MODULE_TYPE_RTD} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isPlainObject, mergeDeep, logMessage, logError} from '../src/utils.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'idWard'; diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index 4e266e0898d..ab10288f38f 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -8,7 +8,8 @@ import * as utils from '../src/utils.js' import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'identityLink'; diff --git a/modules/idxIdSystem.js b/modules/idxIdSystem.js index 5800b7d5cd2..3c00bbde34c 100644 --- a/modules/idxIdSystem.js +++ b/modules/idxIdSystem.js @@ -6,7 +6,8 @@ */ import { isStr, isPlainObject, logError } from '../src/utils.js'; import { submodule } from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const IDX_MODULE_NAME = 'idx'; const IDX_COOKIE_NAME = '_idx'; diff --git a/modules/imRtdProvider.js b/modules/imRtdProvider.js index 29b8d544865..26d49c49f8c 100644 --- a/modules/imRtdProvider.js +++ b/modules/imRtdProvider.js @@ -7,7 +7,7 @@ import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {getGlobal} from '../src/prebidGlobal.js' -import {getStorageManager, MODULE_TYPE_RTD} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import { deepSetValue, deepAccess, @@ -18,6 +18,7 @@ import { isFn } from '../src/utils.js' import {submodule} from '../src/hook.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; export const imUidLocalName = '__im_uid'; export const imVidCookieName = '_im_vid'; diff --git a/modules/imuIdSystem.js b/modules/imuIdSystem.js index 5a8685e0b8c..898f32b27b0 100644 --- a/modules/imuIdSystem.js +++ b/modules/imuIdSystem.js @@ -8,7 +8,8 @@ import { timestamp, logError } from '../src/utils.js'; import { ajax } from '../src/ajax.js' import { submodule } from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'imuid'; diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index cac4cab16f1..b16624ac368 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -8,7 +8,8 @@ import { logError, logInfo } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js' -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const PCID_EXPIRY = 365; diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index 24618f2cfe3..8dc81b11f39 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -9,7 +9,8 @@ import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { LiveConnect } from 'live-connect-js'; // eslint-disable-line prebid/validate-imports import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'liveIntentId'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); diff --git a/modules/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js index 012f1764171..02b01b8bd9d 100644 --- a/modules/lotamePanoramaIdSystem.js +++ b/modules/lotamePanoramaIdSystem.js @@ -16,8 +16,9 @@ import { } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import { uspDataHandler } from '../src/adapterManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const KEY_ID = 'panoramaId'; const KEY_EXPIRY = `${KEY_ID}_expiry`; diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 56a5ff58304..afe73c097fb 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -23,7 +23,8 @@ import CONSTANTS from '../src/constants.json'; import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const RUBICON_GVL_ID = 52; export const storage = getStorageManager({ moduleType: MODULE_TYPE_ANALYTICS, moduleName: 'magnite' }); diff --git a/modules/merkleIdSystem.js b/modules/merkleIdSystem.js index a637ca98e9a..c522d588970 100644 --- a/modules/merkleIdSystem.js +++ b/modules/merkleIdSystem.js @@ -8,7 +8,8 @@ import { logInfo, logError, logWarn } from '../src/utils.js'; import * as ajaxLib from '../src/ajax.js'; import {submodule} from '../src/hook.js' -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'merkleId'; const ID_URL = 'https://prebid.sv.rkdms.com/identity/'; diff --git a/modules/mgidRtdProvider.js b/modules/mgidRtdProvider.js index de9670f7c53..fd2c0bbe6fd 100644 --- a/modules/mgidRtdProvider.js +++ b/modules/mgidRtdProvider.js @@ -1,8 +1,9 @@ import { submodule } from '../src/hook.js'; import {ajax} from '../src/ajax.js'; import {deepAccess, logError, logInfo, mergeDeep} from '../src/utils.js'; -import {getStorageManager, MODULE_TYPE_RTD} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'mgid'; diff --git a/modules/mwOpenLinkIdSystem.js b/modules/mwOpenLinkIdSystem.js index 05ec8e6a055..9b1035cbf18 100644 --- a/modules/mwOpenLinkIdSystem.js +++ b/modules/mwOpenLinkIdSystem.js @@ -8,7 +8,8 @@ import { timestamp, logError, deepClone, generateUUID, isPlainObject } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const openLinkID = { name: 'mwol', diff --git a/modules/naveggIdSystem.js b/modules/naveggIdSystem.js index fcf74ce83af..878ae7fadb2 100644 --- a/modules/naveggIdSystem.js +++ b/modules/naveggIdSystem.js @@ -7,7 +7,8 @@ import { isStr, isPlainObject, logError } from '../src/utils.js'; import { submodule } from '../src/hook.js'; import { ajax } from '../src/ajax.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'naveggId'; const OLD_NAVEGG_ID = 'nid'; diff --git a/modules/novatiqIdSystem.js b/modules/novatiqIdSystem.js index a5a24523cee..7a801a945ae 100644 --- a/modules/novatiqIdSystem.js +++ b/modules/novatiqIdSystem.js @@ -8,7 +8,8 @@ import { logInfo, getWindowLocation } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'novatiq'; diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index e47475e7c37..01ffec3c249 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -13,7 +13,8 @@ import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {uspDataHandler} from '../src/adapterManager.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const PARRABLE_URL = 'https://h.parrable.com/prebid'; const PARRABLE_COOKIE_NAME = '_parrable_id'; diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index 05d115d8683..6d5b91c0e82 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -7,9 +7,10 @@ */ import {getGlobal} from '../src/prebidGlobal.js'; import {submodule} from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_RTD} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {deepAccess, deepSetValue, isFn, logError, mergeDeep, isPlainObject, safeJSONParse, prefixLog} from '../src/utils.js'; import {includes} from '../src/polyfill.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'permutive' diff --git a/modules/prebidmanagerAnalyticsAdapter.js b/modules/prebidmanagerAnalyticsAdapter.js index 2833df7110d..b877918d16d 100644 --- a/modules/prebidmanagerAnalyticsAdapter.js +++ b/modules/prebidmanagerAnalyticsAdapter.js @@ -2,8 +2,9 @@ import { generateUUID, getParameterByName, logError, parseUrl, logInfo } from '. import {ajaxBuilder} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import CONSTANTS from '../src/constants.json'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; /** * prebidmanagerAnalyticsAdapter.js - analytics adapter for prebidmanager diff --git a/modules/pubCommonId.js b/modules/pubCommonId.js index 7a6c2bbb659..1854cf25428 100644 --- a/modules/pubCommonId.js +++ b/modules/pubCommonId.js @@ -7,9 +7,10 @@ import { logMessage, parseUrl, buildUrl, triggerPixel, generateUUID, isArray } f import { config } from '../src/config.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {timedAuctionHook} from '../src/utils/perfMetrics.js'; import {VENDORLESS_GVLID} from '../src/consentHandler.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: 'pubCommonId'}); diff --git a/modules/publinkIdSystem.js b/modules/publinkIdSystem.js index c924753835e..6a5504b5ba0 100644 --- a/modules/publinkIdSystem.js +++ b/modules/publinkIdSystem.js @@ -6,10 +6,11 @@ */ import {submodule} from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import { parseUrl, buildUrl, logError } from '../src/utils.js'; import {uspDataHandler} from '../src/adapterManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'publinkId'; const GVLID = 24; diff --git a/modules/pubwiseAnalyticsAdapter.js b/modules/pubwiseAnalyticsAdapter.js index 9a536c97d40..6aed462f2d5 100644 --- a/modules/pubwiseAnalyticsAdapter.js +++ b/modules/pubwiseAnalyticsAdapter.js @@ -3,7 +3,8 @@ import {ajax} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_CODE = 'pubwise'; const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index eea309bc6fe..6a07082b61c 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -6,9 +6,10 @@ */ import {submodule} from '../src/hook.js' -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import { triggerPixel, logInfo } from '../src/utils.js'; import { uspDataHandler, coppaDataHandler, gdprDataHandler } from '../src/adapterManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const QUANTCAST_FPA = '__qca'; const DEFAULT_COOKIE_EXP_DAYS = 392; // (13 months - 2 days) diff --git a/modules/realvuAnalyticsAdapter.js b/modules/realvuAnalyticsAdapter.js index 2866d42e2d3..e4bcf1b474a 100644 --- a/modules/realvuAnalyticsAdapter.js +++ b/modules/realvuAnalyticsAdapter.js @@ -2,8 +2,9 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import { logMessage, logError } from '../src/utils.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_CODE = 'realvuAnalytics'; const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); diff --git a/modules/roxotAnalyticsAdapter.js b/modules/roxotAnalyticsAdapter.js index fa3730405cc..2c3be3e1757 100644 --- a/modules/roxotAnalyticsAdapter.js +++ b/modules/roxotAnalyticsAdapter.js @@ -4,7 +4,8 @@ import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; import {includes} from '../src/polyfill.js'; import {ajaxBuilder} from '../src/ajax.js'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_CODE = 'roxot'; diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 05594c63132..29e2ce3de43 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -166,6 +166,8 @@ import CONSTANTS from '../../src/constants.json'; import adapterManager, {gdprDataHandler, uspDataHandler, gppDataHandler} from '../../src/adapterManager.js'; import {find} from '../../src/polyfill.js'; import {timedAuctionHook} from '../../src/utils/perfMetrics.js'; +import {GDPR_GVLIDS} from '../../src/consentHandler.js'; +import {MODULE_TYPE_RTD} from '../../src/activities/modules.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -188,6 +190,7 @@ let _userConsent; */ export function attachRealTimeDataProvider(submodule) { registeredSubModules.push(submodule); + GDPR_GVLIDS.register(MODULE_TYPE_RTD, submodule.name, submodule.gvlid) return function detach() { const idx = registeredSubModules.indexOf(submodule) if (idx >= 0) { diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index d0df446c23a..7bbc435cb27 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -5,7 +5,8 @@ import CONSTANTS from '../src/constants.json'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import { getGlobal } from '../src/prebidGlobal.js'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const RUBICON_GVL_ID = 52; export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: 'rubicon'}); diff --git a/modules/sharedIdSystem.js b/modules/sharedIdSystem.js index 87cfa299cd0..2deba1fb500 100644 --- a/modules/sharedIdSystem.js +++ b/modules/sharedIdSystem.js @@ -8,8 +8,9 @@ import { parseUrl, buildUrl, triggerPixel, logInfo, hasDeviceAccess, generateUUID } from '../src/utils.js'; import {submodule} from '../src/hook.js'; import { coppaDataHandler } from '../src/adapterManager.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {VENDORLESS_GVLID} from '../src/consentHandler.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: 'pubCommonId'}); const COOKIE = 'cookie'; diff --git a/modules/sigmoidAnalyticsAdapter.js b/modules/sigmoidAnalyticsAdapter.js index 67b79895c30..18e1e20e3e3 100644 --- a/modules/sigmoidAnalyticsAdapter.js +++ b/modules/sigmoidAnalyticsAdapter.js @@ -4,8 +4,9 @@ import {includes} from '../src/polyfill.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {generateUUID, logError, logInfo} from '../src/utils.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_CODE = 'sigmoid'; const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); diff --git a/modules/staqAnalyticsAdapter.js b/modules/staqAnalyticsAdapter.js index 5f4fceafe7b..c1aaa727af5 100644 --- a/modules/staqAnalyticsAdapter.js +++ b/modules/staqAnalyticsAdapter.js @@ -4,7 +4,8 @@ import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; import { getRefererInfo } from '../src/refererDetection.js'; import { ajax } from '../src/ajax.js'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_CODE = 'staq'; const storageObj = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); diff --git a/modules/teadsIdSystem.js b/modules/teadsIdSystem.js index 3cccd80b4a4..b4067bf75c3 100644 --- a/modules/teadsIdSystem.js +++ b/modules/teadsIdSystem.js @@ -8,8 +8,9 @@ import {isStr, isNumber, logError, logInfo, isEmpty, timestamp} from '../src/utils.js' import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {uspDataHandler} from '../src/adapterManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'teadsId'; const GVL_ID = 132; diff --git a/modules/trustpidSystem.js b/modules/trustpidSystem.js index d5e61d7dfac..1d971b3c813 100644 --- a/modules/trustpidSystem.js +++ b/modules/trustpidSystem.js @@ -6,7 +6,8 @@ */ import { logInfo } from '../src/utils.js'; import { submodule } from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'trustpid'; const LOG_PREFIX = 'Trustpid module' diff --git a/modules/uid2IdSystem.js b/modules/uid2IdSystem.js index 3dee7b2038c..2ffab8cc68f 100644 --- a/modules/uid2IdSystem.js +++ b/modules/uid2IdSystem.js @@ -8,7 +8,8 @@ import { logInfo } from '../src/utils.js'; import {submodule} from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'uid2'; const MODULE_REVISION = `1.0`; diff --git a/modules/userId/index.js b/modules/userId/index.js index 4c8d4c0be38..8946777077f 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -157,6 +157,8 @@ import {hasPurpose1Consent} from '../../src/utils/gpdr.js'; import {registerOrtbProcessor, REQUEST} from '../../src/pbjsORTB.js'; import {newMetrics, timedAuctionHook, useMetrics} from '../../src/utils/perfMetrics.js'; import {findRootDomain} from '../../src/fpd/rootDomain.js'; +import {GDPR_GVLIDS} from '../../src/consentHandler.js'; +import {MODULE_TYPE_UID} from '../../src/activities/modules.js'; const MODULE_NAME = 'User ID'; const COOKIE = STORAGE_TYPE_COOKIES; @@ -1019,6 +1021,7 @@ export function requestDataDeletion(next, ...args) { export function attachIdSystem(submodule) { if (!find(submoduleRegistry, i => i.name === submodule.name)) { submoduleRegistry.push(submodule); + GDPR_GVLIDS.register(MODULE_TYPE_UID, submodule.name, submodule.gvlid) updateSubmodules(); // TODO: a test case wants this to work even if called after init (the setConfig({userId})) // so we trigger a refresh. But is that even possible outside of tests? diff --git a/modules/weboramaRtdProvider.js b/modules/weboramaRtdProvider.js index 894d86d47a0..7e5b21de5a6 100644 --- a/modules/weboramaRtdProvider.js +++ b/modules/weboramaRtdProvider.js @@ -119,9 +119,10 @@ import { ajax } from '../src/ajax.js'; import { - getStorageManager, MODULE_TYPE_RTD + getStorageManager } from '../src/storageManager.js'; import adapterManager from '../src/adapterManager.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; diff --git a/modules/yuktamediaAnalyticsAdapter.js b/modules/yuktamediaAnalyticsAdapter.js index bc8a90c5bb6..fe460bd5d76 100644 --- a/modules/yuktamediaAnalyticsAdapter.js +++ b/modules/yuktamediaAnalyticsAdapter.js @@ -3,9 +3,10 @@ import {ajax} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import {getStorageManager, MODULE_TYPE_ANALYTICS} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {includes as strIncludes} from '../src/polyfill.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_CODE = 'yuktamedia'; const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); diff --git a/modules/zeotapIdPlusIdSystem.js b/modules/zeotapIdPlusIdSystem.js index 270f55c6651..4cb827cbdff 100644 --- a/modules/zeotapIdPlusIdSystem.js +++ b/modules/zeotapIdPlusIdSystem.js @@ -6,7 +6,8 @@ */ import { isStr, isPlainObject } from '../src/utils.js'; import {submodule} from '../src/hook.js'; -import {getStorageManager, MODULE_TYPE_UID} from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const ZEOTAP_COOKIE_NAME = 'IDP'; const ZEOTAP_VENDOR_ID = 301; diff --git a/src/activities/modules.js b/src/activities/modules.js new file mode 100644 index 00000000000..d140b10387f --- /dev/null +++ b/src/activities/modules.js @@ -0,0 +1,5 @@ +export const MODULE_TYPE_CORE = 'core'; +export const MODULE_TYPE_BIDDER = 'bidder'; +export const MODULE_TYPE_UID = 'userId'; +export const MODULE_TYPE_RTD = 'rtd'; +export const MODULE_TYPE_ANALYTICS = 'analytics'; diff --git a/src/adapterManager.js b/src/adapterManager.js index c9715fe0168..0798eb0bf8c 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -31,11 +31,12 @@ import {hook} from './hook.js'; import {find, includes} from './polyfill.js'; import {adunitCounter} from './adUnits.js'; import {getRefererInfo} from './refererDetection.js'; -import {GdprConsentHandler, UspConsentHandler, GppConsentHandler} from './consentHandler.js'; +import {GdprConsentHandler, UspConsentHandler, GppConsentHandler, GDPR_GVLIDS} from './consentHandler.js'; import * as events from './events.js'; import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; import {auctionManager} from './auctionManager.js'; +import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER} from './activities/modules.js'; export const PARTITIONS = { CLIENT: 'client', @@ -470,6 +471,7 @@ adapterManager.registerBidAdapter = function (bidAdapter, bidderCode, {supported if (bidAdapter && bidderCode) { if (typeof bidAdapter.callBids === 'function') { _bidderRegistry[bidderCode] = bidAdapter; + GDPR_GVLIDS.register(MODULE_TYPE_BIDDER, bidderCode, bidAdapter.getSpec?.().gvlid); if (includes(supportedMediaTypes, 'video')) { adapterManager.videoAdapters.push(bidderCode); @@ -539,6 +541,7 @@ adapterManager.registerAnalyticsAdapter = function ({adapter, code, gvlid}) { if (typeof adapter.enableAnalytics === 'function') { adapter.code = code; _analyticsRegistry[code] = { adapter, gvlid }; + GDPR_GVLIDS.register(MODULE_TYPE_ANALYTICS, code, gvlid); } else { logError(`Prebid Error: Analytics adaptor error for analytics "${code}" analytics adapter must implement an enableAnalytics() function`); diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 55c84e57062..8faa862cf4b 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -187,7 +187,7 @@ export function registerBidder(spec) { export function newBidder(spec) { return Object.assign(new Adapter(spec.code), { getSpec: function() { - return Object.freeze(spec); + return Object.freeze(Object.assign({}, spec)); }, registerSyncs, callBids: function(bidderRequest, addBidResponse, done, ajax, onTimelyResponse, configEnabledCallback) { diff --git a/src/consentHandler.js b/src/consentHandler.js index b1b2a04c043..c9f0bb0c5ec 100644 --- a/src/consentHandler.js +++ b/src/consentHandler.js @@ -118,3 +118,19 @@ export class GppConsentHandler extends ConsentHandler { } } } + +export function gvlidRegistry() { + const registry = {}; + return { + register(moduleType, moduleName, gvlid) { + if (gvlid) { + (registry[moduleType] = registry[moduleType] || {})[moduleName] = gvlid; + } + }, + get(moduleType, moduleName) { + return registry[moduleType]?.[moduleName]; + } + } +} + +export const GDPR_GVLIDS = gvlidRegistry(); diff --git a/src/storageManager.js b/src/storageManager.js index 3bc70e97052..0248237fbc4 100644 --- a/src/storageManager.js +++ b/src/storageManager.js @@ -1,12 +1,7 @@ import {hook} from './hook.js'; import {checkCookieSupport, hasDeviceAccess, logError, logInfo} from './utils.js'; import {bidderSettings as defaultBidderSettings} from './bidderSettings.js'; - -export const MODULE_TYPE_CORE = 'core'; -export const MODULE_TYPE_BIDDER = 'bidder'; -export const MODULE_TYPE_UID = 'userId'; -export const MODULE_TYPE_RTD = 'rtd'; -export const MODULE_TYPE_ANALYTICS = 'analytics'; +import {MODULE_TYPE_BIDDER, MODULE_TYPE_CORE} from './activities/modules.js'; export const STORAGE_TYPE_LOCALSTORAGE = 'html5'; export const STORAGE_TYPE_COOKIES = 'cookie'; diff --git a/test/spec/modules/growthCodeIdSystem_spec.js b/test/spec/modules/growthCodeIdSystem_spec.js index 58a9d12b6f8..97083047d4e 100644 --- a/test/spec/modules/growthCodeIdSystem_spec.js +++ b/test/spec/modules/growthCodeIdSystem_spec.js @@ -3,7 +3,8 @@ import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; import { uspDataHandler } from 'src/adapterManager.js'; import {expect} from 'chai'; -import {getStorageManager, MODULE_TYPE_UID} from '../../../src/storageManager.js'; +import {getStorageManager} from '../../../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../../../src/activities/modules.js'; const GCID_EXPIRY = 45; const MODULE_NAME = 'growthCodeId'; diff --git a/test/spec/modules/realTimeDataModule_spec.js b/test/spec/modules/realTimeDataModule_spec.js index ced2f697649..f9c41b2fda0 100644 --- a/test/spec/modules/realTimeDataModule_spec.js +++ b/test/spec/modules/realTimeDataModule_spec.js @@ -5,6 +5,8 @@ import {default as CONSTANTS} from '../../../src/constants.json'; import * as events from '../../../src/events.js'; import 'src/prebid.js'; import {attachRealTimeDataProvider, onDataDeletionRequest} from 'modules/rtdModule/index.js'; +import {GDPR_GVLIDS} from '../../../src/consentHandler.js'; +import {MODULE_TYPE_RTD} from '../../../src/activities/modules.js'; const getBidRequestDataSpy = sinon.spy(); @@ -84,6 +86,26 @@ describe('Real time module', function () { sandbox.restore(); }); + describe('GVL IDs', () => { + beforeEach(() => { + sinon.stub(GDPR_GVLIDS, 'register'); + }); + + afterEach(() => { + GDPR_GVLIDS.register.restore(); + }); + + it('are registered when RTD module is registered', () => { + let mod; + try { + mod = attachRealTimeDataProvider({name: 'mockRtd', gvlid: 123}); + sinon.assert.calledWith(GDPR_GVLIDS.register, MODULE_TYPE_RTD, 'mockRtd', 123); + } finally { + mod && mod(); + } + }) + }) + describe('', () => { const PROVIDERS = [validSM, invalidSM, failureSM, nonConfSM, validSMWait]; let _detachers; diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index bf27ef0ff81..7beddf419b6 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -53,6 +53,8 @@ import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; import {getPPID} from '../../../src/adserver.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; +import {GDPR_GVLIDS} from '../../../src/consentHandler.js'; +import {MODULE_TYPE_UID} from '../../../src/activities/modules.js'; let assert = require('chai').assert; let expect = require('chai').expect; @@ -165,6 +167,20 @@ describe('User ID', function () { sandbox.restore(); }); + describe('GVL IDs', () => { + beforeEach(() => { + sinon.stub(GDPR_GVLIDS, 'register'); + }); + afterEach(() => { + GDPR_GVLIDS.register.restore(); + }); + + it('are registered when ID submodule is registered', () => { + attachIdSystem({name: 'gvlidMock', gvlid: 123}); + sinon.assert.calledWith(GDPR_GVLIDS.register, MODULE_TYPE_UID, 'gvlidMock', 123); + }) + }) + describe('Decorate Ad Units', function () { beforeEach(function () { // reset mockGpt so nothing else interferes diff --git a/test/spec/modules/zeotapIdPlusIdSystem_spec.js b/test/spec/modules/zeotapIdPlusIdSystem_spec.js index be9294e38c9..54483f0c00e 100644 --- a/test/spec/modules/zeotapIdPlusIdSystem_spec.js +++ b/test/spec/modules/zeotapIdPlusIdSystem_spec.js @@ -4,7 +4,7 @@ import { config } from 'src/config.js'; import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; import { storage, getStorage, zeotapIdPlusSubmodule } from 'modules/zeotapIdPlusIdSystem.js'; import * as storageManager from 'src/storageManager.js'; -import {MODULE_TYPE_UID} from 'src/storageManager.js'; +import {MODULE_TYPE_UID} from '../../../src/activities/modules.js'; const ZEOTAP_COOKIE_NAME = 'IDP'; const ZEOTAP_COOKIE = 'THIS-IS-A-DUMMY-COOKIE'; diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 06366e8cc5c..541ae5b21c9 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -21,6 +21,8 @@ import {find, includes} from 'src/polyfill.js'; import s2sTesting from 'modules/s2sTesting.js'; import {hook} from '../../../../src/hook.js'; import {auctionManager} from '../../../../src/auctionManager.js'; +import {GDPR_GVLIDS} from '../../../../src/consentHandler.js'; +import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER} from '../../../../src/activities/modules.js'; var events = require('../../../../src/events'); const CONFIG = { @@ -2792,4 +2794,23 @@ describe('adapterManager tests', function () { }) }) }); + + describe('registers GVL IDs', () => { + beforeEach(() => { + sinon.stub(GDPR_GVLIDS, 'register'); + }); + afterEach(() => { + GDPR_GVLIDS.register.restore(); + }); + + it('for bid adapters', () => { + adapterManager.registerBidAdapter({getSpec: () => ({gvlid: 123}), callBids: sinon.stub()}, 'mock'); + sinon.assert.calledWith(GDPR_GVLIDS.register, MODULE_TYPE_BIDDER, 'mock', 123); + }); + + it('for analytics adapters', () => { + adapterManager.registerAnalyticsAdapter({adapter: {enableAnalytics: sinon.stub()}, code: 'mock', gvlid: 123}); + sinon.assert.calledWith(GDPR_GVLIDS.register, MODULE_TYPE_ANALYTICS, 'mock', 123); + }); + }); }); diff --git a/test/spec/unit/core/consentHandler_spec.js b/test/spec/unit/core/consentHandler_spec.js index 082ff34f90c..cc7e49ba9f1 100644 --- a/test/spec/unit/core/consentHandler_spec.js +++ b/test/spec/unit/core/consentHandler_spec.js @@ -1,4 +1,4 @@ -import {ConsentHandler} from '../../../../src/consentHandler.js'; +import {ConsentHandler, gvlidRegistry} from '../../../../src/consentHandler.js'; describe('Consent data handler', () => { let handler; @@ -57,3 +57,29 @@ describe('Consent data handler', () => { }) }); }) + +describe('gvlidRegistry', () => { + let registry; + beforeEach(() => { + registry = gvlidRegistry(); + }); + + it('returns undef when id cannoot be found', () => { + expect(registry.get('type', 'name')).to.not.exist; + }); + + it('can retrieve registered GVL IDs', () => { + registry.register('type', 'name', 123); + expect(registry.get('type', 'name')).to.eql(123); + }); + + it('partitions IDs by module type', () => { + registry.register('type', 'name', 123); + expect(registry.get('otherType', 'name')).to.not.exist; + }); + + it('does not register null ids', () => { + registry.register('type', 'name', null); + expect(registry.get('type', 'name')).to.eql(undefined); + }) +}) diff --git a/test/spec/unit/core/storageManager_spec.js b/test/spec/unit/core/storageManager_spec.js index 21c4bbf41b7..9e31389d96f 100644 --- a/test/spec/unit/core/storageManager_spec.js +++ b/test/spec/unit/core/storageManager_spec.js @@ -1,6 +1,5 @@ import { getCoreStorageManager, getStorageManager, - MODULE_TYPE_BIDDER, newStorageManager, resetData, storageCallbacks, @@ -9,6 +8,7 @@ import { import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import {hook} from '../../../../src/hook.js'; +import {MODULE_TYPE_BIDDER} from '../../../../src/activities/modules.js'; describe('storage manager', function() { before(() => { From 0a58854999886a994c1456346a6d90d072552af0 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 30 Mar 2023 10:05:15 -0700 Subject: [PATCH 06/37] Refactor gdprEnforcement gvlid lookup --- modules/gdprEnforcement.js | 124 ++++----- src/consentHandler.js | 32 ++- test/spec/modules/gdprEnforcement_spec.js | 298 +++++++++------------ test/spec/unit/core/consentHandler_spec.js | 20 +- 4 files changed, 230 insertions(+), 244 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 9553ad0586a..798dfc848da 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -11,7 +11,13 @@ import {getHook} from '../src/hook.js'; import {validateStorageEnforcement} from '../src/storageManager.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; -import {VENDORLESS_GVLID} from '../src/consentHandler.js'; +import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../src/consentHandler.js'; +import { + MODULE_TYPE_ANALYTICS, + MODULE_TYPE_BIDDER, + MODULE_TYPE_CORE, MODULE_TYPE_RTD, + MODULE_TYPE_UID +} from '../src/activities/modules.js'; export const STRICT_STORAGE_ENFORCEMENT = 'strictStorageEnforcement'; @@ -49,77 +55,56 @@ const analyticsBlocked = []; let hooksAdded = false; let strictStorageEnforcement = false; -// Helps in stubbing these functions in unit tests. -export const internal = { - getGvlidForBidAdapter, - getGvlidForUserIdModule, - getGvlidForAnalyticsAdapter -}; +const GVLID_LOOKUP_PRIORITY = [ + MODULE_TYPE_BIDDER, + MODULE_TYPE_UID, + MODULE_TYPE_ANALYTICS, + MODULE_TYPE_RTD +]; /** - * Returns GVL ID for a Bid adapter / an USERID submodule / an Analytics adapter. - * If modules of different types have the same moduleCode: For example, 'appnexus' is the code for both Bid adapter and Analytics adapter, - * then, we assume that their GVL IDs are same. This function first checks if GVL ID is defined for a Bid adapter, if not found, tries to find User ID - * submodule's GVL ID, if not found, tries to find Analytics adapter's GVL ID. In this process, as soon as it finds a GVL ID, it returns it - * without going to the next check. - * @param {{string|Object}} - module - * @return {number} - GVL ID + * Retrieve a module's GVL ID. */ -export function getGvlid(module, ...args) { - let gvlid = null; - if (module) { +export function getGvlid(moduleType, moduleName, fallbackFn) { + if (moduleName) { // Check user defined GVL Mapping in pbjs.setConfig() const gvlMapping = config.getConfig('gvlMapping'); - // For USER ID Module, we pass the submodule object itself as the "module" parameter, this check is required to grab the module code - const moduleCode = typeof module === 'string' ? module : module.name; - // Return GVL ID from user defined gvlMapping - if (gvlMapping && gvlMapping[moduleCode]) { - gvlid = gvlMapping[moduleCode]; - return gvlid; - } - - gvlid = internal.getGvlidForBidAdapter(moduleCode) || internal.getGvlidForUserIdModule(module) || internal.getGvlidForAnalyticsAdapter(moduleCode, ...args); - } - return gvlid; -} - -/** - * Returns GVL ID for a bid adapter. If the adapter does not have an associated GVL ID, it returns 'null'. - * @param {string=} bidderCode - The 'code' property of the Bidder spec. - * @return {number} GVL ID - */ -function getGvlidForBidAdapter(bidderCode) { - let gvlid = null; - bidderCode = bidderCode || config.getCurrentBidder(); - if (bidderCode) { - const bidder = adapterManager.getBidAdapter(bidderCode); - if (bidder && bidder.getSpec) { - gvlid = bidder.getSpec().gvlid; + if (gvlMapping && gvlMapping[moduleName]) { + return gvlMapping[moduleName]; + } else if (moduleType === MODULE_TYPE_CORE) { + return VENDORLESS_GVLID; + } else { + let {gvlid, modules} = GDPR_GVLIDS.get(moduleName); + if (gvlid == null && Object.keys(modules).length > 0) { + // this behavior is for backwards compatibility; if multiple modules with the same + // name declare different GVL IDs, pick the bidder's first, then userId, then analytics + for (const type of GVLID_LOOKUP_PRIORITY) { + if (modules.hasOwnProperty(type)) { + gvlid = modules[type]; + if (type !== moduleType && !fallbackFn) { + logWarn(`Multiple GVL IDs found for module '${moduleName}'; using the ${type} module's ID (${gvlid}) instead of the ${moduleType}'s ID (${modules[moduleType]})`) + } + break; + } + } + } + if (gvlid == null && fallbackFn) { + gvlid = fallbackFn(); + } + return gvlid || null; } } - return gvlid; -} - -/** - * Returns GVL ID for an userId submodule. If an userId submodules does not have an associated GVL ID, it returns 'null'. - * @param {Object} userIdModule - * @return {number} GVL ID - */ -function getGvlidForUserIdModule(userIdModule) { - return (typeof userIdModule === 'object' ? userIdModule.gvlid : null); + return null; } /** - * Returns GVL ID for an analytics adapter. If an analytics adapter does not have an associated GVL ID, it returns 'null'. - * @param {string} code - 'provider' property on the analytics adapter config - * @param {{}} config - analytics configuration object - * @return {number} GVL ID + * Retrieve GVL IDs that are dynamically set on analytics adapters. */ -function getGvlidForAnalyticsAdapter(code, config) { +export function getGvlidFromAnalyticsAdapter(code, config) { const adapter = adapterManager.getAnalyticsAdapter(code); - return adapter?.gvlid || ((gvlid) => { + return ((gvlid) => { if (typeof gvlid !== 'function') return gvlid; try { return gvlid.call(adapter.adapter, config); @@ -185,30 +170,33 @@ export function validateRules(rule, consentData, currentModule, gvlId) { /** * This hook checks whether module has permission to access device or not. Device access include cookie and local storage + * * @param {Function} fn reference to original function (used by hook logic) - * @param {Number=} gvlid gvlid of the module + * @param {string} moduleType type of the module * @param {string=} moduleName name of the module * @param result + * @param validate */ -export function deviceAccessHook(fn, gvlid, moduleName, result, {validate = validateRules} = {}) { +export function deviceAccessHook(fn, moduleType, moduleName, result, {validate = validateRules} = {}) { result = Object.assign({}, { hasEnforcementHook: true }); if (!hasDeviceAccess()) { logWarn('Device access is disabled by Publisher'); result.valid = false; - } else if (gvlid === VENDORLESS_GVLID && !strictStorageEnforcement) { + } else if (moduleType === MODULE_TYPE_CORE && !strictStorageEnforcement) { // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set result.valid = true; } else { const consentData = gdprDataHandler.getConsentData(); + let gvlid; if (shouldEnforce(consentData, 1, moduleName)) { const curBidder = config.getCurrentBidder(); // Bidders have a copy of storage object with bidder code binded. Aliases will also pass the same bidder code when invoking storage functions and hence if alias tries to access device we will try to grab the gvl id for alias instead of original bidder if (curBidder && (curBidder !== moduleName) && adapterManager.aliasRegistry[curBidder] === moduleName) { - gvlid = getGvlid(curBidder); + gvlid = getGvlid(moduleType, curBidder); } else { - gvlid = getGvlid(moduleName) || gvlid; + gvlid = getGvlid(moduleType, moduleName) } const curModule = moduleName || curBidder; let isAllowed = validate(purpose1Rule, consentData, curModule, gvlid,); @@ -223,7 +211,7 @@ export function deviceAccessHook(fn, gvlid, moduleName, result, {validate = vali result.valid = true; } } - fn.call(this, gvlid, moduleName, result); + fn.call(this, moduleType, moduleName, result); } /** @@ -235,7 +223,7 @@ export function userSyncHook(fn, ...args) { const consentData = gdprDataHandler.getConsentData(); const curBidder = config.getCurrentBidder(); if (shouldEnforce(consentData, 1, curBidder)) { - const gvlid = getGvlid(curBidder); + const gvlid = getGvlid(MODULE_TYPE_BIDDER, curBidder); let isAllowed = validateRules(purpose1Rule, consentData, curBidder, gvlid); if (isAllowed) { fn.call(this, ...args); @@ -257,8 +245,8 @@ export function userSyncHook(fn, ...args) { export function userIdHook(fn, submodules, consentData) { if (shouldEnforce(consentData, 1, 'User ID')) { let userIdModules = submodules.map((submodule) => { - const gvlid = getGvlid(submodule.submodule); const moduleName = submodule.submodule.name; + const gvlid = getGvlid(MODULE_TYPE_UID, moduleName); let isAllowed = validateRules(purpose1Rule, consentData, moduleName, gvlid); if (isAllowed) { return submodule; @@ -286,7 +274,7 @@ export function makeBidRequestsHook(fn, adUnits, ...args) { adUnits.forEach(adUnit => { adUnit.bids = adUnit.bids.filter(bid => { const currBidder = bid.bidder; - const gvlId = getGvlid(currBidder); + const gvlId = getGvlid(MODULE_TYPE_BIDDER, currBidder); if (includes(biddersBlocked, currBidder)) return false; const isAllowed = !!validateRules(purpose2Rule, consentData, currBidder, gvlId); if (!isAllowed) { @@ -316,7 +304,7 @@ export function enableAnalyticsHook(fn, config) { } config = config.filter(conf => { const analyticsAdapterCode = conf.provider; - const gvlid = getGvlid(analyticsAdapterCode, conf); + const gvlid = getGvlid(MODULE_TYPE_ANALYTICS, analyticsAdapterCode, () => getGvlidFromAnalyticsAdapter(analyticsAdapterCode, conf)); const isAllowed = !!validateRules(purpose7Rule, consentData, analyticsAdapterCode, gvlid); if (!isAllowed) { analyticsBlocked.push(analyticsAdapterCode); diff --git a/src/consentHandler.js b/src/consentHandler.js index c9f0bb0c5ec..4776a8ece02 100644 --- a/src/consentHandler.js +++ b/src/consentHandler.js @@ -121,14 +121,40 @@ export class GppConsentHandler extends ConsentHandler { export function gvlidRegistry() { const registry = {}; + const flat = {}; + const none = {}; return { + /** + * Register a module's GVL ID. + * @param {string} moduleType defined in `activities/modules.js` + * @param {string} moduleName + * @param {number} gvlid + */ register(moduleType, moduleName, gvlid) { if (gvlid) { - (registry[moduleType] = registry[moduleType] || {})[moduleName] = gvlid; + (registry[moduleName] = registry[moduleName] || {})[moduleType] = gvlid; + if (flat.hasOwnProperty(moduleName)) { + if (flat[moduleName] !== gvlid) flat[moduleName] = none; + } else { + flat[moduleName] = gvlid; + } } }, - get(moduleType, moduleName) { - return registry[moduleType]?.[moduleName]; + /** + * Get a module's GVL ID(s). + * + * @param {string} moduleName + * @return {{modules: {[moduleType]: number}, gvlid?: number}} an object where: + * `modules` is a map from module type to that module's GVL ID; + * `gvlid` is the single GVL ID for this family of modules (only defined + * if all modules with this name declared the same ID). + */ + get(moduleName) { + const result = {modules: registry[moduleName] || {}}; + if (flat.hasOwnProperty(moduleName) && flat[moduleName] !== none) { + result.gvlid = flat[moduleName]; + } + return result; } } } diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 8d58990bb66..d28d45693a6 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -1,26 +1,33 @@ import { deviceAccessHook, - setEnforcementConfig, - userSyncHook, - userIdHook, - makeBidRequestsHook, - validateRules, + enableAnalyticsHook, enforcementRules, + getGvlid, + getGvlidFromAnalyticsAdapter, + makeBidRequestsHook, purpose1Rule, purpose2Rule, - enableAnalyticsHook, - getGvlid, - internal, STRICT_STORAGE_ENFORCEMENT + setEnforcementConfig, + STRICT_STORAGE_ENFORCEMENT, + userIdHook, + userSyncHook, + validateRules } from 'modules/gdprEnforcement.js'; -import { config } from 'src/config.js'; -import adapterManager, { gdprDataHandler } from 'src/adapterManager.js'; +import {config} from 'src/config.js'; +import adapterManager, {gdprDataHandler} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import { validateStorageEnforcement } from 'src/storageManager.js'; +import { + MODULE_TYPE_ANALYTICS, + MODULE_TYPE_BIDDER, + MODULE_TYPE_CORE, + MODULE_TYPE_UID +} from '../../../src/activities/modules.js'; import * as events from 'src/events.js'; import 'modules/appnexusBidAdapter.js'; // some tests expect this to be in the adapter registry -import 'src/prebid.js' +import 'src/prebid.js'; import {hook} from '../../../src/hook.js'; -import {VENDORLESS_GVLID} from '../../../src/consentHandler.js'; +import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../../../src/consentHandler.js'; +import {validateStorageEnforcement} from '../../../src/storageManager.js'; describe('gdpr enforcement', function () { let nextFnSpy; @@ -100,6 +107,7 @@ describe('gdpr enforcement', function () { } } }; + let gvlids; before(() => { hook.ready(); @@ -111,31 +119,28 @@ describe('gdpr enforcement', function () { adapterManager.makeBidRequests.getHooks({ hook: makeBidRequestsHook }).remove(); }) - describe('deviceAccessHook', function () { - let adapterManagerStub; + beforeEach(() => { + gvlids = {}; + sinon.stub(GDPR_GVLIDS, 'get').callsFake((name) => ({gvlid: gvlids[name], modules: {}})); + }); - function getBidderSpec(gvlid) { - return { - getSpec: () => { - return { - gvlid - } - } - } - } + afterEach(() => { + GDPR_GVLIDS.get.restore(); + }); + describe('deviceAccessHook', function () { beforeEach(function () { nextFnSpy = sinon.spy(); gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); logWarnSpy = sinon.spy(utils, 'logWarn'); - adapterManagerStub = sinon.stub(adapterManager, 'getBidAdapter'); }); + afterEach(function () { config.resetConfig(); gdprDataHandler.getConsentData.restore(); logWarnSpy.restore(); - adapterManagerStub.restore(); }); + it('should not allow device access when device access flag is set to false', function () { config.setConfig({ deviceAccess: false, @@ -161,8 +166,10 @@ describe('gdpr enforcement', function () { }); it('should only check for consent for vendor exceptions when enforcePurpose and enforceVendor are false', function () { - adapterManagerStub.withArgs('appnexus').returns(getBidderSpec(1)); - adapterManagerStub.withArgs('rubicon').returns(getBidderSpec(5)); + Object.assign(gvlids, { + appnexus: 1, + rubicon: 5 + }); setEnforcementConfig({ gdpr: { rules: [{ @@ -179,14 +186,16 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); - deviceAccessHook(nextFnSpy, 5, 'rubicon'); + deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); + deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'rubicon'); expect(logWarnSpy.callCount).to.equal(0); }); it('should check consent for all vendors when enforcePurpose and enforceVendor are true', function () { - adapterManagerStub.withArgs('appnexus').returns(getBidderSpec(1)); - adapterManagerStub.withArgs('rubicon').returns(getBidderSpec(3)); + Object.assign(gvlids, { + appnexus: 1, + rubicon: 3 + }); setEnforcementConfig({ gdpr: { rules: [{ @@ -202,13 +211,13 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); - deviceAccessHook(nextFnSpy, 3, 'rubicon'); + deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); + deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'rubicon'); expect(logWarnSpy.callCount).to.equal(1); }); it('should allow device access when gdprApplies is false and hasDeviceAccess flag is true', function () { - adapterManagerStub.withArgs('appnexus').returns(getBidderSpec(1)); + gvlids.appnexus = 1; setEnforcementConfig({ gdpr: { rules: [{ @@ -225,13 +234,13 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); expect(nextFnSpy.calledOnce).to.equal(true); let result = { hasEnforcementHook: true, valid: true } - sinon.assert.calledWith(nextFnSpy, 1, 'appnexus', result); + sinon.assert.calledWith(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus', result); }); it('should use gvlMapping set by publisher', function() { @@ -256,13 +265,13 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); expect(nextFnSpy.calledOnce).to.equal(true); let result = { hasEnforcementHook: true, valid: true } - sinon.assert.calledWith(nextFnSpy, 4, 'appnexus', result); + sinon.assert.calledWith(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus', result); config.resetConfig(); }); @@ -291,13 +300,13 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); expect(nextFnSpy.calledOnce).to.equal(true); let result = { hasEnforcementHook: true, valid: true } - sinon.assert.calledWith(nextFnSpy, 4, 'appnexus', result); + sinon.assert.calledWith(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus', result); config.resetConfig(); curBidderStub.restore(); }); @@ -310,9 +319,9 @@ describe('gdpr enforcement', function () { } gdprDataHandlerStub.returns(consentData); const validate = sinon.stub().callsFake(() => false); - deviceAccessHook(nextFnSpy, VENDORLESS_GVLID, 'mockModule', undefined, {validate}); + deviceAccessHook(nextFnSpy, MODULE_TYPE_CORE, 'mockModule', undefined, {validate}); sinon.assert.callCount(validate, 0); - sinon.assert.calledWith(nextFnSpy, VENDORLESS_GVLID, 'mockModule', {hasEnforcementHook: true, valid: true}); + sinon.assert.calledWith(nextFnSpy, MODULE_TYPE_CORE, 'mockModule', {hasEnforcementHook: true, valid: true}); }) }); @@ -354,23 +363,11 @@ describe('gdpr enforcement', function () { gdprDataHandlerStub.returns(consentData); curBidderStub.returns('sampleBidder1'); - adapterManagerStub.withArgs('sampleBidder1').returns({ - getSpec: function () { - return { - 'gvlid': 1 - } - } - }); + gvlids.sampleBidder1 = 1; userSyncHook(nextFnSpy); curBidderStub.returns('sampleBidder2'); - adapterManagerStub.withArgs('sampleBidder2').returns({ - getSpec: function () { - return { - 'gvlid': 3 - } - } - }); + gvlids.sampleBidder2 = 3; userSyncHook(nextFnSpy); expect(nextFnSpy.calledTwice).to.equal(true); }); @@ -393,23 +390,11 @@ describe('gdpr enforcement', function () { gdprDataHandlerStub.returns(consentData); curBidderStub.returns('sampleBidder1'); - adapterManagerStub.withArgs('sampleBidder1').returns({ - getSpec: function () { - return { - 'gvlid': 1 - } - } - }); + gvlids.sampleBidder1 = 1; userSyncHook(nextFnSpy); curBidderStub.returns('sampleBidder2'); - adapterManagerStub.withArgs('sampleBidder2').returns({ - getSpec: function () { - return { - 'gvlid': 3 - } - } - }); + gvlids.sampleBidder2 = 3; userSyncHook(nextFnSpy); expect(nextFnSpy.calledOnce).to.equal(true); expect(logWarnSpy.callCount).to.equal(1); @@ -433,23 +418,11 @@ describe('gdpr enforcement', function () { gdprDataHandlerStub.returns(consentData); curBidderStub.returns('sampleBidder1'); - adapterManagerStub.withArgs('sampleBidder1').returns({ - getSpec: function () { - return { - 'gvlid': 1 - } - } - }); + gvlids.sampleBidder1 = 1; userSyncHook(nextFnSpy); curBidderStub.returns('sampleBidder2'); - adapterManagerStub.withArgs('sampleBidder2').returns({ - getSpec: function () { - return { - 'gvlid': 3 - } - } - }); + gvlids.sampleBidder2 = 3; userSyncHook(nextFnSpy); expect(nextFnSpy.calledTwice).to.equal(true); expect(logWarnSpy.callCount).to.equal(0); @@ -486,6 +459,7 @@ describe('gdpr enforcement', function () { name: 'sampleUserId' } }] + gvlids.sampleUserId = 1; userIdHook(nextFnSpy, submodules, consentData); // Should pass back hasValidated flag since version 2 const args = nextFnSpy.getCalls()[0].args; @@ -501,6 +475,7 @@ describe('gdpr enforcement', function () { name: 'sampleUserId' } }]; + gvlids.sampleUserId = 1; let consentData = null; userIdHook(nextFnSpy, submodules, consentData); // Should not pass back hasValidated flag since version 2 @@ -537,6 +512,10 @@ describe('gdpr enforcement', function () { name: 'sampleUserId1' } }] + Object.assign(gvlids, { + sampleUserId: 1, + sampleUserId1: 3 + }); userIdHook(nextFnSpy, submodules, consentData); expect(logWarnSpy.callCount).to.equal(1); let expectedSubmodules = [{ @@ -602,20 +581,9 @@ describe('gdpr enforcement', function () { consentData.gdprApplies = true; gdprDataHandlerStub.returns(consentData); - adapterManagerStub.withArgs('bidder_1').returns({ - getSpec: function () { - return { 'gvlid': 4 } - } - }); - adapterManagerStub.withArgs('bidder_2').returns({ - getSpec: function () { - return { 'gvlid': 5 } - } - }); - adapterManagerStub.withArgs('bidder_3').returns({ - getSpec: function () { - return { 'gvlid': undefined } - } + Object.assign(gvlids, { + bidder_1: 4, + biddder_2: 5, }); makeBidRequestsHook(nextFnSpy, MOCK_AD_UNITS, []); @@ -660,21 +628,10 @@ describe('gdpr enforcement', function () { consentData.gdprApplies = true; gdprDataHandlerStub.returns(consentData); - adapterManagerStub.withArgs('bidder_1').returns({ - getSpec: function () { - return { 'gvlid': 4 } - } - }); - adapterManagerStub.withArgs('bidder_2').returns({ - getSpec: function () { - return { 'gvlid': 5 } - } - }); - adapterManagerStub.withArgs('bidder_3').returns({ - getSpec: function () { - return { 'gvlid': undefined } - } - }); + Object.assign(gvlids, { + bidder_1: 4, + biddder_2: 5, + }) makeBidRequestsHook(nextFnSpy, MOCK_AD_UNITS, []); @@ -771,9 +728,11 @@ describe('gdpr enforcement', function () { consentData.gdprApplies = true; gdprDataHandlerStub.returns(consentData); - adapterManagerStub.withArgs('analyticsAdapter_A').returns({ gvlid: 3 }); - adapterManagerStub.withArgs('analyticsAdapter_B').returns({ gvlid: 5 }); - adapterManagerStub.withArgs('analyticsAdapter_C').returns({ gvlid: 1 }); + Object.assign(gvlids, { + analyticsAdapter_A: 3, + analyticsAdapter_B: 5, + analyticsAdapter_C: 1 + }); enableAnalyticsHook(nextFnSpy, MOCK_ANALYTICS_ADAPTER_CONFIG); @@ -1142,13 +1101,13 @@ describe('gdpr enforcement', function () { }); describe('getGvlid', function() { - let getGvlidForBidAdapterStub; - let getGvlidForUserIdModuleStub; - let getGvlidForAnalyticsAdapterStub; + const MOCK_MODULE = 'moduleA'; + let entry; + beforeEach(function() { - getGvlidForBidAdapterStub = sandbox.stub(internal, 'getGvlidForBidAdapter'); - getGvlidForUserIdModuleStub = sandbox.stub(internal, 'getGvlidForUserIdModule'); - getGvlidForAnalyticsAdapterStub = sandbox.stub(internal, 'getGvlidForAnalyticsAdapter'); + entry = {modules: {}}; + GDPR_GVLIDS.get.reset(); + GDPR_GVLIDS.get.callsFake((mod) => mod === MOCK_MODULE ? entry : {modules: {}}); }); it('should return "null" if called without passing any argument', function() { @@ -1156,46 +1115,63 @@ describe('gdpr enforcement', function () { expect(gvlid).to.equal(null); }); - it('should return "null" if GVL ID is not defined for any of these modules: Bid adapter, UserId submodule and Analytics adapter', function() { - getGvlidForBidAdapterStub.withArgs('moduleA').returns(null); - getGvlidForUserIdModuleStub.withArgs('moduleA').returns(null); - getGvlidForAnalyticsAdapterStub.withArgs('moduleA').returns(null); - - const gvlid = getGvlid('moduleA'); + it('should return "null" if no GVL ID was registered', function() { + const gvlid = getGvlid('type', MOCK_MODULE); expect(gvlid).to.equal(null); }); - it('should return the GVL ID from gvlMapping if it is defined in setConfig', function() { - config.setConfig({ - gvlMapping: { - moduleA: 1 - } - }); - - // Actual GVL ID for moduleA is 2, as defined on its the bidAdapter.js file. - getGvlidForBidAdapterStub.withArgs('moduleA').returns(2); - - const gvlid = getGvlid('moduleA'); - expect(gvlid).to.equal(1); - }); - - it('should return the GVL ID by calling getGvlidForBidAdapter -> getGvlidForUserIdModule -> getGvlidForAnalyticsAdapter in sequence', function() { - getGvlidForBidAdapterStub.withArgs('moduleA').returns(null); - getGvlidForUserIdModuleStub.withArgs('moduleA').returns(null); - getGvlidForAnalyticsAdapterStub.withArgs('moduleA').returns(7); + it('should return null if the wrong GVL ID was registered', () => { + entry = {gvlid: 123}; + expect(getGvlid('type', 'someOtherModule')).to.equal(null); + }) - expect(getGvlid('moduleA')).to.equal(7); - }); + Object.entries({ + 'without fallback': null, + 'with fallback': () => 'shouldBeIgnored' + }).forEach(([t, fallbackFn]) => { + describe(t, () => { + it('should return the GVL ID from gvlMapping if it is defined in setConfig', function() { + config.setConfig({ + gvlMapping: { + [MOCK_MODULE]: 1 + } + }); + + entry = {gvlid: 2}; + + const gvlid = getGvlid('type', MOCK_MODULE, fallbackFn); + expect(gvlid).to.equal(1); + }); + + it('should return the GVL ID that was registered', function() { + entry = {gvlid: 7}; + expect(getGvlid('type', MOCK_MODULE, fallbackFn)).to.equal(7); + }); + + it('should return VENDORLESS_GVLID for core modules', () => { + entry = {gvlid: 123}; + expect(getGvlid(MODULE_TYPE_CORE, MOCK_MODULE, fallbackFn)).to.equal(VENDORLESS_GVLID); + }); + + describe('multiple GVL IDs are found', () => { + it('should use bidder over others', () => { + entry = {modules: {[MODULE_TYPE_BIDDER]: 123, [MODULE_TYPE_UID]: 321}}; + expect(getGvlid(MODULE_TYPE_UID, MOCK_MODULE, fallbackFn)).to.equal(123); + }); + it('should use uid over analytics', () => { + entry = {modules: {[MODULE_TYPE_UID]: 123, [MODULE_TYPE_ANALYTICS]: 321}}; + expect(getGvlid(MODULE_TYPE_ANALYTICS, MOCK_MODULE, fallbackFn)).to.equal(123); + }) + }) + }) + }) - it('should pass extra arguments to analytics\' getGvlid', () => { - getGvlidForAnalyticsAdapterStub.withArgs('analytics').returns(321); - const cfg = {some: 'args'}; - getGvlid('analytics', cfg); - sinon.assert.calledWith(getGvlidForAnalyticsAdapterStub, 'analytics', cfg); + it('should use fallbackFn if no other lookup produces a gvl id', () => { + expect(getGvlid('type', MOCK_MODULE, () => 321)).to.equal(321); }); }); - describe('getGvlidForAnalyticsAdapter', () => { + describe('getGvlidFromAnalyticsConfig', () => { let getAnalyticsAdapter, adapter, adapterEntry; beforeEach(() => { @@ -1207,26 +1183,20 @@ describe('gdpr enforcement', function () { getAnalyticsAdapter.withArgs('analytics').returns(adapterEntry); }); - it('should return gvlid from adapterManager if defined', () => { - adapterEntry.gvlid = 123; - adapter.gvlid = 321 - expect(internal.getGvlidForAnalyticsAdapter('analytics')).to.equal(123); - }); - it('should return gvlid from adapter if defined', () => { adapter.gvlid = 321; - expect(internal.getGvlidForAnalyticsAdapter('analytics')).to.equal(321); + expect(getGvlidFromAnalyticsAdapter('analytics')).to.equal(321); }); it('should invoke adapter.gvlid if it\'s a function', () => { adapter.gvlid = (cfg) => cfg.k const cfg = {k: 231}; - expect(internal.getGvlidForAnalyticsAdapter('analytics', cfg)).to.eql(231); + expect(getGvlidFromAnalyticsAdapter('analytics', cfg)).to.eql(231); }); it('should not choke if adapter gvlid fn throws', () => { adapter.gvlid = () => { throw new Error(); }; - expect(internal.getGvlidForAnalyticsAdapter('analytics')).to.not.be.ok; + expect(getGvlidFromAnalyticsAdapter('analytics')).to.not.be.ok; }); }); }) diff --git a/test/spec/unit/core/consentHandler_spec.js b/test/spec/unit/core/consentHandler_spec.js index cc7e49ba9f1..98b317e0d36 100644 --- a/test/spec/unit/core/consentHandler_spec.js +++ b/test/spec/unit/core/consentHandler_spec.js @@ -65,21 +65,23 @@ describe('gvlidRegistry', () => { }); it('returns undef when id cannoot be found', () => { - expect(registry.get('type', 'name')).to.not.exist; + expect(registry.get('name')).to.eql({modules: {}}) }); + it('does not register null ids', () => { + registry.register('type', 'name', null); + expect(registry.get('type', 'name')).to.eql({modules: {}}); + }) + it('can retrieve registered GVL IDs', () => { registry.register('type', 'name', 123); - expect(registry.get('type', 'name')).to.eql(123); + registry.register('otherType', 'name', 123); + expect(registry.get('name')).to.eql({gvlid: 123, modules: {type: 123, otherType: 123}}); }); - it('partitions IDs by module type', () => { + it('does not return `gvlid` if there is more than one', () => { registry.register('type', 'name', 123); - expect(registry.get('otherType', 'name')).to.not.exist; + registry.register('otherType', 'name', 321); + expect(registry.get('name')).to.eql({modules: {type: 123, otherType: 321}}) }); - - it('does not register null ids', () => { - registry.register('type', 'name', null); - expect(registry.get('type', 'name')).to.eql(undefined); - }) }) From c87a1674f98dde51a8d9cc6e9ac59f329436007c Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 30 Mar 2023 10:09:55 -0700 Subject: [PATCH 07/37] fix lint --- modules/amxBidAdapter.js | 18 +++++++++--------- modules/criteoBidAdapter.js | 2 +- modules/ftrackIdSystem.js | 7 +++---- modules/ixBidAdapter.js | 2 +- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index 22ca49838ee..f8a32f79941 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -1,21 +1,21 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; import { - parseUrl, - deepAccess, _each, + deepAccess, formatQS, getUniqueIdentifierStr, - triggerPixel, + isArray, isFn, logError, - isArray, + parseUrl, + triggerPixel, } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {config} from '../src/config.js'; +import {getStorageManager} from '../src/storageManager.js'; const BIDDER_CODE = 'amx'; -const storage = getStorageManager({bidderCode: BIDDER_CODE }); +const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const SIMPLE_TLD_TEST = /\.com?\.\w{2,4}$/; const DEFAULT_ENDPOINT = 'https://prebid.a-mo.net/a/c'; const VERSION = 'pba1.3.2'; diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 54c98289f56..b51d6637a4c 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -16,7 +16,7 @@ const BIDDER_CODE = 'criteo'; const CDB_ENDPOINT = 'https://bidder.criteo.com/cdb'; const PROFILE_ID_INLINE = 207; export const PROFILE_ID_PUBLISHERTAG = 185; -export const storage = getStorageManager({bidderCode: BIDDER_CODE }); +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const LOG_PREFIX = 'Criteo: '; /* diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js index 9777883683e..5f09a315b34 100644 --- a/modules/ftrackIdSystem.js +++ b/modules/ftrackIdSystem.js @@ -6,16 +6,15 @@ */ import * as utils from '../src/utils.js'; -import { submodule } from '../src/hook.js'; +import {submodule} from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; -import { uspDataHandler } from '../src/adapterManager.js'; -import { loadExternalScript } from '../src/adloader.js'; +import {uspDataHandler} from '../src/adapterManager.js'; +import {loadExternalScript} from '../src/adloader.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'ftrackId'; const LOG_PREFIX = 'FTRACK - '; const LOCAL_STORAGE_EXP_DAYS = 30; -const VENDOR_ID = null; const LOCAL_STORAGE = 'html5'; const FTRACK_STORAGE_NAME = 'ftrackId'; const FTRACK_PRIVACY_STORAGE_NAME = `${FTRACK_STORAGE_NAME}_privacy`; diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 637c512aee1..ee07c34fb95 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -102,7 +102,7 @@ const VIDEO_PARAMS_ALLOW_LIST = [ const LOCAL_STORAGE_KEY = 'ixdiag'; export const LOCAL_STORAGE_FEATURE_TOGGLES_KEY = `${BIDDER_CODE}_features`; let hasRegisteredHandler = false; -export const storage = getStorageManager({bidderCode: BIDDER_CODE }); +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); export const FEATURE_TOGGLES = { featureToggles: {}, isFeatureEnabled: function (ft) { From b0b91351baa7ee2d63542f931705c56e8304e7df Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 30 Mar 2023 10:30:53 -0700 Subject: [PATCH 08/37] Remove empty file --- src/modules.js | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/modules.js diff --git a/src/modules.js b/src/modules.js deleted file mode 100644 index 1c33e8f40aa..00000000000 --- a/src/modules.js +++ /dev/null @@ -1 +0,0 @@ -// module type definitions - for storageManager / activity controls From 1795ab638ff99bc2d70ada58f597eabd4ac6e675 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 30 Mar 2023 11:36:16 -0700 Subject: [PATCH 09/37] Undo https://github.com/prebid/Prebid.js/pull/9728 for realVu --- test/spec/modules/realvuAnalyticsAdapter_spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/spec/modules/realvuAnalyticsAdapter_spec.js b/test/spec/modules/realvuAnalyticsAdapter_spec.js index 221efc2d374..e51a4e2e3a2 100644 --- a/test/spec/modules/realvuAnalyticsAdapter_spec.js +++ b/test/spec/modules/realvuAnalyticsAdapter_spec.js @@ -50,7 +50,6 @@ describe('RealVu', function() { describe('Analytics Adapter.', function () { it('enableAnalytics', function () { - this.timeout(3500) const config = { options: { partnerId: '1Y', From 995b42a2a6aaa96f44125b7b66df99b76a5960fd Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 4 Apr 2023 10:49:48 -0700 Subject: [PATCH 10/37] Fix typo --- test/spec/modules/gdprEnforcement_spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index d28d45693a6..941f2b3c8df 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -583,7 +583,7 @@ describe('gdpr enforcement', function () { gdprDataHandlerStub.returns(consentData); Object.assign(gvlids, { bidder_1: 4, - biddder_2: 5, + bidder_2: 5, }); makeBidRequestsHook(nextFnSpy, MOCK_AD_UNITS, []); @@ -630,7 +630,7 @@ describe('gdpr enforcement', function () { gdprDataHandlerStub.returns(consentData); Object.assign(gvlids, { bidder_1: 4, - biddder_2: 5, + bidder_2: 5, }) makeBidRequestsHook(nextFnSpy, MOCK_AD_UNITS, []); From 72ec01b6e253465f5857d5b668088caaf387630b Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 28 Mar 2023 10:46:12 -0700 Subject: [PATCH 11/37] Activity control rules --- src/activities/activities.js | 47 +++++++++++++++ src/activities/params.js | 12 ++++ src/activities/rules.js | 73 ++++++++++++++++++++++ test/spec/activities/params_spec.js | 16 +++++ test/spec/activities/rules_spec.js | 94 +++++++++++++++++++++++++++++ 5 files changed, 242 insertions(+) create mode 100644 src/activities/activities.js create mode 100644 src/activities/params.js create mode 100644 src/activities/rules.js create mode 100644 test/spec/activities/params_spec.js create mode 100644 test/spec/activities/rules_spec.js diff --git a/src/activities/activities.js b/src/activities/activities.js new file mode 100644 index 00000000000..fdb64587bfa --- /dev/null +++ b/src/activities/activities.js @@ -0,0 +1,47 @@ +/** + * Activity (that are relevant for privacy) definitions + * + * ref. https://docs.google.com/document/d/1dRxFUFmhh2jGanzGZvfkK_6jtHPpHXWD7Qsi6KEugeE + * & https://github.com/prebid/Prebid.js/issues/9546 + */ + +/** + * accessDevice: some component wants to read or write to localStorage or cookies. + */ +export const ACTIVITY_ACCESS_DEVICE = 'accessDevice'; +/** + * syncUser: A bid adapter wants to run a user sync. + */ +export const ACTIVITY_SYNC_USER = 'syncUser'; +/** + * enrichUfpd: some component wants to add user first-party data to bid requests. + */ +export const ACTIVITY_ENRICH_UFPD = 'enrichUfpd'; +/** + * enrichEids: some component wants to add user IDs to bid requests. + */ +export const ACTIVITY_ENRICH_EIDS = 'enrichEids'; +/** + * fetchBid: a bidder wants to bid. + */ +export const ACTIVITY_FETCH_BIDS = 'fetchBids'; + +/** + * reportAnalytics: some component wants to phone home with analytics data. + */ +export const ACTIVITY_REPORT_ANALYTICS = 'reportAnalytics'; + +/** + * some component wants access to (and send along) user IDs + */ +export const ACTIVITY_TRANSMIT_EIDS = 'transmitEids' + +/** + * transmitUfpd: some component wants access to (and send along) user FPD + */ +export const ACTIVITY_TRANSMIT_UFPD = 'transmitUfpd'; + +/** + * transmitPreciseGeo: some component wants access to (and send along) geolocation info + */ +export const ACTIVITY_TRANSMIT_PRECISE_GEO = 'transmitPreciseGeo'; diff --git a/src/activities/params.js b/src/activities/params.js new file mode 100644 index 00000000000..ba0746a935f --- /dev/null +++ b/src/activities/params.js @@ -0,0 +1,12 @@ + +export const ACTIVITY_PARAM_COMPONENT = 'component'; +export const ACTIVITY_PARAM_COMPONENT_TYPE = ACTIVITY_PARAM_COMPONENT + 'Type'; +export const ACTIVITY_PARAM_COMPONENT_NAME = ACTIVITY_PARAM_COMPONENT + 'Name'; + +export function activityParams(moduleType, moduleName, params) { + return Object.assign({ + [ACTIVITY_PARAM_COMPONENT_TYPE]: moduleType, + [ACTIVITY_PARAM_COMPONENT_NAME]: moduleName, + [ACTIVITY_PARAM_COMPONENT]: `${moduleType}.${moduleName}` + }, params); +} diff --git a/src/activities/rules.js b/src/activities/rules.js new file mode 100644 index 00000000000..efde90b87ca --- /dev/null +++ b/src/activities/rules.js @@ -0,0 +1,73 @@ +import {prefixLog} from '../utils.js'; + +export function ruleRegistry(logger = prefixLog('Activity control:')) { + const registry = {}; + + function getRules(activity) { + return registry[activity] = registry[activity] || []; + } + + function runRule(activity, name, rule, params) { + let res; + try { + res = rule(params); + } catch (e) { + logger.logError(`Exception in rule ${name} for '${activity}'`, e); + res = {allow: false, reason: e}; + } + return res && Object.assign({activity, name}, res); + } + + function logResult({activity, name, allow, reason}) { + let msg = `'${activity}' is ${allow ? 'allowed' : 'denied'} by ${name}`; + if (reason) { + msg = `${msg}: ${reason}`; + } + (allow ? logger.logInfo : logger.logWarn)(msg); + } + + return [ + /** + * Register an activity control rule. + * + * @param {string} activity activity name - set is defined in `activities.js` + * @param {string} ruleName a name for this rule; used for logging. + * @param {function({}): {allow: boolean, reason?: string}} rule definition function. Takes in activity + * parameters as a single object; MAY return an object {allow, reason}, where allow is true/false, + * and reason is an optional message used for logging. + * @param {number} priority rule priority; lower number means higher priority + */ + function registerActivityControl(activity, ruleName, rule, priority = 10) { + const rules = getRules(activity); + const pos = rules.findIndex(([itemPriority]) => priority < itemPriority); + rules.splice(pos < 0 ? rules.length : pos, 0, [priority, ruleName, rule]); + }, + /** + * Test whether an activity is allowed. + * + * @param {string} activity activity name + * @param {{}} params activity parameters; should be generated through `activityParams` below + * @return {boolean} true for allow, false for deny. + */ + function isActivityAllowed(activity, params) { + let lastPriority, foundAllow; + for (const [priority, name, rule] of getRules(activity)) { + if (lastPriority !== priority && foundAllow) break; + lastPriority = priority; + const ruleResult = runRule(activity, name, rule, params); + if (ruleResult) { + if (!ruleResult.allow) { + logResult(ruleResult); + return false; + } else { + foundAllow = ruleResult; + } + } + } + foundAllow && logResult(foundAllow); + return true; + } + ]; +} + +export const [registerActivityControl, isActivityAllowed] = ruleRegistry(); diff --git a/test/spec/activities/params_spec.js b/test/spec/activities/params_spec.js new file mode 100644 index 00000000000..4470caaed38 --- /dev/null +++ b/test/spec/activities/params_spec.js @@ -0,0 +1,16 @@ +import { + ACTIVITY_PARAM_COMPONENT, ACTIVITY_PARAM_COMPONENT_NAME, + ACTIVITY_PARAM_COMPONENT_TYPE, + activityParams +} from '../../../src/activities/params.js'; + +describe('activityParams', () => { + it('fills out component params', () => { + expect(activityParams('bidder', 'mockBidder', {foo: 'bar'})).to.eql({ + [ACTIVITY_PARAM_COMPONENT]: 'bidder.mockBidder', + [ACTIVITY_PARAM_COMPONENT_TYPE]: 'bidder', + [ACTIVITY_PARAM_COMPONENT_NAME]: 'mockBidder', + foo: 'bar' + }); + }); +}); diff --git a/test/spec/activities/rules_spec.js b/test/spec/activities/rules_spec.js new file mode 100644 index 00000000000..d5ee3c7e845 --- /dev/null +++ b/test/spec/activities/rules_spec.js @@ -0,0 +1,94 @@ +import {ruleRegistry} from '../../../src/activities/rules.js'; + +describe('Activity control rules', () => { + const MOCK_ACTIVITY = 'mockActivity'; + const MOCK_RULE = 'mockRule'; + + let registerRule, isAllowed, logger; + + beforeEach(() => { + logger = { + logInfo: sinon.stub(), + logWarn: sinon.stub(), + logError: sinon.stub(), + }; + [registerRule, isAllowed] = ruleRegistry(logger); + }); + + it('allows by default', () => { + expect(isAllowed(MOCK_ACTIVITY, {})).to.be.true; + }); + + it('denies if a rule throws', () => { + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => { + throw new Error('argh'); + }); + expect(isAllowed(MOCK_ACTIVITY, {})).to.be.false; + }); + + it('denies if a rule denies', () => { + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: false})); + expect(isAllowed(MOCK_ACTIVITY, {})).to.be.false; + }); + + it('partitions rules by activity', () => { + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: false})); + expect(isAllowed('other', {})).to.be.true; + }); + + it('passes params to rules', () => { + registerRule(MOCK_ACTIVITY, MOCK_RULE, (params) => ({allow: params.foo !== 'bar'})); + expect(isAllowed(MOCK_ACTIVITY, {foo: 'notbar'})).to.be.true; + expect(isAllowed(MOCK_ACTIVITY, {foo: 'bar'})).to.be.false; + }); + + it('allows if rules do not opine', () => { + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => null); + expect(isAllowed(MOCK_ACTIVITY, {foo: 'bar'})).to.be.true; + }); + + it('denies if any rule denies', () => { + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => null); + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: true})); + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: false})); + expect(isAllowed(MOCK_ACTIVITY, {foo: 'bar'})).to.be.false; + }); + + it('allows if higher priority allow rule trumps a lower priority deny rule', () => { + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: false})); + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: true}), 0); + expect(isAllowed(MOCK_ACTIVITY, {})).to.be.true; + }); + + it('denies if a higher priority deny rule trumps a lower priority allow rule', () => { + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: true})); + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: false}), 0); + expect(isAllowed(MOCK_ACTIVITY, {})).to.be.false; + }); + + it('logs INFO when explicit allow is found', () => { + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: true})); + isAllowed(MOCK_ACTIVITY, {}); + sinon.assert.calledWithMatch(logger.logInfo, new RegExp(MOCK_RULE)); + }); + + it('logs INFO with reason if the rule provides one', () => { + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: true, reason: 'because'})); + isAllowed(MOCK_ACTIVITY, {}); + sinon.assert.calledWithMatch(logger.logInfo, new RegExp(MOCK_RULE)); + sinon.assert.calledWithMatch(logger.logInfo, /because/); + }); + + it('logs WARN when a deny is found', () => { + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: false})); + isAllowed(MOCK_ACTIVITY, {}); + sinon.assert.calledWithMatch(logger.logWarn, new RegExp(MOCK_RULE)); + }); + + it('logs WARN with reason if the rule provides one', () => { + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: false, reason: 'fail'})); + isAllowed(MOCK_ACTIVITY, {}); + sinon.assert.calledWithMatch(logger.logWarn, new RegExp(MOCK_RULE)); + sinon.assert.calledWithMatch(logger.logWarn, /fail/); + }); +}); From 7c4c15b4783b07b29086b39e6001440561caf44e Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 4 Apr 2023 10:36:00 -0700 Subject: [PATCH 12/37] Rule un-registration --- src/activities/rules.js | 8 +++++++- test/spec/activities/rules_spec.js | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/activities/rules.js b/src/activities/rules.js index efde90b87ca..91f33ac5913 100644 --- a/src/activities/rules.js +++ b/src/activities/rules.js @@ -36,11 +36,17 @@ export function ruleRegistry(logger = prefixLog('Activity control:')) { * parameters as a single object; MAY return an object {allow, reason}, where allow is true/false, * and reason is an optional message used for logging. * @param {number} priority rule priority; lower number means higher priority + * @returns a function that unregisters the rule when called. */ function registerActivityControl(activity, ruleName, rule, priority = 10) { const rules = getRules(activity); const pos = rules.findIndex(([itemPriority]) => priority < itemPriority); - rules.splice(pos < 0 ? rules.length : pos, 0, [priority, ruleName, rule]); + const entry = [priority, ruleName, rule]; + rules.splice(pos < 0 ? rules.length : pos, 0, entry); + return function () { + const idx = rules.indexOf(entry); + if (idx >= 0) rules.splice(idx, 1); + } }, /** * Test whether an activity is allowed. diff --git a/test/spec/activities/rules_spec.js b/test/spec/activities/rules_spec.js index d5ee3c7e845..256e2f93692 100644 --- a/test/spec/activities/rules_spec.js +++ b/test/spec/activities/rules_spec.js @@ -66,6 +66,14 @@ describe('Activity control rules', () => { expect(isAllowed(MOCK_ACTIVITY, {})).to.be.false; }); + it('can unregister rules', () => { + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: true})); + const r = registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: false}), 0); + expect(isAllowed(MOCK_ACTIVITY, {})).to.be.false; + r(); + expect(isAllowed(MOCK_ACTIVITY, {})).to.be.true; + }) + it('logs INFO when explicit allow is found', () => { registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: true})); isAllowed(MOCK_ACTIVITY, {}); From 0610dc2205e677a1e3ce01789c03176f9a01d922 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 4 Apr 2023 12:14:30 -0700 Subject: [PATCH 13/37] fetchBids enforcement --- modules/gdprEnforcement.js | 37 ++--- src/adapterManager.js | 12 ++ test/spec/modules/gdprEnforcement_spec.js | 167 +++++++-------------- test/spec/unit/core/adapterManager_spec.js | 72 ++++++++- 4 files changed, 150 insertions(+), 138 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 798dfc848da..e777332e37f 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -18,6 +18,9 @@ import { MODULE_TYPE_CORE, MODULE_TYPE_RTD, MODULE_TYPE_UID } from '../src/activities/modules.js'; +import {ACTIVITY_PARAM_COMPONENT_NAME, ACTIVITY_PARAM_COMPONENT_TYPE} from '../src/activities/params.js'; +import {registerActivityControl} from '../src/activities/rules.js'; +import {ACTIVITY_FETCH_BIDS} from '../src/activities/activities.js'; export const STRICT_STORAGE_ENFORCEMENT = 'strictStorageEnforcement'; @@ -263,30 +266,15 @@ export function userIdHook(fn, submodules, consentData) { } /** - * Checks if bidders are allowed in the auction. - * Enforces "purpose 2 (Basic Ads)" of TCF v2.0 spec - * @param {Function} fn - Function reference to the original function. - * @param {Array} adUnits + * fetchBids activity control: disallow bidders from auctions if they do not have consent for purpose 2 */ -export function makeBidRequestsHook(fn, adUnits, ...args) { +export function fetchBidsRule(params) { const consentData = gdprDataHandler.getConsentData(); if (shouldEnforce(consentData, 2)) { - adUnits.forEach(adUnit => { - adUnit.bids = adUnit.bids.filter(bid => { - const currBidder = bid.bidder; - const gvlId = getGvlid(MODULE_TYPE_BIDDER, currBidder); - if (includes(biddersBlocked, currBidder)) return false; - const isAllowed = !!validateRules(purpose2Rule, consentData, currBidder, gvlId); - if (!isAllowed) { - logWarn(`TCF2 blocked auction for ${currBidder}`); - biddersBlocked.push(currBidder); - } - return isAllowed; - }); - }); - fn.call(this, adUnits, ...args); - } else { - fn.call(this, adUnits, ...args); + const bidder = params[ACTIVITY_PARAM_COMPONENT_NAME] + const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], bidder); + const allow = !!validateRules(purpose2Rule, consentData, bidder, gvlid); + if (!allow) return {allow}; } } @@ -344,6 +332,9 @@ const hasPurpose1 = (rule) => { return rule.purpose === TCF2.purpose1.name } const hasPurpose2 = (rule) => { return rule.purpose === TCF2.purpose2.name } const hasPurpose7 = (rule) => { return rule.purpose === TCF2.purpose7.name } +const RULE_NAME = 'TCF2'; +const RULE_HANDLES = []; + /** * A configuration function that initializes some module variables, as well as adds hooks * @param {Object} config - GDPR enforcement config object @@ -379,7 +370,7 @@ export function setEnforcementConfig(config) { getHook('validateGdprEnforcement').before(userIdHook, 47); } if (purpose2Rule) { - getHook('makeBidRequests').before(makeBidRequestsHook); + RULE_HANDLES.push(registerActivityControl(ACTIVITY_FETCH_BIDS, RULE_NAME, fetchBidsRule)); } if (purpose7Rule) { getHook('enableAnalyticsCb').before(enableAnalyticsHook); @@ -388,11 +379,11 @@ export function setEnforcementConfig(config) { } export function uninstall() { + while (RULE_HANDLES.length) RULE_HANDLES.pop()(); [ validateStorageEnforcement.getHooks({hook: deviceAccessHook}), registerSyncInner.getHooks({hook: userSyncHook}), getHook('validateGdprEnforcement').getHooks({hook: userIdHook}), - getHook('makeBidRequests').getHooks({hook: makeBidRequestsHook}), getHook('enableAnalyticsCb').getHooks({hook: enableAnalyticsHook}), ].forEach(hook => hook.remove()); hooksAdded = false; diff --git a/src/adapterManager.js b/src/adapterManager.js index 45438f59b55..471e62b06b6 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -37,12 +37,19 @@ import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; import {auctionManager} from './auctionManager.js'; import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER} from './activities/modules.js'; +import {isActivityAllowed} from './activities/rules.js'; +import {ACTIVITY_FETCH_BIDS} from './activities/activities.js'; +import {activityParams} from './activities/params.js'; export const PARTITIONS = { CLIENT: 'client', SERVER: 'server' } +export const dep = { + isAllowed: isActivityAllowed +} + let adapterManager = {}; let _bidderRegistry = adapterManager.bidderRegistry = {}; @@ -237,6 +244,11 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a if (FEATURES.NATIVE) { decorateAdUnitsWithNativeParams(adUnits); } + + // filter out bidders that cannot participate in the auction + // TODO: PBS requests should be checked against the PBS vendor + adUnits.forEach(au => au.bids = au.bids.filter((bid) => !bid.bidder || dep.isAllowed(ACTIVITY_FETCH_BIDS, activityParams(MODULE_TYPE_BIDDER, bid.bidder)))) + adUnits = setupAdUnitMediaTypes(adUnits, labels); let {[PARTITIONS.CLIENT]: clientBidders, [PARTITIONS.SERVER]: serverBidders} = partitionBidders(adUnits, _s2sConfigs); diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 941f2b3c8df..b666290909b 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -1,10 +1,9 @@ import { deviceAccessHook, enableAnalyticsHook, - enforcementRules, + enforcementRules, fetchBidsRule, getGvlid, getGvlidFromAnalyticsAdapter, - makeBidRequestsHook, purpose1Rule, purpose2Rule, setEnforcementConfig, @@ -28,6 +27,7 @@ import 'src/prebid.js'; import {hook} from '../../../src/hook.js'; import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../../../src/consentHandler.js'; import {validateStorageEnforcement} from '../../../src/storageManager.js'; +import {activityParams} from '../../../src/activities/params.js'; describe('gdpr enforcement', function () { let nextFnSpy; @@ -107,7 +107,18 @@ describe('gdpr enforcement', function () { } } }; - let gvlids; + let gvlids, sandbox; + + function setupConsentData({gdprApplies = true, apiVersion = 2} = {}) { + const cd = utils.deepClone(staticConfig); + const consent = { + vendorData: cd.consentData.getTCData, + gdprApplies, + apiVersion + }; + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => consent) + return consent; + } before(() => { hook.ready(); @@ -116,17 +127,17 @@ describe('gdpr enforcement', function () { after(function () { validateStorageEnforcement.getHooks({ hook: deviceAccessHook }).remove(); $$PREBID_GLOBAL$$.requestBids.getHooks().remove(); - adapterManager.makeBidRequests.getHooks({ hook: makeBidRequestsHook }).remove(); }) beforeEach(() => { + sandbox = sinon.sandbox.create(); gvlids = {}; - sinon.stub(GDPR_GVLIDS, 'get').callsFake((name) => ({gvlid: gvlids[name], modules: {}})); + sandbox.stub(GDPR_GVLIDS, 'get').callsFake((name) => ({gvlid: gvlids[name], modules: {}})); }); afterEach(() => { - GDPR_GVLIDS.get.restore(); - }); + sandbox.restore(); + }) describe('deviceAccessHook', function () { beforeEach(function () { @@ -528,40 +539,9 @@ describe('gdpr enforcement', function () { }); }); - describe('makeBidRequestsHook', function () { - let sandbox; - let adapterManagerStub; - let emitEventSpy; - - const MOCK_AD_UNITS = [{ - code: 'ad-unit-1', - mediaTypes: {}, - bids: [{ - bidder: 'bidder_1' // has consent - }, { - bidder: 'bidder_2' // doesn't have consent, but liTransparency is true. Bidder remains active. - }] - }, { - code: 'ad-unit-2', - mediaTypes: {}, - bids: [{ - bidder: 'bidder_2' - }, { - bidder: 'bidder_3' - }] - }]; - - beforeEach(function () { - sandbox = sinon.createSandbox(); - gdprDataHandlerStub = sandbox.stub(gdprDataHandler, 'getConsentData'); - adapterManagerStub = sandbox.stub(adapterManager, 'getBidAdapter'); - logWarnSpy = sandbox.spy(utils, 'logWarn'); - nextFnSpy = sandbox.spy(); - emitEventSpy = sandbox.spy(events, 'emit'); - }); + describe('fetchBidsRule', () => { afterEach(function () { config.resetConfig(); - sandbox.restore(); }); it('should block bidder which does not have consent and allow bidder which has consent (liTransparency is established)', function () { @@ -575,35 +555,12 @@ describe('gdpr enforcement', function () { }] } }); - const consentData = {}; - consentData.vendorData = staticConfig.consentData.getTCData; - consentData.apiVersion = 2; - consentData.gdprApplies = true; - - gdprDataHandlerStub.returns(consentData); + setupConsentData() Object.assign(gvlids, { bidder_1: 4, bidder_2: 5, }); - makeBidRequestsHook(nextFnSpy, MOCK_AD_UNITS, []); - - // Assertions - expect(nextFnSpy.calledOnce).to.equal(true); - sinon.assert.calledWith(nextFnSpy, [{ - code: 'ad-unit-1', - mediaTypes: {}, - bids: [ - sinon.match({ bidder: 'bidder_1' }), - sinon.match({ bidder: 'bidder_2' }) - ] - }, { - code: 'ad-unit-2', - mediaTypes: {}, - bids: [ - sinon.match({ bidder: 'bidder_2' }), - sinon.match({ bidder: 'bidder_3' }) // should be allowed even though it's doesn't have a gvlId because liTransparency is established. - ] - }], []); + ['bidder_1', 'bidder_2', 'bidder_3'].forEach(bidder => expect(fetchBidsRule(activityParams(MODULE_TYPE_BIDDER, bidder))).to.not.exist); }); it('should block bidder which does not have consent and allow bidder which has consent (liTransparency is NOT established)', function() { @@ -617,41 +574,20 @@ describe('gdpr enforcement', function () { }] } }); - const consentData = {}; - - // set li for purpose 2 to false - const newConsentData = utils.deepClone(staticConfig); - newConsentData.consentData.getTCData.purpose.legitimateInterests['2'] = false; - - consentData.vendorData = newConsentData.consentData.getTCData; - consentData.apiVersion = 2; - consentData.gdprApplies = true; - - gdprDataHandlerStub.returns(consentData); + const consent = setupConsentData(); + consent.vendorData.purpose.legitimateInterests['2'] = false; Object.assign(gvlids, { bidder_1: 4, bidder_2: 5, }) - - makeBidRequestsHook(nextFnSpy, MOCK_AD_UNITS, []); - - // Assertions - expect(nextFnSpy.calledOnce).to.equal(true); - sinon.assert.calledWith(nextFnSpy, [{ - code: 'ad-unit-1', - mediaTypes: {}, - bids: [ - sinon.match({ bidder: 'bidder_1' }), // 'bidder_2' is not present because it doesn't have vendorConsent - ] - }, { - code: 'ad-unit-2', - mediaTypes: {}, - bids: [ - sinon.match({ bidder: 'bidder_3' }), // 'bidder_3' is allowed despite gvlId being undefined because it's part of vendorExceptions - ] - }], []); - - expect(logWarnSpy.calledOnce).to.equal(true); + Object.entries({ + bidder_1: true, + bidder_2: false, + bidder_3: true + }).forEach(([bidder, allowed]) => { + const res = fetchBidsRule(activityParams(MODULE_TYPE_BIDDER, bidder)); + allowed ? expect(res).to.not.exist : expect(res).to.eql({allow: false}); + }) }); it('should skip validation checks if GDPR version is not equal to "2"', function () { @@ -659,31 +595,36 @@ describe('gdpr enforcement', function () { gdpr: { rules: [{ purpose: 'storage', - enforePurpose: false, - enforceVendor: false, + enforcePurpose: true, + enforceVendor: true, vendorExceptions: [] }] } }); - - const consentData = {}; - consentData.vendorData = staticConfig.consentData.getTCData; - consentData.apiVersion = 1; - consentData.gdprApplies = true; - gdprDataHandlerStub.returns(consentData); - - makeBidRequestsHook(nextFnSpy, MOCK_AD_UNITS, []); - - // Assertions - expect(nextFnSpy.calledOnce).to.equal(true); - sinon.assert.calledWith(nextFnSpy, sinon.match.array.deepEquals(MOCK_AD_UNITS), []); - expect(emitEventSpy.notCalled).to.equal(true); - expect(logWarnSpy.notCalled).to.equal(true); + const consent = setupConsentData(); + consent.vendorData.purpose.consents['2'] = false; + consent.apiVersion = 1; + ['bidder_1', 'bidder_2', 'bidder_3'].forEach(bidder => expect(fetchBidsRule(activityParams(MODULE_TYPE_BIDDER, bidder))).to.not.exist); }); + + it('should skip validation if enforcePurpose is false', () => { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: true, + vendorExceptions: [] + }] + } + }); + const consent = setupConsentData(); + consent.vendorData.purpose.consents['2'] = false; + ['bidder_1', 'bidder_2', 'bidder_3'].forEach(bidder => expect(fetchBidsRule(activityParams(MODULE_TYPE_BIDDER, bidder))).to.not.exist); + }) }); describe('enableAnalyticsHook', function () { - let sandbox; let adapterManagerStub; const MOCK_ANALYTICS_ADAPTER_CONFIG = [{ @@ -698,7 +639,6 @@ describe('gdpr enforcement', function () { }]; beforeEach(function () { - sandbox = sinon.createSandbox(); gdprDataHandlerStub = sandbox.stub(gdprDataHandler, 'getConsentData'); adapterManagerStub = sandbox.stub(adapterManager, 'getAnalyticsAdapter'); logWarnSpy = sandbox.spy(utils, 'logWarn'); @@ -707,7 +647,6 @@ describe('gdpr enforcement', function () { afterEach(function() { config.resetConfig(); - sandbox.restore(); }); it('should block analytics adapter which does not have consent and allow the one(s) which have consent', function() { diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 8d887474180..2669de8b4c2 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -4,7 +4,7 @@ import adapterManager, { coppaDataHandler, _partitionBidders, PARTITIONS, - getS2SBidderSet, _filterBidsForAdUnit + getS2SBidderSet, _filterBidsForAdUnit, dep } from 'src/adapterManager.js'; import { getAdUnits, @@ -23,6 +23,7 @@ import {hook} from '../../../../src/hook.js'; import {auctionManager} from '../../../../src/auctionManager.js'; import {GDPR_GVLIDS} from '../../../../src/consentHandler.js'; import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER} from '../../../../src/activities/modules.js'; +import {ACTIVITY_FETCH_BIDS} from '../../../../src/activities/activities.js'; var events = require('../../../../src/events'); const CONFIG = { @@ -1721,6 +1722,75 @@ describe('adapterManager tests', function () { expect(sizes1).not.to.deep.equal(sizes2); }); + describe('and activity controls', () => { + const MOCK_BIDDERS = ['1', '2', '3', '4', '5'].map((n) => `mockBidder${n}`); + + beforeEach(() => { + sinon.stub(dep, 'isAllowed'); + MOCK_BIDDERS.forEach((bidder) => adapterManager.bidderRegistry[bidder] = {}); + }); + afterEach(() => { + dep.isAllowed.restore(); + MOCK_BIDDERS.forEach(bidder => { delete adapterManager.bidderRegistry[bidder] }); + config.resetConfig(); + }) + it('should not generate requests for bidders that cannot fetchBids', () => { + adUnits = [ + {code: 'one', bids: ['mockBidder1', 'mockBidder2', 'mockBidder3'].map((bidder) => ({bidder}))}, + {code: 'two', bids: ['mockBidder4', 'mockBidder5', 'mockBidder4'].map((bidder) => ({bidder}))} + ]; + const allowed = ['mockBidder2', 'mockBidder5']; + dep.isAllowed.callsFake((activity, {componentType, componentName}) => { + return activity === ACTIVITY_FETCH_BIDS && + componentType === MODULE_TYPE_BIDDER && + allowed.includes(componentName); + }); + let bidRequests = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + const bidders = Array.from(new Set(bidRequests.flatMap(br => br.bids).map(bid => bid.bidder)).keys()); + expect(bidders).to.have.members(allowed); + }); + + it('should keep stored impressions, even if everything else is denied', () => { + config.setConfig({ + s2sConfig: [ + { + enabled: true, + adapter: 'mockS2SDefault', + }, + { + enabled: true, + adapter: 'mockS2S1', + configName: 'mock1', + }, + { + enabled: true, + adapter: 'mockS2S2', + configName: 'mock2', + } + ] + }) + adUnits = [ + {code: 'one', bids: [{bidder: null}]}, + {code: 'two', bids: [{module: 'pbsBidAdapter', configName: 'mock1'}, {module: 'pbsBidAdapter', configName: 'mock2'}]} + ] + dep.isAllowed.callsFake(() => false); + let bidRequests = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + expect(bidRequests.flatMap(br => br.bids).length).to.equal(3); + }) + }) + it('should make FPD available under `ortb2`', () => { const global = { k1: 'v1', From afa843a5f7751ff9f68304ace005aeac528ab465 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 4 Apr 2023 14:25:29 -0700 Subject: [PATCH 14/37] fetchBids rule for gdpr --- modules/gdprEnforcement.js | 11 ++- src/activities/params.js | 5 +- src/activities/rules.js | 21 +++-- src/adapterManager.js | 16 ++-- test/spec/unit/core/adapterManager_spec.js | 93 ++++++++++++++-------- 5 files changed, 99 insertions(+), 47 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index e777332e37f..ac945668274 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -5,7 +5,7 @@ import {deepAccess, hasDeviceAccess, isArray, logError, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import adapterManager, {gdprDataHandler} from '../src/adapterManager.js'; -import {find, includes} from '../src/polyfill.js'; +import {find} from '../src/polyfill.js'; import {registerSyncInner} from '../src/adapters/bidderFactory.js'; import {getHook} from '../src/hook.js'; import {validateStorageEnforcement} from '../src/storageManager.js'; @@ -266,9 +266,16 @@ export function userIdHook(fn, submodules, consentData) { } /** - * fetchBids activity control: disallow bidders from auctions if they do not have consent for purpose 2 + * fetchBids activity control: disallow bidders from auctions if they do not have consent for purpose 2 (and purpose 2 + * enforcement is enabled) */ export function fetchBidsRule(params) { + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] !== MODULE_TYPE_BIDDER) { + // TODO: this special case is for the PBS adapter (componentType is core) + // we should check for generic purpose 2 consent & vendor consent based on the PBS vendor's GVL ID; + // that is, however, a breaking change and skipped for now + return; + } const consentData = gdprDataHandler.getConsentData(); if (shouldEnforce(consentData, 2)) { const bidder = params[ACTIVITY_PARAM_COMPONENT_NAME] diff --git a/src/activities/params.js b/src/activities/params.js index ba0746a935f..9d4126ee80d 100644 --- a/src/activities/params.js +++ b/src/activities/params.js @@ -1,7 +1,10 @@ - export const ACTIVITY_PARAM_COMPONENT = 'component'; export const ACTIVITY_PARAM_COMPONENT_TYPE = ACTIVITY_PARAM_COMPONENT + 'Type'; export const ACTIVITY_PARAM_COMPONENT_NAME = ACTIVITY_PARAM_COMPONENT + 'Name'; +/** + * s2sConfig[].configName, used to identify a particular s2s instance (applies to fetchBids) + */ +export const ACTIVITY_PARAM_S2S_NAME = 'configName'; export function activityParams(moduleType, moduleName, params) { return Object.assign({ diff --git a/src/activities/rules.js b/src/activities/rules.js index 91f33ac5913..26ac4b6e6e3 100644 --- a/src/activities/rules.js +++ b/src/activities/rules.js @@ -1,4 +1,5 @@ import {prefixLog} from '../utils.js'; +import {ACTIVITY_PARAM_COMPONENT} from './params.js'; export function ruleRegistry(logger = prefixLog('Activity control:')) { const registry = {}; @@ -15,11 +16,11 @@ export function ruleRegistry(logger = prefixLog('Activity control:')) { logger.logError(`Exception in rule ${name} for '${activity}'`, e); res = {allow: false, reason: e}; } - return res && Object.assign({activity, name}, res); + return res && Object.assign({activity, name, component: params[ACTIVITY_PARAM_COMPONENT]}, res); } - function logResult({activity, name, allow, reason}) { - let msg = `'${activity}' is ${allow ? 'allowed' : 'denied'} by ${name}`; + function logResult({activity, name, allow, reason, component}) { + let msg = `${name} ${allow ? 'allowed' : 'denied'} '${activity}' for '${component}'` if (reason) { msg = `${msg}: ${reason}`; } @@ -33,10 +34,16 @@ export function ruleRegistry(logger = prefixLog('Activity control:')) { * @param {string} activity activity name - set is defined in `activities.js` * @param {string} ruleName a name for this rule; used for logging. * @param {function({}): {allow: boolean, reason?: string}} rule definition function. Takes in activity - * parameters as a single object; MAY return an object {allow, reason}, where allow is true/false, - * and reason is an optional message used for logging. + * parameters as a single map; MAY return an object {allow, reason}, where allow is true/false, + * and reason is an optional message used for logging. + * + * {allow: true} will allow this activity AS LONG AS no other rules with same or higher priority return {allow: false}; + * {allow: false} will deny this activity AS LONG AS no other rules with higher priority return {allow: true}; + * returning null/undefined has no effect - the decision is left to other rules. + * If no rule returns an allow value, the default is to allow the activity. + * * @param {number} priority rule priority; lower number means higher priority - * @returns a function that unregisters the rule when called. + * @returns {function(void): void} a function that unregisters the rule when called. */ function registerActivityControl(activity, ruleName, rule, priority = 10) { const rules = getRules(activity); @@ -52,7 +59,7 @@ export function ruleRegistry(logger = prefixLog('Activity control:')) { * Test whether an activity is allowed. * * @param {string} activity activity name - * @param {{}} params activity parameters; should be generated through `activityParams` below + * @param {{}} params activity parameters; should be generated through the `activityParams` utility. * @return {boolean} true for allow, false for deny. */ function isActivityAllowed(activity, params) { diff --git a/src/adapterManager.js b/src/adapterManager.js index 471e62b06b6..c47268472cf 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -36,11 +36,12 @@ import * as events from './events.js'; import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; import {auctionManager} from './auctionManager.js'; -import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER} from './activities/modules.js'; +import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, MODULE_TYPE_CORE} from './activities/modules.js'; import {isActivityAllowed} from './activities/rules.js'; import {ACTIVITY_FETCH_BIDS} from './activities/activities.js'; -import {activityParams} from './activities/params.js'; +import {ACTIVITY_PARAM_S2S_NAME, activityParams} from './activities/params.js'; +const PBS_ADAPTER_NAME = 'pbsBidAdapter'; export const PARTITIONS = { CLIENT: 'client', SERVER: 'server' @@ -146,7 +147,7 @@ function getAdUnitCopyForPrebidServer(adUnits, s2sConfig) { adUnitsCopy.forEach((adUnit) => { // filter out client side bids - const s2sBids = adUnit.bids.filter((b) => b.module === 'pbsBidAdapter' && b.params?.configName === s2sConfig.configName); + const s2sBids = adUnit.bids.filter((b) => b.module === PBS_ADAPTER_NAME && b.params?.configName === s2sConfig.configName); if (s2sBids.length === 1) { adUnit.s2sBid = s2sBids[0]; hasModuleBids = true; @@ -246,7 +247,6 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a } // filter out bidders that cannot participate in the auction - // TODO: PBS requests should be checked against the PBS vendor adUnits.forEach(au => au.bids = au.bids.filter((bid) => !bid.bidder || dep.isAllowed(ACTIVITY_FETCH_BIDS, activityParams(MODULE_TYPE_BIDDER, bid.bidder)))) adUnits = setupAdUnitMediaTypes(adUnits, labels); @@ -270,8 +270,14 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a return bidderRequest; } + function isS2SAllowed(s2sConfig) { + return dep.isAllowed(ACTIVITY_FETCH_BIDS, activityParams(MODULE_TYPE_CORE, PBS_ADAPTER_NAME, { + [ACTIVITY_PARAM_S2S_NAME]: s2sConfig.configName + })); + } + _s2sConfigs.forEach(s2sConfig => { - if (s2sConfig && s2sConfig.enabled) { + if (s2sConfig && s2sConfig.enabled && isS2SAllowed(s2sConfig)) { let {adUnits: adUnitsS2SCopy, hasModuleBids} = getAdUnitCopyForPrebidServer(adUnits, s2sConfig); // uniquePbsTid is so we know which server to send which bids to during the callBids function diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 2669de8b4c2..3b87f1480cb 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -1756,40 +1756,69 @@ describe('adapterManager tests', function () { expect(bidders).to.have.members(allowed); }); - it('should keep stored impressions, even if everything else is denied', () => { - config.setConfig({ - s2sConfig: [ - { - enabled: true, - adapter: 'mockS2SDefault', - }, - { - enabled: true, - adapter: 'mockS2S1', - configName: 'mock1', - }, + describe('with multiple s2s configs', () => { + beforeEach(() => { + config.setConfig({ + s2sConfig: [ + { + enabled: true, + adapter: 'mockS2SDefault', + bidders: ['mockBidder1'] + }, + { + enabled: true, + adapter: 'mockS2S1', + configName: 'mock1', + }, + { + enabled: true, + adapter: 'mockS2S2', + configName: 'mock2', + } + ] + }); + }); + it('should keep stored impressions, even if everything else is denied', () => { + adUnits = [ + {code: 'one', bids: [{bidder: null}]}, + {code: 'two', bids: [{module: 'pbsBidAdapter', params: {configName: 'mock1'}}, {module: 'pbsBidAdapter', params: {configName: 'mock2'}}]} + ] + dep.isAllowed.callsFake(({componentType}) => componentType !== 'bidder'); + let bidRequests = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + expect(new Set(bidRequests.map(br => br.uniquePbsTid)).size).to.equal(3); + }); + + it('should check if the s2s adapter itself is allowed to fetch bids', () => { + adUnits = [ { - enabled: true, - adapter: 'mockS2S2', - configName: 'mock2', + code: 'au', + bids: [ + {bidder: null}, + {module: 'pbsBidAdapter', params: {configName: 'mock1'}}, + {module: 'pbsBidAdapter', params: {configName: 'mock2'}}, + {bidder: 'mockBidder1'} + ] } - ] - }) - adUnits = [ - {code: 'one', bids: [{bidder: null}]}, - {code: 'two', bids: [{module: 'pbsBidAdapter', configName: 'mock1'}, {module: 'pbsBidAdapter', configName: 'mock2'}]} - ] - dep.isAllowed.callsFake(() => false); - let bidRequests = adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - expect(bidRequests.flatMap(br => br.bids).length).to.equal(3); - }) - }) + ]; + dep.isAllowed.callsFake((_, {configName, componentName}) => !(componentName === 'pbsBidAdapter' && configName === 'mock1')); + let bidRequests = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() { + }, + [] + ); + expect(new Set(bidRequests.map(br => br.uniquePbsTid)).size).to.eql(2) + }); + }); + }); it('should make FPD available under `ortb2`', () => { const global = { From 52cbd409c36fe4823658ee23cae45459edfb9e8c Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 4 Apr 2023 14:48:58 -0700 Subject: [PATCH 15/37] enableAnalytics check --- modules/gdprEnforcement.js | 20 ++++++++--- src/activities/params.js | 9 ++++- src/activities/rules.js | 10 +++--- src/adapterManager.js | 8 +++-- test/spec/activities/rules_spec.js | 6 ++-- test/spec/unit/core/adapterManager_spec.js | 42 +++++++++++++++++++++- 6 files changed, 77 insertions(+), 18 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index ac945668274..21d2e6469f2 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -18,9 +18,13 @@ import { MODULE_TYPE_CORE, MODULE_TYPE_RTD, MODULE_TYPE_UID } from '../src/activities/modules.js'; -import {ACTIVITY_PARAM_COMPONENT_NAME, ACTIVITY_PARAM_COMPONENT_TYPE} from '../src/activities/params.js'; +import { + ACTIVITY_PARAM_ANL_CONFIG, + ACTIVITY_PARAM_COMPONENT_NAME, + ACTIVITY_PARAM_COMPONENT_TYPE +} from '../src/activities/params.js'; import {registerActivityControl} from '../src/activities/rules.js'; -import {ACTIVITY_FETCH_BIDS} from '../src/activities/activities.js'; +import {ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from '../src/activities/activities.js'; export const STRICT_STORAGE_ENFORCEMENT = 'strictStorageEnforcement'; @@ -285,6 +289,15 @@ export function fetchBidsRule(params) { } } +export function reportAnalyticsRule(params) { + const consentData = gdprDataHandler.getConsentData(); + if (shouldEnforce(consentData, 7, 'Analytics')) { + const analyticsAdapterCode = params[ACTIVITY_PARAM_COMPONENT_NAME]; + const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], analyticsAdapterCode, () => getGvlidFromAnalyticsAdapter(analyticsAdapterCode, params[ACTIVITY_PARAM_ANL_CONFIG])); + const allow = !!validateRules(purpose7Rule, consentData, analyticsAdapterCode, gvlid); + if (!allow) return {allow}; + } +} /** * Checks if Analytics adapters are allowed to send data to their servers for furhter processing. * Enforces "purpose 7 (Measurement)" of TCF v2.0 spec @@ -380,7 +393,7 @@ export function setEnforcementConfig(config) { RULE_HANDLES.push(registerActivityControl(ACTIVITY_FETCH_BIDS, RULE_NAME, fetchBidsRule)); } if (purpose7Rule) { - getHook('enableAnalyticsCb').before(enableAnalyticsHook); + RULE_HANDLES.push(registerActivityControl(ACTIVITY_REPORT_ANALYTICS, RULE_NAME, reportAnalyticsRule)) } } } @@ -391,7 +404,6 @@ export function uninstall() { validateStorageEnforcement.getHooks({hook: deviceAccessHook}), registerSyncInner.getHooks({hook: userSyncHook}), getHook('validateGdprEnforcement').getHooks({hook: userIdHook}), - getHook('enableAnalyticsCb').getHooks({hook: enableAnalyticsHook}), ].forEach(hook => hook.remove()); hooksAdded = false; } diff --git a/src/activities/params.js b/src/activities/params.js index 9d4126ee80d..034e8dc02e7 100644 --- a/src/activities/params.js +++ b/src/activities/params.js @@ -2,9 +2,16 @@ export const ACTIVITY_PARAM_COMPONENT = 'component'; export const ACTIVITY_PARAM_COMPONENT_TYPE = ACTIVITY_PARAM_COMPONENT + 'Type'; export const ACTIVITY_PARAM_COMPONENT_NAME = ACTIVITY_PARAM_COMPONENT + 'Name'; /** - * s2sConfig[].configName, used to identify a particular s2s instance (applies to fetchBids) + * s2sConfig[].configName, used to identify a particular s2s instance + * relevant for: fetchBids */ export const ACTIVITY_PARAM_S2S_NAME = 'configName'; +/** + * @private + * configuration options for analytics adapter - the argument passed to `enableAnalytics`. + * relevant for: reportAnalytics + */ +export const ACTIVITY_PARAM_ANL_CONFIG = '_config'; export function activityParams(moduleType, moduleName, params) { return Object.assign({ diff --git a/src/activities/rules.js b/src/activities/rules.js index 26ac4b6e6e3..28dbba2d317 100644 --- a/src/activities/rules.js +++ b/src/activities/rules.js @@ -20,11 +20,11 @@ export function ruleRegistry(logger = prefixLog('Activity control:')) { } function logResult({activity, name, allow, reason, component}) { - let msg = `${name} ${allow ? 'allowed' : 'denied'} '${activity}' for '${component}'` - if (reason) { - msg = `${msg}: ${reason}`; - } - (allow ? logger.logInfo : logger.logWarn)(msg); + const msg = [ + `${name} ${allow ? 'allowed' : 'denied'} '${activity}' for '${component}'${reason ? ':' : ''}` + ]; + reason && msg.push(reason); + (allow ? logger.logInfo : logger.logWarn).apply(logger, msg); } return [ diff --git a/src/adapterManager.js b/src/adapterManager.js index c47268472cf..e04d4c83f7c 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -38,8 +38,8 @@ import {useMetrics} from './utils/perfMetrics.js'; import {auctionManager} from './auctionManager.js'; import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, MODULE_TYPE_CORE} from './activities/modules.js'; import {isActivityAllowed} from './activities/rules.js'; -import {ACTIVITY_FETCH_BIDS} from './activities/activities.js'; -import {ACTIVITY_PARAM_S2S_NAME, activityParams} from './activities/params.js'; +import {ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from './activities/activities.js'; +import {ACTIVITY_PARAM_ANL_CONFIG, ACTIVITY_PARAM_S2S_NAME, activityParams} from './activities/params.js'; const PBS_ADAPTER_NAME = 'pbsBidAdapter'; export const PARTITIONS = { @@ -570,7 +570,9 @@ adapterManager.enableAnalytics = function (config) { _each(config, adapterConfig => { const entry = _analyticsRegistry[adapterConfig.provider]; if (entry && entry.adapter) { - entry.adapter.enableAnalytics(adapterConfig); + if (dep.isAllowed(ACTIVITY_REPORT_ANALYTICS, activityParams(MODULE_TYPE_ANALYTICS, adapterConfig.provider, {[ACTIVITY_PARAM_ANL_CONFIG]: adapterConfig}))) { + entry.adapter.enableAnalytics(adapterConfig); + } } else { logError(`Prebid Error: no analytics adapter found in registry for '${adapterConfig.provider}'.`); } diff --git a/test/spec/activities/rules_spec.js b/test/spec/activities/rules_spec.js index 256e2f93692..cd3bb9d849a 100644 --- a/test/spec/activities/rules_spec.js +++ b/test/spec/activities/rules_spec.js @@ -83,8 +83,7 @@ describe('Activity control rules', () => { it('logs INFO with reason if the rule provides one', () => { registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: true, reason: 'because'})); isAllowed(MOCK_ACTIVITY, {}); - sinon.assert.calledWithMatch(logger.logInfo, new RegExp(MOCK_RULE)); - sinon.assert.calledWithMatch(logger.logInfo, /because/); + sinon.assert.calledWithMatch(logger.logInfo, new RegExp(MOCK_RULE), /because/); }); it('logs WARN when a deny is found', () => { @@ -96,7 +95,6 @@ describe('Activity control rules', () => { it('logs WARN with reason if the rule provides one', () => { registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({allow: false, reason: 'fail'})); isAllowed(MOCK_ACTIVITY, {}); - sinon.assert.calledWithMatch(logger.logWarn, new RegExp(MOCK_RULE)); - sinon.assert.calledWithMatch(logger.logWarn, /fail/); + sinon.assert.calledWithMatch(logger.logWarn, new RegExp(MOCK_RULE), /fail/); }); }); diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 3b87f1480cb..1da73c0ac61 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -23,7 +23,8 @@ import {hook} from '../../../../src/hook.js'; import {auctionManager} from '../../../../src/auctionManager.js'; import {GDPR_GVLIDS} from '../../../../src/consentHandler.js'; import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER} from '../../../../src/activities/modules.js'; -import {ACTIVITY_FETCH_BIDS} from '../../../../src/activities/activities.js'; +import {ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from '../../../../src/activities/activities.js'; +import {sandbox} from 'sinon'; var events = require('../../../../src/events'); const CONFIG = { @@ -2839,6 +2840,45 @@ describe('adapterManager tests', function () { }) }); + describe('reportAnalytics check', () => { + beforeEach(() => { + sinon.stub(dep, 'isAllowed'); + }); + afterEach(() => { + dep.isAllowed.restore(); + }); + + it('should check for reportAnalytics before registering analytics adapter', () => { + const enabled = {}; + ['mockAnalytics1', 'mockAnalytics2'].forEach((code) => { + adapterManager.registerAnalyticsAdapter({ + code, + adapter: { + enableAnalytics: sinon.stub().callsFake(() => { enabled[code] = true }) + } + }) + }) + + const anlCfg = [ + { + provider: 'mockAnalytics1', + random: 'values' + }, + { + provider: 'mockAnalytics2' + } + ] + dep.isAllowed.callsFake((activity, {component, _config}) => { + return activity === ACTIVITY_REPORT_ANALYTICS && + component === `${MODULE_TYPE_ANALYTICS}.${anlCfg[0].provider}` && + _config === anlCfg[0] + }) + + adapterManager.enableAnalytics(anlCfg); + expect(enabled).to.eql({mockAnalytics1: true}); + }); + }); + describe('registers GVL IDs', () => { beforeEach(() => { sinon.stub(GDPR_GVLIDS, 'register'); From 93cce10d6efe634f5cdfc4c542641a89d323fe8b Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 5 Apr 2023 07:27:22 -0700 Subject: [PATCH 16/37] reportAnalytics TCF2 rule --- modules/gdprEnforcement.js | 64 +++++++++-------------- src/adapterManager.js | 1 - test/spec/modules/gdprEnforcement_spec.js | 55 ++++--------------- 3 files changed, 35 insertions(+), 85 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 21d2e6469f2..e290c231498 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -2,7 +2,7 @@ * This module gives publishers extra set of features to enforce individual purposes of TCF v2 */ -import {deepAccess, hasDeviceAccess, isArray, logError, logWarn} from '../src/utils.js'; +import {deepAccess, hasDeviceAccess, logError, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import adapterManager, {gdprDataHandler} from '../src/adapterManager.js'; import {find} from '../src/polyfill.js'; @@ -55,9 +55,9 @@ export let purpose7Rule; export let enforcementRules; -const storageBlocked = []; -const biddersBlocked = []; -const analyticsBlocked = []; +const storageBlocked = new Set(); +const biddersBlocked = new Set(); +const analyticsBlocked = new Set(); let hooksAdded = false; let strictStorageEnforcement = false; @@ -212,7 +212,7 @@ export function deviceAccessHook(fn, moduleType, moduleName, result, {validate = } else { curModule && logWarn(`TCF2 denied device access for ${curModule}`); result.valid = false; - storageBlocked.push(curModule); + storageBlocked.add(curModule); } } else { result.valid = true; @@ -236,7 +236,7 @@ export function userSyncHook(fn, ...args) { fn.call(this, ...args); } else { logWarn(`User sync not allowed for ${curBidder}`); - storageBlocked.push(curBidder); + storageBlocked.add(curBidder); } } else { fn.call(this, ...args); @@ -259,7 +259,7 @@ export function userIdHook(fn, submodules, consentData) { return submodule; } else { logWarn(`User denied permission to fetch user id for ${moduleName} User id module`); - storageBlocked.push(moduleName); + storageBlocked.add(moduleName); } return undefined; }).filter(module => module) @@ -285,44 +285,27 @@ export function fetchBidsRule(params) { const bidder = params[ACTIVITY_PARAM_COMPONENT_NAME] const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], bidder); const allow = !!validateRules(purpose2Rule, consentData, bidder, gvlid); - if (!allow) return {allow}; + if (!allow) { + biddersBlocked.add(bidder); + return {allow}; + } } } +/** + * reportAnalytics activity control: disallow analytics adapters that do not have purpose 7 consent + * (if purpose 7 consent enforcement is enabled). + */ export function reportAnalyticsRule(params) { const consentData = gdprDataHandler.getConsentData(); if (shouldEnforce(consentData, 7, 'Analytics')) { const analyticsAdapterCode = params[ACTIVITY_PARAM_COMPONENT_NAME]; const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], analyticsAdapterCode, () => getGvlidFromAnalyticsAdapter(analyticsAdapterCode, params[ACTIVITY_PARAM_ANL_CONFIG])); const allow = !!validateRules(purpose7Rule, consentData, analyticsAdapterCode, gvlid); - if (!allow) return {allow}; - } -} -/** - * Checks if Analytics adapters are allowed to send data to their servers for furhter processing. - * Enforces "purpose 7 (Measurement)" of TCF v2.0 spec - * @param {Function} fn - Function reference to the original function. - * @param {Array} config - Configuration object passed to pbjs.enableAnalytics() - */ -export function enableAnalyticsHook(fn, config) { - const consentData = gdprDataHandler.getConsentData(); - if (shouldEnforce(consentData, 7, 'Analytics')) { - if (!isArray(config)) { - config = [config] + if (!allow) { + analyticsBlocked.add(analyticsAdapterCode); + return {allow}; } - config = config.filter(conf => { - const analyticsAdapterCode = conf.provider; - const gvlid = getGvlid(MODULE_TYPE_ANALYTICS, analyticsAdapterCode, () => getGvlidFromAnalyticsAdapter(analyticsAdapterCode, conf)); - const isAllowed = !!validateRules(purpose7Rule, consentData, analyticsAdapterCode, gvlid); - if (!isAllowed) { - analyticsBlocked.push(analyticsAdapterCode); - logWarn(`TCF2 blocked analytics adapter ${conf.provider}`); - } - return isAllowed; - }); - fn.call(this, config); - } else { - fn.call(this, config); } } @@ -331,16 +314,17 @@ export function enableAnalyticsHook(fn, config) { */ function emitTCF2FinalResults() { // remove null and duplicate values - const formatArray = function (arr) { - return arr.filter((i, k) => i !== null && arr.indexOf(i) === k); + const formatSet = function (st) { + return Array.from(st.keys()).filter(el => el != null); } const tcf2FinalResults = { - storageBlocked: formatArray(storageBlocked), - biddersBlocked: formatArray(biddersBlocked), - analyticsBlocked: formatArray(analyticsBlocked) + storageBlocked: formatSet(storageBlocked), + biddersBlocked: formatSet(biddersBlocked), + analyticsBlocked: formatSet(analyticsBlocked) }; events.emit(CONSTANTS.EVENTS.TCF2_ENFORCEMENT, tcf2FinalResults); + [storageBlocked, biddersBlocked, analyticsBlocked].forEach(el => el.clear()); } events.on(CONSTANTS.EVENTS.AUCTION_END, emitTCF2FinalResults); diff --git a/src/adapterManager.js b/src/adapterManager.js index e04d4c83f7c..03ede473601 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -356,7 +356,6 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a bidRequest['gppConsent'] = gppDataHandler.getConsentData(); } }); - return bidRequests; }, 'makeBidRequests'); diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index b666290909b..2c563681475 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -1,11 +1,10 @@ import { deviceAccessHook, - enableAnalyticsHook, enforcementRules, fetchBidsRule, getGvlid, getGvlidFromAnalyticsAdapter, purpose1Rule, - purpose2Rule, + purpose2Rule, reportAnalyticsRule, setEnforcementConfig, STRICT_STORAGE_ENFORCEMENT, userIdHook, @@ -624,31 +623,7 @@ describe('gdpr enforcement', function () { }) }); - describe('enableAnalyticsHook', function () { - let adapterManagerStub; - - const MOCK_ANALYTICS_ADAPTER_CONFIG = [{ - provider: 'analyticsAdapter_A', - options: {} - }, { - provider: 'analyticsAdapter_B', - options: {} - }, { - provider: 'analyticsAdapter_C', - options: {} - }]; - - beforeEach(function () { - gdprDataHandlerStub = sandbox.stub(gdprDataHandler, 'getConsentData'); - adapterManagerStub = sandbox.stub(adapterManager, 'getAnalyticsAdapter'); - logWarnSpy = sandbox.spy(utils, 'logWarn'); - nextFnSpy = sandbox.spy(); - }); - - afterEach(function() { - config.resetConfig(); - }); - + describe('reportAnalyticsRule', () => { it('should block analytics adapter which does not have consent and allow the one(s) which have consent', function() { setEnforcementConfig({ gdpr: { @@ -661,30 +636,22 @@ describe('gdpr enforcement', function () { } }); - const consentData = {}; - consentData.vendorData = staticConfig.consentData.getTCData; - consentData.apiVersion = 2; - consentData.gdprApplies = true; - - gdprDataHandlerStub.returns(consentData); Object.assign(gvlids, { analyticsAdapter_A: 3, analyticsAdapter_B: 5, analyticsAdapter_C: 1 }); - enableAnalyticsHook(nextFnSpy, MOCK_ANALYTICS_ADAPTER_CONFIG); + setupConsentData() - // Assertions - expect(nextFnSpy.calledOnce).to.equal(true); - sinon.assert.calledWith(nextFnSpy, [{ - provider: 'analyticsAdapter_B', - options: {} - }, { - provider: 'analyticsAdapter_C', - options: {} - }]); - expect(logWarnSpy.calledOnce).to.equal(true); + Object.entries({ + analyticsAdapter_A: false, + analyticsAdapter_B: true, + analyticsAdapter_C: true + }).forEach(([adapter, allow]) => { + const res = reportAnalyticsRule(activityParams(MODULE_TYPE_ANALYTICS, adapter)); + allow ? expect(res).to.not.exist : expect(res).to.eql({allow}); + }) }); }); From 38f610ac9c905246ab95ab7fb00512bdeb9f6fc8 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 5 Apr 2023 07:38:32 -0700 Subject: [PATCH 17/37] Update logging condition for multiple GVL IDs --- modules/gdprEnforcement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index e290c231498..386858f120e 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -90,7 +90,7 @@ export function getGvlid(moduleType, moduleName, fallbackFn) { for (const type of GVLID_LOOKUP_PRIORITY) { if (modules.hasOwnProperty(type)) { gvlid = modules[type]; - if (type !== moduleType && !fallbackFn) { + if (type !== moduleType) { logWarn(`Multiple GVL IDs found for module '${moduleName}'; using the ${type} module's ID (${gvlid}) instead of the ${moduleType}'s ID (${modules[moduleType]})`) } break; From 167e44f66c9c6bd1339abc687d3052ed98a2657c Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 5 Apr 2023 08:22:47 -0700 Subject: [PATCH 18/37] Change core to prebid --- modules/gdprEnforcement.js | 6 +++--- modules/userId/index.js | 3 +++ src/activities/modules.js | 2 +- src/adapterManager.js | 4 ++-- src/storageManager.js | 4 ++-- test/spec/modules/gdprEnforcement_spec.js | 8 ++++---- test/spec/unit/core/storageManager_spec.js | 4 ++-- 7 files changed, 17 insertions(+), 14 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 386858f120e..1fab8921b5b 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -15,7 +15,7 @@ import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../src/consentHandler.js'; import { MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, - MODULE_TYPE_CORE, MODULE_TYPE_RTD, + MODULE_TYPE_PREBID, MODULE_TYPE_RTD, MODULE_TYPE_UID } from '../src/activities/modules.js'; import { @@ -80,7 +80,7 @@ export function getGvlid(moduleType, moduleName, fallbackFn) { // Return GVL ID from user defined gvlMapping if (gvlMapping && gvlMapping[moduleName]) { return gvlMapping[moduleName]; - } else if (moduleType === MODULE_TYPE_CORE) { + } else if (moduleType === MODULE_TYPE_PREBID) { return VENDORLESS_GVLID; } else { let {gvlid, modules} = GDPR_GVLIDS.get(moduleName); @@ -191,7 +191,7 @@ export function deviceAccessHook(fn, moduleType, moduleName, result, {validate = if (!hasDeviceAccess()) { logWarn('Device access is disabled by Publisher'); result.valid = false; - } else if (moduleType === MODULE_TYPE_CORE && !strictStorageEnforcement) { + } else if (moduleType === MODULE_TYPE_PREBID && !strictStorageEnforcement) { // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set result.valid = true; } else { diff --git a/modules/userId/index.js b/modules/userId/index.js index d16d341fd2d..0c3f789ee2f 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -773,6 +773,8 @@ function getUserIdsAsync() { * This hook returns updated list of submodules which are allowed to do get user id based on TCF 2 enforcement rules configured */ export const validateGdprEnforcement = hook('sync', function (submodules, consentData) { + // TODO: remove the `hasValidated` check in v8. Enforcement should be OFF by default. + // https://github.com/prebid/Prebid.js/issues/9766 return { userIdModules: submodules, hasValidated: consentData && consentData.hasValidated }; }, 'validateGdprEnforcement'); @@ -862,6 +864,7 @@ function initSubmodules(dest, submodules, consentData, forceRefresh = false) { // another consent check, this time each module is checked for consent with its own gvlid let { userIdModules, hasValidated } = validateGdprEnforcement(submodules, consentData); if (!hasValidated && !hasPurpose1Consent(consentData)) { + // TODO: remove this check in v8 (https://github.com/prebid/Prebid.js/issues/9766) logWarn(`${MODULE_NAME} - gdpr permission not valid for local storage or cookies, exit module`); return []; } diff --git a/src/activities/modules.js b/src/activities/modules.js index d140b10387f..474c546c07b 100644 --- a/src/activities/modules.js +++ b/src/activities/modules.js @@ -1,4 +1,4 @@ -export const MODULE_TYPE_CORE = 'core'; +export const MODULE_TYPE_PREBID = 'prebid'; export const MODULE_TYPE_BIDDER = 'bidder'; export const MODULE_TYPE_UID = 'userId'; export const MODULE_TYPE_RTD = 'rtd'; diff --git a/src/adapterManager.js b/src/adapterManager.js index 03ede473601..d6cca0c9959 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -36,7 +36,7 @@ import * as events from './events.js'; import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; import {auctionManager} from './auctionManager.js'; -import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, MODULE_TYPE_CORE} from './activities/modules.js'; +import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, MODULE_TYPE_PREBID} from './activities/modules.js'; import {isActivityAllowed} from './activities/rules.js'; import {ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from './activities/activities.js'; import {ACTIVITY_PARAM_ANL_CONFIG, ACTIVITY_PARAM_S2S_NAME, activityParams} from './activities/params.js'; @@ -271,7 +271,7 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a } function isS2SAllowed(s2sConfig) { - return dep.isAllowed(ACTIVITY_FETCH_BIDS, activityParams(MODULE_TYPE_CORE, PBS_ADAPTER_NAME, { + return dep.isAllowed(ACTIVITY_FETCH_BIDS, activityParams(MODULE_TYPE_PREBID, PBS_ADAPTER_NAME, { [ACTIVITY_PARAM_S2S_NAME]: s2sConfig.configName })); } diff --git a/src/storageManager.js b/src/storageManager.js index 0248237fbc4..bc328207ebc 100644 --- a/src/storageManager.js +++ b/src/storageManager.js @@ -1,7 +1,7 @@ import {hook} from './hook.js'; import {checkCookieSupport, hasDeviceAccess, logError, logInfo} from './utils.js'; import {bidderSettings as defaultBidderSettings} from './bidderSettings.js'; -import {MODULE_TYPE_BIDDER, MODULE_TYPE_CORE} from './activities/modules.js'; +import {MODULE_TYPE_BIDDER, MODULE_TYPE_PREBID} from './activities/modules.js'; export const STORAGE_TYPE_LOCALSTORAGE = 'html5'; export const STORAGE_TYPE_COOKIES = 'cookie'; @@ -262,7 +262,7 @@ export function getStorageManager({moduleType, moduleName, bidderCode} = {}) { * @param {string} moduleName Module name */ export function getCoreStorageManager(moduleName) { - return newStorageManager({moduleName: moduleName, moduleType: MODULE_TYPE_CORE}); + return newStorageManager({moduleName: moduleName, moduleType: MODULE_TYPE_PREBID}); } export function resetData() { diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 2c563681475..86c2e559d78 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -17,7 +17,7 @@ import * as utils from 'src/utils.js'; import { MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, - MODULE_TYPE_CORE, + MODULE_TYPE_PREBID, MODULE_TYPE_UID } from '../../../src/activities/modules.js'; import * as events from 'src/events.js'; @@ -329,9 +329,9 @@ describe('gdpr enforcement', function () { } gdprDataHandlerStub.returns(consentData); const validate = sinon.stub().callsFake(() => false); - deviceAccessHook(nextFnSpy, MODULE_TYPE_CORE, 'mockModule', undefined, {validate}); + deviceAccessHook(nextFnSpy, MODULE_TYPE_PREBID, 'mockModule', undefined, {validate}); sinon.assert.callCount(validate, 0); - sinon.assert.calledWith(nextFnSpy, MODULE_TYPE_CORE, 'mockModule', {hasEnforcementHook: true, valid: true}); + sinon.assert.calledWith(nextFnSpy, MODULE_TYPE_PREBID, 'mockModule', {hasEnforcementHook: true, valid: true}); }) }); @@ -1056,7 +1056,7 @@ describe('gdpr enforcement', function () { it('should return VENDORLESS_GVLID for core modules', () => { entry = {gvlid: 123}; - expect(getGvlid(MODULE_TYPE_CORE, MOCK_MODULE, fallbackFn)).to.equal(VENDORLESS_GVLID); + expect(getGvlid(MODULE_TYPE_PREBID, MOCK_MODULE, fallbackFn)).to.equal(VENDORLESS_GVLID); }); describe('multiple GVL IDs are found', () => { diff --git a/test/spec/unit/core/storageManager_spec.js b/test/spec/unit/core/storageManager_spec.js index 9e31389d96f..09fb905fc87 100644 --- a/test/spec/unit/core/storageManager_spec.js +++ b/test/spec/unit/core/storageManager_spec.js @@ -8,7 +8,7 @@ import { import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import {hook} from '../../../../src/hook.js'; -import {MODULE_TYPE_BIDDER} from '../../../../src/activities/modules.js'; +import {MODULE_TYPE_BIDDER, MODULE_TYPE_PREBID} from '../../../../src/activities/modules.js'; describe('storage manager', function() { before(() => { @@ -72,7 +72,7 @@ describe('storage manager', function() { }) Object.entries({ - 'core': () => getCoreStorageManager('mock'), + [MODULE_TYPE_PREBID]: () => getCoreStorageManager('mock'), 'other': () => getStorageManager({moduleType: 'other', moduleName: 'mock'}) }).forEach(([moduleType, getMgr]) => { describe(`for ${moduleType} modules`, () => { From a92d766f75c7370ce79002c4e96cc0564d8461a6 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 5 Apr 2023 09:27:40 -0700 Subject: [PATCH 19/37] Refactor userID to use non-core storage manager when storing for submodules --- modules/userId/index.js | 93 ++++++++++++++++++-------------- test/spec/modules/userId_spec.js | 50 ++++++++++------- 2 files changed, 83 insertions(+), 60 deletions(-) diff --git a/modules/userId/index.js b/modules/userId/index.js index 0c3f789ee2f..930745bf6ad 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -110,6 +110,7 @@ * @property {SubmoduleConfig} config * @property {(Object|undefined)} idObj - cache decoded id value (this is copied to every adUnit bid) * @property {(function|undefined)} callback - holds reference to submodule.getId() result if it returned a function. Will be set to undefined after callback executes + * @property {StorageManager} storageMgr */ /** @@ -133,7 +134,12 @@ import adapterManager, {gdprDataHandler} from '../../src/adapterManager.js'; import CONSTANTS from '../../src/constants.json'; import {hook, module, ready as hooksReady} from '../../src/hook.js'; import {buildEidPermissions, createEidsArray, USER_IDS_CONFIG} from './eids.js'; -import {getCoreStorageManager, STORAGE_TYPE_COOKIES, STORAGE_TYPE_LOCALSTORAGE} from '../../src/storageManager.js'; +import { + getCoreStorageManager, + getStorageManager, + STORAGE_TYPE_COOKIES, + STORAGE_TYPE_LOCALSTORAGE +} from '../../src/storageManager.js'; import { cyrb53Hash, deepAccess, @@ -170,7 +176,7 @@ const CONSENT_DATA_COOKIE_STORAGE_CONFIG = { expires: 30 // 30 days expiration, which should match how often consent is refreshed by CMPs }; export const PBJS_USER_ID_OPTOUT_NAME = '_pbjs_id_optout'; -export const coreStorage = getCoreStorageManager('userid'); +export const coreStorage = getCoreStorageManager('userId'); /** @type {boolean} */ let addedUserIdHook = false; @@ -220,11 +226,12 @@ export function setSubmoduleRegistry(submodules) { submoduleRegistry = submodules; } -function cookieSetter(submodule) { +function cookieSetter(submodule, storageMgr) { + storageMgr = storageMgr || submodule.storageMgr; const domainOverride = (typeof submodule.submodule.domainOverride === 'function') ? submodule.submodule.domainOverride() : null; const name = submodule.config.storage.name; return function setCookie(suffix, value, expiration) { - coreStorage.setCookie(name + (suffix || ''), value, expiration, 'Lax', domainOverride); + storageMgr.setCookie(name + (suffix || ''), value, expiration, 'Lax', domainOverride); } } @@ -237,6 +244,7 @@ export function setStoredValue(submodule, value) { * @type {SubmoduleStorage} */ const storage = submodule.config.storage; + const mgr = submodule.storageMgr; try { const expiresStr = (new Date(Date.now() + (storage.expires * (60 * 60 * 24 * 1000)))).toUTCString(); @@ -248,10 +256,10 @@ export function setStoredValue(submodule, value) { setCookie('_last', new Date().toUTCString(), expiresStr); } } else if (storage.type === LOCAL_STORAGE) { - coreStorage.setDataInLocalStorage(`${storage.name}_exp`, expiresStr); - coreStorage.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr)); + mgr.setDataInLocalStorage(`${storage.name}_exp`, expiresStr); + mgr.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr)); if (typeof storage.refreshInSeconds === 'number') { - coreStorage.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString()); + mgr.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString()); } } } catch (error) { @@ -263,7 +271,7 @@ export function deleteStoredValue(submodule) { let deleter, suffixes; switch (submodule.config?.storage?.type) { case COOKIE: - const setCookie = cookieSetter(submodule); + const setCookie = cookieSetter(submodule, coreStorage); const expiry = (new Date(Date.now() - 1000 * 60 * 60 * 24)).toUTCString(); deleter = (suffix) => setCookie(suffix, '', expiry) suffixes = ['', '_last']; @@ -292,25 +300,26 @@ function setPrebidServerEidPermissions(initializedSubmodules) { } /** -/** - * @param {SubmoduleStorage} storage + * @param {SubmoduleContainer} submodule * @param {String|undefined} key optional key of the value * @returns {string} */ -function getStoredValue(storage, key = undefined) { +function getStoredValue(submodule, key = undefined) { + const mgr = submodule.storageMgr; + const storage = submodule.config.storage; const storedKey = key ? `${storage.name}_${key}` : storage.name; let storedValue; try { if (storage.type === COOKIE) { - storedValue = coreStorage.getCookie(storedKey); + storedValue = mgr.getCookie(storedKey); } else if (storage.type === LOCAL_STORAGE) { - const storedValueExp = coreStorage.getDataFromLocalStorage(`${storage.name}_exp`); + const storedValueExp = mgr.getDataFromLocalStorage(`${storage.name}_exp`); // empty string means no expiration set if (storedValueExp === '') { - storedValue = coreStorage.getDataFromLocalStorage(storedKey); + storedValue = mgr.getDataFromLocalStorage(storedKey); } else if (storedValueExp) { if ((new Date(storedValueExp)).getTime() - Date.now() > 0) { - storedValue = decodeURIComponent(coreStorage.getDataFromLocalStorage(storedKey)); + storedValue = decodeURIComponent(mgr.getDataFromLocalStorage(storedKey)); } } } @@ -415,7 +424,7 @@ function processSubmoduleCallbacks(submodules, cb) { moduleDone(); } try { - submodule.callback(callbackCompleted, getStoredValue.bind(null, submodule.config?.storage)); + submodule.callback(callbackCompleted, getStoredValue.bind(null, submodule)); } catch (e) { logError(`Error in userID module '${submodule.submodule.name}':`, e); moduleDone(); @@ -783,12 +792,12 @@ function populateSubmoduleId(submodule, consentData, storedConsentData, forceRef // 1. storage: retrieve user id data from cookie/html storage or with the submodule's getId method // 2. value: pass directly to bids if (submodule.config.storage) { - let storedId = getStoredValue(submodule.config.storage); + let storedId = getStoredValue(submodule); let response; let refreshNeeded = false; if (typeof submodule.config.storage.refreshInSeconds === 'number') { - const storedDate = new Date(getStoredValue(submodule.config.storage, 'last')); + const storedDate = new Date(getStoredValue(submodule, 'last')); refreshNeeded = storedDate && (Date.now() - storedDate.getTime() > submodule.config.storage.refreshInSeconds * 1000); } @@ -851,13 +860,12 @@ function initSubmodules(dest, submodules, consentData, forceRefresh = false) { return uidMetrics().fork().measureTime('userId.init.modules', function () { if (!submodules.length) return []; // to simplify log messages from here on - // filter out submodules whose storage type is not enabled - // this needs to be done here (after consent data has loaded) so that enforcement may disable storage globally - const storageTypes = getActiveStorageTypes(); - submodules = submodules.filter((submod) => !submod.config.storage || storageTypes.has(submod.config.storage.type)); + // filter out submodules whose storage type is not enabled or allowed + // this needs to be done here (after consent data has loaded) so that this checks for allowed storage + submodules = submodules.filter((submod) => !submod.config.storage || canUseStorage(submod)); if (!submodules.length) { - logWarn(`${MODULE_NAME} - no ID module is configured for one of the available storage types:`, Array.from(storageTypes)); + logWarn(`${MODULE_NAME} - no ID module is configured for one of the available storage types`); return []; } @@ -943,24 +951,28 @@ function getValidSubmoduleConfigs(configRegistry, submoduleRegistry) { const ALL_STORAGE_TYPES = new Set([LOCAL_STORAGE, COOKIE]); -function getActiveStorageTypes() { - const storageTypes = []; - let disabled = false; - if (coreStorage.localStorageIsEnabled()) { - storageTypes.push(LOCAL_STORAGE); - if (coreStorage.getDataFromLocalStorage(PBJS_USER_ID_OPTOUT_NAME)) { - logInfo(`${MODULE_NAME} - opt-out localStorage found, storage disabled`); - disabled = true; - } - } - if (coreStorage.cookiesAreEnabled()) { - storageTypes.push(COOKIE); - if (coreStorage.getCookie(PBJS_USER_ID_OPTOUT_NAME)) { - logInfo(`${MODULE_NAME} - opt-out cookie found, storage disabled`); - disabled = true; - } +function canUseStorage(submodule) { + switch (submodule.config?.storage?.type) { + case LOCAL_STORAGE: + if (submodule.storageMgr.cookiesAreEnabled()) { + if (coreStorage.getDataFromLocalStorage(PBJS_USER_ID_OPTOUT_NAME)) { + logInfo(`${MODULE_NAME} - opt-out localStorage found, storage disabled`); + return false + } + return true; + } + break; + case COOKIE: + if (submodule.storageMgr.cookiesAreEnabled()) { + if (coreStorage.getCookie(PBJS_USER_ID_OPTOUT_NAME)) { + logInfo(`${MODULE_NAME} - opt-out cookie found, storage disabled`); + return false; + } + return true + } + break; } - return new Set(disabled ? [] : storageTypes) + return false; } /** @@ -988,6 +1000,7 @@ function updateSubmodules() { config: submoduleConfig, callback: undefined, idObj: undefined, + storageMgr: getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: submoduleConfig.name}), } : null; }).filter(submodule => submodule !== null) .forEach((sm) => submodules.push(sm)); diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index a2f2bfd8713..3de4b826772 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -2,24 +2,24 @@ import { attachIdSystem, auctionDelay, coreStorage, + findRootDomain, init, + PBJS_USER_ID_OPTOUT_NAME, requestBidsHook, + requestDataDeletion, setStoredConsentData, setStoredValue, setSubmoduleRegistry, syncDelay, - PBJS_USER_ID_OPTOUT_NAME, - findRootDomain, requestDataDeletion, } from 'modules/userId/index.js'; import {createEidsArray} from 'modules/userId/eids.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; +import {getPrebidInternal} from 'src/utils.js'; import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import {getGlobal} from 'src/prebidGlobal.js'; -import { - resetConsentData, -} from 'modules/consentManagement.js'; +import {resetConsentData, } from 'modules/consentManagement.js'; import {server} from 'test/mocks/xhr.js'; import {find} from 'src/polyfill.js'; import {unifiedIdSubmodule} from 'modules/unifiedIdSystem.js'; @@ -27,7 +27,10 @@ import {britepoolIdSubmodule} from 'modules/britepoolIdSystem.js'; import {id5IdSubmodule} from 'modules/id5IdSystem.js'; import {identityLinkSubmodule} from 'modules/identityLinkIdSystem.js'; import {dmdIdSubmodule} from 'modules/dmdIdSystem.js'; -import {liveIntentIdSubmodule, setEventFiredFlag as liveIntentIdSubmoduleDoNotFireEvent} from 'modules/liveIntentIdSystem.js'; +import { + liveIntentIdSubmodule, + setEventFiredFlag as liveIntentIdSubmoduleDoNotFireEvent +} from 'modules/liveIntentIdSystem.js'; import {merkleIdSubmodule} from 'modules/merkleIdSystem.js'; import {netIdSubmodule} from 'modules/netIdSystem.js'; import {intentIqIdSubmodule} from 'modules/intentIqIdSystem.js'; @@ -39,7 +42,6 @@ import {criteoIdSubmodule} from 'modules/criteoIdSystem.js'; import {mwOpenLinkIdSubModule} from 'modules/mwOpenLinkIdSystem.js'; import {tapadIdSubmodule} from 'modules/tapadIdSystem.js'; import {tncidSubModule} from 'modules/tncIdSystem.js'; -import {getPrebidInternal} from 'src/utils.js'; import {uid2IdSubmodule} from 'modules/uid2IdSystem.js'; import {admixerIdSubmodule} from 'modules/admixerIdSystem.js'; import {deepintentDpesSubmodule} from 'modules/deepintentDpesIdSystem.js'; @@ -188,8 +190,7 @@ describe('User ID', function () { mockGpt.disable(); mockGpt.enable(); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie('pubcid_alt', 'altpubcid200000', (new Date(Date.now() + 5000).toUTCString())); - let origSK = coreStorage.setCookie.bind(coreStorage); + coreStorage.setCookie('pubcid_alt', 'altpubcid200000', (new Date(Date.now() + 20000).toUTCString())); sinon.spy(coreStorage, 'setCookie'); sinon.stub(utils, 'logWarn'); }); @@ -320,10 +321,6 @@ describe('User ID', function () { }); }); }); - // Because the consent cookie doesn't exist yet, we'll have 2 setCookie calls: - // 1) for the consent cookie - // 2) from the getId() call that results in a new call to store the results - expect(coreStorage.setCookie.callCount).to.equal(2); }); }); @@ -350,7 +347,6 @@ describe('User ID', function () { }); }); }); - expect(coreStorage.setCookie.callCount).to.equal(2); }); }); @@ -2591,15 +2587,21 @@ describe('User ID', function () { }); describe('Set cookie behavior', function () { - let coreStorageSpy; + let cookie, cookieStub; + beforeEach(function () { - coreStorageSpy = sinon.spy(coreStorage, 'setCookie'); setSubmoduleRegistry([sharedIdSystemSubmodule]); init(config); + cookie = document.cookie; + cookieStub = sinon.stub(document, 'cookie'); + cookieStub.get(() => cookie); + cookieStub.set((val) => cookie = val); }); + afterEach(function () { - coreStorageSpy.restore(); + cookieStub.restore(); }); + it('should allow submodules to override the domain', function () { const submodule = { submodule: { @@ -2608,26 +2610,34 @@ describe('User ID', function () { } }, config: { + name: 'mockId', storage: { type: 'cookie' } + }, + storageMgr: { + setCookie: sinon.stub() } } setStoredValue(submodule, 'bar'); - expect(coreStorage.setCookie.getCall(0).args[4]).to.equal('foo.com'); + expect(submodule.storageMgr.setCookie.getCall(0).args[4]).to.equal('foo.com'); }); - it('should pass null for domain if submodule does not override the domain', function () { + it('should pass no domain if submodule does not override the domain', function () { const submodule = { submodule: {}, config: { + name: 'mockId', storage: { type: 'cookie' } + }, + storageMgr: { + setCookie: sinon.stub() } } setStoredValue(submodule, 'bar'); - expect(coreStorage.setCookie.getCall(0).args[4]).to.equal(null); + expect(submodule.storageMgr.setCookie.getCall(0).args[4]).to.equal(null); }); }); From 4a3b73481bc736fc74ec7810703284731e3bd6ef Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 5 Apr 2023 11:12:28 -0700 Subject: [PATCH 20/37] enrichEids check --- modules/userId/index.js | 24 +++++++++++---- test/spec/modules/userId_spec.js | 51 +++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/modules/userId/index.js b/modules/userId/index.js index 930745bf6ad..8f96bee5caa 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -165,6 +165,9 @@ import {newMetrics, timedAuctionHook, useMetrics} from '../../src/utils/perfMetr import {findRootDomain} from '../../src/fpd/rootDomain.js'; import {GDPR_GVLIDS} from '../../src/consentHandler.js'; import {MODULE_TYPE_UID} from '../../src/activities/modules.js'; +import {isActivityAllowed} from '../../src/activities/rules.js'; +import {ACTIVITY_ENRICH_EIDS} from '../../src/activities/activities.js'; +import {activityParams} from '../../src/activities/params.js'; const MODULE_NAME = 'User ID'; const COOKIE = STORAGE_TYPE_COOKIES; @@ -177,6 +180,9 @@ const CONSENT_DATA_COOKIE_STORAGE_CONFIG = { }; export const PBJS_USER_ID_OPTOUT_NAME = '_pbjs_id_optout'; export const coreStorage = getCoreStorageManager('userId'); +export const dep = { + isAllowed: isActivityAllowed +} /** @type {boolean} */ let addedUserIdHook = false; @@ -860,19 +866,25 @@ function initSubmodules(dest, submodules, consentData, forceRefresh = false) { return uidMetrics().fork().measureTime('userId.init.modules', function () { if (!submodules.length) return []; // to simplify log messages from here on - // filter out submodules whose storage type is not enabled or allowed - // this needs to be done here (after consent data has loaded) so that this checks for allowed storage - submodules = submodules.filter((submod) => !submod.config.storage || canUseStorage(submod)); + /** + * filter out submodules that: + * + * - cannot use the storage they've been set up with (storage not available / not allowed / disabled) + * - are not allowed to perform the `enrichEids` activity + */ + submodules = submodules.filter((submod) => { + return (!submod.config.storage || canUseStorage(submod)) && + dep.isAllowed(ACTIVITY_ENRICH_EIDS, activityParams(MODULE_TYPE_UID, submod.config.name)); + }); if (!submodules.length) { - logWarn(`${MODULE_NAME} - no ID module is configured for one of the available storage types`); + logWarn(`${MODULE_NAME} - no ID module configured`); return []; } - // another consent check, this time each module is checked for consent with its own gvlid + // TODO: remove this check in v8 (https://github.com/prebid/Prebid.js/issues/9766) let { userIdModules, hasValidated } = validateGdprEnforcement(submodules, consentData); if (!hasValidated && !hasPurpose1Consent(consentData)) { - // TODO: remove this check in v8 (https://github.com/prebid/Prebid.js/issues/9766) logWarn(`${MODULE_NAME} - gdpr permission not valid for local storage or cookies, exit module`); return []; } diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 3de4b826772..33ebf557e15 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -1,7 +1,7 @@ import { attachIdSystem, auctionDelay, - coreStorage, + coreStorage, dep, findRootDomain, init, PBJS_USER_ID_OPTOUT_NAME, @@ -57,6 +57,8 @@ import {getPPID} from '../../../src/adserver.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; import {GDPR_GVLIDS} from '../../../src/consentHandler.js'; import {MODULE_TYPE_UID} from '../../../src/activities/modules.js'; +import {ACTIVITY_ENRICH_EIDS} from '../../../src/activities/activities.js'; +import {ACTIVITY_PARAM_COMPONENT_NAME, ACTIVITY_PARAM_COMPONENT_TYPE} from '../../../src/activities/params.js'; let assert = require('chai').assert; let expect = require('chai').expect; @@ -2502,6 +2504,53 @@ describe('User ID', function () { done(); }, {adUnits}); }); + + describe('activity controls', () => { + let isAllowed; + const MOCK_IDS = ['mockId1', 'mockId2'] + beforeEach(() => { + isAllowed = sinon.stub(dep, 'isAllowed'); + init(config); + setSubmoduleRegistry([]); + const mods = MOCK_IDS.map((name) => ({ + name, + decode: function (value) { + return { + [name]: value + }; + }, + getId: function () { + return {id: `${name}Value`}; + } + })); + mods.forEach(attachIdSystem); + }); + afterEach(() => { + isAllowed.restore(); + }); + + it('should check for enrichEids activity permissions', (done) => { + isAllowed.callsFake((activity, params) => { + return !(activity === ACTIVITY_ENRICH_EIDS && + params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_UID && + params[ACTIVITY_PARAM_COMPONENT_NAME] === MOCK_IDS[0]) + }) + + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: MOCK_IDS.map(name => ({ + name, storage: {name, type: 'cookie'} + })) + } + }); + requestBidsHook((req) => { + const activeIds = req.adUnits.flatMap(au => au.bids).flatMap(bid => Object.keys(bid.userId)); + expect(Array.from(new Set(activeIds))).to.have.members([MOCK_IDS[1]]); + done(); + }, {adUnits}) + }); + }) }); describe('callbacks at the end of auction', function () { From eeb3769932ebb835b97b5041898ba4fac4c81549 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 5 Apr 2023 11:42:50 -0700 Subject: [PATCH 21/37] gdpr enforcement for enrichEids --- modules/gdprEnforcement.js | 39 +++++----- test/spec/modules/gdprEnforcement_spec.js | 89 +++++------------------ 2 files changed, 38 insertions(+), 90 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 1fab8921b5b..83b38d32726 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -24,7 +24,7 @@ import { ACTIVITY_PARAM_COMPONENT_TYPE } from '../src/activities/params.js'; import {registerActivityControl} from '../src/activities/rules.js'; -import {ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from '../src/activities/activities.js'; +import {ACTIVITY_ENRICH_EIDS, ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from '../src/activities/activities.js'; export const STRICT_STORAGE_ENFORCEMENT = 'strictStorageEnforcement'; @@ -243,27 +243,23 @@ export function userSyncHook(fn, ...args) { } } -/** - * This hook checks if user id module is given consent or not - * @param {Function} fn reference to original function (used by hook logic) - * @param {Submodule[]} submodules Array of user id submodules - * @param {Object} consentData GDPR consent data - */ +export function enrichEidsRule(params) { + const consentData = gdprDataHandler.getConsentData(); + if (shouldEnforce(consentData, 1, 'User ID')) { + const moduleName = params[ACTIVITY_PARAM_COMPONENT_NAME]; + const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], moduleName); + let allow = !!validateRules(purpose1Rule, consentData, moduleName, gvlid); + if (!allow) { + storageBlocked.add(moduleName); + return {allow}; + } + } +} + export function userIdHook(fn, submodules, consentData) { + // TODO: remove this in v8 (https://github.com/prebid/Prebid.js/issues/9766) if (shouldEnforce(consentData, 1, 'User ID')) { - let userIdModules = submodules.map((submodule) => { - const moduleName = submodule.submodule.name; - const gvlid = getGvlid(MODULE_TYPE_UID, moduleName); - let isAllowed = validateRules(purpose1Rule, consentData, moduleName, gvlid); - if (isAllowed) { - return submodule; - } else { - logWarn(`User denied permission to fetch user id for ${moduleName} User id module`); - storageBlocked.add(moduleName); - } - return undefined; - }).filter(module => module) - fn.call(this, userIdModules, { ...consentData, hasValidated: true }); + fn.call(this, submodules, { ...consentData, hasValidated: true }); } else { fn.call(this, submodules, consentData); } @@ -370,7 +366,8 @@ export function setEnforcementConfig(config) { hooksAdded = true; validateStorageEnforcement.before(deviceAccessHook, 49); registerSyncInner.before(userSyncHook, 48); - // Using getHook as user id and gdprEnforcement are both optional modules. Using import will auto include the file in build + RULE_HANDLES.push(registerActivityControl(ACTIVITY_ENRICH_EIDS, RULE_NAME, enrichEidsRule)); + // TODO: remove this hook in v8 (https://github.com/prebid/Prebid.js/issues/9766) getHook('validateGdprEnforcement').before(userIdHook, 47); } if (purpose2Rule) { diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 86c2e559d78..57e70604f06 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -1,13 +1,12 @@ import { deviceAccessHook, - enforcementRules, fetchBidsRule, + enforcementRules, enrichEidsRule, fetchBidsRule, getGvlid, getGvlidFromAnalyticsAdapter, purpose1Rule, purpose2Rule, reportAnalyticsRule, setEnforcementConfig, STRICT_STORAGE_ENFORCEMENT, - userIdHook, userSyncHook, validateRules } from 'modules/gdprEnforcement.js'; @@ -128,6 +127,10 @@ describe('gdpr enforcement', function () { $$PREBID_GLOBAL$$.requestBids.getHooks().remove(); }) + function expectAllow(allow, ruleResult) { + allow ? expect(ruleResult).to.not.exist : sinon.assert.match(ruleResult, {allow: false}); + } + beforeEach(() => { sandbox = sinon.sandbox.create(); gvlids = {}; @@ -439,15 +442,7 @@ describe('gdpr enforcement', function () { }); }); - describe('userIdHook', function () { - beforeEach(function () { - logWarnSpy = sinon.spy(utils, 'logWarn'); - nextFnSpy = sinon.spy(); - }); - afterEach(function () { - config.resetConfig(); - logWarnSpy.restore(); - }); + describe('enrichEidsRule', () => { it('should allow user id module if consent is given', function () { setEnforcementConfig({ gdpr: { @@ -459,40 +454,16 @@ describe('gdpr enforcement', function () { }] } }); - let consentData = {} - consentData.vendorData = staticConfig.consentData.getTCData; - consentData.apiVersion = 2; - consentData.gdprApplies = true; - let submodules = [{ - submodule: { - gvlid: 1, - name: 'sampleUserId' - } - }] + setupConsentData(); gvlids.sampleUserId = 1; - userIdHook(nextFnSpy, submodules, consentData); - // Should pass back hasValidated flag since version 2 - const args = nextFnSpy.getCalls()[0].args; - expect(args[1].hasValidated).to.be.true; - expect(nextFnSpy.calledOnce).to.equal(true); - sinon.assert.calledWith(nextFnSpy, submodules, { ...consentData, hasValidated: true }); + expect(enrichEidsRule(activityParams(MODULE_TYPE_UID, 'sampleUserId'))).to.not.exist; }); it('should allow userId module if gdpr not in scope', function () { - let submodules = [{ - submodule: { - gvlid: 1, - name: 'sampleUserId' - } - }]; gvlids.sampleUserId = 1; - let consentData = null; - userIdHook(nextFnSpy, submodules, consentData); - // Should not pass back hasValidated flag since version 2 - const args = nextFnSpy.getCalls()[0].args; - expect(args[1]).to.be.null; - expect(nextFnSpy.calledOnce).to.equal(true); - sinon.assert.calledWith(nextFnSpy, submodules, consentData); + const consent = setupConsentData({gdprApplies: false}); + consent.vendorData.purpose.consents['1'] = false; + expect(enrichEidsRule(activityParams(MODULE_TYPE_UID, 'sampleUserId'))).to.not.exist; }); it('should not allow user id module if user denied consent', function () { @@ -506,35 +477,17 @@ describe('gdpr enforcement', function () { }] } }); - let consentData = {} - consentData.vendorData = staticConfig.consentData.getTCData; - consentData.apiVersion = 2; - consentData.gdprApplies = true; - - let submodules = [{ - submodule: { - gvlid: 1, - name: 'sampleUserId' - } - }, { - submodule: { - gvlid: 3, - name: 'sampleUserId1' - } - }] + setupConsentData(); Object.assign(gvlids, { sampleUserId: 1, sampleUserId1: 3 }); - userIdHook(nextFnSpy, submodules, consentData); - expect(logWarnSpy.callCount).to.equal(1); - let expectedSubmodules = [{ - submodule: { - gvlid: 1, - name: 'sampleUserId' - } - }] - sinon.assert.calledWith(nextFnSpy, expectedSubmodules, { ...consentData, hasValidated: true }); + Object.entries({ + sampleUserId: true, + sampleUserId1: false + }).forEach(([name, allow]) => { + expectAllow(allow, enrichEidsRule(activityParams(MODULE_TYPE_UID, name))) + }); }); }); @@ -584,8 +537,7 @@ describe('gdpr enforcement', function () { bidder_2: false, bidder_3: true }).forEach(([bidder, allowed]) => { - const res = fetchBidsRule(activityParams(MODULE_TYPE_BIDDER, bidder)); - allowed ? expect(res).to.not.exist : expect(res).to.eql({allow: false}); + expectAllow(allowed, fetchBidsRule(activityParams(MODULE_TYPE_BIDDER, bidder))); }) }); @@ -649,8 +601,7 @@ describe('gdpr enforcement', function () { analyticsAdapter_B: true, analyticsAdapter_C: true }).forEach(([adapter, allow]) => { - const res = reportAnalyticsRule(activityParams(MODULE_TYPE_ANALYTICS, adapter)); - allow ? expect(res).to.not.exist : expect(res).to.eql({allow}); + expectAllow(allow, reportAnalyticsRule(activityParams(MODULE_TYPE_ANALYTICS, adapter))) }) }); }); From b58795a1b2118f3279217e16f5aac34c67490222 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 5 Apr 2023 12:50:36 -0700 Subject: [PATCH 22/37] syncUser activity check --- src/activities/params.js | 11 +++++++ src/userSync.js | 52 +++++++++++++++++++++----------- test/spec/userSync_spec.js | 61 ++++++++++++++++++++++++++++---------- 3 files changed, 90 insertions(+), 34 deletions(-) diff --git a/src/activities/params.js b/src/activities/params.js index 034e8dc02e7..75edfdd7862 100644 --- a/src/activities/params.js +++ b/src/activities/params.js @@ -1,11 +1,22 @@ export const ACTIVITY_PARAM_COMPONENT = 'component'; export const ACTIVITY_PARAM_COMPONENT_TYPE = ACTIVITY_PARAM_COMPONENT + 'Type'; export const ACTIVITY_PARAM_COMPONENT_NAME = ACTIVITY_PARAM_COMPONENT + 'Name'; + /** * s2sConfig[].configName, used to identify a particular s2s instance * relevant for: fetchBids */ export const ACTIVITY_PARAM_S2S_NAME = 'configName'; +/** + * user sync type - 'iframe' or 'pixel' + * relevant for: syncUser + */ +export const ACTIVITY_PARAM_SYNC_TYPE = 'syncType' +/** + * user sync URL + * relevant for: syncUser + */ +export const ACTIVITY_PARAM_SYNC_URL = 'syncUrl'; /** * @private * configuration options for analytics adapter - the argument passed to `enableAnalytics`. diff --git a/src/userSync.js b/src/userSync.js index ed3cbb5d5f6..2474deafd64 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -5,6 +5,14 @@ import { import { config } from './config.js'; import {includes} from './polyfill.js'; import { getCoreStorageManager } from './storageManager.js'; +import {isActivityAllowed, registerActivityControl} from './activities/rules.js'; +import {ACTIVITY_SYNC_USER} from './activities/activities.js'; +import { + ACTIVITY_PARAM_COMPONENT_NAME, + ACTIVITY_PARAM_COMPONENT_TYPE, + ACTIVITY_PARAM_SYNC_TYPE, ACTIVITY_PARAM_SYNC_URL, activityParams +} from './activities/params.js'; +import {MODULE_TYPE_BIDDER} from './activities/modules.js'; export const USERSYNC_DEFAULT_CONFIG = { syncEnabled: true, @@ -29,10 +37,10 @@ const storage = getCoreStorageManager('usersync'); /** * Factory function which creates a new UserSyncPool. * - * @param {UserSyncDependencies} userSyncDependencies Configuration options and dependencies which the + * @param {} deps Configuration options and dependencies which the * UserSync object needs in order to behave properly. */ -export function newUserSync(userSyncDependencies) { +export function newUserSync(deps) { let publicApi = {}; // A queue of user syncs for each adapter // Let getDefaultQueue() set the defaults @@ -50,7 +58,7 @@ export function newUserSync(userSyncDependencies) { }; // Use what is in config by default - let usConfig = userSyncDependencies.config; + let usConfig = deps.config; // Update if it's (re)set config.getConfig('userSync', (conf) => { // Added this logic for https://github.com/prebid/Prebid.js/issues/4864 @@ -70,6 +78,19 @@ export function newUserSync(userSyncDependencies) { usConfig = Object.assign(usConfig, conf.userSync); }); + deps.regRule(ACTIVITY_SYNC_USER, 'userSync config', (params) => { + if (!usConfig.syncEnabled) { + return {allow: false, reason: 'syncs are disabled'} + } + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_BIDDER) { + const syncType = params[ACTIVITY_PARAM_SYNC_TYPE]; + const bidder = params[ACTIVITY_PARAM_COMPONENT_NAME]; + if (!publicApi.canBidderRegisterSync(syncType, bidder)) { + return {allow: false, reason: `${syncType} syncs are not enabled for ${bidder}`} + } + } + }); + /** * @function getDefaultQueue * @summary Returns the default empty queue @@ -89,7 +110,7 @@ export function newUserSync(userSyncDependencies) { * @private */ function fireSyncs() { - if (!usConfig.syncEnabled || !userSyncDependencies.browserSupportsCookies) { + if (!usConfig.syncEnabled || !deps.browserSupportsCookies) { return; } @@ -199,14 +220,14 @@ export function newUserSync(userSyncDependencies) { return logWarn(`Number of user syncs exceeded for "${bidder}"`); } - const canBidderRegisterSync = publicApi.canBidderRegisterSync(type, bidder); - if (!canBidderRegisterSync) { - return logWarn(`Bidder "${bidder}" not permitted to register their "${type}" userSync pixels.`); + if (deps.isAllowed(ACTIVITY_SYNC_USER, activityParams(MODULE_TYPE_BIDDER, bidder, { + [ACTIVITY_PARAM_SYNC_TYPE]: type, + [ACTIVITY_PARAM_SYNC_URL]: url + }))) { + // the bidder's pixel has passed all checks and is allowed to register + queue[type].push([bidder, url]); + numAdapterBids = incrementAdapterBids(numAdapterBids, bidder); } - - // the bidder's pixel has passed all checks and is allowed to register - queue[type].push([bidder, url]); - numAdapterBids = incrementAdapterBids(numAdapterBids, bidder); }; /** @@ -320,6 +341,8 @@ export function newUserSync(userSyncDependencies) { export const userSync = newUserSync(Object.defineProperties({ config: config.getConfig('userSync'), + isAllowed: isActivityAllowed, + regRule: registerActivityControl, }, { browserSupportsCookies: { get: function() { @@ -329,13 +352,6 @@ export const userSync = newUserSync(Object.defineProperties({ } })); -/** - * @typedef {Object} UserSyncDependencies - * - * @property {UserSyncConfig} config - * @property {boolean} browserSupportsCookies True if the current browser supports cookies, and false otherwise. - */ - /** * @typedef {Object} UserSyncConfig * diff --git a/test/spec/userSync_spec.js b/test/spec/userSync_spec.js index 6d0953f68ac..c403014fcd6 100644 --- a/test/spec/userSync_spec.js +++ b/test/spec/userSync_spec.js @@ -1,5 +1,13 @@ import { expect } from 'chai'; import { config } from 'src/config.js'; +import {ruleRegistry} from '../../src/activities/rules.js'; +import {ACTIVITY_SYNC_USER} from '../../src/activities/activities.js'; +import { + ACTIVITY_PARAM_COMPONENT, + ACTIVITY_PARAM_SYNC_TYPE, + ACTIVITY_PARAM_SYNC_URL +} from '../../src/activities/params.js'; +import {MODULE_TYPE_BIDDER} from '../../src/activities/modules.js'; // Use require since we need to be able to write to these vars const utils = require('../../src/utils'); let { newUserSync, USERSYNC_DEFAULT_CONFIG } = require('../../src/userSync'); @@ -14,12 +22,18 @@ describe('user sync', function () { let idPrefix = 'test-generated-id-'; let lastId = 0; let defaultUserSyncConfig = config.getConfig('userSync'); - function getUserSyncConfig(userSyncConfig) { - return Object.assign({}, defaultUserSyncConfig, userSyncConfig); + let regRule, isAllowed; + + function mkUserSync(deps) { + [regRule, isAllowed] = ruleRegistry(); + return newUserSync(Object.assign({ + regRule, isAllowed + }, deps)) } + function newTestUserSync(configOverrides, disableBrowserCookies) { const thisConfig = Object.assign({}, defaultUserSyncConfig, configOverrides); - return newUserSync({ + return mkUserSync({ config: thisConfig, browserSupportsCookies: !disableBrowserCookies, }) @@ -59,6 +73,22 @@ describe('user sync', function () { expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.equal('http://example.com'); }); + it('should NOT fire a sync if a rule blocks syncUser', () => { + const userSync = newTestUserSync() + regRule(ACTIVITY_SYNC_USER, 'testRule', (params) => { + if ( + params[ACTIVITY_PARAM_COMPONENT] === `${MODULE_TYPE_BIDDER}.testBidder` && + params[ACTIVITY_PARAM_SYNC_TYPE] === 'image' && + params[ACTIVITY_PARAM_SYNC_URL] === 'http://example.com' + ) { + return {allow: false} + } + }) + userSync.registerSync('image', 'testBidder', 'http://example.com'); + userSync.syncUsers(); + expect(triggerPixelStub.called).to.be.false; + }) + it('should clear queue after sync', function () { const userSync = newTestUserSync(); userSync.syncUsers(); @@ -371,14 +401,13 @@ describe('user sync', function () { userSync.registerSync('image', 'atestBidder', 'http://example.com/1'); userSync.registerSync('iframe', 'testBidder', 'http://example.com/iframe'); userSync.syncUsers(); - expect(logWarnStub.getCall(0).args[0]).to.exist; expect(triggerPixelStub.getCall(0)).to.not.be.null; expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.equal('http://example.com/1'); expect(insertUserSyncIframeStub.getCall(0)).to.be.null; }); it('should still allow default image syncs if setConfig only defined iframe', function () { - const userSync = newUserSync({ + const userSync = mkUserSync({ config: config.getConfig('userSync'), browserSupportsCookies: true }); @@ -403,7 +432,7 @@ describe('user sync', function () { }); it('should not fire image pixel for a bidder if iframe pixel is fired for same bidder', function() { - const userSync = newUserSync({ + const userSync = mkUserSync({ config: config.getConfig('userSync'), browserSupportsCookies: true }); @@ -430,7 +459,7 @@ describe('user sync', function () { }); it('should override default image syncs if setConfig used image filter', function () { - const userSync = newUserSync({ + const userSync = mkUserSync({ config: config.getConfig('userSync'), browserSupportsCookies: true }); @@ -455,7 +484,7 @@ describe('user sync', function () { }); it('should override default image syncs if setConfig used all filter', function() { - const userSync = newUserSync({ + const userSync = mkUserSync({ config: config.getConfig('userSync'), browserSupportsCookies: true }); @@ -488,7 +517,7 @@ describe('user sync', function () { describe('canBidderRegisterSync', function () { describe('with filterSettings', function () { it('should return false if filter settings does not allow it', function () { - const userSync = newUserSync({ + const userSync = mkUserSync({ config: { filterSettings: { image: { @@ -505,7 +534,7 @@ describe('user sync', function () { expect(userSync.canBidderRegisterSync('iframe', 'otherTestBidder')).to.equal(false); }); it('should return false for iframe if there is no iframe filterSettings', function () { - const userSync = newUserSync({ + const userSync = mkUserSync({ config: { syncEnabled: true, filterSettings: { @@ -523,7 +552,7 @@ describe('user sync', function () { expect(userSync.canBidderRegisterSync('iframe', 'otherTestBidder')).to.equal(false); }); it('should return true if filter settings does allow it', function () { - const userSync = newUserSync({ + const userSync = mkUserSync({ config: { filterSettings: { image: { @@ -543,7 +572,7 @@ describe('user sync', function () { describe('almost deprecated - without filterSettings', function () { describe('enabledBidders contains testBidder', function () { it('should return false if type is iframe and iframeEnabled is false', function () { - const userSync = newUserSync({ + const userSync = mkUserSync({ config: { filterSettings: { iframe: { @@ -557,7 +586,7 @@ describe('user sync', function () { }); it('should return true if type is iframe and iframeEnabled is true', function () { - const userSync = newUserSync({ + const userSync = mkUserSync({ config: { pixelEnabled: true, iframeEnabled: true, @@ -568,7 +597,7 @@ describe('user sync', function () { }); it('should return false if type is image and pixelEnabled is false', function () { - const userSync = newUserSync({ + const userSync = mkUserSync({ config: { filterSettings: { image: { @@ -582,7 +611,7 @@ describe('user sync', function () { }); it('should return true if type is image and pixelEnabled is true', function () { - const userSync = newUserSync({ + const userSync = mkUserSync({ config: { pixelEnabled: true, iframeEnabled: true, @@ -595,7 +624,7 @@ describe('user sync', function () { describe('enabledBidders does not container testBidder', function () { it('should return false since testBidder is not in enabledBidders', function () { - const userSync = newUserSync({ + const userSync = mkUserSync({ config: { filterSettings: { image: { From 72b325c4b23e5b4a98ccd0b0eb709714a0090e3d Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 6 Apr 2023 07:46:16 -0700 Subject: [PATCH 23/37] gdpr enforcement for syncUser --- modules/gdprEnforcement.js | 30 ++++++-- test/spec/modules/gdprEnforcement_spec.js | 92 ++++++++--------------- 2 files changed, 53 insertions(+), 69 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 83b38d32726..168c5972bb8 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -6,7 +6,6 @@ import {deepAccess, hasDeviceAccess, logError, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import adapterManager, {gdprDataHandler} from '../src/adapterManager.js'; import {find} from '../src/polyfill.js'; -import {registerSyncInner} from '../src/adapters/bidderFactory.js'; import {getHook} from '../src/hook.js'; import {validateStorageEnforcement} from '../src/storageManager.js'; import * as events from '../src/events.js'; @@ -24,7 +23,12 @@ import { ACTIVITY_PARAM_COMPONENT_TYPE } from '../src/activities/params.js'; import {registerActivityControl} from '../src/activities/rules.js'; -import {ACTIVITY_ENRICH_EIDS, ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from '../src/activities/activities.js'; +import { + ACTIVITY_ENRICH_EIDS, + ACTIVITY_FETCH_BIDS, + ACTIVITY_REPORT_ANALYTICS, + ACTIVITY_SYNC_USER +} from '../src/activities/activities.js'; export const STRICT_STORAGE_ENFORCEMENT = 'strictStorageEnforcement'; @@ -69,6 +73,9 @@ const GVLID_LOOKUP_PRIORITY = [ MODULE_TYPE_RTD ]; +const RULE_NAME = 'TCF2'; +const RULE_HANDLES = []; + /** * Retrieve a module's GVL ID. */ @@ -221,6 +228,19 @@ export function deviceAccessHook(fn, moduleType, moduleName, result, {validate = fn.call(this, moduleType, moduleName, result); } +export function syncUserRule(params) { + const consentData = gdprDataHandler.getConsentData(); + const modName = params[ACTIVITY_PARAM_COMPONENT_NAME]; + if (shouldEnforce(consentData, 1, modName)) { + const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], modName); + let allow = !!validateRules(purpose1Rule, consentData, modName, gvlid); + if (!allow) { + storageBlocked.add(modName); + return {allow}; + } + } +} + /** * This hook checks if a bidder has consent for user sync or not * @param {Function} fn reference to original function (used by hook logic) @@ -332,9 +352,6 @@ const hasPurpose1 = (rule) => { return rule.purpose === TCF2.purpose1.name } const hasPurpose2 = (rule) => { return rule.purpose === TCF2.purpose2.name } const hasPurpose7 = (rule) => { return rule.purpose === TCF2.purpose7.name } -const RULE_NAME = 'TCF2'; -const RULE_HANDLES = []; - /** * A configuration function that initializes some module variables, as well as adds hooks * @param {Object} config - GDPR enforcement config object @@ -365,7 +382,7 @@ export function setEnforcementConfig(config) { if (purpose1Rule) { hooksAdded = true; validateStorageEnforcement.before(deviceAccessHook, 49); - registerSyncInner.before(userSyncHook, 48); + RULE_HANDLES.push(registerActivityControl(ACTIVITY_SYNC_USER, RULE_NAME, syncUserRule)) RULE_HANDLES.push(registerActivityControl(ACTIVITY_ENRICH_EIDS, RULE_NAME, enrichEidsRule)); // TODO: remove this hook in v8 (https://github.com/prebid/Prebid.js/issues/9766) getHook('validateGdprEnforcement').before(userIdHook, 47); @@ -383,7 +400,6 @@ export function uninstall() { while (RULE_HANDLES.length) RULE_HANDLES.pop()(); [ validateStorageEnforcement.getHooks({hook: deviceAccessHook}), - registerSyncInner.getHooks({hook: userSyncHook}), getHook('validateGdprEnforcement').getHooks({hook: userIdHook}), ].forEach(hook => hook.remove()); hooksAdded = false; diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 57e70604f06..884759e67d6 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -1,13 +1,16 @@ import { deviceAccessHook, - enforcementRules, enrichEidsRule, fetchBidsRule, + enforcementRules, + enrichEidsRule, + fetchBidsRule, getGvlid, getGvlidFromAnalyticsAdapter, purpose1Rule, - purpose2Rule, reportAnalyticsRule, + purpose2Rule, + reportAnalyticsRule, setEnforcementConfig, STRICT_STORAGE_ENFORCEMENT, - userSyncHook, + syncUserRule, validateRules } from 'modules/gdprEnforcement.js'; import {config} from 'src/config.js'; @@ -338,26 +341,7 @@ describe('gdpr enforcement', function () { }) }); - describe('userSyncHook', function () { - let curBidderStub; - let adapterManagerStub; - - beforeEach(function () { - gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); - logWarnSpy = sinon.spy(utils, 'logWarn'); - curBidderStub = sinon.stub(config, 'getCurrentBidder'); - adapterManagerStub = sinon.stub(adapterManager, 'getBidAdapter'); - nextFnSpy = sinon.spy(); - }); - - afterEach(function () { - config.getCurrentBidder.restore(); - config.resetConfig(); - gdprDataHandler.getConsentData.restore(); - adapterManager.getBidAdapter.restore(); - logWarnSpy.restore(); - }); - + describe('syncUserRule', () => { it('should allow bidder to do user sync if consent is true', function () { setEnforcementConfig({ gdpr: { @@ -369,20 +353,17 @@ describe('gdpr enforcement', function () { }] } }); + setupConsentData(); let consentData = {} consentData.vendorData = staticConfig.consentData.getTCData; consentData.gdprApplies = true; consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - - curBidderStub.returns('sampleBidder1'); - gvlids.sampleBidder1 = 1; - userSyncHook(nextFnSpy); - - curBidderStub.returns('sampleBidder2'); - gvlids.sampleBidder2 = 3; - userSyncHook(nextFnSpy); - expect(nextFnSpy.calledTwice).to.equal(true); + Object.assign(gvlids, { + sampleBidder1: 1, + sampleBidder2: 2 + }) + Object.keys(gvlids).forEach(bidder => expect(syncUserRule(activityParams(MODULE_TYPE_BIDDER, bidder))).to.not.exist); }); it('should not allow bidder to do user sync if user has denied consent', function () { @@ -396,21 +377,18 @@ describe('gdpr enforcement', function () { }] } }); - let consentData = {} - consentData.vendorData = staticConfig.consentData.getTCData; - consentData.apiVersion = 2; - consentData.gdprApplies = true; - gdprDataHandlerStub.returns(consentData); - - curBidderStub.returns('sampleBidder1'); - gvlids.sampleBidder1 = 1; - userSyncHook(nextFnSpy); + setupConsentData(); + Object.assign(gvlids, { + sampleBidder1: 1, + sampleBidder2: 3 + }) - curBidderStub.returns('sampleBidder2'); - gvlids.sampleBidder2 = 3; - userSyncHook(nextFnSpy); - expect(nextFnSpy.calledOnce).to.equal(true); - expect(logWarnSpy.callCount).to.equal(1); + Object.entries({ + sampleBidder1: true, + sampleBidder2: false + }).forEach(([bidder, isAllowed]) => { + expectAllow(isAllowed, syncUserRule(activityParams(MODULE_TYPE_BIDDER, bidder))); + }) }); it('should not check vendor consent when enforceVendor is false', function () { @@ -424,24 +402,14 @@ describe('gdpr enforcement', function () { }] } }); - let consentData = {} - consentData.vendorData = staticConfig.consentData.getTCData; - consentData.apiVersion = 2; - consentData.gdprApplies = true; - gdprDataHandlerStub.returns(consentData); - - curBidderStub.returns('sampleBidder1'); - gvlids.sampleBidder1 = 1; - userSyncHook(nextFnSpy); - - curBidderStub.returns('sampleBidder2'); - gvlids.sampleBidder2 = 3; - userSyncHook(nextFnSpy); - expect(nextFnSpy.calledTwice).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); + setupConsentData(); + Object.assign(gvlids, { + sampleBidder1: 1, + sampleBidder2: 3 + }) + Object.keys(gvlids).forEach(bidder => expect(syncUserRule(activityParams(MODULE_TYPE_BIDDER, bidder))).to.not.exist); }); }); - describe('enrichEidsRule', () => { it('should allow user id module if consent is given', function () { setEnforcementConfig({ From c9b40774e0a71cf33d1001d5e0e4e4cd2d41229f Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 6 Apr 2023 08:08:56 -0700 Subject: [PATCH 24/37] refactor gdprEnforcement --- modules/gdprEnforcement.js | 107 +++++++++++-------------------------- 1 file changed, 30 insertions(+), 77 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 168c5972bb8..eacd958186d 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -228,53 +228,32 @@ export function deviceAccessHook(fn, moduleType, moduleName, result, {validate = fn.call(this, moduleType, moduleName, result); } -export function syncUserRule(params) { - const consentData = gdprDataHandler.getConsentData(); - const modName = params[ACTIVITY_PARAM_COMPONENT_NAME]; - if (shouldEnforce(consentData, 1, modName)) { - const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], modName); - let allow = !!validateRules(purpose1Rule, consentData, modName, gvlid); - if (!allow) { - storageBlocked.add(modName); - return {allow}; - } - } -} - /** - * This hook checks if a bidder has consent for user sync or not - * @param {Function} fn reference to original function (used by hook logic) - * @param {...any} args args + * all activity rules follow the same structure: + * if GDPR is in scope, check configuration for a particular purpose + * + * @param purposeNo TCF purpose number to check for this activity + * @param getEnforcementRule getter for gdprEnforcement rule definition to use + * @param blocked optional set to use for collecting denied vendors + * @param gvlidFallback optional factory function for a gvlid falllback function */ -export function userSyncHook(fn, ...args) { - const consentData = gdprDataHandler.getConsentData(); - const curBidder = config.getCurrentBidder(); - if (shouldEnforce(consentData, 1, curBidder)) { - const gvlid = getGvlid(MODULE_TYPE_BIDDER, curBidder); - let isAllowed = validateRules(purpose1Rule, consentData, curBidder, gvlid); - if (isAllowed) { - fn.call(this, ...args); - } else { - logWarn(`User sync not allowed for ${curBidder}`); - storageBlocked.add(curBidder); +function gdprRule(purposeNo, getEnforcementRule, blocked = null, gvlidFallback = () => null) { + return function (params) { + const consentData = gdprDataHandler.getConsentData(); + const modName = params[ACTIVITY_PARAM_COMPONENT_NAME]; + if (shouldEnforce(consentData, purposeNo, modName)) { + const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], modName, gvlidFallback(params)); + let allow = !!validateRules(getEnforcementRule(), consentData, modName, gvlid); + if (!allow) { + blocked && blocked.add(modName); + return {allow}; + } } - } else { - fn.call(this, ...args); } } -export function enrichEidsRule(params) { - const consentData = gdprDataHandler.getConsentData(); - if (shouldEnforce(consentData, 1, 'User ID')) { - const moduleName = params[ACTIVITY_PARAM_COMPONENT_NAME]; - const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], moduleName); - let allow = !!validateRules(purpose1Rule, consentData, moduleName, gvlid); - if (!allow) { - storageBlocked.add(moduleName); - return {allow}; - } - } -} +export const syncUserRule = gdprRule(1, () => purpose1Rule, storageBlocked); +export const enrichEidsRule = gdprRule(1, () => purpose1Rule, storageBlocked) export function userIdHook(fn, submodules, consentData) { // TODO: remove this in v8 (https://github.com/prebid/Prebid.js/issues/9766) @@ -285,45 +264,19 @@ export function userIdHook(fn, submodules, consentData) { } } -/** - * fetchBids activity control: disallow bidders from auctions if they do not have consent for purpose 2 (and purpose 2 - * enforcement is enabled) - */ -export function fetchBidsRule(params) { - if (params[ACTIVITY_PARAM_COMPONENT_TYPE] !== MODULE_TYPE_BIDDER) { - // TODO: this special case is for the PBS adapter (componentType is core) - // we should check for generic purpose 2 consent & vendor consent based on the PBS vendor's GVL ID; - // that is, however, a breaking change and skipped for now - return; - } - const consentData = gdprDataHandler.getConsentData(); - if (shouldEnforce(consentData, 2)) { - const bidder = params[ACTIVITY_PARAM_COMPONENT_NAME] - const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], bidder); - const allow = !!validateRules(purpose2Rule, consentData, bidder, gvlid); - if (!allow) { - biddersBlocked.add(bidder); - return {allow}; +export const fetchBidsRule = ((rule) => { + return function (params) { + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] !== MODULE_TYPE_BIDDER) { + // TODO: this special case is for the PBS adapter (componentType is core) + // we should check for generic purpose 2 consent & vendor consent based on the PBS vendor's GVL ID; + // that is, however, a breaking change and skipped for now + return; } + return rule(params); } -} +})(gdprRule(2, () => purpose2Rule, biddersBlocked)) -/** - * reportAnalytics activity control: disallow analytics adapters that do not have purpose 7 consent - * (if purpose 7 consent enforcement is enabled). - */ -export function reportAnalyticsRule(params) { - const consentData = gdprDataHandler.getConsentData(); - if (shouldEnforce(consentData, 7, 'Analytics')) { - const analyticsAdapterCode = params[ACTIVITY_PARAM_COMPONENT_NAME]; - const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], analyticsAdapterCode, () => getGvlidFromAnalyticsAdapter(analyticsAdapterCode, params[ACTIVITY_PARAM_ANL_CONFIG])); - const allow = !!validateRules(purpose7Rule, consentData, analyticsAdapterCode, gvlid); - if (!allow) { - analyticsBlocked.add(analyticsAdapterCode); - return {allow}; - } - } -} +export const reportAnalyticsRule = gdprRule(7, () => purpose7Rule, analyticsBlocked, (params) => getGvlidFromAnalyticsAdapter(params[ACTIVITY_PARAM_COMPONENT_NAME], params[ACTIVITY_PARAM_ANL_CONFIG])) /** * Compiles the TCF2.0 enforcement results into an object, which is emitted as an event payload to "tcf2Enforcement" event. From f976ba78d94bf7b64a7daed54c74183596ff1a2e Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 6 Apr 2023 09:56:29 -0700 Subject: [PATCH 25/37] storageManager activity checks --- src/activities/params.js | 35 ++++- src/storageManager.js | 87 ++++++++----- test/spec/activities/params_spec.js | 13 +- test/spec/unit/core/storageManager_spec.js | 142 +++++++++++---------- 4 files changed, 172 insertions(+), 105 deletions(-) diff --git a/src/activities/params.js b/src/activities/params.js index 75edfdd7862..0ccd72f0edd 100644 --- a/src/activities/params.js +++ b/src/activities/params.js @@ -1,10 +1,31 @@ +import {MODULE_TYPE_BIDDER} from './modules.js'; +import adapterManager from '../adapterManager.js'; + +/** + * Component ID - who is trying to perform the activity? + * Relevant for all activities. + */ export const ACTIVITY_PARAM_COMPONENT = 'component'; export const ACTIVITY_PARAM_COMPONENT_TYPE = ACTIVITY_PARAM_COMPONENT + 'Type'; export const ACTIVITY_PARAM_COMPONENT_NAME = ACTIVITY_PARAM_COMPONENT + 'Name'; +/** + * Code of the bid adapter that `componentName` is an alias of. + * May be the same as the component name. + * + * relevant for all activities, but only when componentType is 'bidder'. + */ +export const ACTIVITY_PARAM_ADAPTER_CODE = 'adapterCode'; + +/** + * Storage type - either 'html5' or 'cookie'. + * Relevant for: accessDevice + */ +export const ACTIVITY_PARAM_STORAGE_TYPE = 'storageType'; + /** * s2sConfig[].configName, used to identify a particular s2s instance - * relevant for: fetchBids + * relevant for: fetchBids, but only when component is 'prebid.pbsBidAdapter' */ export const ACTIVITY_PARAM_S2S_NAME = 'configName'; /** @@ -25,9 +46,17 @@ export const ACTIVITY_PARAM_SYNC_URL = 'syncUrl'; export const ACTIVITY_PARAM_ANL_CONFIG = '_config'; export function activityParams(moduleType, moduleName, params) { - return Object.assign({ + const defaults = { [ACTIVITY_PARAM_COMPONENT_TYPE]: moduleType, [ACTIVITY_PARAM_COMPONENT_NAME]: moduleName, [ACTIVITY_PARAM_COMPONENT]: `${moduleType}.${moduleName}` - }, params); + }; + if (moduleType === MODULE_TYPE_BIDDER) { + let adapterCode = moduleName; + while (adapterManager.aliasRegistry[adapterCode]) { + adapterCode = adapterManager.aliasRegistry[adapterCode]; + } + defaults[ACTIVITY_PARAM_ADAPTER_CODE] = adapterCode; + } + return Object.assign(defaults, params); } diff --git a/src/storageManager.js b/src/storageManager.js index bc328207ebc..b3c0dacf19c 100644 --- a/src/storageManager.js +++ b/src/storageManager.js @@ -1,7 +1,17 @@ import {hook} from './hook.js'; -import {checkCookieSupport, hasDeviceAccess, logError, logInfo} from './utils.js'; -import {bidderSettings as defaultBidderSettings} from './bidderSettings.js'; +import {checkCookieSupport, hasDeviceAccess, logError} from './utils.js'; +import {bidderSettings} from './bidderSettings.js'; import {MODULE_TYPE_BIDDER, MODULE_TYPE_PREBID} from './activities/modules.js'; +import { + ACTIVITY_PARAM_ADAPTER_CODE, + ACTIVITY_PARAM_COMPONENT_TYPE, + ACTIVITY_PARAM_STORAGE_TYPE, + activityParams +} from './activities/params.js'; +import {isActivityAllowed, registerActivityControl} from './activities/rules.js'; +import {ACTIVITY_ACCESS_DEVICE} from './activities/activities.js'; +import {config} from './config.js'; +import adapterManager from './adapterManager.js'; export const STORAGE_TYPE_LOCALSTORAGE = 'html5'; export const STORAGE_TYPE_COOKIES = 'cookie'; @@ -11,40 +21,20 @@ export let storageCallbacks = []; /* * Storage manager constructor. Consumers should prefer one of `getStorageManager` or `getCoreStorageManager`. */ -export function newStorageManager({moduleName, moduleType} = {}, {bidderSettings = defaultBidderSettings} = {}) { - function isBidderAllowed(storageType) { - if (moduleType !== MODULE_TYPE_BIDDER) { - return true; - } - const storageAllowed = bidderSettings.get(moduleName, 'storageAllowed'); - if (!storageAllowed || storageAllowed === true) return !!storageAllowed; - if (Array.isArray(storageAllowed)) return storageAllowed.some((e) => e === storageType); - return storageAllowed === storageType; - } +export function newStorageManager({moduleName, moduleType} = {}, {isAllowed = isActivityAllowed} = {}) { function isValid(cb, storageType) { - if (!isBidderAllowed(storageType)) { - logInfo(`bidderSettings denied access to device storage for bidder '${moduleName}'`); - const result = {valid: false}; - return cb(result); - } else { - let value; - let hookDetails = { - hasEnforcementHook: false - } - validateStorageEnforcement(moduleType, moduleName, hookDetails, function(result) { - if (result && result.hasEnforcementHook) { - value = cb(result); - } else { - let result = { - hasEnforcementHook: false, - valid: hasDeviceAccess() - } - value = cb(result); - } - }); - return value; + let mod = moduleName; + const curBidder = config.getCurrentBidder(); + if (curBidder && moduleType === MODULE_TYPE_BIDDER && adapterManager.aliasRegistry[curBidder] === moduleName) { + mod = curBidder; } + const result = { + valid: isAllowed(ACTIVITY_ACCESS_DEVICE, activityParams(moduleType, mod, { + [ACTIVITY_PARAM_STORAGE_TYPE]: storageType + })) + }; + return cb(result); } function schedule(operation, storageType, done) { @@ -265,6 +255,37 @@ export function getCoreStorageManager(moduleName) { return newStorageManager({moduleName: moduleName, moduleType: MODULE_TYPE_PREBID}); } +/** + * Block all access to storage when deviceAccess = false + */ +export function deviceAccessRule() { + if (!hasDeviceAccess()) { + return {allow: false} + } +} +registerActivityControl(ACTIVITY_ACCESS_DEVICE, 'deviceAccess config', deviceAccessRule); + +/** + * By default, deny bidders accessDevice unless they enable it through bidderSettings + * + * // TODO: for backwards compat, the check is done on the adapter - rather than bidder's code. + */ +export function storageAllowedRule(params, bs = bidderSettings) { + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] !== MODULE_TYPE_BIDDER) return; + let allow = bs.get(params[ACTIVITY_PARAM_ADAPTER_CODE], 'storageAllowed'); + if (!allow || allow === true) { + allow = !!allow + } else { + const storageType = params[ACTIVITY_PARAM_STORAGE_TYPE]; + allow = Array.isArray(allow) ? allow.some((e) => e === storageType) : allow === storageType; + } + if (!allow) { + return {allow}; + } +} + +registerActivityControl(ACTIVITY_ACCESS_DEVICE, 'bidderSettings.*.storageAllowed', storageAllowedRule); + export function resetData() { storageCallbacks = []; } diff --git a/test/spec/activities/params_spec.js b/test/spec/activities/params_spec.js index 4470caaed38..d64c5e2a736 100644 --- a/test/spec/activities/params_spec.js +++ b/test/spec/activities/params_spec.js @@ -1,16 +1,25 @@ import { + ACTIVITY_PARAM_ADAPTER_CODE, ACTIVITY_PARAM_COMPONENT, ACTIVITY_PARAM_COMPONENT_NAME, ACTIVITY_PARAM_COMPONENT_TYPE, activityParams } from '../../../src/activities/params.js'; +import adapterManager from '../../../src/adapterManager.js'; +import {MODULE_TYPE_BIDDER} from '../../../src/activities/modules.js'; describe('activityParams', () => { it('fills out component params', () => { - expect(activityParams('bidder', 'mockBidder', {foo: 'bar'})).to.eql({ + sinon.assert.match(activityParams('bidder', 'mockBidder', {foo: 'bar'}), { [ACTIVITY_PARAM_COMPONENT]: 'bidder.mockBidder', [ACTIVITY_PARAM_COMPONENT_TYPE]: 'bidder', [ACTIVITY_PARAM_COMPONENT_NAME]: 'mockBidder', foo: 'bar' - }); + }) + }); + + it('fills out adapterCode', () => { + adapterManager.registerBidAdapter({callBids: sinon.stub(), getSpec: sinon.stub().returns({})}, 'mockBidder') + adapterManager.aliasBidAdapter('mockBidder', 'mockAlias'); + expect(activityParams(MODULE_TYPE_BIDDER, 'mockAlias')[ACTIVITY_PARAM_ADAPTER_CODE]).to.equal('mockBidder'); }); }); diff --git a/test/spec/unit/core/storageManager_spec.js b/test/spec/unit/core/storageManager_spec.js index 09fb905fc87..1ed36d456b7 100644 --- a/test/spec/unit/core/storageManager_spec.js +++ b/test/spec/unit/core/storageManager_spec.js @@ -1,14 +1,25 @@ import { - getCoreStorageManager, getStorageManager, + deviceAccessRule, + getCoreStorageManager, newStorageManager, resetData, + STORAGE_TYPE_COOKIES, + STORAGE_TYPE_LOCALSTORAGE, + storageAllowedRule, storageCallbacks, - validateStorageEnforcement } from 'src/storageManager.js'; +import adapterManager from 'src/adapterManager.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import {hook} from '../../../../src/hook.js'; import {MODULE_TYPE_BIDDER, MODULE_TYPE_PREBID} from '../../../../src/activities/modules.js'; +import {ACTIVITY_ACCESS_DEVICE} from '../../../../src/activities/activities.js'; +import { + ACTIVITY_PARAM_COMPONENT_NAME, + ACTIVITY_PARAM_COMPONENT_TYPE, + ACTIVITY_PARAM_STORAGE_TYPE, + activityParams +} from '../../../../src/activities/params.js'; describe('storage manager', function() { before(() => { @@ -56,41 +67,42 @@ describe('storage manager', function() { deviceAccessSpy.restore(); }); - describe(`enforcement`, () => { - let validateHook; + describe(`accessDevice activity check`, () => { + let isAllowed; + + function mkManager(moduleType, moduleName) { + return newStorageManager({moduleType, moduleName}, {isAllowed}); + } beforeEach(() => { - validateHook = sinon.stub().callsFake(function (next, ...args) { - next.apply(this, args); - }); - validateStorageEnforcement.before(validateHook, 99); + isAllowed = sinon.stub(); }); - afterEach(() => { - validateStorageEnforcement.getHooks({hook: validateHook}).remove(); - config.resetConfig(); - }) - - Object.entries({ - [MODULE_TYPE_PREBID]: () => getCoreStorageManager('mock'), - 'other': () => getStorageManager({moduleType: 'other', moduleName: 'mock'}) - }).forEach(([moduleType, getMgr]) => { - describe(`for ${moduleType} modules`, () => { - let storage; - beforeEach(() => { - storage = getMgr(); - }); - it(`should pass '${moduleType}' module type to consent enforcement`, () => { - storage.localStorageIsEnabled(); - expect(validateHook.args[0][1]).to.equal(moduleType); - }); + it('should pass module type and name as activity params', () => { + mkManager(MODULE_TYPE_PREBID, 'mockMod').localStorageIsEnabled(); + sinon.assert.calledWith(isAllowed, ACTIVITY_ACCESS_DEVICE, sinon.match({ + [ACTIVITY_PARAM_COMPONENT_TYPE]: MODULE_TYPE_PREBID, + [ACTIVITY_PARAM_COMPONENT_NAME]: 'mockMod', + [ACTIVITY_PARAM_STORAGE_TYPE]: STORAGE_TYPE_LOCALSTORAGE + })); + }); - it('should respect the deviceAccess flag', () => { - config.setConfig({deviceAccess: false}); - expect(storage.localStorageIsEnabled()).to.be.false - }); - }); + it('should deny access if activity is denied', () => { + isAllowed.returns(false); + const mgr = mkManager(MODULE_TYPE_PREBID, 'mockMod'); + mgr.setDataInLocalStorage('testKey', 'val'); + expect(mgr.getDataFromLocalStorage('testKey')).to.not.exist; }); + + it('should use bidder aliases when possible', () => { + adapterManager.registerBidAdapter({callBids: sinon.stub(), getSpec: () => ({})}, 'mockBidder'); + adapterManager.aliasBidAdapter('mockBidder', 'mockAlias'); + const mgr = mkManager(MODULE_TYPE_BIDDER, 'mockBidder'); + config.runWithBidder('mockAlias', () => mgr.cookiesAreEnabled()); + sinon.assert.calledWith(isAllowed, ACTIVITY_ACCESS_DEVICE, sinon.match({ + [ACTIVITY_PARAM_COMPONENT_NAME]: 'mockAlias' + })) + }) }) describe('localstorage forbidden access in 3rd-party context', function() { @@ -145,13 +157,26 @@ describe('storage manager', function() { }); }); - describe('when bidderSettings.allowStorage is defined', () => { + describe('deviceAccess control', ()=> { + afterEach(() => { + config.resetConfig() + }); + + it('should allow by default', () => { + config.resetConfig(); + expect(deviceAccessRule()).to.not.exist; + }); + + it('should deny access when set', () => { + config.setConfig({deviceAccess: false}); + sinon.assert.match(deviceAccessRule(), {allow: false}); + }) + }); + + describe('allowStorage access control rule', () => { const ALLOWED_BIDDER = 'allowed-bidder'; const ALLOW_KEY = 'storageAllowed'; - const COOKIE = 'test-cookie'; - const LS_KEY = 'test-localstorage'; - function mockBidderSettings(val) { return { get(bidder, key) { @@ -213,39 +238,22 @@ describe('storage manager', function() { }).forEach(([t, {configValues, shouldWork: {cookie, html5}}]) => { describe(`when ${t} is allowed`, () => { configValues.forEach(configValue => describe(`storageAllowed = ${configValue}`, () => { - let mgr; - - beforeEach(() => { - mgr = newStorageManager({moduleType: MODULE_TYPE_BIDDER, moduleName: bidderCode}, {bidderSettings: mockBidderSettings(configValue)}); - }) - - afterEach(() => { - mgr.setCookie(COOKIE, 'delete', new Date().toUTCString()); - mgr.removeDataFromLocalStorage(LS_KEY); - }) - - function scenario(type, desc, fn) { + Object.entries({ + [STORAGE_TYPE_LOCALSTORAGE]: 'allow localStorage', + [STORAGE_TYPE_COOKIES]: 'allow cookies' + }).forEach(([type, desc]) => { const shouldWork = isBidderAllowed && ({html5, cookie})[type]; - it(`${shouldWork ? '' : 'NOT'} ${desc}`, () => fn(shouldWork)); - } - - scenario('cookie', 'allow cookies', (shouldWork) => { - mgr.setCookie(COOKIE, 'value'); - expect(mgr.getCookie(COOKIE)).to.equal(shouldWork ? 'value' : null); - }); - - scenario('html5', 'allow localStorage', (shouldWork) => { - mgr.setDataInLocalStorage(LS_KEY, 'value'); - expect(mgr.getDataFromLocalStorage(LS_KEY)).to.equal(shouldWork ? 'value' : null); - }); - - scenario('html5', 'report localStorage as available', (shouldWork) => { - expect(mgr.hasLocalStorage()).to.equal(shouldWork); - }); - - scenario('cookie', 'report cookies as available', (shouldWork) => { - expect(mgr.cookiesAreEnabled()).to.equal(shouldWork); - }); + it(`${shouldWork ? '' : 'NOT'} ${desc}`, () => { + const res = storageAllowedRule(activityParams(MODULE_TYPE_BIDDER, bidderCode, { + [ACTIVITY_PARAM_STORAGE_TYPE]: type + }), mockBidderSettings(configValue)); + if (shouldWork) { + expect(res).to.not.exist; + } else { + sinon.assert.match(res, {allow: false}); + } + }); + }) })); }); }); From 886d09112cde16e528bff24802accf0e5f085850 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 6 Apr 2023 10:32:48 -0700 Subject: [PATCH 26/37] gdpr enforcement for accessDevice --- modules/gdprEnforcement.js | 64 ++------- test/spec/modules/gdprEnforcement_spec.js | 150 +++------------------- 2 files changed, 32 insertions(+), 182 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index eacd958186d..501ee371c1a 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -7,7 +7,6 @@ import {config} from '../src/config.js'; import adapterManager, {gdprDataHandler} from '../src/adapterManager.js'; import {find} from '../src/polyfill.js'; import {getHook} from '../src/hook.js'; -import {validateStorageEnforcement} from '../src/storageManager.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../src/consentHandler.js'; @@ -24,6 +23,7 @@ import { } from '../src/activities/params.js'; import {registerActivityControl} from '../src/activities/rules.js'; import { + ACTIVITY_ACCESS_DEVICE, ACTIVITY_ENRICH_EIDS, ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS, @@ -182,55 +182,10 @@ export function validateRules(rule, consentData, currentModule, gvlId) { return purposeAllowed && vendorAllowed; } -/** - * This hook checks whether module has permission to access device or not. Device access include cookie and local storage - * - * @param {Function} fn reference to original function (used by hook logic) - * @param {string} moduleType type of the module - * @param {string=} moduleName name of the module - * @param result - * @param validate - */ -export function deviceAccessHook(fn, moduleType, moduleName, result, {validate = validateRules} = {}) { - result = Object.assign({}, { - hasEnforcementHook: true - }); - if (!hasDeviceAccess()) { - logWarn('Device access is disabled by Publisher'); - result.valid = false; - } else if (moduleType === MODULE_TYPE_PREBID && !strictStorageEnforcement) { - // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set - result.valid = true; - } else { - const consentData = gdprDataHandler.getConsentData(); - let gvlid; - if (shouldEnforce(consentData, 1, moduleName)) { - const curBidder = config.getCurrentBidder(); - // Bidders have a copy of storage object with bidder code binded. Aliases will also pass the same bidder code when invoking storage functions and hence if alias tries to access device we will try to grab the gvl id for alias instead of original bidder - if (curBidder && (curBidder !== moduleName) && adapterManager.aliasRegistry[curBidder] === moduleName) { - gvlid = getGvlid(moduleType, curBidder); - } else { - gvlid = getGvlid(moduleType, moduleName) - } - const curModule = moduleName || curBidder; - let isAllowed = validate(purpose1Rule, consentData, curModule, gvlid,); - if (isAllowed) { - result.valid = true; - } else { - curModule && logWarn(`TCF2 denied device access for ${curModule}`); - result.valid = false; - storageBlocked.add(curModule); - } - } else { - result.valid = true; - } - } - fn.call(this, moduleType, moduleName, result); -} - /** * all activity rules follow the same structure: - * if GDPR is in scope, check configuration for a particular purpose + * if GDPR is in scope, check configuration for a particular purpose, and if that enables enforcement, + * check against consent data for that purpose and vendor * * @param purposeNo TCF purpose number to check for this activity * @param getEnforcementRule getter for gdprEnforcement rule definition to use @@ -252,6 +207,14 @@ function gdprRule(purposeNo, getEnforcementRule, blocked = null, gvlidFallback = } } +export const accessDeviceRule = ((rule) => { + return function(params) { + // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID && !strictStorageEnforcement) return; + return rule(params); + } +})(gdprRule(1, () => purpose1Rule, storageBlocked)) + export const syncUserRule = gdprRule(1, () => purpose1Rule, storageBlocked); export const enrichEidsRule = gdprRule(1, () => purpose1Rule, storageBlocked) @@ -267,7 +230,7 @@ export function userIdHook(fn, submodules, consentData) { export const fetchBidsRule = ((rule) => { return function (params) { if (params[ACTIVITY_PARAM_COMPONENT_TYPE] !== MODULE_TYPE_BIDDER) { - // TODO: this special case is for the PBS adapter (componentType is core) + // TODO: this special case is for the PBS adapter (componentType is 'prebid') // we should check for generic purpose 2 consent & vendor consent based on the PBS vendor's GVL ID; // that is, however, a breaking change and skipped for now return; @@ -334,7 +297,7 @@ export function setEnforcementConfig(config) { if (!hooksAdded) { if (purpose1Rule) { hooksAdded = true; - validateStorageEnforcement.before(deviceAccessHook, 49); + RULE_HANDLES.push(registerActivityControl(ACTIVITY_ACCESS_DEVICE, RULE_NAME, accessDeviceRule)) RULE_HANDLES.push(registerActivityControl(ACTIVITY_SYNC_USER, RULE_NAME, syncUserRule)) RULE_HANDLES.push(registerActivityControl(ACTIVITY_ENRICH_EIDS, RULE_NAME, enrichEidsRule)); // TODO: remove this hook in v8 (https://github.com/prebid/Prebid.js/issues/9766) @@ -352,7 +315,6 @@ export function setEnforcementConfig(config) { export function uninstall() { while (RULE_HANDLES.length) RULE_HANDLES.pop()(); [ - validateStorageEnforcement.getHooks({hook: deviceAccessHook}), getHook('validateGdprEnforcement').getHooks({hook: userIdHook}), ].forEach(hook => hook.remove()); hooksAdded = false; diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 884759e67d6..b04b8db0bfc 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -1,4 +1,5 @@ import { + accessDeviceRule, deviceAccessHook, enforcementRules, enrichEidsRule, @@ -126,7 +127,6 @@ describe('gdpr enforcement', function () { }); after(function () { - validateStorageEnforcement.getHooks({ hook: deviceAccessHook }).remove(); $$PREBID_GLOBAL$$.requestBids.getHooks().remove(); }) @@ -144,44 +144,12 @@ describe('gdpr enforcement', function () { sandbox.restore(); }) - describe('deviceAccessHook', function () { - beforeEach(function () { - nextFnSpy = sinon.spy(); - gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); - logWarnSpy = sinon.spy(utils, 'logWarn'); - }); - - afterEach(function () { + describe('deviceAccessRule', () => { + afterEach(() => { config.resetConfig(); - gdprDataHandler.getConsentData.restore(); - logWarnSpy.restore(); }); - it('should not allow device access when device access flag is set to false', function () { - config.setConfig({ - deviceAccess: false, - consentManagement: { - gdpr: { - rules: [{ - purpose: 'storage', - enforcePurpose: false, - enforceVendor: false, - vendorExceptions: ['appnexus', 'rubicon'] - }] - } - } - }); - - deviceAccessHook(nextFnSpy); - expect(nextFnSpy.calledOnce).to.equal(true); - let result = { - hasEnforcementHook: true, - valid: false - } - sinon.assert.calledWith(nextFnSpy, undefined, undefined, result); - }); - - it('should only check for consent for vendor exceptions when enforcePurpose and enforceVendor are false', function () { + it('should not check for consent when enforcePurpose and enforceVendor are false', function () { Object.assign(gvlids, { appnexus: 1, rubicon: 5 @@ -196,15 +164,8 @@ describe('gdpr enforcement', function () { }] } }); - let consentData = {} - consentData.vendorData = staticConfig.consentData.getTCData; - consentData.gdprApplies = true; - consentData.apiVersion = 2; - gdprDataHandlerStub.returns(consentData); - - deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); - deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'rubicon'); - expect(logWarnSpy.callCount).to.equal(0); + setupConsentData(); + ['appnexus', 'rubicon'].forEach(bidder => expectAllow(true, accessDeviceRule(activityParams(MODULE_TYPE_BIDDER, bidder)))); }); it('should check consent for all vendors when enforcePurpose and enforceVendor are true', function () { @@ -221,15 +182,13 @@ describe('gdpr enforcement', function () { }] } }); - let consentData = {} - consentData.vendorData = staticConfig.consentData.getTCData; - consentData.gdprApplies = true; - consentData.apiVersion = 2; - gdprDataHandlerStub.returns(consentData); - - deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); - deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'rubicon'); - expect(logWarnSpy.callCount).to.equal(1); + setupConsentData(); + Object.entries({ + appnexus: true, + rubicon: false + }).forEach(([bidder, isAllowed]) => { + expectAllow(isAllowed, accessDeviceRule(activityParams(MODULE_TYPE_BIDDER, bidder))); + }) }); it('should allow device access when gdprApplies is false and hasDeviceAccess flag is true', function () { @@ -244,19 +203,8 @@ describe('gdpr enforcement', function () { }] } }); - let consentData = {} - consentData.vendorData = staticConfig.consentData.getTCData; - consentData.gdprApplies = false; - consentData.apiVersion = 2; - gdprDataHandlerStub.returns(consentData); - - deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); - expect(nextFnSpy.calledOnce).to.equal(true); - let result = { - hasEnforcementHook: true, - valid: true - } - sinon.assert.calledWith(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus', result); + setupConsentData(); + expectAllow(true, accessDeviceRule(activityParams(MODULE_TYPE_BIDDER, 'appnexus'))); }); it('should use gvlMapping set by publisher', function() { @@ -275,69 +223,14 @@ describe('gdpr enforcement', function () { }] } }); - let consentData = {} - consentData.vendorData = staticConfig.consentData.getTCData; - consentData.gdprApplies = true; - consentData.apiVersion = 2; - gdprDataHandlerStub.returns(consentData); - - deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); - expect(nextFnSpy.calledOnce).to.equal(true); - let result = { - hasEnforcementHook: true, - valid: true - } - sinon.assert.calledWith(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus', result); - config.resetConfig(); - }); - - it('should use gvl id of alias and not of parent', function() { - let curBidderStub = sinon.stub(config, 'getCurrentBidder'); - curBidderStub.returns('appnexus-alias'); - adapterManager.aliasBidAdapter('appnexus', 'appnexus-alias'); - config.setConfig({ - 'gvlMapping': { - 'appnexus-alias': 4 - } - }); - setEnforcementConfig({ - gdpr: { - rules: [{ - purpose: 'storage', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: [] - }] - } - }); - let consentData = {} - consentData.vendorData = staticConfig.consentData.getTCData; - consentData.gdprApplies = true; - consentData.apiVersion = 2; - gdprDataHandlerStub.returns(consentData); - - deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); - expect(nextFnSpy.calledOnce).to.equal(true); - let result = { - hasEnforcementHook: true, - valid: true - } - sinon.assert.calledWith(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus', result); - config.resetConfig(); - curBidderStub.restore(); + setupConsentData(); + expectAllow(true, accessDeviceRule(activityParams(MODULE_TYPE_BIDDER, 'appnexus'))); }); it(`should not enforce consent for vendorless modules if ${STRICT_STORAGE_ENFORCEMENT} is not set`, () => { setEnforcementConfig({}); - let consentData = { - vendorData: staticConfig.consentData.getTCData, - gdprApplies: true - } - gdprDataHandlerStub.returns(consentData); - const validate = sinon.stub().callsFake(() => false); - deviceAccessHook(nextFnSpy, MODULE_TYPE_PREBID, 'mockModule', undefined, {validate}); - sinon.assert.callCount(validate, 0); - sinon.assert.calledWith(nextFnSpy, MODULE_TYPE_PREBID, 'mockModule', {hasEnforcementHook: true, valid: true}); + setupConsentData(); + expectAllow(true, accessDeviceRule(activityParams(MODULE_TYPE_PREBID, 'mockCoreModule'))); }) }); @@ -354,11 +247,6 @@ describe('gdpr enforcement', function () { } }); setupConsentData(); - let consentData = {} - consentData.vendorData = staticConfig.consentData.getTCData; - consentData.gdprApplies = true; - consentData.apiVersion = 2; - gdprDataHandlerStub.returns(consentData); Object.assign(gvlids, { sampleBidder1: 1, sampleBidder2: 2 From 103a6cade2cd2f8b7796046a323e1846fcc21213 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 6 Apr 2023 10:57:30 -0700 Subject: [PATCH 27/37] move alias resolution logic to adapterManager --- src/activities/params.js | 6 +----- src/adapterManager.js | 10 ++++++++++ test/spec/unit/core/adapterManager_spec.js | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/activities/params.js b/src/activities/params.js index 0ccd72f0edd..ecf3e8824d1 100644 --- a/src/activities/params.js +++ b/src/activities/params.js @@ -52,11 +52,7 @@ export function activityParams(moduleType, moduleName, params) { [ACTIVITY_PARAM_COMPONENT]: `${moduleType}.${moduleName}` }; if (moduleType === MODULE_TYPE_BIDDER) { - let adapterCode = moduleName; - while (adapterManager.aliasRegistry[adapterCode]) { - adapterCode = adapterManager.aliasRegistry[adapterCode]; - } - defaults[ACTIVITY_PARAM_ADAPTER_CODE] = adapterCode; + defaults[ACTIVITY_PARAM_ADAPTER_CODE] = adapterManager.resolveAlias(moduleName); } return Object.assign(defaults, params); } diff --git a/src/adapterManager.js b/src/adapterManager.js index d6cca0c9959..d8d425f3f69 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -546,6 +546,16 @@ adapterManager.aliasBidAdapter = function (bidderCode, alias, options) { } }; +adapterManager.resolveAlias = function (alias) { + let code = alias; + let visited; + while (_aliasRegistry[code] && (!visited || !visited.has(code))) { + code = _aliasRegistry[code]; + (visited = visited || new Set()).add(code); + } + return code; +} + adapterManager.registerAnalyticsAdapter = function ({adapter, code, gvlid}) { if (adapter && code) { if (typeof adapter.enableAnalytics === 'function') { diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 1da73c0ac61..c6c34df971d 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -2792,12 +2792,29 @@ describe('adapterManager tests', function () { beforeEach(() => { bidderRequests = []; + ['mockBidder', 'mockBidder1', 'mockBidder2'].forEach(bidder => { + adapterManager.registerBidAdapter({callBids: sinon.stub(), getSpec: () =>({code: bidder})}, bidder); + }) sinon.stub(auctionManager, 'getBidsRequested').callsFake(() => bidderRequests); }) afterEach(() => { auctionManager.getBidsRequested.restore(); }) + it('can resolve aliases', () => { + adapterManager.aliasBidAdapter('mockBidder', 'mockBidderAlias'); + expect(adapterManager.resolveAlias('mockBidderAlias')).to.eql('mockBidder'); + }); + it('does not stuck in alias cycles', () => { + adapterManager.aliasRegistry['alias1'] = 'alias2'; + adapterManager.aliasRegistry['alias2'] = 'alias2'; + expect(adapterManager.resolveAlias('alias2')).to.eql('alias2'); + }) + it('returns self when not an alias', () => { + delete adapterManager.aliasRegistry['missing']; + expect(adapterManager.resolveAlias('missing')).to.eql('missing'); + }) + it('does not invoke onDataDeletionRequest on aliases', () => { const del = delMethodForBidder('mockBidder'); adapterManager.aliasBidAdapter('mockBidder', 'mockBidderAlias'); From 806d78b0097065a4dc912cafd732310053f5097a Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 6 Apr 2023 12:11:04 -0700 Subject: [PATCH 28/37] Refactor file structure to get around circular deps --- modules/gdprEnforcement.js | 61 ++++++++++++---------- modules/userId/index.js | 2 +- src/activities/activityParams.js | 8 +++ src/activities/params.js | 21 ++++---- src/adapterManager.js | 5 +- src/fpd/rootDomain.js | 2 +- src/storageManager.js | 16 ++---- src/userSync.js | 3 +- test/spec/activities/params_spec.js | 4 +- test/spec/modules/gdprEnforcement_spec.js | 2 +- test/spec/unit/core/adapterManager_spec.js | 2 +- test/spec/unit/core/storageManager_spec.js | 6 +-- 12 files changed, 72 insertions(+), 60 deletions(-) create mode 100644 src/activities/activityParams.js diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 501ee371c1a..a834c0da2d5 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -2,7 +2,7 @@ * This module gives publishers extra set of features to enforce individual purposes of TCF v2 */ -import {deepAccess, hasDeviceAccess, logError, logWarn} from '../src/utils.js'; +import {deepAccess, logError, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import adapterManager, {gdprDataHandler} from '../src/adapterManager.js'; import {find} from '../src/polyfill.js'; @@ -13,7 +13,8 @@ import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../src/consentHandler.js'; import { MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, - MODULE_TYPE_PREBID, MODULE_TYPE_RTD, + MODULE_TYPE_PREBID, + MODULE_TYPE_RTD, MODULE_TYPE_UID } from '../src/activities/modules.js'; import { @@ -33,10 +34,10 @@ import { export const STRICT_STORAGE_ENFORCEMENT = 'strictStorageEnforcement'; const TCF2 = { - 'purpose1': { id: 1, name: 'storage' }, - 'purpose2': { id: 2, name: 'basicAds' }, - 'purpose7': { id: 7, name: 'measurement' } -} + 'purpose1': {id: 1, name: 'storage'}, + 'purpose2': {id: 2, name: 'basicAds'}, + 'purpose7': {id: 7, name: 'measurement'} +}; /* These rules would be used if `consentManagement.gdpr.rules` is undefined by the publisher. @@ -98,7 +99,7 @@ export function getGvlid(moduleType, moduleName, fallbackFn) { if (modules.hasOwnProperty(type)) { gvlid = modules[type]; if (type !== moduleType) { - logWarn(`Multiple GVL IDs found for module '${moduleName}'; using the ${type} module's ID (${gvlid}) instead of the ${moduleType}'s ID (${modules[moduleType]})`) + logWarn(`Multiple GVL IDs found for module '${moduleName}'; using the ${type} module's ID (${gvlid}) instead of the ${moduleType}'s ID (${modules[moduleType]})`); } break; } @@ -123,9 +124,9 @@ export function getGvlidFromAnalyticsAdapter(code, config) { try { return gvlid.call(adapter.adapter, config); } catch (e) { - logError(`Error invoking ${code} adapter.gvlid()`, e) + logError(`Error invoking ${code} adapter.gvlid()`, e); } - })(adapter?.adapter?.gvlid) + })(adapter?.adapter?.gvlid); } export function shouldEnforce(consentData, purpose, name) { @@ -134,7 +135,7 @@ export function shouldEnforce(consentData, purpose, name) { // NOTE: this check is not foolproof, as when Prebid first loads, enforcement hooks have not been attached yet // This piece of code would not run at all, and `gdprDataHandler.enabled` would be false, until the first // `setConfig({consentManagement})` - logWarn(`Attempting operation that requires purpose ${purpose} consent while consent data is not available${name ? ` (module: ${name})` : ''}. Assuming no consent was given.`) + logWarn(`Attempting operation that requires purpose ${purpose} consent while consent data is not available${name ? ` (module: ${name})` : ''}. Assuming no consent was given.`); return true; } return consentData && consentData.gdprApplies; @@ -156,7 +157,7 @@ export function validateRules(rule, consentData, currentModule, gvlId) { if ((rule.vendorExceptions || []).includes(currentModule)) { return true; } - const vendorConsentRequred = !((gvlId === VENDORLESS_GVLID || (rule.softVendorExceptions || []).includes(currentModule))) + const vendorConsentRequred = !((gvlId === VENDORLESS_GVLID || (rule.softVendorExceptions || []).includes(currentModule))); // get data from the consent string const purposeConsent = deepAccess(consentData, `vendorData.purpose.consents.${purposeId}`); @@ -204,24 +205,24 @@ function gdprRule(purposeNo, getEnforcementRule, blocked = null, gvlidFallback = return {allow}; } } - } + }; } export const accessDeviceRule = ((rule) => { - return function(params) { + return function (params) { // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID && !strictStorageEnforcement) return; return rule(params); - } -})(gdprRule(1, () => purpose1Rule, storageBlocked)) + }; +})(gdprRule(1, () => purpose1Rule, storageBlocked)); export const syncUserRule = gdprRule(1, () => purpose1Rule, storageBlocked); -export const enrichEidsRule = gdprRule(1, () => purpose1Rule, storageBlocked) +export const enrichEidsRule = gdprRule(1, () => purpose1Rule, storageBlocked); export function userIdHook(fn, submodules, consentData) { // TODO: remove this in v8 (https://github.com/prebid/Prebid.js/issues/9766) if (shouldEnforce(consentData, 1, 'User ID')) { - fn.call(this, submodules, { ...consentData, hasValidated: true }); + fn.call(this, submodules, {...consentData, hasValidated: true}); } else { fn.call(this, submodules, consentData); } @@ -236,10 +237,10 @@ export const fetchBidsRule = ((rule) => { return; } return rule(params); - } -})(gdprRule(2, () => purpose2Rule, biddersBlocked)) + }; +})(gdprRule(2, () => purpose2Rule, biddersBlocked)); -export const reportAnalyticsRule = gdprRule(7, () => purpose7Rule, analyticsBlocked, (params) => getGvlidFromAnalyticsAdapter(params[ACTIVITY_PARAM_COMPONENT_NAME], params[ACTIVITY_PARAM_ANL_CONFIG])) +export const reportAnalyticsRule = gdprRule(7, () => purpose7Rule, analyticsBlocked, (params) => getGvlidFromAnalyticsAdapter(params[ACTIVITY_PARAM_COMPONENT_NAME], params[ACTIVITY_PARAM_ANL_CONFIG])); /** * Compiles the TCF2.0 enforcement results into an object, which is emitted as an event payload to "tcf2Enforcement" event. @@ -248,7 +249,7 @@ function emitTCF2FinalResults() { // remove null and duplicate values const formatSet = function (st) { return Array.from(st.keys()).filter(el => el != null); - } + }; const tcf2FinalResults = { storageBlocked: formatSet(storageBlocked), biddersBlocked: formatSet(biddersBlocked), @@ -264,9 +265,15 @@ events.on(CONSTANTS.EVENTS.AUCTION_END, emitTCF2FinalResults); /* Set of callback functions used to detect presence of a TCF rule, passed as the second argument to find(). */ -const hasPurpose1 = (rule) => { return rule.purpose === TCF2.purpose1.name } -const hasPurpose2 = (rule) => { return rule.purpose === TCF2.purpose2.name } -const hasPurpose7 = (rule) => { return rule.purpose === TCF2.purpose7.name } +const hasPurpose1 = (rule) => { + return rule.purpose === TCF2.purpose1.name; +}; +const hasPurpose2 = (rule) => { + return rule.purpose === TCF2.purpose2.name; +}; +const hasPurpose7 = (rule) => { + return rule.purpose === TCF2.purpose7.name; +}; /** * A configuration function that initializes some module variables, as well as adds hooks @@ -297,8 +304,8 @@ export function setEnforcementConfig(config) { if (!hooksAdded) { if (purpose1Rule) { hooksAdded = true; - RULE_HANDLES.push(registerActivityControl(ACTIVITY_ACCESS_DEVICE, RULE_NAME, accessDeviceRule)) - RULE_HANDLES.push(registerActivityControl(ACTIVITY_SYNC_USER, RULE_NAME, syncUserRule)) + RULE_HANDLES.push(registerActivityControl(ACTIVITY_ACCESS_DEVICE, RULE_NAME, accessDeviceRule)); + RULE_HANDLES.push(registerActivityControl(ACTIVITY_SYNC_USER, RULE_NAME, syncUserRule)); RULE_HANDLES.push(registerActivityControl(ACTIVITY_ENRICH_EIDS, RULE_NAME, enrichEidsRule)); // TODO: remove this hook in v8 (https://github.com/prebid/Prebid.js/issues/9766) getHook('validateGdprEnforcement').before(userIdHook, 47); @@ -307,7 +314,7 @@ export function setEnforcementConfig(config) { RULE_HANDLES.push(registerActivityControl(ACTIVITY_FETCH_BIDS, RULE_NAME, fetchBidsRule)); } if (purpose7Rule) { - RULE_HANDLES.push(registerActivityControl(ACTIVITY_REPORT_ANALYTICS, RULE_NAME, reportAnalyticsRule)) + RULE_HANDLES.push(registerActivityControl(ACTIVITY_REPORT_ANALYTICS, RULE_NAME, reportAnalyticsRule)); } } } diff --git a/modules/userId/index.js b/modules/userId/index.js index 8f96bee5caa..65e73b697ab 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -167,7 +167,7 @@ import {GDPR_GVLIDS} from '../../src/consentHandler.js'; import {MODULE_TYPE_UID} from '../../src/activities/modules.js'; import {isActivityAllowed} from '../../src/activities/rules.js'; import {ACTIVITY_ENRICH_EIDS} from '../../src/activities/activities.js'; -import {activityParams} from '../../src/activities/params.js'; +import {activityParams} from '../../src/activities/activityParams.js'; const MODULE_NAME = 'User ID'; const COOKIE = STORAGE_TYPE_COOKIES; diff --git a/src/activities/activityParams.js b/src/activities/activityParams.js new file mode 100644 index 00000000000..5888252b27d --- /dev/null +++ b/src/activities/activityParams.js @@ -0,0 +1,8 @@ +import adapterManager from '../adapterManager.js'; +import {activityParamsBuilder} from './params.js'; + +/** + * Utility function for building common activity parameters - broken out to its own + * file to avoid circular imports. + */ +export const activityParams = activityParamsBuilder(adapterManager.resolveAlias.bind(adapterManager)); diff --git a/src/activities/params.js b/src/activities/params.js index ecf3e8824d1..ff181bb55a4 100644 --- a/src/activities/params.js +++ b/src/activities/params.js @@ -1,5 +1,4 @@ import {MODULE_TYPE_BIDDER} from './modules.js'; -import adapterManager from '../adapterManager.js'; /** * Component ID - who is trying to perform the activity? @@ -45,14 +44,16 @@ export const ACTIVITY_PARAM_SYNC_URL = 'syncUrl'; */ export const ACTIVITY_PARAM_ANL_CONFIG = '_config'; -export function activityParams(moduleType, moduleName, params) { - const defaults = { - [ACTIVITY_PARAM_COMPONENT_TYPE]: moduleType, - [ACTIVITY_PARAM_COMPONENT_NAME]: moduleName, - [ACTIVITY_PARAM_COMPONENT]: `${moduleType}.${moduleName}` - }; - if (moduleType === MODULE_TYPE_BIDDER) { - defaults[ACTIVITY_PARAM_ADAPTER_CODE] = adapterManager.resolveAlias(moduleName); +export function activityParamsBuilder(resolveAlias) { + return function activityParams(moduleType, moduleName, params) { + const defaults = { + [ACTIVITY_PARAM_COMPONENT_TYPE]: moduleType, + [ACTIVITY_PARAM_COMPONENT_NAME]: moduleName, + [ACTIVITY_PARAM_COMPONENT]: `${moduleType}.${moduleName}` + }; + if (moduleType === MODULE_TYPE_BIDDER) { + defaults[ACTIVITY_PARAM_ADAPTER_CODE] = resolveAlias(moduleName); + } + return Object.assign(defaults, params); } - return Object.assign(defaults, params); } diff --git a/src/adapterManager.js b/src/adapterManager.js index d8d425f3f69..63a902bef71 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -39,7 +39,8 @@ import {auctionManager} from './auctionManager.js'; import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, MODULE_TYPE_PREBID} from './activities/modules.js'; import {isActivityAllowed} from './activities/rules.js'; import {ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from './activities/activities.js'; -import {ACTIVITY_PARAM_ANL_CONFIG, ACTIVITY_PARAM_S2S_NAME, activityParams} from './activities/params.js'; +import {ACTIVITY_PARAM_ANL_CONFIG, ACTIVITY_PARAM_S2S_NAME, activityParamsBuilder} from './activities/params.js'; + const PBS_ADAPTER_NAME = 'pbsBidAdapter'; export const PARTITIONS = { @@ -65,6 +66,8 @@ config.getConfig('s2sConfig', config => { var _analyticsRegistry = {}; +const activityParams = activityParamsBuilder((alias) => adapterManager.resolveAlias(alias)); + /** * @typedef {object} LabelDescriptor * @property {boolean} labelAll describes whether or not this object expects all labels to match, or any label to match diff --git a/src/fpd/rootDomain.js b/src/fpd/rootDomain.js index 4095613672f..21547de8e2e 100644 --- a/src/fpd/rootDomain.js +++ b/src/fpd/rootDomain.js @@ -1,7 +1,7 @@ import {memoize, timestamp} from '../utils.js'; import {getCoreStorageManager} from '../storageManager.js'; -export const coreStorage = getCoreStorageManager(); +export const coreStorage = getCoreStorageManager('fpdEnrichment'); /** * Find the root domain by testing for the topmost domain that will allow setting cookies. diff --git a/src/storageManager.js b/src/storageManager.js index b3c0dacf19c..87d714f77b8 100644 --- a/src/storageManager.js +++ b/src/storageManager.js @@ -1,17 +1,17 @@ -import {hook} from './hook.js'; import {checkCookieSupport, hasDeviceAccess, logError} from './utils.js'; import {bidderSettings} from './bidderSettings.js'; import {MODULE_TYPE_BIDDER, MODULE_TYPE_PREBID} from './activities/modules.js'; +import {isActivityAllowed, registerActivityControl} from './activities/rules.js'; import { ACTIVITY_PARAM_ADAPTER_CODE, ACTIVITY_PARAM_COMPONENT_TYPE, - ACTIVITY_PARAM_STORAGE_TYPE, - activityParams + ACTIVITY_PARAM_STORAGE_TYPE } from './activities/params.js'; -import {isActivityAllowed, registerActivityControl} from './activities/rules.js'; + import {ACTIVITY_ACCESS_DEVICE} from './activities/activities.js'; import {config} from './config.js'; import adapterManager from './adapterManager.js'; +import {activityParams} from './activities/activityParams.js'; export const STORAGE_TYPE_LOCALSTORAGE = 'html5'; export const STORAGE_TYPE_COOKIES = 'cookie'; @@ -22,7 +22,6 @@ export let storageCallbacks = []; * Storage manager constructor. Consumers should prefer one of `getStorageManager` or `getCoreStorageManager`. */ export function newStorageManager({moduleName, moduleType} = {}, {isAllowed = isActivityAllowed} = {}) { - function isValid(cb, storageType) { let mod = moduleName; const curBidder = config.getCurrentBidder(); @@ -218,13 +217,6 @@ export function newStorageManager({moduleName, moduleType} = {}, {isAllowed = is } } -/** - * This hook validates the storage enforcement if gdprEnforcement module is included - */ -export const validateStorageEnforcement = hook('async', function(moduleType, moduleName, hookDetails, callback) { - callback(hookDetails); -}, 'validateStorageEnforcement'); - /** * Get a storage manager for a particular module. * diff --git a/src/userSync.js b/src/userSync.js index 2474deafd64..936836eb12e 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -10,9 +10,10 @@ import {ACTIVITY_SYNC_USER} from './activities/activities.js'; import { ACTIVITY_PARAM_COMPONENT_NAME, ACTIVITY_PARAM_COMPONENT_TYPE, - ACTIVITY_PARAM_SYNC_TYPE, ACTIVITY_PARAM_SYNC_URL, activityParams + ACTIVITY_PARAM_SYNC_TYPE, ACTIVITY_PARAM_SYNC_URL } from './activities/params.js'; import {MODULE_TYPE_BIDDER} from './activities/modules.js'; +import {activityParams} from './activities/activityParams.js'; export const USERSYNC_DEFAULT_CONFIG = { syncEnabled: true, diff --git a/test/spec/activities/params_spec.js b/test/spec/activities/params_spec.js index d64c5e2a736..d949cd41cb4 100644 --- a/test/spec/activities/params_spec.js +++ b/test/spec/activities/params_spec.js @@ -1,11 +1,11 @@ import { ACTIVITY_PARAM_ADAPTER_CODE, ACTIVITY_PARAM_COMPONENT, ACTIVITY_PARAM_COMPONENT_NAME, - ACTIVITY_PARAM_COMPONENT_TYPE, - activityParams + ACTIVITY_PARAM_COMPONENT_TYPE } from '../../../src/activities/params.js'; import adapterManager from '../../../src/adapterManager.js'; import {MODULE_TYPE_BIDDER} from '../../../src/activities/modules.js'; +import {activityParams} from '../../../src/activities/activityParams.js'; describe('activityParams', () => { it('fills out component params', () => { diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index b04b8db0bfc..1585b8346ba 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -29,7 +29,7 @@ import 'src/prebid.js'; import {hook} from '../../../src/hook.js'; import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../../../src/consentHandler.js'; import {validateStorageEnforcement} from '../../../src/storageManager.js'; -import {activityParams} from '../../../src/activities/params.js'; +import {activityParams} from '../../../src/activities/activityParams.js'; describe('gdpr enforcement', function () { let nextFnSpy; diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index c6c34df971d..ac9a760863e 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -2793,7 +2793,7 @@ describe('adapterManager tests', function () { beforeEach(() => { bidderRequests = []; ['mockBidder', 'mockBidder1', 'mockBidder2'].forEach(bidder => { - adapterManager.registerBidAdapter({callBids: sinon.stub(), getSpec: () =>({code: bidder})}, bidder); + adapterManager.registerBidAdapter({callBids: sinon.stub(), getSpec: () => ({code: bidder})}, bidder); }) sinon.stub(auctionManager, 'getBidsRequested').callsFake(() => bidderRequests); }) diff --git a/test/spec/unit/core/storageManager_spec.js b/test/spec/unit/core/storageManager_spec.js index 1ed36d456b7..edead126c2c 100644 --- a/test/spec/unit/core/storageManager_spec.js +++ b/test/spec/unit/core/storageManager_spec.js @@ -17,9 +17,9 @@ import {ACTIVITY_ACCESS_DEVICE} from '../../../../src/activities/activities.js'; import { ACTIVITY_PARAM_COMPONENT_NAME, ACTIVITY_PARAM_COMPONENT_TYPE, - ACTIVITY_PARAM_STORAGE_TYPE, - activityParams + ACTIVITY_PARAM_STORAGE_TYPE } from '../../../../src/activities/params.js'; +import {activityParams} from '../../../../src/activities/activityParams.js'; describe('storage manager', function() { before(() => { @@ -157,7 +157,7 @@ describe('storage manager', function() { }); }); - describe('deviceAccess control', ()=> { + describe('deviceAccess control', () => { afterEach(() => { config.resetConfig() }); From 5dc478b55d9112f114f959b3bf716be84c3464e7 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 11 Apr 2023 14:21:46 -0700 Subject: [PATCH 29/37] transmit(Eids/Ufpd/PreciseGeo) enforcement for bid adapters --- libraries/objectGuard/objectGuard.js | 37 +++ src/activities/activityParams.js | 2 +- src/activities/redactor.js | 119 +++++++++ src/adapterManager.js | 15 +- test/spec/activities/objectGuard_spec.js | 59 +++++ test/spec/activities/redactor_spec.js | 274 +++++++++++++++++++++ test/spec/unit/core/adapterManager_spec.js | 30 ++- 7 files changed, 527 insertions(+), 9 deletions(-) create mode 100644 libraries/objectGuard/objectGuard.js create mode 100644 src/activities/redactor.js create mode 100644 test/spec/activities/objectGuard_spec.js create mode 100644 test/spec/activities/redactor_spec.js diff --git a/libraries/objectGuard/objectGuard.js b/libraries/objectGuard/objectGuard.js new file mode 100644 index 00000000000..9e9f9094b73 --- /dev/null +++ b/libraries/objectGuard/objectGuard.js @@ -0,0 +1,37 @@ + +export function objectGuard(accessRules) { + const rules = {}; + Object.entries(accessRules).forEach(([path, rule]) => { + let node = rules; + path.split('.').forEach(el => { + (node.children = node.children || {})[el] = {}; + node = node.children[el]; + }) + node.rule = rule; + }) + + function mkGuard(obj, rules) { + return new Proxy(obj, { + get(target, prop, receiver) { + const getVal = () => Reflect.get(target, prop, receiver); + if (rules.hasOwnProperty(prop)) { + const {children, rule} = rules[prop] + if (children) { + return mkGuard(getVal(), children); + } else if (rule?.read) { + return rule.read(getVal); + } + } + return getVal(); + }, + set(target, prop, val, receiver) { + const setVal = Reflect.set(target, prop, val, receiver); + + } + }) + } + + return function guard(obj) { + return mkGuard(obj, rules.children); + } +} diff --git a/src/activities/activityParams.js b/src/activities/activityParams.js index 5888252b27d..f33ceb2a9a4 100644 --- a/src/activities/activityParams.js +++ b/src/activities/activityParams.js @@ -5,4 +5,4 @@ import {activityParamsBuilder} from './params.js'; * Utility function for building common activity parameters - broken out to its own * file to avoid circular imports. */ -export const activityParams = activityParamsBuilder(adapterManager.resolveAlias.bind(adapterManager)); +export const activityParams = activityParamsBuilder((alias) => adapterManager.resolveAlias(alias)); diff --git a/src/activities/redactor.js b/src/activities/redactor.js new file mode 100644 index 00000000000..8e9ae0bcf16 --- /dev/null +++ b/src/activities/redactor.js @@ -0,0 +1,119 @@ +import {deepAccess} from '../utils.js'; +import {isActivityAllowed} from './rules.js'; +import {ACTIVITY_TRANSMIT_EIDS, ACTIVITY_TRANSMIT_PRECISE_GEO, ACTIVITY_TRANSMIT_UFPD} from './activities.js'; + +export const ORTB_UFPD_PATHS = ['user.data', 'user.ext.data']; +export const ORTB_EIDS_PATHS = ['user.eids']; +export const ORTB_GEO_PATHS = ['user.geo.lat', 'user.geo.lon', 'device.geo.lat', 'device.geo.lon']; + +/** + * Factory for transformation functions that apply the given rules to an object. + * + * @typedef {Object} TransformationRule + * @property {function(*): boolean} applies a predicate that should return true if this rule applies + * (and the transformation defined herein should be applied). The arguments are those passed to the transformation function. + * @property {name} a name for the rule; used to debounce calls to `applies`: + * if a rule with the same name was already found to apply (or not), this one will (or won't) as well. + * @property {Array[string]} paths dot-separated list of paths that this rule applies to. If the input object + * contains any of these paths, `applies` will be invoked; and if it returns true, properties at these paths + * will be replaced by running through `get`. + * @property {function(*): *} get? a transformation function. Takes a single value, and returns a replacement - which will + * be run against all the values in `paths`, if `applies` returns true. By default, this returns undefined, and removes + * any values under `paths`. + * + * @param {Array[TransformationRule]} rules + * @return {function(*): *} + */ +export function objectTransformer(rules) { + rules.forEach(rule => { + rule.paths = rule.paths.map((path) => { + path = path.split('.'); + const tail = path.pop(); + return [path.length > 0 ? path.join('.') : null, tail]; + }); + rule.get = rule.get || function () {} + }); + return function transformer(session, obj, ...args) { + rules.forEach(({name, get, paths, applies}) => { + if (session.hasOwnProperty(name) && !session[name]) { + return; + } + for (const [head, tail] of paths) { + const parent = head == null ? obj : deepAccess(obj, head); + if (parent) { + const val = parent[tail]; + if (isData(val)) { + if (!session.hasOwnProperty(name)) { + session[name] = applies(...args); + if (!session[name]) break; + } + const repl = get(val); + if (repl === undefined) { + delete parent[tail]; + } else { + parent[tail] = repl; + } + } + } + } + }) + return obj; + } +} + +export function isData(val) { + return val != null && (typeof val !== 'object' || Object.keys(val).length > 0) +} + +function appliesWhenActivityDenied(activity, isAllowed = isActivityAllowed) { + return function applies(params) { + return !isAllowed(activity, params); + }; +} + +function bidRequestTransmitRules(isAllowed = isActivityAllowed) { + return [ + { + name: ACTIVITY_TRANSMIT_EIDS, + paths: ['userId', 'userIdAsEids'], + applies: appliesWhenActivityDenied(ACTIVITY_TRANSMIT_EIDS, isAllowed), + } + ] +} + +export function ortb2TransmitRules(isAllowed = isActivityAllowed) { + return [ + { + name: ACTIVITY_TRANSMIT_UFPD, + paths: ORTB_UFPD_PATHS, + applies: appliesWhenActivityDenied(ACTIVITY_TRANSMIT_UFPD, isAllowed), + }, + { + name: ACTIVITY_TRANSMIT_EIDS, + paths: ORTB_EIDS_PATHS, + applies: appliesWhenActivityDenied(ACTIVITY_TRANSMIT_EIDS, isAllowed), + }, + { + name: ACTIVITY_TRANSMIT_PRECISE_GEO, + paths: ORTB_GEO_PATHS, + applies: appliesWhenActivityDenied(ACTIVITY_TRANSMIT_PRECISE_GEO, isAllowed), + get(val) { + return Math.round((val + Number.EPSILON) * 100) / 100; + } + } + ]; +} + +export function redactorFactory(isAllowed = isActivityAllowed) { + const redactOrtb2 = objectTransformer(ortb2TransmitRules(isAllowed)); + const redactBidRequest = objectTransformer(bidRequestTransmitRules(isAllowed)); + return function redactor(params) { + const session = {}; + return { + ortb2(obj) { return redactOrtb2(session, obj, params) }, + bidRequest(obj) { return redactBidRequest(session, obj, params) } + } + } +} + +export const redactor = redactorFactory(); diff --git a/src/adapterManager.js b/src/adapterManager.js index 63a902bef71..f785e2c4eeb 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -36,11 +36,11 @@ import * as events from './events.js'; import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; import {auctionManager} from './auctionManager.js'; -import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, MODULE_TYPE_PREBID} from './activities/modules.js'; +import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, MODULE_TYPE_PREBID, MODULE_TYPE_UID} from './activities/modules.js'; import {isActivityAllowed} from './activities/rules.js'; import {ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from './activities/activities.js'; import {ACTIVITY_PARAM_ANL_CONFIG, ACTIVITY_PARAM_S2S_NAME, activityParamsBuilder} from './activities/params.js'; - +import {redactor} from './activities/redactor.js'; const PBS_ADAPTER_NAME = 'pbsBidAdapter'; export const PARTITIONS = { @@ -49,7 +49,8 @@ export const PARTITIONS = { } export const dep = { - isAllowed: isActivityAllowed + isAllowed: isActivityAllowed, + redact: redactor } let adapterManager = {}; @@ -267,9 +268,13 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a const bidderOrtb2 = ortb2Fragments.bidder || {}; function addOrtb2(bidderRequest) { - const fpd = Object.freeze(mergeDeep({}, ortb2, bidderOrtb2[bidderRequest.bidderCode])); + const redact = dep.redact(activityParams(MODULE_TYPE_BIDDER, bidderRequest.bidderCode)); + const fpd = Object.freeze(redact.ortb2(mergeDeep({}, ortb2, bidderOrtb2[bidderRequest.bidderCode]))); bidderRequest.ortb2 = fpd; - bidderRequest.bids.forEach((bid) => bid.ortb2 = fpd); + bidderRequest.bids = bidderRequest.bids.map((bid) => { + bid.ortb2 = fpd; + return redact.bidRequest(bid); + }) return bidderRequest; } diff --git a/test/spec/activities/objectGuard_spec.js b/test/spec/activities/objectGuard_spec.js new file mode 100644 index 00000000000..d8dba407631 --- /dev/null +++ b/test/spec/activities/objectGuard_spec.js @@ -0,0 +1,59 @@ +import {guardObject, objectGuard} from '../../../src/activities/objectGuard.js'; + +describe('objectGuard', () => { + describe('read rule', () => { + let rule, allow; + beforeEach(() => { + allow = false; + rule = { + read(getVal) { if (allow) return getVal() } + } + }) + it('can prevent top level read access', () => { + const obj = objectGuard({'foo': rule})({'foo': 1, 'bar': 2}); + expect(obj).to.eql({ + foo: undefined, + bar: 2 + }); + allow = true; + expect(obj).to.eql({ + foo: 1, + bar: 2 + }) + }); + + it('does not choke if a guarded property is missing', () => { + const obj = objectGuard({foo: rule})({}); + expect(obj.foo).to.not.exist; + allow = true; + expect(obj.foo).to.not.exist; + }); + + it('can prevent nested property access', () => { + const obj = objectGuard({'outer.inner.foo': rule})({ + foo: 0, + outer: { + foo: 1, + inner: { + foo: 2 + }, + bar: { + foo: 3 + } + } + }); + expect(obj).to.eql({ + foo: 0, + outer: { + foo: 1, + inner: { + foo: undefined, + }, + bar: { + foo: 3 + } + } + }) + }) + }); +}); diff --git a/test/spec/activities/redactor_spec.js b/test/spec/activities/redactor_spec.js new file mode 100644 index 00000000000..adf8d35dbfd --- /dev/null +++ b/test/spec/activities/redactor_spec.js @@ -0,0 +1,274 @@ +import { + objectTransformer, + ORTB_EIDS_PATHS, ORTB_GEO_PATHS, + ORTB_UFPD_PATHS, + redactorFactory +} from '../../../src/activities/redactor.js'; +import {ACTIVITY_PARAM_COMPONENT_NAME, ACTIVITY_PARAM_COMPONENT_TYPE} from '../../../src/activities/params.js'; +import { + ACTIVITY_TRANSMIT_EIDS, + ACTIVITY_TRANSMIT_PRECISE_GEO, + ACTIVITY_TRANSMIT_UFPD +} from '../../../src/activities/activities.js'; +import {deepAccess, deepSetValue} from '../../../src/utils.js'; +import {activityParams} from '../../../src/activities/activityParams.js'; + +describe('objectTransformer', () => { + Object.entries({ + replacement: { + get(path, val) { + return `repl${val}` + }, + expectation(parent, prop, val) { + sinon.assert.match(parent, { + [prop]: val + }) + } + }, + removal: { + get(path, val) {}, + expectation(parent, prop, val) { + expect(Object.keys(parent)).to.not.include.members([prop]); + } + } + }).forEach(([t, {get, expectation}]) => { + describe(`property ${t}`, () => { + it('should work on top level properties', () => { + const actual = objectTransformer([ + { + name: 'test', + get, + paths: ['foo'], + applies() { return true } + } + ])({}, {foo: 1, bar: 2}); + sinon.assert.match(actual, { + bar: 2 + }); + expectation(actual, 'foo', get(1)); + }); + it('should work on nested properties', () => { + const actual = objectTransformer([ + { + name: 'test', + get, + paths: ['outer.inner.foo'], + applies() { return true; } + } + ])({}, {outer: {inner: {foo: 'bar'}, baz: 0}}); + sinon.assert.match(actual, { + outer: { + baz: 0 + } + }); + expectation(actual.outer.inner, 'foo', get('bar')) + }) + }); + }); + + describe('should not run rule if property is', () => { + Object.entries({ + 'missing': {}, + 'empty array': {foo: []}, + 'empty object': {foo: {}}, + 'null': {foo: null}, + 'undefined': {foo: undefined} + }).forEach(([t, obj]) => { + it(t, () => { + const get = sinon.stub(); + const applies = sinon.stub() + objectTransformer([{ + name: 'test', + paths: ['foo'], + applies, + get, + }])({}, obj); + expect(get.called).to.be.false; + expect(applies.called).to.be.false; + }) + }) + }); + + describe('should run rule on falsy, but non-empty, value', () => { + Object.entries({ + zero: 0, + false: false + }).forEach(([t, val]) => { + it(t, () => { + const actual = objectTransformer([{ + name: 'test', + paths: ['foo'], + applies() { return true }, + get(val) { return 'repl' }, + }])({}, {foo: val}); + expect(actual).to.eql({foo: 'repl'}); + }) + }) + }); + + it('should pass arguments to applies', () => { + const applies = sinon.stub(); + const transform = objectTransformer([ + { + name: 'test', + paths: ['foo'], + applies, + get() {} + }, + ]); + const arg1 = {n: 1}; + const arg2 = {n: 2}; + transform({}, {foo: 'bar'}, arg1, arg2); + sinon.assert.calledWith(applies, arg1, arg2); + }); + + describe('when multiple paths match for the same rule', () => { + it('should run applies only once', () => { + const applies = sinon.stub().callsFake(() => true); + const actual = objectTransformer([ + { + name: 'test', + paths: ['foo.bar', 'foo.baz'], + applies, + get(val) { return `repl${val}` } + } + ])({}, { + foo: { + bar: 1, + baz: 2 + } + }); + expect(actual).to.eql({ + foo: { + bar: 'repl1', + baz: 'repl2' + } + }); + expect(applies.callCount).to.equal(1); + }) + }); + + it('should not run applies twice for the same name/session combination', () => { + const applies = sinon.stub().callsFake(() => true); + const notApplies = sinon.stub().callsFake(() => false); + const t1 = objectTransformer([ + { + name: 'applies', + paths: ['foo'], + applies, + get(val) { return `repl_r1_${val}`; }, + }, + { + name: 'notApplies', + paths: ['notFoo'], + applies: notApplies, + } + ]); + const t2 = objectTransformer([ + { + name: 'applies', + paths: ['bar'], + applies, + get(val) { return `repl_r2_${val}` } + }, + { + name: 'notApplies', + paths: ['notBar'], + applies: notApplies, + } + ]); + const obj = { + foo: '1', + notFoo: '2', + bar: '3', + notBar: '4' + } + const session = {}; + t1(session, obj); + t2(session, obj); + expect(obj).to.eql({ + foo: 'repl_r1_1', + notFoo: '2', + bar: 'repl_r2_3', + notBar: '4' + }); + expect(applies.callCount).to.equal(1); + expect(notApplies.callCount).to.equal(1); + }) +}); + +describe('redactor', () => { + const MODULE_TYPE = 'mockType'; + const MODULE_NAME = 'mockModule'; + + let isAllowed, redactor; + + beforeEach(() => { + isAllowed = sinon.stub(); + redactor = redactorFactory((activity, params) => { + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE && params[ACTIVITY_PARAM_COMPONENT_NAME] === MODULE_NAME) { + return isAllowed(activity) + } else { + throw new Error('wrong component') + } + })(activityParams(MODULE_TYPE, MODULE_NAME)); + }); + + function testAllowDeny(activity, fn) { + Object.entries({ + allowed: true, + denied: false + }).forEach(([t, allowed]) => { + describe(`when '${activity}' is ${t}`, () => { + beforeEach(() => { + isAllowed.callsFake((act) => { + if (act === activity) { + return allowed; + } else { + throw new Error('wrong activity'); + } + }); + }); + fn(allowed); + }); + }); + } + + function testPropertiesAreRemoved(method, properties, allowed) { + properties.forEach(prop => { + it(`should ${allowed ? 'NOT ' : ''}remove ${prop}`, () => { + const obj = {}; + deepSetValue(obj, prop, 'mockVal'); + method()(obj); + expect(deepAccess(obj, prop)).to.eql(allowed ? 'mockVal' : undefined); + }) + }) + } + + describe('.bidRequest', () => { + testAllowDeny(ACTIVITY_TRANSMIT_EIDS, (allowed) => { + testPropertiesAreRemoved(() => redactor.bidRequest, ['userId', 'userIdAsEids'], allowed); + }); + }); + + describe('.ortb2', () => { + testAllowDeny(ACTIVITY_TRANSMIT_EIDS, (allowed) => { + testPropertiesAreRemoved(() => redactor.ortb2, ORTB_EIDS_PATHS, allowed) + }); + + testAllowDeny(ACTIVITY_TRANSMIT_UFPD, (allowed) => { + testPropertiesAreRemoved(() => redactor.ortb2, ORTB_UFPD_PATHS, allowed) + }); + + testAllowDeny(ACTIVITY_TRANSMIT_PRECISE_GEO, (allowed) => { + ORTB_GEO_PATHS.forEach(path => { + it(`should ${allowed ? 'NOT ' : ''} round down ${path}`, () => { + const ortb2 = {}; + deepSetValue(ortb2, path, 1.2345); + redactor.ortb2(ortb2); + expect(deepAccess(ortb2, path)).to.eql(allowed ? 1.2345 : 1.23); + }) + }) + }) + }); +}) diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index ac9a760863e..c60293bbf3f 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -24,7 +24,6 @@ import {auctionManager} from '../../../../src/auctionManager.js'; import {GDPR_GVLIDS} from '../../../../src/consentHandler.js'; import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER} from '../../../../src/activities/modules.js'; import {ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from '../../../../src/activities/activities.js'; -import {sandbox} from 'sinon'; var events = require('../../../../src/events'); const CONFIG = { @@ -1724,14 +1723,23 @@ describe('adapterManager tests', function () { }); describe('and activity controls', () => { + let redactOrtb2; + let redactBidRequest; const MOCK_BIDDERS = ['1', '2', '3', '4', '5'].map((n) => `mockBidder${n}`); beforeEach(() => { sinon.stub(dep, 'isAllowed'); + redactOrtb2 = sinon.stub().callsFake(ob => ob); + redactBidRequest = sinon.stub().callsFake(ob => ob); + sinon.stub(dep, 'redact').callsFake(() => ({ + ortb2: redactOrtb2, + bidRequest: redactBidRequest + })) MOCK_BIDDERS.forEach((bidder) => adapterManager.bidderRegistry[bidder] = {}); }); afterEach(() => { dep.isAllowed.restore(); + dep.redact.restore(); MOCK_BIDDERS.forEach(bidder => { delete adapterManager.bidderRegistry[bidder] }); config.resetConfig(); }) @@ -1746,17 +1754,33 @@ describe('adapterManager tests', function () { componentType === MODULE_TYPE_BIDDER && allowed.includes(componentName); }); - let bidRequests = adapterManager.makeBidRequests( + let reqs = adapterManager.makeBidRequests( adUnits, Date.now(), utils.getUniqueIdentifierStr(), function callback() {}, [] ); - const bidders = Array.from(new Set(bidRequests.flatMap(br => br.bids).map(bid => bid.bidder)).keys()); + const bidders = Array.from(new Set(reqs.flatMap(br => br.bids).map(bid => bid.bidder)).keys()); expect(bidders).to.have.members(allowed); }); + it('should redact ortb2 and bid request objects', () => { + dep.isAllowed.callsFake(() => true); + adUnits = [ + {code: 'one', bids: [{bidder: 'mockBidder1'}]} + ]; + let reqs = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + sinon.assert.calledWith(redactBidRequest, reqs[0].bids[0]); + sinon.assert.calledWith(redactOrtb2, reqs[0].ortb2); + }) + describe('with multiple s2s configs', () => { beforeEach(() => { config.setConfig({ From 305f62356e3f32ccd85030cf9f6225ea3f546004 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 12 Apr 2023 10:45:14 -0700 Subject: [PATCH 30/37] Object transformers and guards --- libraries/objectGuard/objectGuard.js | 112 ++++++-- libraries/objectGuard/ortbGuard.js | 69 +++++ src/activities/redactor.js | 122 +++++--- test/spec/activities/objectGuard_spec.js | 114 ++++++-- test/spec/activities/ortb2Guard_spec.js | 140 +++++++++ test/spec/activities/redactor_spec.js | 346 ++++++++++++----------- 6 files changed, 650 insertions(+), 253 deletions(-) create mode 100644 libraries/objectGuard/ortbGuard.js create mode 100644 test/spec/activities/ortb2Guard_spec.js diff --git a/libraries/objectGuard/objectGuard.js b/libraries/objectGuard/objectGuard.js index 9e9f9094b73..b3814f14890 100644 --- a/libraries/objectGuard/objectGuard.js +++ b/libraries/objectGuard/objectGuard.js @@ -1,37 +1,95 @@ +import {isData, objectTransformer} from '../../src/activities/redactor.js'; +import {deepAccess, deepClone, deepEqual, deepSetValue} from '../../src/utils.js'; -export function objectGuard(accessRules) { - const rules = {}; - Object.entries(accessRules).forEach(([path, rule]) => { - let node = rules; - path.split('.').forEach(el => { - (node.children = node.children || {})[el] = {}; - node = node.children[el]; - }) - node.rule = rule; - }) - - function mkGuard(obj, rules) { +/** + * @typedef {Object} Guard + * @property {{}} obj a view on the guarded object where reads are passed through redaction rules + * @property {function(): void} verify a method that, when called, verifies that no disallowed writes were done; + * and undoes them if they were. + */ + +/** + * @param {Array[TransformationRule]} rules + * @return {function(*, ...[*]): Guard} + */ +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 getVal = () => Reflect.get(target, prop, receiver); - if (rules.hasOwnProperty(prop)) { - const {children, rule} = rules[prop] - if (children) { - return mkGuard(getVal(), children); - } else if (rule?.read) { - return rule.read(getVal); + 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 getVal(); + return val; }, - set(target, prop, val, receiver) { - const setVal = Reflect.set(target, prop, val, receiver); - - } - }) + }); } - return function guard(obj) { - return mkGuard(obj, rules.children); + 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)) + } + }; +} + +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 && 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) } diff --git a/libraries/objectGuard/ortbGuard.js b/libraries/objectGuard/ortbGuard.js new file mode 100644 index 00000000000..a4bcc36859f --- /dev/null +++ b/libraries/objectGuard/ortbGuard.js @@ -0,0 +1,69 @@ +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))); +} + +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()); + } + } + } +} + +export const guardOrtb2Fragments = ortb2FragmentsGuardFactory(); diff --git a/src/activities/redactor.js b/src/activities/redactor.js index 8e9ae0bcf16..c0e97cb0887 100644 --- a/src/activities/redactor.js +++ b/src/activities/redactor.js @@ -3,61 +3,85 @@ import {isActivityAllowed} from './rules.js'; import {ACTIVITY_TRANSMIT_EIDS, ACTIVITY_TRANSMIT_PRECISE_GEO, ACTIVITY_TRANSMIT_UFPD} from './activities.js'; export const ORTB_UFPD_PATHS = ['user.data', 'user.ext.data']; -export const ORTB_EIDS_PATHS = ['user.eids']; +export const ORTB_EIDS_PATHS = ['user.eids', 'user.ext.eids']; export const ORTB_GEO_PATHS = ['user.geo.lat', 'user.geo.lon', 'device.geo.lat', 'device.geo.lon']; /** - * Factory for transformation functions that apply the given rules to an object. - * - * @typedef {Object} TransformationRule + * @typedef TransformationRuleDef + * @property {name} * @property {function(*): boolean} applies a predicate that should return true if this rule applies * (and the transformation defined herein should be applied). The arguments are those passed to the transformation function. - * @property {name} a name for the rule; used to debounce calls to `applies`: - * if a rule with the same name was already found to apply (or not), this one will (or won't) as well. - * @property {Array[string]} paths dot-separated list of paths that this rule applies to. If the input object - * contains any of these paths, `applies` will be invoked; and if it returns true, properties at these paths - * will be replaced by running through `get`. - * @property {function(*): *} get? a transformation function. Takes a single value, and returns a replacement - which will - * be run against all the values in `paths`, if `applies` returns true. By default, this returns undefined, and removes - * any values under `paths`. + * @property {name} a name for the rule; used to debounce calls to `applies` (and avoid excessive logging): + * if a rule with the same name was already found to apply (or not), this one will (or won't) as well. + * @property {Array[string]} paths dot-separated list of paths that this rule applies to.* + */ + +/** + * @typedef RedactRuleDef + * @property {function(*): *} get? substitution functions for values that should be redacted; + * takes in the original (unredacted) value as an input, and returns a substitute to use in the redacted + * version. If it returns undefined, or this option is omitted, protected paths will be removed + * from the redacted object. + */ + +/** + * @param {RedactRuleDef} ruleDef + * @return {TransformationRule} + */ +export function redactRule(ruleDef) { + return Object.assign({ + get() {}, + run(root, path, object, property, applies) { + const val = object && object[property]; + if (isData(val) && applies()) { + const repl = this.get(val); + if (repl === undefined) { + delete object[property]; + } else { + object[property] = repl; + } + } + } + }, ruleDef) +} + +/** + * @typedef TransformationRule + * @extends TransformationRuleDef + * @property {function} run rule logic - see `redactRule` for an example. + */ + +/** + * Factory for transformation functions that apply the given rules to an object. + * * * @param {Array[TransformationRule]} rules - * @return {function(*): *} + * @return {function({}, {}, ...[*]): *} */ export function objectTransformer(rules) { rules.forEach(rule => { rule.paths = rule.paths.map((path) => { - path = path.split('.'); - const tail = path.pop(); - return [path.length > 0 ? path.join('.') : null, tail]; - }); - rule.get = rule.get || function () {} - }); - return function transformer(session, obj, ...args) { - rules.forEach(({name, get, paths, applies}) => { - if (session.hasOwnProperty(name) && !session[name]) { - return; - } - for (const [head, tail] of paths) { + const parts = path.split('.'); + const tail = parts.pop(); + return [parts.length > 0 ? parts.join('.') : null, tail] + }) + }) + return function applyTransform(session, obj, ...args) { + const result = []; + rules.forEach(rule => { + if (session[rule.name] === false) return; + for (const [head, tail] of rule.paths) { const parent = head == null ? obj : deepAccess(obj, head); - if (parent) { - const val = parent[tail]; - if (isData(val)) { - if (!session.hasOwnProperty(name)) { - session[name] = applies(...args); - if (!session[name]) break; - } - const repl = get(val); - if (repl === undefined) { - delete parent[tail]; - } else { - parent[tail] = repl; - } + result.push(rule.run(obj, head, parent, tail, () => { + if (!session.hasOwnProperty(rule.name)) { + session[rule.name] = !!rule.applies(...args); } - } + return session[rule.name] + })) + if (session[rule.name] === false) return; } }) - return obj; + return result.filter(el => el != null); } } @@ -65,7 +89,7 @@ export function isData(val) { return val != null && (typeof val !== 'object' || Object.keys(val).length > 0) } -function appliesWhenActivityDenied(activity, isAllowed = isActivityAllowed) { +export function appliesWhenActivityDenied(activity, isAllowed = isActivityAllowed) { return function applies(params) { return !isAllowed(activity, params); }; @@ -78,7 +102,7 @@ function bidRequestTransmitRules(isAllowed = isActivityAllowed) { paths: ['userId', 'userIdAsEids'], applies: appliesWhenActivityDenied(ACTIVITY_TRANSMIT_EIDS, isAllowed), } - ] + ].map(redactRule) } export function ortb2TransmitRules(isAllowed = isActivityAllowed) { @@ -101,7 +125,7 @@ export function ortb2TransmitRules(isAllowed = isActivityAllowed) { return Math.round((val + Number.EPSILON) * 100) / 100; } } - ]; + ].map(redactRule); } export function redactorFactory(isAllowed = isActivityAllowed) { @@ -110,10 +134,18 @@ export function redactorFactory(isAllowed = isActivityAllowed) { return function redactor(params) { const session = {}; return { - ortb2(obj) { return redactOrtb2(session, obj, params) }, - bidRequest(obj) { return redactBidRequest(session, obj, params) } + ortb2(obj) { redactOrtb2(session, obj, params); return obj }, + bidRequest(obj) { redactBidRequest(session, obj, params); return obj } } } } +/** + * Returns an object that can redact other privacy-sensitive objects according + * to activity rules. + * + * @param {{}} params activity parameters to use for activity checks + * @return {{ortb2: function({}): {}, bidRequest: function({}): {}}} a collection of methods + * that can redact disallowed data from ORTB2 and/or bid request objects. + */ export const redactor = redactorFactory(); diff --git a/test/spec/activities/objectGuard_spec.js b/test/spec/activities/objectGuard_spec.js index d8dba407631..4cb500bb302 100644 --- a/test/spec/activities/objectGuard_spec.js +++ b/test/spec/activities/objectGuard_spec.js @@ -1,37 +1,41 @@ -import {guardObject, objectGuard} from '../../../src/activities/objectGuard.js'; +import {objectGuard, writeProtectRule} from '../../../libraries/objectGuard/objectGuard.js'; describe('objectGuard', () => { describe('read rule', () => { - let rule, allow; + let rule, applies; beforeEach(() => { - allow = false; + applies = true; rule = { - read(getVal) { if (allow) return getVal() } + paths: ['foo', 'outer.inner.foo'], + name: 'testRule', + applies: sinon.stub().callsFake(() => applies), + get(val) { return `repl${val}` }, } }) it('can prevent top level read access', () => { - const obj = objectGuard({'foo': rule})({'foo': 1, 'bar': 2}); + const {obj} = objectGuard([rule])({'foo': 1, 'other': 2}); expect(obj).to.eql({ - foo: undefined, - bar: 2 + foo: 'repl1', + other: 2 }); - allow = true; - expect(obj).to.eql({ - foo: 1, - bar: 2 - }) }); it('does not choke if a guarded property is missing', () => { - const obj = objectGuard({foo: rule})({}); - expect(obj.foo).to.not.exist; - allow = true; + const {obj} = objectGuard([rule])({}); expect(obj.foo).to.not.exist; }); + it('does not prevent access if applies returns false', () => { + applies = false; + const {obj} = objectGuard([rule])({foo: 1}); + expect(obj).to.eql({ + foo: 1 + }); + }) + it('can prevent nested property access', () => { - const obj = objectGuard({'outer.inner.foo': rule})({ - foo: 0, + const {obj} = objectGuard([rule])({ + other: 0, outer: { foo: 1, inner: { @@ -43,17 +47,89 @@ describe('objectGuard', () => { } }); expect(obj).to.eql({ - foo: 0, + other: 0, outer: { foo: 1, inner: { - foo: undefined, + foo: 'repl2', }, bar: { foo: 3 } } }) + }); + + it('does not call applies more than once', () => { + JSON.stringify(objectGuard([rule])({ + foo: 0, + outer: { + inner: { + foo: 1 + } + } + }).obj); + expect(rule.applies.callCount).to.equal(1); + }) + }); + + describe('write protection', () => { + let applies, rule; + + beforeEach(() => { + applies = true; + rule = writeProtectRule({ + paths: ['foo', 'bar', 'outer.inner.foo', 'outer.inner.bar'], + applies: sinon.stub().callsFake(() => applies) + }); + }); + + it('should undo top-level writes', () => { + const obj = {bar: {nested: 'val'}, other: 'val'}; + const guard = objectGuard([rule])(obj); + guard.obj.foo = 'denied'; + guard.obj.bar.nested = 'denied'; + guard.obj.bar.other = 'denied'; + guard.obj.other = 'allowed'; + guard.verify(); + expect(obj).to.eql({bar: {nested: 'val'}, other: 'allowed'}); + }); + + it('should undo top-level deletes', () => { + const obj = {foo: {nested: 'val'}, bar: 'val'}; + const guard = objectGuard([rule])(obj); + delete guard.obj.foo.nested; + delete guard.obj.bar; + guard.verify(); + expect(obj).to.eql({foo: {nested: 'val'}, bar: 'val'}); + }) + + it('should undo nested writes', () => { + const obj = {outer: {inner: {bar: {nested: 'val'}, other: 'val'}}}; + const guard = objectGuard([rule])(obj); + guard.obj.outer.inner.bar.other = 'denied'; + guard.obj.outer.inner.bar.nested = 'denied'; + guard.obj.outer.inner.foo = 'denied'; + guard.obj.outer.inner.other = 'allowed'; + guard.verify(); + expect(obj).to.eql({ + outer: { + inner: { + bar: { + nested: 'val' + }, + other: 'allowed' + } + } + }) + }) + it('should undo nested deletes', () => { + const obj = {outer: {inner: {foo: {nested: 'val'}, bar: 'val'}}}; + const guard = objectGuard([rule])(obj); + delete guard.obj.outer.inner.foo.nested; + delete guard.obj.outer.inner.bar; + guard.verify(); + expect(obj).to.eql({outer: {inner: {foo: {nested: 'val'}, bar: 'val'}}}) }) }); }); diff --git a/test/spec/activities/ortb2Guard_spec.js b/test/spec/activities/ortb2Guard_spec.js new file mode 100644 index 00000000000..828cbe4e328 --- /dev/null +++ b/test/spec/activities/ortb2Guard_spec.js @@ -0,0 +1,140 @@ +import {ortb2FragmentsGuardFactory, ortb2GuardFactory} from '../../../libraries/objectGuard/ortbGuard.js'; +import {ACTIVITY_PARAM_COMPONENT_NAME, ACTIVITY_PARAM_COMPONENT_TYPE} from '../../../src/activities/params.js'; +import { + ACTIVITY_ENRICH_EIDS, ACTIVITY_ENRICH_UFPD, + ACTIVITY_TRANSMIT_EIDS, + ACTIVITY_TRANSMIT_UFPD +} from '../../../src/activities/activities.js'; +import {activityParams} from '../../../src/activities/activityParams.js'; +import {deepAccess, deepClone, deepSetValue, mergeDeep} from '../../../src/utils.js'; +import {ORTB_EIDS_PATHS, ORTB_UFPD_PATHS} from '../../../src/activities/redactor.js'; +import {objectGuard, writeProtectRule} from '../../../libraries/objectGuard/objectGuard.js'; + +describe('ortb2Guard', () => { + const MOD_TYPE = 'test'; + const MOD_NAME = 'mock'; + let isAllowed, ortb2Guard; + beforeEach(() => { + isAllowed = sinon.stub(); + ortb2Guard = ortb2GuardFactory(function (activity, params) { + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MOD_TYPE && params[ACTIVITY_PARAM_COMPONENT_NAME] === MOD_NAME) { + return isAllowed(activity) + } else { + throw new Error('wrong component') + } + }) + }); + + function testAllowDeny(transmitActivity, enrichActivity, fn) { + Object.entries({ + allowed: true, + denied: false + }).forEach(([t, allowed]) => { + describe(`when '${enrichActivity}' is ${t}`, () => { + beforeEach(() => { + isAllowed.callsFake((activity) => { + if (activity === transmitActivity) return true; + if (activity === enrichActivity) return allowed; + throw new Error('wrong activity'); + }) + }); + fn(allowed); + }) + }) + } + + function testPropertiesAreProtected(properties, allowed) { + properties.forEach(prop => { + it(`should ${allowed ? 'keep' : 'undo'} additions to ${prop}`, () => { + const orig = [{n: 'orig'}]; + const ortb2 = {}; + deepSetValue(ortb2, prop, deepClone(orig)); + const guard = ortb2Guard(ortb2, activityParams(MOD_TYPE, MOD_NAME)); + const mod = {}; + const insert = [{n: 'new'}]; + deepSetValue(mod, prop, insert); + mergeDeep(guard.obj, mod); + guard.verify(); + const actual = deepAccess(ortb2, prop); + if (allowed) { + expect(actual).to.eql(orig.concat(insert)) + } else { + expect(actual).to.eql(orig); + } + }); + + it(`should ${allowed ? 'keep' : 'undo'} modifications to ${prop}`, () => { + const orig = [{n: 'orig'}]; + const ortb2 = {}; + deepSetValue(ortb2, prop, orig); + const guard = ortb2Guard(ortb2, activityParams(MOD_TYPE, MOD_NAME)); + deepSetValue(guard.obj, `${prop}.0.n`, 'new'); + guard.verify(); + const actual = deepAccess(ortb2, prop); + if (allowed) { + expect(actual).to.eql([{n: 'new'}]); + } else { + expect(actual).to.eql([{n: 'orig'}]); + } + }); + }) + } + + testAllowDeny(ACTIVITY_TRANSMIT_EIDS, ACTIVITY_ENRICH_EIDS, (allowed) => { + testPropertiesAreProtected(ORTB_EIDS_PATHS, allowed); + }); + + testAllowDeny(ACTIVITY_TRANSMIT_UFPD, ACTIVITY_ENRICH_UFPD, (allowed) => { + testPropertiesAreProtected(ORTB_UFPD_PATHS, allowed); + }); +}); + +describe('ortb2FragmentsGuard', () => { + let guardFragments + beforeEach(() => { + const testGuard = objectGuard([ + writeProtectRule({ + paths: ['foo'], + applies: () => true, + name: 'testRule' + }) + ]) + guardFragments = ortb2FragmentsGuardFactory(testGuard); + }); + + it('should undo changes to global FPD', () => { + const fragments = { + global: { + foo: {inner: 'val'} + } + } + const guard = guardFragments(fragments); + guard.obj.global.foo = 'other'; + guard.verify(); + expect(fragments.global.foo).to.eql({inner: 'val'}); + }); + + it('should undo changes to bidder FPD', () => { + const fragments = { + bidder: { + A: { + foo: 'val' + } + } + }; + const guard = guardFragments(fragments); + guard.obj.bidder.A.foo = 'denied'; + guard.verify(); + expect(fragments.bidder.A).to.eql({foo: 'val'}); + }); + + it('should undo changes to bidder FPD that was not initially there', () => { + const fragments = { + bidder: {} + }; + const guard = guardFragments(fragments); + guard.obj.bidder.A = {foo: 'denied', other: 'allowed'}; + guard.verify(); + expect(fragments.bidder.A).to.eql({other: 'allowed'}); + }); +}) diff --git a/test/spec/activities/redactor_spec.js b/test/spec/activities/redactor_spec.js index adf8d35dbfd..5cfa1cfc643 100644 --- a/test/spec/activities/redactor_spec.js +++ b/test/spec/activities/redactor_spec.js @@ -2,7 +2,7 @@ import { objectTransformer, ORTB_EIDS_PATHS, ORTB_GEO_PATHS, ORTB_UFPD_PATHS, - redactorFactory + redactorFactory, redactRule } from '../../../src/activities/redactor.js'; import {ACTIVITY_PARAM_COMPONENT_NAME, ACTIVITY_PARAM_COMPONENT_TYPE} from '../../../src/activities/params.js'; import { @@ -14,187 +14,209 @@ import {deepAccess, deepSetValue} from '../../../src/utils.js'; import {activityParams} from '../../../src/activities/activityParams.js'; describe('objectTransformer', () => { - Object.entries({ - replacement: { - get(path, val) { - return `repl${val}` - }, - expectation(parent, prop, val) { - sinon.assert.match(parent, { - [prop]: val - }) - } - }, - removal: { - get(path, val) {}, - expectation(parent, prop, val) { - expect(Object.keys(parent)).to.not.include.members([prop]); + describe('using dummy rules', () => { + let rule, applies, run; + beforeEach(() => { + run = sinon.stub(); + applies = sinon.stub().callsFake(() => true) + rule = { + name: 'mockRule', + paths: ['foo', 'bar.baz'], + applies, + run, } - } - }).forEach(([t, {get, expectation}]) => { - describe(`property ${t}`, () => { - it('should work on top level properties', () => { - const actual = objectTransformer([ - { - name: 'test', - get, - paths: ['foo'], - applies() { return true } - } - ])({}, {foo: 1, bar: 2}); - sinon.assert.match(actual, { - bar: 2 - }); - expectation(actual, 'foo', get(1)); + }); + + it('runs rule for each path', () => { + const obj = {foo: 'val'}; + objectTransformer([rule])({}, obj); + sinon.assert.calledWith(run, obj, null, obj, 'foo'); + sinon.assert.calledWith(run, obj, 'bar', undefined, 'baz'); + }); + + it('does not run rule once it is known that it does not apply', () => { + applies.reset(); + applies.callsFake(() => false); + run.callsFake((_1, _2, _3, _4, applies) => applies()); + objectTransformer([rule])({}, {}); + expect(applies.callCount).to.equal(1); + expect(run.callCount).to.equal(1); + }); + + it('does not call apply more than once', () => { + run.callsFake((_1, _2, _3, _4, applies) => { + applies(); + applies(); }); - it('should work on nested properties', () => { - const actual = objectTransformer([ - { - name: 'test', - get, - paths: ['outer.inner.foo'], - applies() { return true; } - } - ])({}, {outer: {inner: {foo: 'bar'}, baz: 0}}); - sinon.assert.match(actual, { - outer: { - baz: 0 - } - }); - expectation(actual.outer.inner, 'foo', get('bar')) - }) + objectTransformer([rule])({}, {}); + expect(applies.callCount).to.equal(1); }); - }); - describe('should not run rule if property is', () => { - Object.entries({ - 'missing': {}, - 'empty array': {foo: []}, - 'empty object': {foo: {}}, - 'null': {foo: null}, - 'undefined': {foo: undefined} - }).forEach(([t, obj]) => { - it(t, () => { - const get = sinon.stub(); - const applies = sinon.stub() - objectTransformer([{ - name: 'test', - paths: ['foo'], - applies, - get, - }])({}, obj); - expect(get.called).to.be.false; - expect(applies.called).to.be.false; - }) + it('does not call apply if session already contains a result for the rule', () => { + objectTransformer([rule])({[rule.name]: false}, {}); + expect(applies.callCount).to.equal(0); + expect(run.callCount).to.equal(0); }) - }); - describe('should run rule on falsy, but non-empty, value', () => { - Object.entries({ - zero: 0, - false: false - }).forEach(([t, val]) => { - it(t, () => { - const actual = objectTransformer([{ - name: 'test', - paths: ['foo'], - applies() { return true }, - get(val) { return 'repl' }, - }])({}, {foo: val}); - expect(actual).to.eql({foo: 'repl'}); - }) + it('passes arguments to applies', () => { + run.callsFake((_1, _2, _3, _4, applies) => applies()); + const arg1 = {n: 0}; + const arg2 = {n: 1}; + objectTransformer([rule])({}, {}, arg1, arg2); + sinon.assert.calledWith(applies, arg1, arg2); + }); + + it('collects rule results', () => { + let i = 0; + run.callsFake(() => i++); + const result = objectTransformer([rule])({}, {}); + expect(result).to.eql([0, 1]); }) }); - it('should pass arguments to applies', () => { - const applies = sinon.stub(); - const transform = objectTransformer([ - { - name: 'test', - paths: ['foo'], - applies, - get() {} + describe('using redact rules', () => { + Object.entries({ + replacement: { + get(path, val) { + return `repl${val}` + }, + expectation(parent, prop, val) { + sinon.assert.match(parent, { + [prop]: val + }) + } }, - ]); - const arg1 = {n: 1}; - const arg2 = {n: 2}; - transform({}, {foo: 'bar'}, arg1, arg2); - sinon.assert.calledWith(applies, arg1, arg2); - }); + removal: { + get(path, val) {}, + expectation(parent, prop, val) { + expect(Object.keys(parent)).to.not.include.members([prop]); + } + } + }).forEach(([t, {get, expectation}]) => { + describe(`property ${t}`, () => { + it('should work on top level properties', () => { + const obj = {foo: 1, bar: 2}; + objectTransformer([ + redactRule({ + name: 'test', + get, + paths: ['foo'], + applies() { return true } + }) + ])({}, obj); + sinon.assert.match(obj, { + bar: 2 + }); + expectation(obj, 'foo', get(1)); + }); + it('should work on nested properties', () => { + const obj = {outer: {inner: {foo: 'bar'}, baz: 0}}; + objectTransformer([ + redactRule({ + name: 'test', + get, + paths: ['outer.inner.foo'], + applies() { return true; } + }) + ])({}, obj); + sinon.assert.match(obj, { + outer: { + baz: 0 + } + }); + expectation(obj.outer.inner, 'foo', get('bar')) + }) + }); + }); + describe('should not run rule if property is', () => { + Object.entries({ + 'missing': {}, + 'empty array': {foo: []}, + 'empty object': {foo: {}}, + 'null': {foo: null}, + 'undefined': {foo: undefined} + }).forEach(([t, obj]) => { + it(t, () => { + const get = sinon.stub(); + const applies = sinon.stub() + objectTransformer([redactRule({ + name: 'test', + paths: ['foo'], + applies, + get, + })])({}, obj); + expect(get.called).to.be.false; + expect(applies.called).to.be.false; + }) + }) + }); + + describe('should run rule on falsy, but non-empty, value', () => { + Object.entries({ + zero: 0, + false: false + }).forEach(([t, val]) => { + it(t, () => { + const obj = {foo: val}; + objectTransformer([redactRule({ + name: 'test', + paths: ['foo'], + applies() { return true }, + get(val) { return 'repl' }, + })])({}, obj); + expect(obj).to.eql({foo: 'repl'}); + }) + }) + }); - describe('when multiple paths match for the same rule', () => { - it('should run applies only once', () => { + it('should not run applies twice for the same name/session combination', () => { const applies = sinon.stub().callsFake(() => true); - const actual = objectTransformer([ + const notApplies = sinon.stub().callsFake(() => false); + const t1 = objectTransformer([ { - name: 'test', - paths: ['foo.bar', 'foo.baz'], + name: 'applies', + paths: ['foo'], applies, - get(val) { return `repl${val}` } - } - ])({}, { - foo: { - bar: 1, - baz: 2 + get(val) { return `repl_r1_${val}`; }, + }, + { + name: 'notApplies', + paths: ['notFoo'], + applies: notApplies, } - }); - expect(actual).to.eql({ - foo: { - bar: 'repl1', - baz: 'repl2' + ].map(redactRule)); + const t2 = objectTransformer([ + { + name: 'applies', + paths: ['bar'], + applies, + get(val) { return `repl_r2_${val}` } + }, + { + name: 'notApplies', + paths: ['notBar'], + applies: notApplies, } + ].map(redactRule)); + const obj = { + foo: '1', + notFoo: '2', + bar: '3', + notBar: '4' + } + const session = {}; + t1(session, obj); + t2(session, obj); + expect(obj).to.eql({ + foo: 'repl_r1_1', + notFoo: '2', + bar: 'repl_r2_3', + notBar: '4' }); expect(applies.callCount).to.equal(1); + expect(notApplies.callCount).to.equal(1); }) }); - - it('should not run applies twice for the same name/session combination', () => { - const applies = sinon.stub().callsFake(() => true); - const notApplies = sinon.stub().callsFake(() => false); - const t1 = objectTransformer([ - { - name: 'applies', - paths: ['foo'], - applies, - get(val) { return `repl_r1_${val}`; }, - }, - { - name: 'notApplies', - paths: ['notFoo'], - applies: notApplies, - } - ]); - const t2 = objectTransformer([ - { - name: 'applies', - paths: ['bar'], - applies, - get(val) { return `repl_r2_${val}` } - }, - { - name: 'notApplies', - paths: ['notBar'], - applies: notApplies, - } - ]); - const obj = { - foo: '1', - notFoo: '2', - bar: '3', - notBar: '4' - } - const session = {}; - t1(session, obj); - t2(session, obj); - expect(obj).to.eql({ - foo: 'repl_r1_1', - notFoo: '2', - bar: 'repl_r2_3', - notBar: '4' - }); - expect(applies.callCount).to.equal(1); - expect(notApplies.callCount).to.equal(1); - }) }); describe('redactor', () => { From 33d822af7bf4259e1520981b4904d63d80fd70fd Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 12 Apr 2023 11:13:57 -0700 Subject: [PATCH 31/37] transmit* and enrich* enforcement for RTD modules --- modules/rtdModule/index.js | 13 ++++++++++++- test/spec/modules/realTimeDataModule_spec.js | 2 +- test/spec/unit/pbjs_api_spec.js | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 29e2ce3de43..633c4f4cdc1 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -168,6 +168,10 @@ import {find} from '../../src/polyfill.js'; import {timedAuctionHook} from '../../src/utils/perfMetrics.js'; import {GDPR_GVLIDS} from '../../src/consentHandler.js'; import {MODULE_TYPE_RTD} from '../../src/activities/modules.js'; +import {guardOrtb2Fragments} from '../../libraries/objectGuard/ortbGuard.js'; +import {activityParamsBuilder} from '../../src/activities/params.js'; + +const activityParams = activityParamsBuilder((al) => adapterManager.resolveAlias(al)); /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -299,6 +303,7 @@ export const setBidRequestsData = timedAuctionHook('rtd', function setBidRequest let callbacksExpected = prioritySubModules.length; let isDone = false; let waitTimeout; + const verifiers = []; if (!relevantSubModules.length) { return exitHook(); @@ -307,7 +312,12 @@ export const setBidRequestsData = timedAuctionHook('rtd', function setBidRequest waitTimeout = setTimeout(exitHook, shouldDelayAuction ? _moduleConfig.auctionDelay : 0); relevantSubModules.forEach(sm => { - sm.getBidRequestData(reqBidsConfigObj, onGetBidRequestDataCallback.bind(sm), sm.config, _userConsent) + const fpdGuard = guardOrtb2Fragments(reqBidsConfigObj.ortb2Fragments || {}, activityParams(MODULE_TYPE_RTD, sm.name)); + verifiers.push(fpdGuard.verify); + sm.getBidRequestData({ + ...reqBidsConfigObj, + ortb2Fragments: fpdGuard.obj + }, onGetBidRequestDataCallback.bind(sm), sm.config, _userConsent) }); function onGetBidRequestDataCallback() { @@ -328,6 +338,7 @@ export const setBidRequestsData = timedAuctionHook('rtd', function setBidRequest } isDone = true; clearTimeout(waitTimeout); + verifiers.forEach(fn => fn()); fn.call(this, reqBidsConfigObj); } }); diff --git a/test/spec/modules/realTimeDataModule_spec.js b/test/spec/modules/realTimeDataModule_spec.js index f9c41b2fda0..938e2e2f3c1 100644 --- a/test/spec/modules/realTimeDataModule_spec.js +++ b/test/spec/modules/realTimeDataModule_spec.js @@ -128,7 +128,7 @@ describe('Real time module', function () { it('should be able to modify bid request', function (done) { rtdModule.setBidRequestsData(() => { assert(getBidRequestDataSpy.calledTwice); - assert(getBidRequestDataSpy.calledWith({bidRequest: {}})); + assert(getBidRequestDataSpy.calledWith(sinon.match({bidRequest: {}}))); done(); }, {bidRequest: {}}) }); diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index b069b13fc37..a6d8ac651dc 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -25,6 +25,7 @@ import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {createBid} from '../../../src/bidfactory.js'; import {enrichFPD} from '../../../src/fpd/enrichment.js'; import {mockFpdEnrichments} from '../../helpers/fpd.js'; + var assert = require('chai').assert; var expect = require('chai').expect; From 5d5338e85e4e2c55b4fd0facd92387403121768f Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 13 Apr 2023 06:21:11 -0700 Subject: [PATCH 32/37] allowActivities configuration --- src/activities/cfg.js | 65 +++++++++ src/activities/redactor.js | 5 +- src/activities/rules.js | 2 + test/spec/activities/cfg_spec.js | 127 ++++++++++++++++++ .../{ortb2Guard_spec.js => ortbGuard_spec.js} | 0 5 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 src/activities/cfg.js create mode 100644 test/spec/activities/cfg_spec.js rename test/spec/activities/{ortb2Guard_spec.js => ortbGuard_spec.js} (100%) diff --git a/src/activities/cfg.js b/src/activities/cfg.js new file mode 100644 index 00000000000..6e7738213f0 --- /dev/null +++ b/src/activities/cfg.js @@ -0,0 +1,65 @@ +import {config} from '../config.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() { + 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 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(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 default is false'} + }, Number.POSITIVE_INFINITY)) + } + } + + config.getConfig(CFG_NAME, (cfg) => { + clearAllRules(); + Object.entries(cfg[CFG_NAME]).forEach(([activity, cfg]) => { + if (cfg.default === false) { + setupDefaultRule(activity); + } + const rules = new Map(); + rulesByActivity.set(activity, rules); + + (cfg.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)); + }); + }) +} diff --git a/src/activities/redactor.js b/src/activities/redactor.js index c0e97cb0887..b5879594f6a 100644 --- a/src/activities/redactor.js +++ b/src/activities/redactor.js @@ -18,6 +18,7 @@ export const ORTB_GEO_PATHS = ['user.geo.lat', 'user.geo.lon', 'device.geo.lat', /** * @typedef RedactRuleDef + * @augments TransformationRuleDef * @property {function(*): *} get? substitution functions for values that should be redacted; * takes in the original (unredacted) value as an input, and returns a substitute to use in the redacted * version. If it returns undefined, or this option is omitted, protected paths will be removed @@ -47,7 +48,7 @@ export function redactRule(ruleDef) { /** * @typedef TransformationRule - * @extends TransformationRuleDef + * @augments TransformationRuleDef * @property {function} run rule logic - see `redactRule` for an example. */ @@ -145,7 +146,7 @@ export function redactorFactory(isAllowed = isActivityAllowed) { * to activity rules. * * @param {{}} params activity parameters to use for activity checks - * @return {{ortb2: function({}): {}, bidRequest: function({}): {}}} a collection of methods + * @return {{ortb2: function({}): {}, bidRequest: function({}): {}}} methods * that can redact disallowed data from ORTB2 and/or bid request objects. */ export const redactor = redactorFactory(); diff --git a/src/activities/rules.js b/src/activities/rules.js index 28dbba2d317..92ea0f1930b 100644 --- a/src/activities/rules.js +++ b/src/activities/rules.js @@ -1,5 +1,6 @@ import {prefixLog} from '../utils.js'; import {ACTIVITY_PARAM_COMPONENT} from './params.js'; +import {updateRulesFromConfig} from './cfg.js'; export function ruleRegistry(logger = prefixLog('Activity control:')) { const registry = {}; @@ -84,3 +85,4 @@ export function ruleRegistry(logger = prefixLog('Activity control:')) { } export const [registerActivityControl, isActivityAllowed] = ruleRegistry(); +updateRulesFromConfig(registerActivityControl); diff --git a/test/spec/activities/cfg_spec.js b/test/spec/activities/cfg_spec.js new file mode 100644 index 00000000000..45b063f1e2d --- /dev/null +++ b/test/spec/activities/cfg_spec.js @@ -0,0 +1,127 @@ +import {config} from 'src/config.js'; +import {ruleRegistry} from '../../../src/activities/rules.js'; +import {updateRulesFromConfig} from '../../../src/activities/cfg.js'; +import {activityParams} from '../../../src/activities/activityParams.js'; + +describe('allowActivities config', () => { + const MODULE_TYPE = 'test' + const MODULE_NAME = 'testMod'; + const ACTIVITY = 'testActivity'; + + let isAllowed, params; + + beforeEach(() => { + let registerRule; + [registerRule, isAllowed] = ruleRegistry(); + updateRulesFromConfig(registerRule); + params = activityParams(MODULE_TYPE, MODULE_NAME) + }); + + afterEach(() => { + config.resetConfig(); + }); + + function setupActivityConfig(cfg) { + config.setConfig({ + allowActivities: { + [ACTIVITY]: cfg + } + }) + } + + describe('default = false', () => { + it('should deny activites with no other rules', () => { + setupActivityConfig({ + default: false + }) + expect(isAllowed(ACTIVITY, {})).to.be.false; + }); + it('should not deny activities that are explicitly allowed', () => { + setupActivityConfig({ + default: false, + rules: [ + { + condition({componentName}) { + return componentName === MODULE_NAME + }, + allow: true + } + ] + }) + expect(isAllowed(ACTIVITY, params)).to.be.true; + }); + it('should be removable by a config update', () => { + setupActivityConfig({ + default: false + }); + setupActivityConfig({}); + expect(isAllowed(ACTIVITY, params)).to.be.true; + }) + }); + + describe('rules', () => { + it('are tested for their condition', () => { + setupActivityConfig({ + rules: [{ + condition({flag}) { return flag }, + allow: false + }] + }); + expect(isAllowed(ACTIVITY, params)).to.be.true; + params.flag = true; + expect(isAllowed(ACTIVITY, params)).to.be.false; + }); + + it('always apply if they have no condition', () => { + setupActivityConfig({ + rules: [{allow: false}] + }); + expect(isAllowed(ACTIVITY, params)).to.be.false; + }); + + it('do not choke when the condition throws', () => { + setupActivityConfig({ + rules: [{ + condition() { + throw new Error() + }, + allow: true + }] + }); + expect(isAllowed(ACTIVITY, params)).to.be.false; + }); + + it('are evaluated in order of priority', () => { + setupActivityConfig({ + rules: [{ + priority: 1000, + allow: false + }, { + priority: 100, + allow: true + }] + }); + expect(isAllowed(ACTIVITY, params)).to.be.true; + }); + + it('can be set with priority 0', () => { + setupActivityConfig({ + rules: [{ + allow: false + }, { + priority: 0, + allow: true + }] + }); + expect(isAllowed(ACTIVITY, params)).to.be.true; + }) + + it('can be reset with a config update', () => { + setupActivityConfig({ + allow: false + }); + config.setConfig({allowActivities: {}}); + expect(isAllowed(ACTIVITY, params)).to.be.true; + }); + }); +}); diff --git a/test/spec/activities/ortb2Guard_spec.js b/test/spec/activities/ortbGuard_spec.js similarity index 100% rename from test/spec/activities/ortb2Guard_spec.js rename to test/spec/activities/ortbGuard_spec.js From 539db587ffdfc1f7d77746c8f6c2f45d2e0c61df Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 13 Apr 2023 06:52:10 -0700 Subject: [PATCH 33/37] improve comments --- libraries/objectGuard/objectGuard.js | 23 ++++++++++++++++++----- libraries/objectGuard/ortbGuard.js | 19 +++++++++++++++++++ src/activities/cfg.js | 3 ++- src/activities/redactor.js | 15 ++++++++++----- src/adapterManager.js | 4 ++-- 5 files changed, 51 insertions(+), 13 deletions(-) diff --git a/libraries/objectGuard/objectGuard.js b/libraries/objectGuard/objectGuard.js index b3814f14890..a3d55f4999b 100644 --- a/libraries/objectGuard/objectGuard.js +++ b/libraries/objectGuard/objectGuard.js @@ -2,15 +2,24 @@ import {isData, objectTransformer} from '../../src/activities/redactor.js'; import {deepAccess, deepClone, deepEqual, deepSetValue} from '../../src/utils.js'; /** - * @typedef {Object} Guard - * @property {{}} obj a view on the guarded object where reads are passed through redaction rules - * @property {function(): void} verify a method that, when called, verifies that no disallowed writes were done; - * and undoes them if they were. + * @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(*, ...[*]): Guard} + * @return {function(*, ...[*]): ObjectGuard} */ export function objectGuard(rules) { const root = {}; @@ -73,6 +82,10 @@ export function objectGuard(rules) { }; } +/** + * @param {TransformationRuleDef} ruleDef + * @return {TransformationRule} + */ export function writeProtectRule(ruleDef) { return Object.assign({ wp: true, diff --git a/libraries/objectGuard/ortbGuard.js b/libraries/objectGuard/ortbGuard.js index a4bcc36859f..7911b378c3d 100644 --- a/libraries/objectGuard/ortbGuard.js +++ b/libraries/objectGuard/ortbGuard.js @@ -28,6 +28,21 @@ 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) { @@ -66,4 +81,8 @@ export function ortb2FragmentsGuardFactory(guardOrtb2 = ortb2Guard) { } } +/** + * Get a guard for an ortb2Fragments object. + * @type {function(*, *): ObjectGuard} + */ export const guardOrtb2Fragments = ortb2FragmentsGuardFactory(); diff --git a/src/activities/cfg.js b/src/activities/cfg.js index 6e7738213f0..00bb682d8da 100644 --- a/src/activities/cfg.js +++ b/src/activities/cfg.js @@ -10,6 +10,7 @@ export function updateRulesFromConfig(registerRule) { const rulesByActivity = new Map(); function clearAllRules() { + rulesByActivity.clear(); Array.from(activeRuleHandles.values()) .flatMap(ruleset => Array.from(ruleset.values())) .forEach(fn => fn()); @@ -37,7 +38,7 @@ export function updateRulesFromConfig(registerRule) { function setupDefaultRule(activity) { if (!defaultRuleHandles.has(activity)) { defaultRuleHandles.set(activity, registerRule(activity, RULE_NAME, function () { - return {allow: false, reason: 'activity default is false'} + return {allow: false, reason: 'activity denied by default'} }, Number.POSITIVE_INFINITY)) } } diff --git a/src/activities/redactor.js b/src/activities/redactor.js index b5879594f6a..3c80019c750 100644 --- a/src/activities/redactor.js +++ b/src/activities/redactor.js @@ -9,15 +9,15 @@ export const ORTB_GEO_PATHS = ['user.geo.lat', 'user.geo.lon', 'device.geo.lat', /** * @typedef TransformationRuleDef * @property {name} + * @property {Array[string]} paths dot-separated list of paths that this rule applies to. * @property {function(*): boolean} applies a predicate that should return true if this rule applies * (and the transformation defined herein should be applied). The arguments are those passed to the transformation function. * @property {name} a name for the rule; used to debounce calls to `applies` (and avoid excessive logging): * if a rule with the same name was already found to apply (or not), this one will (or won't) as well. - * @property {Array[string]} paths dot-separated list of paths that this rule applies to.* */ /** - * @typedef RedactRuleDef + * @typedef RedactRuleDef A rule that removes, or replaces, values from an object (modifications are done in-place). * @augments TransformationRuleDef * @property {function(*): *} get? substitution functions for values that should be redacted; * takes in the original (unredacted) value as an input, and returns a substitute to use in the redacted @@ -53,11 +53,16 @@ export function redactRule(ruleDef) { */ /** - * Factory for transformation functions that apply the given rules to an object. - * + * @typedef {Function} TransformationFunction + * @param object object to transform + * @param ...args arguments to pass down to rule's `apply` methods. + */ + +/** + * Return a transformation function that will apply the given rules to an object. * * @param {Array[TransformationRule]} rules - * @return {function({}, {}, ...[*]): *} + * @return {TransformationFunction} */ export function objectTransformer(rules) { rules.forEach(rule => { diff --git a/src/adapterManager.js b/src/adapterManager.js index f785e2c4eeb..6f3a2ceb245 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -31,12 +31,12 @@ import {hook} from './hook.js'; import {find, includes} from './polyfill.js'; import {adunitCounter} from './adUnits.js'; import {getRefererInfo} from './refererDetection.js'; -import {GdprConsentHandler, UspConsentHandler, GppConsentHandler, GDPR_GVLIDS} from './consentHandler.js'; +import {GDPR_GVLIDS, GdprConsentHandler, GppConsentHandler, UspConsentHandler} from './consentHandler.js'; import * as events from './events.js'; import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; import {auctionManager} from './auctionManager.js'; -import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, MODULE_TYPE_PREBID, MODULE_TYPE_UID} from './activities/modules.js'; +import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, MODULE_TYPE_PREBID} from './activities/modules.js'; import {isActivityAllowed} from './activities/rules.js'; import {ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from './activities/activities.js'; import {ACTIVITY_PARAM_ANL_CONFIG, ACTIVITY_PARAM_S2S_NAME, activityParamsBuilder} from './activities/params.js'; From 64e0d4f90111313cff1eebc1b383cea3d0490542 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 13 Apr 2023 08:33:35 -0700 Subject: [PATCH 34/37] do not pass private activity params to pub-defined rules --- src/activities/cfg.js | 7 ++++++- test/spec/activities/cfg_spec.js | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/activities/cfg.js b/src/activities/cfg.js index 00bb682d8da..4b18b7b4896 100644 --- a/src/activities/cfg.js +++ b/src/activities/cfg.js @@ -19,6 +19,11 @@ export function updateRulesFromConfig(registerRule) { 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()) @@ -27,7 +32,7 @@ export function updateRulesFromConfig(registerRule) { 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(params)) { + if (!rule.condition || rule.condition(cleanParams(params))) { return {allow: rule.allow, reason: rule} } } diff --git a/test/spec/activities/cfg_spec.js b/test/spec/activities/cfg_spec.js index 45b063f1e2d..c727b55252c 100644 --- a/test/spec/activities/cfg_spec.js +++ b/test/spec/activities/cfg_spec.js @@ -91,6 +91,17 @@ describe('allowActivities config', () => { expect(isAllowed(ACTIVITY, params)).to.be.false; }); + it('does not pass private (underscored) parameters to condition', () => { + setupActivityConfig({ + rules: [{ + condition({_priv}) { return _priv }, + allow: false + }] + }); + params._priv = true; + expect(isAllowed(ACTIVITY, params)).to.be.true; + }) + it('are evaluated in order of priority', () => { setupActivityConfig({ rules: [{ From 8ea20426e7d8360d4589e0dcaca7d2816c469f9f Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 13 Apr 2023 09:12:50 -0700 Subject: [PATCH 35/37] fix objectGuard edge case: null values --- libraries/objectGuard/objectGuard.js | 2 +- test/spec/activities/objectGuard_spec.js | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/libraries/objectGuard/objectGuard.js b/libraries/objectGuard/objectGuard.js index a3d55f4999b..a404f8653f8 100644 --- a/libraries/objectGuard/objectGuard.js +++ b/libraries/objectGuard/objectGuard.js @@ -92,7 +92,7 @@ export function writeProtectRule(ruleDef) { run(root, path, object, property, applies) { const origHasProp = object && object.hasOwnProperty(property); const original = origHasProp ? object[property] : undefined; - const origCopy = origHasProp && typeof original === 'object' ? deepClone(original) : original; + 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]); diff --git a/test/spec/activities/objectGuard_spec.js b/test/spec/activities/objectGuard_spec.js index 4cb500bb302..c88442e9111 100644 --- a/test/spec/activities/objectGuard_spec.js +++ b/test/spec/activities/objectGuard_spec.js @@ -122,7 +122,8 @@ describe('objectGuard', () => { } } }) - }) + }); + it('should undo nested deletes', () => { const obj = {outer: {inner: {foo: {nested: 'val'}, bar: 'val'}}}; const guard = objectGuard([rule])(obj); @@ -130,6 +131,14 @@ describe('objectGuard', () => { delete guard.obj.outer.inner.bar; guard.verify(); expect(obj).to.eql({outer: {inner: {foo: {nested: 'val'}, bar: 'val'}}}) - }) + }); + + it('should work on null properties', () => { + const obj = {foo: null}; + const guard = objectGuard([rule])(obj); + guard.obj.foo = 'denied'; + guard.verify(); + expect(obj).to.eql({foo: null}); + }); }); }); From 6e0a85041418934145c3c5aebdd53f96fafde623 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 23 May 2023 09:02:43 -0700 Subject: [PATCH 36/37] move config logic into a module --- src/activities/cfg.js => modules/allowActivities.js | 11 +++++++---- src/activities/rules.js | 2 -- .../{cfg_spec.js => allowActivites_spec.js} | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) rename src/activities/cfg.js => modules/allowActivities.js (86%) rename test/spec/activities/{cfg_spec.js => allowActivites_spec.js} (97%) diff --git a/src/activities/cfg.js b/modules/allowActivities.js similarity index 86% rename from src/activities/cfg.js rename to modules/allowActivities.js index 4b18b7b4896..6af7eb36a62 100644 --- a/src/activities/cfg.js +++ b/modules/allowActivities.js @@ -1,4 +1,5 @@ -import {config} from '../config.js'; +import {config} from '../src/config.js'; +import {registerActivityControl} from '../src/activities/rules.js'; const CFG_NAME = 'allowActivities'; const RULE_NAME = `${CFG_NAME} config`; @@ -50,14 +51,14 @@ export function updateRulesFromConfig(registerRule) { config.getConfig(CFG_NAME, (cfg) => { clearAllRules(); - Object.entries(cfg[CFG_NAME]).forEach(([activity, cfg]) => { - if (cfg.default === false) { + Object.entries(cfg[CFG_NAME]).forEach(([activity, activityCfg]) => { + if (activityCfg.default === false) { setupDefaultRule(activity); } const rules = new Map(); rulesByActivity.set(activity, rules); - (cfg.rules || []).forEach(rule => { + (activityCfg.rules || []).forEach(rule => { const priority = rule.priority == null ? DEFAULT_PRIORITY : rule.priority; if (!rules.has(priority)) { rules.set(priority, []) @@ -69,3 +70,5 @@ export function updateRulesFromConfig(registerRule) { }); }) } + +updateRulesFromConfig(registerActivityControl); diff --git a/src/activities/rules.js b/src/activities/rules.js index 92ea0f1930b..28dbba2d317 100644 --- a/src/activities/rules.js +++ b/src/activities/rules.js @@ -1,6 +1,5 @@ import {prefixLog} from '../utils.js'; import {ACTIVITY_PARAM_COMPONENT} from './params.js'; -import {updateRulesFromConfig} from './cfg.js'; export function ruleRegistry(logger = prefixLog('Activity control:')) { const registry = {}; @@ -85,4 +84,3 @@ export function ruleRegistry(logger = prefixLog('Activity control:')) { } export const [registerActivityControl, isActivityAllowed] = ruleRegistry(); -updateRulesFromConfig(registerActivityControl); diff --git a/test/spec/activities/cfg_spec.js b/test/spec/activities/allowActivites_spec.js similarity index 97% rename from test/spec/activities/cfg_spec.js rename to test/spec/activities/allowActivites_spec.js index c727b55252c..cc1c83ec4c9 100644 --- a/test/spec/activities/cfg_spec.js +++ b/test/spec/activities/allowActivites_spec.js @@ -1,6 +1,6 @@ import {config} from 'src/config.js'; import {ruleRegistry} from '../../../src/activities/rules.js'; -import {updateRulesFromConfig} from '../../../src/activities/cfg.js'; +import {updateRulesFromConfig} from '../../../modules/allowActivities.js'; import {activityParams} from '../../../src/activities/activityParams.js'; describe('allowActivities config', () => { From 73f763ab77a742023ca2e31fd0e66ae398b1e200 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 23 May 2023 09:23:24 -0700 Subject: [PATCH 37/37] dedupe log messages --- modules/userId/index.js | 2 +- src/activities/rules.js | 19 +++++++++++----- test/spec/activities/rules_spec.js | 35 ++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/modules/userId/index.js b/modules/userId/index.js index 2df20443e52..f906458a3f5 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -966,7 +966,7 @@ const ALL_STORAGE_TYPES = new Set([LOCAL_STORAGE, COOKIE]); function canUseStorage(submodule) { switch (submodule.config?.storage?.type) { case LOCAL_STORAGE: - if (submodule.storageMgr.cookiesAreEnabled()) { + if (submodule.storageMgr.localStorageIsEnabled()) { if (coreStorage.getDataFromLocalStorage(PBJS_USER_ID_OPTOUT_NAME)) { logInfo(`${MODULE_NAME} - opt-out localStorage found, storage disabled`); return false diff --git a/src/activities/rules.js b/src/activities/rules.js index 28dbba2d317..f84f1080843 100644 --- a/src/activities/rules.js +++ b/src/activities/rules.js @@ -19,12 +19,21 @@ export function ruleRegistry(logger = prefixLog('Activity control:')) { return res && Object.assign({activity, name, component: params[ACTIVITY_PARAM_COMPONENT]}, res); } + const dupes = {}; + const DEDUPE_INTERVAL = 1000; + function logResult({activity, name, allow, reason, component}) { - const msg = [ - `${name} ${allow ? 'allowed' : 'denied'} '${activity}' for '${component}'${reason ? ':' : ''}` - ]; - reason && msg.push(reason); - (allow ? logger.logInfo : logger.logWarn).apply(logger, msg); + const msg = `${name} ${allow ? 'allowed' : 'denied'} '${activity}' for '${component}'${reason ? ':' : ''}`; + const deduping = dupes.hasOwnProperty(msg); + if (deduping) { + clearTimeout(dupes[msg]); + } + dupes[msg] = setTimeout(() => delete dupes[msg], DEDUPE_INTERVAL); + if (!deduping) { + const parts = [msg]; + reason && parts.push(reason); + (allow ? logger.logInfo : logger.logWarn).apply(logger, parts); + } } return [ diff --git a/test/spec/activities/rules_spec.js b/test/spec/activities/rules_spec.js index cd3bb9d849a..2acfae57980 100644 --- a/test/spec/activities/rules_spec.js +++ b/test/spec/activities/rules_spec.js @@ -97,4 +97,39 @@ describe('Activity control rules', () => { isAllowed(MOCK_ACTIVITY, {}); sinon.assert.calledWithMatch(logger.logWarn, new RegExp(MOCK_RULE), /fail/); }); + + describe('log message deduping', () => { + let clock, allow; + beforeEach(() => { + allow = false; + registerRule(MOCK_ACTIVITY, MOCK_RULE, () => ({ allow })); + clock = sinon.useFakeTimers(); + }); + afterEach(() => { + clock.restore(); + }); + + it('is applied to identical messages that are close in time', () => { + isAllowed(MOCK_ACTIVITY, {}); + clock.tick(100); + isAllowed(MOCK_ACTIVITY, {}); + expect(logger.logWarn.callCount).to.equal(1); + }); + + it('not to messages that show different results', () => { + isAllowed(MOCK_ACTIVITY, {}); + allow = true; + clock.tick(100); + isAllowed(MOCK_ACTIVITY, {}); + expect(logger.logWarn.callCount).to.equal(1); + expect(logger.logInfo.callCount).to.equal(1); + }); + + it('not to messages that are further apart in time', () => { + isAllowed(MOCK_ACTIVITY, {}); + clock.tick(2000); + isAllowed(MOCK_ACTIVITY, {}); + expect(logger.logWarn.callCount).to.equal(2); + }) + }) });