From 3042c1c2c3b1c4a6a215725e691270fd10bcda15 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Thu, 30 Apr 2020 15:08:57 +0530 Subject: [PATCH 01/20] TCF v2.0 enforcement --- modules/gdprEnforcement.js | 88 ++++++++--- src/constants.json | 3 +- test/spec/modules/gdprEnforcement_spec.js | 176 +++++++++++++++++----- 3 files changed, 211 insertions(+), 56 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 363d0e396f8..466786ee9d4 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -11,18 +11,25 @@ import includes from 'core-js/library/fn/array/includes.js'; import { registerSyncInner } from '../src/adapters/bidderFactory.js'; import { getHook } from '../src/hook.js'; import { validateStorageEnforcement } from '../src/storageManager.js'; +import events from '../src/events.js'; +import { EVENTS } from '../src/constants.json'; -const purpose1 = 'storage'; +const PURPOSE_1 = 'storage'; +const PURPOSE_2 = 'basicAds'; +let hasDefinedPurpose1 = false; +let hasDefinedPurpose2 = false; let addedDeviceAccessHook = false; let enforcementRules; -function getGvlid() { +function getGvlid(bidderCode) { let gvlid; - const bidderCode = config.getCurrentBidder(); + bidderCode = bidderCode || config.getCurrentBidder(); if (bidderCode) { const bidder = adapterManager.getBidAdapter(bidderCode); - gvlid = bidder.getSpec().gvlid; + if (bidder && bidder.getSpec) { + gvlid = bidder.getSpec().gvlid; + } } else { utils.logWarn('Current module not found'); } @@ -31,18 +38,21 @@ function getGvlid() { /** * This function takes in rules and consentData as input and validates against the consentData provided. If it returns true Prebid will allow the next call else it will log a warning - * @param {Object} rules enforcement rules set in config - * @param {Object} consentData gdpr consent data + * @param {Object} rule - enforcement rules set in config + * @param {Object} consentData - gdpr consent data + * @param {number} purpose - Defines which purpose (1, 2, 4, 7) is under check + * @param {string=} currentModule - Bidder code of the current module + * @param {number=} gvlid - GVL ID for the module * @returns {boolean} */ -function validateRules(rule, consentData, currentModule, gvlid) { +function validateRules(rule, consentData, purpose, currentModule, gvlid) { let isAllowed = false; if (!rule.vendorExceptions) rule.vendorExceptions = []; - if (rule.enforcePurpose && rule.enforceVendor) { + if (rule.enforcePurpose === true && rule.enforceVendor === true) { if ( includes(rule.vendorExceptions, currentModule) || ( - utils.deepAccess(consentData, 'vendorData.purpose.consents.1') === true && + utils.deepAccess(consentData, `vendorData.purpose.consents.${purpose}`) === true && utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlid}`) === true ) ) { @@ -61,7 +71,7 @@ function validateRules(rule, consentData, currentModule, gvlid) { if ( !includes(rule.vendorExceptions, currentModule) || ( - (utils.deepAccess(consentData, 'vendorData.purpose.consents.1') === true) && + (utils.deepAccess(consentData, `vendorData.purpose.consents.${purpose}`) === true) && (utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlid}`) === true) ) ) { @@ -69,7 +79,7 @@ function validateRules(rule, consentData, currentModule, gvlid) { } } else if (rule.enforcePurpose === true && rule.enforceVendor === false) { if ( - (utils.deepAccess(consentData, 'vendorData.purpose.consents.1') === true) && + (utils.deepAccess(consentData, `vendorData.purpose.consents.${purpose}`) === true) && ( !includes(rule.vendorExceptions, currentModule) || (utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlid}`) === true) @@ -103,8 +113,8 @@ export function deviceAccessHook(fn, gvlid, moduleName, result) { gvlid = getGvlid(); } const curModule = moduleName || config.getCurrentBidder(); - const purpose1Rule = find(enforcementRules, hasPurpose1); - let isAllowed = validateRules(purpose1Rule, consentData, curModule, gvlid); + const purpose1Rule = hasDefinedPurpose1; + let isAllowed = validateRules(purpose1Rule, consentData, 1, curModule, gvlid); if (isAllowed) { result.valid = true; fn.call(this, gvlid, moduleName, result); @@ -137,8 +147,8 @@ export function userSyncHook(fn, ...args) { const gvlid = getGvlid(); const curBidder = config.getCurrentBidder(); if (gvlid) { - const purpose1Rule = find(enforcementRules, hasPurpose1); - let isAllowed = validateRules(purpose1Rule, consentData, curBidder, gvlid); + const purpose1Rule = hasDefinedPurpose1; + let isAllowed = validateRules(purpose1Rule, consentData, 1, curBidder, gvlid); if (isAllowed) { fn.call(this, ...args); } else { @@ -169,8 +179,8 @@ export function userIdHook(fn, submodules, consentData) { const gvlid = submodule.submodule.gvlid; const moduleName = submodule.submodule.name; if (gvlid) { - const purpose1Rule = find(enforcementRules, hasPurpose1); - let isAllowed = validateRules(purpose1Rule, consentData, moduleName, gvlid); + const purpose1Rule = hasDefinedPurpose1; + let isAllowed = validateRules(purpose1Rule, consentData, 1, moduleName, gvlid); if (isAllowed) { return submodule; } else { @@ -191,11 +201,44 @@ export function userIdHook(fn, submodules, consentData) { } } -const hasPurpose1 = (rule) => { return rule.purpose === purpose1 } +/** + * Checks if the bidder is given consent. If yes, bid adapter is allowed to send ajax request to their endpoint, else, no request + * is sent. Enforces "purpose 2 (basic ads)" of TCF v2.0 spec + * @param {Function} fn - Function reference to the original function. + * @param {Array} adUnits + */ +export function makeBidRequestsHook(fn, adUnits, ...args) { + const purpose2Rule = hasDefinedPurpose2; + if (purpose2Rule) { + const consentData = gdprDataHandler.getConsentData(); + const disabledBidders = []; + adUnits.forEach(adUnit => { + adUnit.bids = adUnit.bids.filter(bid => { + const currBidder = bid.bidder; + const gvlId = getGvlid(currBidder); + if (includes(disabledBidders, currBidder)) return false; + const isAllowed = validateRules(purpose2Rule, consentData, 2, currBidder, gvlId); + if (!isAllowed) { + utils.logWarn(`User blocked bidder: ${currBidder}. No bid request will be sent to their endpoint.`); + // Emit an event for the Analytics adapters to listen + events.emit(EVENTS.BIDDER_BLOCKED, currBidder); + disabledBidders.push(currBidder); + } + return isAllowed; + }); + }); + fn.call(this, adUnits, ...args); + } else { + fn.call(this, adUnits, ...args); + } +} + +const hasPurpose1 = (rule) => { return rule.purpose === PURPOSE_1 } +const hasPurpose2 = (rule) => { return rule.purpose === PURPOSE_2 } /** * A configuration function that initializes some module variables, as well as add hooks - * @param {Object} config GDPR enforcement config object + * @param {Object} config - GDPR enforcement config object */ export function setEnforcementConfig(config) { const rules = utils.deepAccess(config, 'gdpr.rules'); @@ -205,7 +248,9 @@ export function setEnforcementConfig(config) { } enforcementRules = rules; - const hasDefinedPurpose1 = find(enforcementRules, hasPurpose1); + hasDefinedPurpose1 = find(enforcementRules, hasPurpose1); + hasDefinedPurpose2 = find(enforcementRules, hasPurpose2); + if (hasDefinedPurpose1 && !addedDeviceAccessHook) { addedDeviceAccessHook = true; validateStorageEnforcement.before(deviceAccessHook, 49); @@ -213,6 +258,9 @@ export function setEnforcementConfig(config) { // Using getHook as user id and gdprEnforcement are both optional modules. Using import will auto include the file in build getHook('validateGdprEnforcement').before(userIdHook, 47); } + if (hasDefinedPurpose2) { + adapterManager.makeBidRequests.before(makeBidRequestsHook); + } } config.getConfig('consentManagement', config => setEnforcementConfig(config.consentManagement)); diff --git a/src/constants.json b/src/constants.json index 7ffef9db1aa..9080a56238a 100644 --- a/src/constants.json +++ b/src/constants.json @@ -36,7 +36,8 @@ "BEFORE_REQUEST_BIDS": "beforeRequestBids", "REQUEST_BIDS": "requestBids", "ADD_AD_UNITS": "addAdUnits", - "AD_RENDER_FAILED" : "adRenderFailed" + "AD_RENDER_FAILED" : "adRenderFailed", + "BIDDER_BLOCKED": "bidderBlocked" }, "AD_RENDER_FAILED_REASON" : { "PREVENT_WRITING_ON_MAIN_DOCUMENT": "preventWritingOnMainDocuemnt", diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 5b46441cbbb..246e0e3c5a2 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -1,11 +1,12 @@ -import { deviceAccessHook, setEnforcementConfig, userSyncHook, userIdHook } from 'modules/gdprEnforcement.js'; +import { deviceAccessHook, setEnforcementConfig, userSyncHook, userIdHook, makeBidRequestsHook } from 'modules/gdprEnforcement.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 { executeStorageCallbacks } from 'src/prebid.js'; +import events from 'src/events.js'; -describe('gdpr enforcement', function() { +describe('gdpr enforcement', function () { let nextFnSpy; let logWarnSpy; let gdprDataHandlerStub; @@ -46,7 +47,9 @@ describe('gdpr enforcement', function() { 'consents': { '1': true, '2': true, - '3': false + '3': false, + '4': true, + '5': false }, 'legitimateInterests': { '1': false, @@ -81,23 +84,24 @@ describe('gdpr enforcement', function() { } }; - after(function() { - validateStorageEnforcement.getHooks({hook: deviceAccessHook}).remove(); - $$PREBID_GLOBAL$$.requestBids.getHooks({hook: executeStorageCallbacks}).remove(); + after(function () { + validateStorageEnforcement.getHooks({ hook: deviceAccessHook }).remove(); + $$PREBID_GLOBAL$$.requestBids.getHooks({ hook: executeStorageCallbacks }).remove(); + adapterManager.makeBidRequests.getHooks({ hook: makeBidRequestsHook }).remove(); }) - describe('deviceAccessHook', function() { - beforeEach(function() { + describe('deviceAccessHook', function () { + beforeEach(function () { nextFnSpy = sinon.spy(); gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); logWarnSpy = sinon.spy(utils, 'logWarn'); }); - afterEach(function() { + afterEach(function () { config.resetConfig(); gdprDataHandler.getConsentData.restore(); logWarnSpy.restore(); }); - it('should not allow device access when device access flag is set to false', function() { + it('should not allow device access when device access flag is set to false', function () { config.setConfig({ deviceAccess: false, consentManagement: { @@ -118,10 +122,11 @@ describe('gdpr enforcement', function() { hasEnforcementHook: true, valid: false } - expect(nextFnSpy.calledWith(undefined, result)); + sinon.assert.calledWith(nextFnSpy, undefined, undefined, result); + // expect(nextFnSpy.calledWith(undefined, result)); }); - it('should only check for consent for vendor exceptions when enforcePurpose and enforceVendor are false', function() { + it('should only check for consent for vendor exceptions when enforcePurpose and enforceVendor are false', function () { setEnforcementConfig({ gdpr: { rules: [{ @@ -143,7 +148,7 @@ describe('gdpr enforcement', function() { expect(logWarnSpy.callCount).to.equal(0); }); - it('should check consent for all vendors when enforcePurpose and enforceVendor are true', function() { + it('should check consent for all vendors when enforcePurpose and enforceVendor are true', function () { setEnforcementConfig({ gdpr: { rules: [{ @@ -164,7 +169,7 @@ describe('gdpr enforcement', function() { expect(logWarnSpy.callCount).to.equal(1); }); - it('should allow device access when gdprApplies is false and hasDeviceAccess flag is true', function() { + it('should allow device access when gdprApplies is false and hasDeviceAccess flag is true', function () { setEnforcementConfig({ gdpr: { rules: [{ @@ -187,15 +192,16 @@ describe('gdpr enforcement', function() { hasEnforcementHook: true, valid: true } - expect(nextFnSpy.calledWith(undefined, result)); + sinon.assert.calledWith(nextFnSpy, 1, 'appnexus', result); + // expect(nextFnSpy.calledWith(undefined, result)); }); }); - describe('userSyncHook', function() { + describe('userSyncHook', function () { let curBidderStub; let adapterManagerStub; - beforeEach(function() { + beforeEach(function () { gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); logWarnSpy = sinon.spy(utils, 'logWarn'); curBidderStub = sinon.stub(config, 'getCurrentBidder'); @@ -203,7 +209,7 @@ describe('gdpr enforcement', function() { nextFnSpy = sinon.spy(); }); - afterEach(function() { + afterEach(function () { config.getCurrentBidder.restore(); config.resetConfig(); gdprDataHandler.getConsentData.restore(); @@ -211,7 +217,7 @@ describe('gdpr enforcement', function() { logWarnSpy.restore(); }); - it('should allow bidder to do user sync if consent is true', function() { + it('should allow bidder to do user sync if consent is true', function () { setEnforcementConfig({ gdpr: { rules: [{ @@ -230,7 +236,7 @@ describe('gdpr enforcement', function() { curBidderStub.returns('sampleBidder1'); adapterManagerStub.withArgs('sampleBidder1').returns({ - getSpec: function() { + getSpec: function () { return { 'gvlid': 1 } @@ -240,7 +246,7 @@ describe('gdpr enforcement', function() { curBidderStub.returns('sampleBidder2'); adapterManagerStub.withArgs('sampleBidder2').returns({ - getSpec: function() { + getSpec: function () { return { 'gvlid': 3 } @@ -250,7 +256,7 @@ describe('gdpr enforcement', function() { expect(nextFnSpy.calledTwice).to.equal(true); }); - it('should not allow bidder to do user sync if user has denied consent', function() { + it('should not allow bidder to do user sync if user has denied consent', function () { setEnforcementConfig({ gdpr: { rules: [{ @@ -269,7 +275,7 @@ describe('gdpr enforcement', function() { curBidderStub.returns('sampleBidder1'); adapterManagerStub.withArgs('sampleBidder1').returns({ - getSpec: function() { + getSpec: function () { return { 'gvlid': 1 } @@ -279,7 +285,7 @@ describe('gdpr enforcement', function() { curBidderStub.returns('sampleBidder2'); adapterManagerStub.withArgs('sampleBidder2').returns({ - getSpec: function() { + getSpec: function () { return { 'gvlid': 3 } @@ -290,7 +296,7 @@ describe('gdpr enforcement', function() { expect(logWarnSpy.callCount).to.equal(1); }); - it('should not check vendor consent when enforceVendor is false', function() { + it('should not check vendor consent when enforceVendor is false', function () { setEnforcementConfig({ gdpr: { rules: [{ @@ -309,7 +315,7 @@ describe('gdpr enforcement', function() { curBidderStub.returns('sampleBidder1'); adapterManagerStub.withArgs('sampleBidder1').returns({ - getSpec: function() { + getSpec: function () { return { 'gvlid': 1 } @@ -319,7 +325,7 @@ describe('gdpr enforcement', function() { curBidderStub.returns('sampleBidder2'); adapterManagerStub.withArgs('sampleBidder2').returns({ - getSpec: function() { + getSpec: function () { return { 'gvlid': 3 } @@ -331,16 +337,16 @@ describe('gdpr enforcement', function() { }); }); - describe('userIdHook', function() { - beforeEach(function() { + describe('userIdHook', function () { + beforeEach(function () { logWarnSpy = sinon.spy(utils, 'logWarn'); nextFnSpy = sinon.spy(); }); - afterEach(function() { + afterEach(function () { config.resetConfig(); logWarnSpy.restore(); }); - it('should allow user id module if consent is given', function() { + it('should allow user id module if consent is given', function () { setEnforcementConfig({ gdpr: { rules: [{ @@ -365,7 +371,7 @@ describe('gdpr enforcement', function() { expect(nextFnSpy.calledOnce).to.equal(true); }); - it('should allow userId module if gdpr not in scope', function() { + it('should allow userId module if gdpr not in scope', function () { let submodules = [{ submodule: { gvlid: 1, @@ -375,10 +381,11 @@ describe('gdpr enforcement', function() { let consentData = null; userIdHook(nextFnSpy, submodules, consentData); expect(nextFnSpy.calledOnce).to.equal(true); - expect(nextFnSpy.calledWith(undefined, submodules, consentData)); + sinon.assert.calledWith(nextFnSpy, submodules, consentData); + // expect(nextFnSpy.calledWith(undefined, submodules, consentData)); }); - it('should not allow user id module if user denied consent', function() { + it('should not allow user id module if user denied consent', function () { setEnforcementConfig({ gdpr: { rules: [{ @@ -412,7 +419,106 @@ describe('gdpr enforcement', function() { name: 'sampleUserId' } }] - expect(nextFnSpy.calledWith(undefined, expectedSubmodules, consentData)); + sinon.assert.calledWith(nextFnSpy, expectedSubmodules, consentData); + // expect(nextFnSpy.calledWith(undefined, expectedSubmodules, consentData)); + }); + }); + + 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 + }] + }, { + code: 'ad-unit-2', + mediaTypes: {}, + bids: [{ + bidder: 'bidder_2' + }] + }]; + 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'); + }); + afterEach(function () { + config.resetConfig(); + sandbox.restore(); + }); + it('should block bidder which does not have consent and allow bidder which has consent', function () { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'basicAds', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + }] + } + }); + const consentData = {}; + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.apiVersion = 2; + 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 } + } + }); + 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' }) + ] + }, { + code: 'ad-unit-2', + mediaTypes: {}, + bids: [] + }], []); + expect(emitEventSpy.calledOnce).to.equal(true); + expect(logWarnSpy.calledOnce).to.equal(true); + }); + + it('should skip TCF v2.0 validation checks if "Purpose 2" enforcment not present in gdpr config rules', function () { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforePurpose: false, + enforceVendor: false, + vendorExceptions: [] + }] + } + }); + 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); }); }); }); From 73d3b52fafe851e487c752fbbe402d2f7da98f91 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Thu, 30 Apr 2020 15:21:23 +0530 Subject: [PATCH 02/20] test/spec/modules/gdprEnforcement_spec.js --- test/spec/modules/gdprEnforcement_spec.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 246e0e3c5a2..7f9c3438aff 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -123,7 +123,6 @@ describe('gdpr enforcement', function () { valid: false } sinon.assert.calledWith(nextFnSpy, undefined, undefined, result); - // expect(nextFnSpy.calledWith(undefined, result)); }); it('should only check for consent for vendor exceptions when enforcePurpose and enforceVendor are false', function () { @@ -193,7 +192,6 @@ describe('gdpr enforcement', function () { valid: true } sinon.assert.calledWith(nextFnSpy, 1, 'appnexus', result); - // expect(nextFnSpy.calledWith(undefined, result)); }); }); @@ -382,7 +380,6 @@ describe('gdpr enforcement', function () { userIdHook(nextFnSpy, submodules, consentData); expect(nextFnSpy.calledOnce).to.equal(true); sinon.assert.calledWith(nextFnSpy, submodules, consentData); - // expect(nextFnSpy.calledWith(undefined, submodules, consentData)); }); it('should not allow user id module if user denied consent', function () { @@ -420,7 +417,6 @@ describe('gdpr enforcement', function () { } }] sinon.assert.calledWith(nextFnSpy, expectedSubmodules, consentData); - // expect(nextFnSpy.calledWith(undefined, expectedSubmodules, consentData)); }); }); From a49f7e15328214b7fdaba6dffd130b76f9570a82 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Tue, 5 May 2020 20:59:36 +0530 Subject: [PATCH 03/20] add check for gdpr version --- modules/gdprEnforcement.js | 20 ++++++++------------ test/spec/modules/gdprEnforcement_spec.js | 9 ++++++++- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 466786ee9d4..3d7fd35380d 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -17,8 +17,8 @@ import { EVENTS } from '../src/constants.json'; const PURPOSE_1 = 'storage'; const PURPOSE_2 = 'basicAds'; -let hasDefinedPurpose1 = false; -let hasDefinedPurpose2 = false; +let purpose1Rule; +let purpose2Rule; let addedDeviceAccessHook = false; let enforcementRules; @@ -113,7 +113,6 @@ export function deviceAccessHook(fn, gvlid, moduleName, result) { gvlid = getGvlid(); } const curModule = moduleName || config.getCurrentBidder(); - const purpose1Rule = hasDefinedPurpose1; let isAllowed = validateRules(purpose1Rule, consentData, 1, curModule, gvlid); if (isAllowed) { result.valid = true; @@ -147,7 +146,6 @@ export function userSyncHook(fn, ...args) { const gvlid = getGvlid(); const curBidder = config.getCurrentBidder(); if (gvlid) { - const purpose1Rule = hasDefinedPurpose1; let isAllowed = validateRules(purpose1Rule, consentData, 1, curBidder, gvlid); if (isAllowed) { fn.call(this, ...args); @@ -179,7 +177,6 @@ export function userIdHook(fn, submodules, consentData) { const gvlid = submodule.submodule.gvlid; const moduleName = submodule.submodule.name; if (gvlid) { - const purpose1Rule = hasDefinedPurpose1; let isAllowed = validateRules(purpose1Rule, consentData, 1, moduleName, gvlid); if (isAllowed) { return submodule; @@ -208,9 +205,8 @@ export function userIdHook(fn, submodules, consentData) { * @param {Array} adUnits */ export function makeBidRequestsHook(fn, adUnits, ...args) { - const purpose2Rule = hasDefinedPurpose2; - if (purpose2Rule) { - const consentData = gdprDataHandler.getConsentData(); + const consentData = gdprDataHandler.getConsentData(); + if (consentData && consentData.apiVersion === 2) { const disabledBidders = []; adUnits.forEach(adUnit => { adUnit.bids = adUnit.bids.filter(bid => { @@ -248,17 +244,17 @@ export function setEnforcementConfig(config) { } enforcementRules = rules; - hasDefinedPurpose1 = find(enforcementRules, hasPurpose1); - hasDefinedPurpose2 = find(enforcementRules, hasPurpose2); + purpose1Rule = find(enforcementRules, hasPurpose1); + purpose2Rule = find(enforcementRules, hasPurpose2); - if (hasDefinedPurpose1 && !addedDeviceAccessHook) { + if (purpose1Rule && !addedDeviceAccessHook) { addedDeviceAccessHook = 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 getHook('validateGdprEnforcement').before(userIdHook, 47); } - if (hasDefinedPurpose2) { + if (purpose2Rule) { adapterManager.makeBidRequests.before(makeBidRequestsHook); } } diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 7f9c3438aff..66e474c23ec 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -497,7 +497,7 @@ describe('gdpr enforcement', function () { expect(logWarnSpy.calledOnce).to.equal(true); }); - it('should skip TCF v2.0 validation checks if "Purpose 2" enforcment not present in gdpr config rules', function () { + it('should skip validation checks if GDPR version is not equal to "2"', function () { setEnforcementConfig({ gdpr: { rules: [{ @@ -508,6 +508,13 @@ describe('gdpr enforcement', function () { }] } }); + + const consentData = {}; + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.apiVersion = 1; + consentData.gdprApplies = true; + gdprDataHandlerStub.returns(consentData); + makeBidRequestsHook(nextFnSpy, MOCK_AD_UNITS, []); // Assertions From 559724edb19663b9bd1211129e2589dce4a5601b Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Tue, 5 May 2020 21:26:07 +0530 Subject: [PATCH 04/20] add logInfo message --- modules/gdprEnforcement.js | 39 +++++++++++++---------- test/spec/modules/gdprEnforcement_spec.js | 3 ++ 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 3d7fd35380d..0940e3a7470 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -206,24 +206,29 @@ export function userIdHook(fn, submodules, consentData) { */ export function makeBidRequestsHook(fn, adUnits, ...args) { const consentData = gdprDataHandler.getConsentData(); - if (consentData && consentData.apiVersion === 2) { - const disabledBidders = []; - adUnits.forEach(adUnit => { - adUnit.bids = adUnit.bids.filter(bid => { - const currBidder = bid.bidder; - const gvlId = getGvlid(currBidder); - if (includes(disabledBidders, currBidder)) return false; - const isAllowed = validateRules(purpose2Rule, consentData, 2, currBidder, gvlId); - if (!isAllowed) { - utils.logWarn(`User blocked bidder: ${currBidder}. No bid request will be sent to their endpoint.`); - // Emit an event for the Analytics adapters to listen - events.emit(EVENTS.BIDDER_BLOCKED, currBidder); - disabledBidders.push(currBidder); - } - return isAllowed; + if (consentData && consentData.gdprApplies) { + if (consentData.apiVersion === 2) { + const disabledBidders = []; + adUnits.forEach(adUnit => { + adUnit.bids = adUnit.bids.filter(bid => { + const currBidder = bid.bidder; + const gvlId = getGvlid(currBidder); + if (includes(disabledBidders, currBidder)) return false; + const isAllowed = validateRules(purpose2Rule, consentData, 2, currBidder, gvlId); + if (!isAllowed) { + utils.logWarn(`User blocked bidder: ${currBidder}. No bid request will be sent to their endpoint.`); + // Emit an event for the Analytics adapters to listen + events.emit(EVENTS.BIDDER_BLOCKED, currBidder); + disabledBidders.push(currBidder); + } + return isAllowed; + }); }); - }); - fn.call(this, adUnits, ...args); + fn.call(this, adUnits, ...args); + } else { + utils.logInfo('Enforcing TCF2 only'); + fn.call(this, adUnits, ...args); + } } else { fn.call(this, adUnits, ...args); } diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 66e474c23ec..4401bb0d22c 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -424,6 +424,7 @@ describe('gdpr enforcement', function () { let sandbox; let adapterManagerStub; let emitEventSpy; + let logInfoSpy; const MOCK_AD_UNITS = [{ code: 'ad-unit-1', mediaTypes: {}, @@ -444,6 +445,7 @@ describe('gdpr enforcement', function () { gdprDataHandlerStub = sandbox.stub(gdprDataHandler, 'getConsentData'); adapterManagerStub = sandbox.stub(adapterManager, 'getBidAdapter'); logWarnSpy = sandbox.spy(utils, 'logWarn'); + logInfoSpy = sandbox.spy(utils, 'logInfo'); nextFnSpy = sandbox.spy(); emitEventSpy = sandbox.spy(events, 'emit'); }); @@ -522,6 +524,7 @@ describe('gdpr enforcement', function () { sinon.assert.calledWith(nextFnSpy, sinon.match.array.deepEquals(MOCK_AD_UNITS), []); expect(emitEventSpy.notCalled).to.equal(true); expect(logWarnSpy.notCalled).to.equal(true); + expect(logInfoSpy.calledOnce).to.equal(true); }); }); }); From d945a8a1bb6e3412ee7815b26824bc1a5e182b18 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Thu, 7 May 2020 11:41:44 +0530 Subject: [PATCH 05/20] remove comment and store value of PURPOSES in an object --- modules/gdprEnforcement.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 0940e3a7470..a4b486f639a 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -14,8 +14,15 @@ import { validateStorageEnforcement } from '../src/storageManager.js'; import events from '../src/events.js'; import { EVENTS } from '../src/constants.json'; -const PURPOSE_1 = 'storage'; -const PURPOSE_2 = 'basicAds'; +const PURPOSE_1 = { + id: 1, + name: 'storage' +} + +const PURPOSE_2 = { + id: 2, + name: 'basicAds' +} let purpose1Rule; let purpose2Rule; @@ -113,7 +120,7 @@ export function deviceAccessHook(fn, gvlid, moduleName, result) { gvlid = getGvlid(); } const curModule = moduleName || config.getCurrentBidder(); - let isAllowed = validateRules(purpose1Rule, consentData, 1, curModule, gvlid); + let isAllowed = validateRules(purpose1Rule, consentData, PURPOSE_1.id, curModule, gvlid); if (isAllowed) { result.valid = true; fn.call(this, gvlid, moduleName, result); @@ -146,7 +153,7 @@ export function userSyncHook(fn, ...args) { const gvlid = getGvlid(); const curBidder = config.getCurrentBidder(); if (gvlid) { - let isAllowed = validateRules(purpose1Rule, consentData, 1, curBidder, gvlid); + let isAllowed = validateRules(purpose1Rule, consentData, PURPOSE_1.id, curBidder, gvlid); if (isAllowed) { fn.call(this, ...args); } else { @@ -177,7 +184,7 @@ export function userIdHook(fn, submodules, consentData) { const gvlid = submodule.submodule.gvlid; const moduleName = submodule.submodule.name; if (gvlid) { - let isAllowed = validateRules(purpose1Rule, consentData, 1, moduleName, gvlid); + let isAllowed = validateRules(purpose1Rule, consentData, PURPOSE_1.id, moduleName, gvlid); if (isAllowed) { return submodule; } else { @@ -214,10 +221,9 @@ export function makeBidRequestsHook(fn, adUnits, ...args) { const currBidder = bid.bidder; const gvlId = getGvlid(currBidder); if (includes(disabledBidders, currBidder)) return false; - const isAllowed = validateRules(purpose2Rule, consentData, 2, currBidder, gvlId); + const isAllowed = validateRules(purpose2Rule, consentData, PURPOSE_2.id, currBidder, gvlId); if (!isAllowed) { utils.logWarn(`User blocked bidder: ${currBidder}. No bid request will be sent to their endpoint.`); - // Emit an event for the Analytics adapters to listen events.emit(EVENTS.BIDDER_BLOCKED, currBidder); disabledBidders.push(currBidder); } @@ -234,8 +240,8 @@ export function makeBidRequestsHook(fn, adUnits, ...args) { } } -const hasPurpose1 = (rule) => { return rule.purpose === PURPOSE_1 } -const hasPurpose2 = (rule) => { return rule.purpose === PURPOSE_2 } +const hasPurpose1 = (rule) => { return rule.purpose === PURPOSE_1.name } +const hasPurpose2 = (rule) => { return rule.purpose === PURPOSE_2.name } /** * A configuration function that initializes some module variables, as well as add hooks From 06e46d29c25748b597009a8a3f145a28ce16da04 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Mon, 11 May 2020 18:11:40 +0530 Subject: [PATCH 06/20] add gvlid check --- modules/gdprEnforcement.js | 2 +- test/spec/modules/gdprEnforcement_spec.js | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index a4b486f639a..2062ec703e5 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -221,7 +221,7 @@ export function makeBidRequestsHook(fn, adUnits, ...args) { const currBidder = bid.bidder; const gvlId = getGvlid(currBidder); if (includes(disabledBidders, currBidder)) return false; - const isAllowed = validateRules(purpose2Rule, consentData, PURPOSE_2.id, currBidder, gvlId); + const isAllowed = gvlId && validateRules(purpose2Rule, consentData, PURPOSE_2.id, currBidder, gvlId); if (!isAllowed) { utils.logWarn(`User blocked bidder: ${currBidder}. No bid request will be sent to their endpoint.`); events.emit(EVENTS.BIDDER_BLOCKED, currBidder); diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 4401bb0d22c..3598a98225b 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -5,6 +5,7 @@ import * as utils from 'src/utils.js'; import { validateStorageEnforcement } from 'src/storageManager.js'; import { executeStorageCallbacks } from 'src/prebid.js'; import events from 'src/events.js'; +import { EVENTS } from 'src/constants.json'; describe('gdpr enforcement', function () { let nextFnSpy; @@ -438,6 +439,8 @@ describe('gdpr enforcement', function () { mediaTypes: {}, bids: [{ bidder: 'bidder_2' + }, { + bidder: 'bidder_3' }] }]; beforeEach(function () { @@ -480,6 +483,11 @@ describe('gdpr enforcement', function () { return { 'gvlid': 5 } } }); + adapterManagerStub.withArgs('bidder_3').returns({ + getSpec: function () { + return { 'gvlid': undefined } + } + }); makeBidRequestsHook(nextFnSpy, MOCK_AD_UNITS, []); // Assertions @@ -495,8 +503,10 @@ describe('gdpr enforcement', function () { mediaTypes: {}, bids: [] }], []); - expect(emitEventSpy.calledOnce).to.equal(true); - expect(logWarnSpy.calledOnce).to.equal(true); + expect(emitEventSpy.calledTwice).to.equal(true); + expect(logWarnSpy.calledTwice).to.equal(true); + sinon.assert.calledWith(emitEventSpy.firstCall, EVENTS.BIDDER_BLOCKED, 'bidder_2'); + sinon.assert.calledWith(emitEventSpy.secondCall, EVENTS.BIDDER_BLOCKED, 'bidder_3'); }); it('should skip validation checks if GDPR version is not equal to "2"', function () { From cc57db85ea185d6c99505c3044676f9a482a6b0b Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Tue, 2 Jun 2020 13:22:02 +0530 Subject: [PATCH 07/20] add unit tests for validateRules function --- test/spec/modules/gdprEnforcement_spec.js | 199 +++++++++++++++++++++- 1 file changed, 194 insertions(+), 5 deletions(-) diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 3598a98225b..7f6722ed675 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -1,4 +1,6 @@ -import { deviceAccessHook, setEnforcementConfig, userSyncHook, userIdHook, makeBidRequestsHook } from 'modules/gdprEnforcement.js'; +/* eslint no-console: 0 */ + +import { deviceAccessHook, setEnforcementConfig, userSyncHook, userIdHook, makeBidRequestsHook, validateRules } from 'modules/gdprEnforcement.js'; import { config } from 'src/config.js'; import adapterManager, { gdprDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; @@ -7,7 +9,7 @@ import { executeStorageCallbacks } from 'src/prebid.js'; import events from 'src/events.js'; import { EVENTS } from 'src/constants.json'; -describe('gdpr enforcement', function () { +describe.skip('gdpr enforcement', function () { let nextFnSpy; let logWarnSpy; let gdprDataHandlerStub; @@ -40,7 +42,7 @@ describe('gdpr enforcement', function () { }, 'legitimateInterests': { '1': false, - '2': false, + '2': true, '3': false } }, @@ -421,7 +423,7 @@ describe('gdpr enforcement', function () { }); }); - describe('makeBidRequestsHook', function () { + describe.skip('makeBidRequestsHook', function () { let sandbox; let adapterManagerStub; let emitEventSpy; @@ -456,7 +458,7 @@ describe('gdpr enforcement', function () { config.resetConfig(); sandbox.restore(); }); - it('should block bidder which does not have consent and allow bidder which has consent', function () { + it.skip('should block bidder which does not have consent and allow bidder which has consent', function () { setEnforcementConfig({ gdpr: { rules: [{ @@ -537,4 +539,191 @@ describe('gdpr enforcement', function () { expect(logInfoSpy.calledOnce).to.equal(true); }); }); + + describe('validateRules(rule, consentData, currentModule, gvlId)', function () { + const createGdprRule = (purposeName = 'storage', enforcePurpose = true, enforceVendor = true, vendorExceptions = []) => ({ + purpose: purposeName, + enforcePurpose: enforcePurpose, + enforceVendor: enforceVendor, + vendorExceptions: vendorExceptions + }); + + const consentData = { + vendorData: staticConfig.consentData.getTCData, + apiVersion: 2, + gdprApplies: true + }; + + // Bidder - 'bidderA' has vendorConsent + const vendorAllowedModule = 'bidderA'; + const vendorAllowedGvlId = 1; + + // Bidder = 'bidderB' doesn't have vendorConsent + const vendorBlockedModule = 'bidderB'; + const vendorBlockedGvlId = 3; + + const consentDataWithPurposeConsentFalse = utils.deepClone(consentData); + consentDataWithPurposeConsentFalse.vendorData.purpose.consents['1'] = false; + + it('should return true when enforcePurpose=true AND purposeConsent[p]==true AND enforceVendor[p,v]==true AND vendorConsent[v]==true', function () { + // 'enforcePurpose' and 'enforceVendor' both are 'true' + const gdprRule = createGdprRule('storage', true, true, []); + + // case 1 - Both purpose consent and vendor consent is 'true'. validateRules must return 'true' + let isAllowed = validateRules(gdprRule, consentData, vendorAllowedModule, vendorAllowedGvlId); + expect(isAllowed).to.equal(true); + + // case 2 - Purpose consent is 'true' but vendor consent is 'false'. validateRules must return 'false' + isAllowed = validateRules(gdprRule, consentData, vendorBlockedModule, vendorBlockedGvlId); + expect(isAllowed).to.equal(false); + + // case 3 - Purpose consent is 'false' but vendor consent is 'true'. validateRules must return 'false' + isAllowed = validateRules(gdprRule, consentDataWithPurposeConsentFalse, vendorAllowedModule, vendorAllowedGvlId); + expect(isAllowed).to.equal(false); + + // case 4 - Both purpose consent and vendor consent is 'false'. validateRules must return 'false' + isAllowed = validateRules(gdprRule, consentDataWithPurposeConsentFalse, vendorBlockedModule, vendorBlockedGvlId); + expect(isAllowed).to.equal(false); + }); + + it('should return true when enforcePurpose=true AND purposeConsent[p]==true AND enforceVendor[p,v]==false', function () { + // 'enforcePurpose' is 'true' and 'enforceVendor' is 'false' + const gdprRule = createGdprRule('storage', true, false, []); + + // case 1 - Both purpose consent and vendor consent is 'true'. validateRules must return 'true' + let isAllowed = validateRules(gdprRule, consentData, vendorAllowedModule, vendorAllowedGvlId); + expect(isAllowed).to.equal(true); + + // case 2 - Purpose consent is 'true' but vendor consent is 'false'. validateRules must return 'true' because vendorConsent doens't matter + isAllowed = validateRules(gdprRule, consentData, vendorBlockedModule, vendorBlockedGvlId); + expect(isAllowed).to.equal(true); + + // case 3 - Purpose consent is 'false' but vendor consent is 'true'. validateRules must return 'false' because vendorConsent doesn't matter + isAllowed = validateRules(gdprRule, consentDataWithPurposeConsentFalse, vendorAllowedModule, vendorAllowedGvlId); + expect(isAllowed).to.equal(false); + + // case 4 - Both purpose consent and vendor consent is 'false'. validateRules must return 'false' and vendorConsent doesn't matter + isAllowed = validateRules(gdprRule, consentDataWithPurposeConsentFalse, vendorBlockedModule, vendorAllowedGvlId); + expect(isAllowed).to.equal(false); + }); + + it('should return true when enforcePurpose=false AND enforceVendor[p,v]==true AND vendorConsent[v]==true', function () { + // 'enforcePurpose' is 'false' and 'enforceVendor' is 'true' + const gdprRule = createGdprRule('storage', false, true, []); + + // case 1 - Both purpose consent and vendor consent is 'true'. validateRules must return 'true' + let isAllowed = validateRules(gdprRule, consentData, vendorAllowedModule, vendorAllowedGvlId); + expect(isAllowed).to.equal(true); + + // case 2 - Purpose consent is 'true' but vendor consent is 'false'. validateRules must return 'false' because purposeConsent doesn't matter + isAllowed = validateRules(gdprRule, consentData, vendorBlockedModule, vendorBlockedGvlId); + expect(isAllowed).to.equal(false); + + // case 3 - urpose consent is 'false' but vendor consent is 'true'. validateRules must return 'true' because purposeConsent doesn't matter + isAllowed = validateRules(gdprRule, consentDataWithPurposeConsentFalse, vendorAllowedModule, vendorAllowedGvlId); + expect(isAllowed).to.equal(true); + + // case 4 - Both purpose consent and vendor consent is 'false'. validateRules must return 'false' and purposeConsent doesn't matter + isAllowed = validateRules(gdprRule, consentDataWithPurposeConsentFalse, vendorBlockedModule, vendorBlockedGvlId); + expect(isAllowed).to.equal(false); + }); + + it('should return true when enforcePurpose=false AND enforceVendor[p,v]==false', function () { + // 'enforcePurpose' is 'false' and 'enforceVendor' is 'false' + const gdprRule = createGdprRule('storage', false, false, []); + + // case 1 - Both purpose consent and vendor consent is 'true'. validateRules must return 'true', both the consents do not matter. + let isAllowed = validateRules(gdprRule, consentData, vendorAllowedModule, vendorAllowedGvlId); + expect(isAllowed).to.equal(true); + + // case 2 - Purpose consent is 'true' but vendor consent is 'false'. validateRules must return 'true', both the consents do not matter. + isAllowed = validateRules(gdprRule, consentData, vendorBlockedModule, vendorBlockedGvlId); + expect(isAllowed).to.equal(true); + + // case 3 - urpose consent is 'false' but vendor consent is 'true'. validateRules must return 'true', both the consents do not matter. + isAllowed = validateRules(gdprRule, consentDataWithPurposeConsentFalse, vendorAllowedModule, vendorAllowedGvlId); + expect(isAllowed).to.equal(true); + + // case 4 - Both purpose consent and vendor consent is 'false'. validateRules must return 'true', both the consents do not matter. + isAllowed = validateRules(gdprRule, consentDataWithPurposeConsentFalse, vendorBlockedModule, vendorBlockedGvlId); + expect(isAllowed).to.equal(true); + }); + + it('should return true when "vendorExceptions" contains the name of the vendor under test', function () { + // 'vendorExceptions' contains 'bidderB' which doesn't have vendor consent. + const gdprRule = createGdprRule('storage', false, true, [vendorBlockedModule]); + + /* 'bidderB' gets a free pass since it's included in the 'vendorExceptions' array. validateRules must disregard + user's choice for purpose and vendor consent and return 'true' for this bidder(s) */ + const isAllowed = validateRules(gdprRule, consentData, vendorBlockedModule, vendorBlockedGvlId); + expect(isAllowed).to.equal(true); + }); + + describe('Purpose 2 special case', function () { + const consentDataWithLIFalse = utils.deepClone(consentData); + consentDataWithLIFalse.vendorData.purpose.legitimateInterests['2'] = false; + + const consentDataWithPurposeConsentFalse = utils.deepClone(consentData); + consentDataWithPurposeConsentFalse.vendorData.purpose.consents['2'] = false; + + const consentDataWithPurposeConsentFalseAndLIFalse = utils.deepClone(consentData); + consentDataWithPurposeConsentFalseAndLIFalse.vendorData.purpose.legitimateInterests['2'] = false; + consentDataWithPurposeConsentFalseAndLIFalse.vendorData.purpose.consents['2'] = false; + + it('should return true when (enforcePurpose=true AND purposeConsent[p]===true AND enforceVendor[p.v]===true AND vendorConsent[v]===true) OR (purposesLITransparency[p]===true)', function () { + // both 'enforcePurpose' and 'enforceVendor' is 'true' + const gdprRule = createGdprRule('basicAds', true, true, []); + + // case 1 - Both purpose consent and vendor consent is 'true', but legitimateInterests for purpose 2 is 'false'. validateRules must return 'true'. + let isAllowed = validateRules(gdprRule, consentDataWithLIFalse, vendorAllowedModule, vendorAllowedGvlId); + expect(isAllowed).to.equal(true); + + // case 2 - Purpose consent is 'true' but vendor consent is 'false', but legitimateInterests for purpose 2 is 'true'. validateRules must return 'true'. + isAllowed = validateRules(gdprRule, consentData, vendorBlockedModule, vendorBlockedGvlId); + expect(isAllowed).to.equal(true); + + // case 3 - Purpose consent is 'true' and vendor consent is 'true', as well as legitimateInterests for purpose 2 is 'true'. validateRules must return 'true'. + isAllowed = validateRules(gdprRule, consentData, vendorAllowedModule, vendorAllowedGvlId); + expect(isAllowed).to.equal(true); + + // case 4 - Purpose consent is 'true' and vendor consent is 'false', and legitimateInterests for purpose 2 is 'false'. validateRules must return 'false'. + isAllowed = validateRules(gdprRule, consentDataWithLIFalse, vendorBlockedModule, vendorBlockedGvlId); + expect(isAllowed).to.equal(false); + }); + + it('should return true when (enforcePurpose=true AND purposeConsent[p]===true AND enforceVendor[p.v]===false) OR (purposesLITransparency[p]===true)', function () { + // 'enforcePurpose' is 'true' and 'enforceVendor' is 'false' + const gdprRule = createGdprRule('basicAds', true, false, []); + + // case 1 - Purpose consent is 'true', vendor consent doesn't matter and legitimateInterests for purpose 2 is 'true'. validateRules must return 'true'. + let isAllowed = validateRules(gdprRule, consentData, vendorBlockedModule, vendorBlockedGvlId); + expect(isAllowed).to.equal(true); + + // case 2 - Purpose consent is 'false', vendor consent doesn't matter and legitimateInterests for purpose 2 is 'true'. validateRules must return 'true'. + isAllowed = validateRules(gdprRule, consentDataWithPurposeConsentFalse, vendorAllowedModule, vendorAllowedGvlId); + expect(isAllowed).to.equal(true); + + // case 3 - Purpose consent is 'false', vendor consent doesn't matter and legitimateInterests for purpose 2 is 'false'. validateRules must return 'false'. + isAllowed = validateRules(gdprRule, consentDataWithPurposeConsentFalseAndLIFalse, vendorAllowedModule, vendorAllowedGvlId); + expect(isAllowed).to.equal(false); + }); + + it('should return true when (enforcePurpose=false AND enforceVendor[p,v]===true AND vendorConsent[v]===true) OR (purposesLITransparency[p]===true)', function() { + // 'enforcePurpose' is 'false' and 'enforceVendor' is 'true' + const gdprRule = createGdprRule('basicAds', false, true, []); + + // case - 1 Vendor consent is 'true', purpose consent doesn't matter and legitimateInterests for purpose 2 is 'true'. validateRules must return 'true'. + let isAllowed = validateRules(gdprRule, consentData, vendorAllowedModule, vendorAllowedGvlId); + expect(isAllowed).to.equal(true); + + // case 2 - Vendor consent is 'false', purpose consent doesn't matter and legitimateInterests for purpose 2 is 'true'. validateRules must return 'true'. + isAllowed = validateRules(gdprRule, consentData, vendorBlockedModule, vendorBlockedGvlId); + expect(isAllowed).to.equal(true); + + // case 3 - Vendor consent is 'false', purpose consent doesn't matter and legitimateInterests for purpose 2 is 'false'. validateRules must return 'false'. + isAllowed = validateRules(gdprRule, consentDataWithLIFalse, vendorBlockedModule, vendorBlockedGvlId); + expect(isAllowed).to.equal(false); + }); + }); + }) }); From 9b98e98535c26d2dc0e218f4c72e37bda601e642 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Tue, 2 Jun 2020 13:24:29 +0530 Subject: [PATCH 08/20] remove purposeId parameter from validateRules function --- integrationExamples/gpt/tcf2-test-page.html | 481 ++++++++++++++++++++ karma.conf.maker.js | 176 ------- modules/gdprEnforcement.js | 63 ++- 3 files changed, 519 insertions(+), 201 deletions(-) create mode 100644 integrationExamples/gpt/tcf2-test-page.html delete mode 100644 karma.conf.maker.js diff --git a/integrationExamples/gpt/tcf2-test-page.html b/integrationExamples/gpt/tcf2-test-page.html new file mode 100644 index 00000000000..2850b5307b1 --- /dev/null +++ b/integrationExamples/gpt/tcf2-test-page.html @@ -0,0 +1,481 @@ + + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+
+ +
+ + + diff --git a/karma.conf.maker.js b/karma.conf.maker.js deleted file mode 100644 index 290d31aa2a5..00000000000 --- a/karma.conf.maker.js +++ /dev/null @@ -1,176 +0,0 @@ -// This configures Karma, describing how to run the tests and where to output code coverage reports. -// -// For more information, see http://karma-runner.github.io/1.0/config/configuration-file.html - -var _ = require('lodash'); -var webpackConf = require('./webpack.conf'); -var karmaConstants = require('karma').constants; - -function newWebpackConfig(codeCoverage) { - // Make a clone here because we plan on mutating this object, and don't want parallel tasks to trample each other. - var webpackConfig = _.cloneDeep(webpackConf); - - // remove optimize plugin for tests - webpackConfig.plugins.pop() - - webpackConfig.devtool = 'inline-source-map'; - - if (codeCoverage) { - webpackConfig.module.rules.push({ - enforce: 'post', - exclude: /(node_modules)|(test)|(integrationExamples)|(build)|polyfill.js|(src\/adapters\/analytics\/ga.js)/, - use: { - loader: 'istanbul-instrumenter-loader', - options: { esModules: true } - }, - test: /\.js$/ - }) - } - return webpackConfig; -} - -function newPluginsArray(browserstack) { - var plugins = [ - 'karma-chrome-launcher', - 'karma-coverage', - 'karma-es5-shim', - 'karma-mocha', - 'karma-chai', - 'karma-sinon', - 'karma-sourcemap-loader', - 'karma-spec-reporter', - 'karma-webpack', - 'karma-mocha-reporter' - ]; - if (browserstack) { - plugins.push('karma-browserstack-launcher'); - } - plugins.push('karma-firefox-launcher'); - plugins.push('karma-opera-launcher'); - plugins.push('karma-safari-launcher'); - plugins.push('karma-script-launcher'); - plugins.push('karma-ie-launcher'); - return plugins; -} - -function setReporters(karmaConf, codeCoverage, browserstack) { - // In browserstack, the default 'progress' reporter floods the logs. - // The karma-spec-reporter reports failures more concisely - if (browserstack) { - karmaConf.reporters = ['spec']; - karmaConf.specReporter = { - maxLogLines: 100, - suppressErrorSummary: false, - suppressSkipped: false, - suppressPassed: true - }; - } - - if (codeCoverage) { - karmaConf.reporters.push('coverage'); - karmaConf.coverageReporter = { - dir: 'build/coverage', - reporters: [ - { type: 'lcov', subdir: '.' } - ] - }; - } -} - -function setBrowsers(karmaConf, browserstack) { - if (browserstack) { - karmaConf.browserStack = { - username: process.env.BROWSERSTACK_USERNAME, - accessKey: process.env.BROWSERSTACK_ACCESS_KEY, - build: 'Prebidjs Unit Tests ' + new Date().toLocaleString() - } - if (process.env.TRAVIS) { - karmaConf.browserStack.startTunnel = false; - karmaConf.browserStack.tunnelIdentifier = process.env.BROWSERSTACK_LOCAL_IDENTIFIER; - } - karmaConf.customLaunchers = require('./browsers.json'); - karmaConf.browsers = Object.keys(karmaConf.customLaunchers); - } else { - var isDocker = require('is-docker')(); - if (isDocker) { - karmaConf.customLaunchers = karmaConf.customLaunchers || {}; - karmaConf.customLaunchers.ChromeCustom = { - base: 'ChromeHeadless', - // We must disable the Chrome sandbox when running Chrome inside Docker (Chrome's sandbox needs - // more permissions than Docker allows by default) - flags: ['--no-sandbox'] - } - karmaConf.browsers = ['ChromeCustom']; - } else { - karmaConf.browsers = ['ChromeHeadless']; - } - } -} - -module.exports = function(codeCoverage, browserstack, watchMode, file) { - var webpackConfig = newWebpackConfig(codeCoverage); - var plugins = newPluginsArray(browserstack); - - var files = file ? ['test/helpers/prebidGlobal.js', file] : ['test/test_index.js']; - // This file opens the /debug.html tab automatically. - // It has no real value unless you're running --watch, and intend to do some debugging in the browser. - if (watchMode) { - files.push('test/helpers/karma-init.js'); - } - - var config = { - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: './', - - webpack: webpackConfig, - webpackMiddleware: { - stats: 'errors-only', - noInfo: true - }, - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['es5-shim', 'mocha', 'chai', 'sinon'], - - files: files, - - // preprocess matching files before serving them to the browser - // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - preprocessors: { - 'test/test_index.js': ['webpack', 'sourcemap'] - }, - - // web server port - port: 9876, - - // enable / disable colors in the output (reporters and logs) - colors: true, - - // level of logging - // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG - logLevel: karmaConstants.LOG_INFO, - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, - - reporters: ['mocha'], - - mochaReporter: { - showDiff: true, - output: 'minimal' - }, - - // Continuous Integration mode - // if true, Karma captures browsers, runs the tests and exits - singleRun: !watchMode, - browserDisconnectTimeout: 3e5, // default 2000 - browserNoActivityTimeout: 3e5, // default 10000 - captureTimeout: 3e5, // default 60000, - browserDisconnectTolerance: 3, - concurrency: 5, - - plugins: plugins - } - setReporters(config, codeCoverage, browserstack); - setBrowsers(config, browserstack); - return config; -} diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 0e52787e6b3..40b21db2706 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -14,15 +14,18 @@ import { validateStorageEnforcement } from '../src/storageManager.js'; import events from '../src/events.js'; import { EVENTS } from '../src/constants.json'; -const PURPOSE_1 = { - id: 1, - name: 'storage' +const TCF2 = { + 'purpose1': { id: 1, name: 'storage' }, + 'purpose2': { id: 2, name: 'basicAds' } } -const PURPOSE_2 = { - id: 2, - name: 'basicAds' -} +const defaultRules = [{ + purpose: 'storage', + enforcePurpose: true +}, { + purpose: 'basicAds', + enforcePurpose: true +}]; let purpose1Rule; let purpose2Rule; @@ -47,20 +50,29 @@ function getGvlid(bidderCode) { * This function takes in rules and consentData as input and validates against the consentData provided. If it returns true Prebid will allow the next call else it will log a warning * @param {Object} rule - enforcement rules set in config * @param {Object} consentData - gdpr consent data - * @param {number} purposeId - Defines which purpose (1, 2, 7) is under check * @param {string=} currentModule - Bidder code of the current module - * @param {number=} gvlid - GVL ID for the module + * @param {number=} gvlId - GVL ID for the module * @returns {boolean} */ -function validateRules(rule, consentData, purposeId, currentModule, gvlid) { - // if vendor has exception => always true +export function validateRules(rule, consentData, currentModule, gvlId) { + const purposeId = TCF2[Object.keys(TCF2).filter(purposeName => TCF2[purposeName].name === rule.purpose)[0]].id; + + // return 'true' if vendor present in 'vendorExceptions' if (includes(rule.vendorExceptions || [], currentModule)) { return true; } - // if enforcePurpose is false or purpose was granted isAllowed is true, otherwise false - const purposeAllowed = rule.enforcePurpose === false || utils.deepAccess(consentData, `vendorData.purpose.consents.${purposeId}`) === true; - // if enforceVendor is false or vendor was granted isAllowed is true, otherwise false - const vendorAllowed = rule.enforceVendor === false || utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlid}`) === true; + + const purposeConsent = utils.deepAccess(consentData, `vendorData.purpose.consents.${purposeId}`); + const vendorConsent = utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlId}`); + const liTransparency = utils.deepAccess(consentData, `vendorData.purpose.legitimateInterests.${purposeId}`); + + const purposeAllowed = rule.enforcePurpose === false || purposeConsent === true; + const vendorAllowed = rule.enforceVendor === false || vendorConsent === true; + + if (purposeId === 2) { + return (purposeAllowed && vendorAllowed) || (liTransparency === true); + } + return purposeAllowed && vendorAllowed; } @@ -86,12 +98,12 @@ export function deviceAccessHook(fn, gvlid, moduleName, result) { gvlid = getGvlid(); } const curModule = moduleName || config.getCurrentBidder(); - let isAllowed = validateRules(purpose1Rule, consentData, PURPOSE_1.id, curModule, gvlid); + let isAllowed = validateRules(purpose1Rule, consentData, curModule, gvlid); if (isAllowed) { result.valid = true; fn.call(this, gvlid, moduleName, result); } else { - utils.logWarn(`User denied Permission for Device access for ${curModule}`); + curModule && utils.logWarn(`User denied Permission for Device access for ${curModule}`); result.valid = false; fn.call(this, gvlid, moduleName, result); } @@ -119,7 +131,7 @@ export function userSyncHook(fn, ...args) { const gvlid = getGvlid(); const curBidder = config.getCurrentBidder(); if (gvlid) { - let isAllowed = validateRules(purpose1Rule, consentData, PURPOSE_1.id, curBidder, gvlid); + let isAllowed = validateRules(purpose1Rule, consentData, curBidder, gvlid); if (isAllowed) { fn.call(this, ...args); } else { @@ -150,7 +162,7 @@ export function userIdHook(fn, submodules, consentData) { const gvlid = submodule.submodule.gvlid; const moduleName = submodule.submodule.name; if (gvlid) { - let isAllowed = validateRules(purpose1Rule, consentData, PURPOSE_1.id, moduleName, gvlid); + let isAllowed = validateRules(purpose1Rule, consentData, moduleName, gvlid); if (isAllowed) { return submodule; } else { @@ -187,7 +199,7 @@ export function makeBidRequestsHook(fn, adUnits, ...args) { const currBidder = bid.bidder; const gvlId = getGvlid(currBidder); if (includes(disabledBidders, currBidder)) return false; - const isAllowed = gvlId && validateRules(purpose2Rule, consentData, PURPOSE_2.id, currBidder, gvlId); + const isAllowed = gvlId && validateRules(purpose2Rule, consentData, currBidder, gvlId); if (!isAllowed) { utils.logWarn(`User blocked bidder: ${currBidder}. No bid request will be sent to their endpoint.`); events.emit(EVENTS.BIDDER_BLOCKED, currBidder); @@ -206,8 +218,8 @@ export function makeBidRequestsHook(fn, adUnits, ...args) { } } -const hasPurpose1 = (rule) => { return rule.purpose === PURPOSE_1.name } -const hasPurpose2 = (rule) => { return rule.purpose === PURPOSE_2.name } +const hasPurpose1 = (rule) => { return rule.purpose === TCF2.purpose1.name } +const hasPurpose2 = (rule) => { return rule.purpose === TCF2.purpose2.name } /** * A configuration function that initializes some module variables, as well as add hooks @@ -216,11 +228,12 @@ const hasPurpose2 = (rule) => { return rule.purpose === PURPOSE_2.name } export function setEnforcementConfig(config) { const rules = utils.deepAccess(config, 'gdpr.rules'); if (!rules) { - utils.logWarn('GDPR enforcement rules not defined, exiting enforcement module'); - return; + utils.logWarn('GDPR enforcement rules not defined, enforcing purpose1 and purpose2 by default'); + enforcementRules = defaultRules; + } else { + enforcementRules = rules; } - enforcementRules = rules; purpose1Rule = find(enforcementRules, hasPurpose1); purpose2Rule = find(enforcementRules, hasPurpose2); From 859d7823f788b03bc6aad1baabcd88a513fea72e Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Thu, 4 Jun 2020 18:47:25 +0530 Subject: [PATCH 09/20] add extra tests --- modules/gdprEnforcement.js | 24 +++++++++++++---------- test/spec/modules/gdprEnforcement_spec.js | 16 +++++++++++---- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 40b21db2706..6468e1484f0 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -19,12 +19,16 @@ const TCF2 = { 'purpose2': { id: 2, name: 'basicAds' } } -const defaultRules = [{ +const DEFAULT_RULES = [{ purpose: 'storage', - enforcePurpose: true + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] }, { purpose: 'basicAds', - enforcePurpose: true + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] }]; let purpose1Rule; @@ -47,7 +51,7 @@ function getGvlid(bidderCode) { } /** - * This function takes in rules and consentData as input and validates against the consentData provided. If it returns true Prebid will allow the next call else it will log a warning + * This function takes in a rule and consentData and validates against the consentData provided. If it returns true Prebid will allow the next call else it will log a warning * @param {Object} rule - enforcement rules set in config * @param {Object} consentData - gdpr consent data * @param {string=} currentModule - Bidder code of the current module @@ -173,7 +177,7 @@ export function userIdHook(fn, submodules, consentData) { } return undefined; }).filter(module => module) - fn.call(this, userIdModules, consentData); + fn.call(this, userIdModules, {...consentData, hasValidated: true}); } else { utils.logInfo('Enforcing TCF2 only'); fn.call(this, submodules, consentData); @@ -184,8 +188,8 @@ export function userIdHook(fn, submodules, consentData) { } /** - * Checks if the bidder is given consent. If yes, bid adapter is allowed to send ajax request to their endpoint, else, no request - * is sent. Enforces "purpose 2 (basic ads)" of TCF v2.0 spec + * Checks if a bidder is allowed. If it's not allowed, the bidder adapter won't send request to the endpoint. + * Enforces "purpose 2 (basic ads)" of TCF v2.0 spec * @param {Function} fn - Function reference to the original function. * @param {Array} adUnits */ @@ -228,8 +232,8 @@ const hasPurpose2 = (rule) => { return rule.purpose === TCF2.purpose2.name } export function setEnforcementConfig(config) { const rules = utils.deepAccess(config, 'gdpr.rules'); if (!rules) { - utils.logWarn('GDPR enforcement rules not defined, enforcing purpose1 and purpose2 by default'); - enforcementRules = defaultRules; + utils.logWarn('GDPR enforcement rules not defined, enforcing TCF2 Purpose 1 and Purpose 2'); + enforcementRules = DEFAULT_RULES; } else { enforcementRules = rules; } @@ -245,7 +249,7 @@ export function setEnforcementConfig(config) { getHook('validateGdprEnforcement').before(userIdHook, 47); } if (purpose2Rule) { - adapterManager.makeBidRequests.before(makeBidRequestsHook); + getHook('makeBidRequests').before(makeBidRequestsHook); } } diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 7f6722ed675..c6bcb12d442 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -9,7 +9,7 @@ import { executeStorageCallbacks } from 'src/prebid.js'; import events from 'src/events.js'; import { EVENTS } from 'src/constants.json'; -describe.skip('gdpr enforcement', function () { +describe('gdpr enforcement', function () { let nextFnSpy; let logWarnSpy; let gdprDataHandlerStub; @@ -369,7 +369,11 @@ describe.skip('gdpr enforcement', function () { } }] 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}); }); it('should allow userId module if gdpr not in scope', function () { @@ -381,6 +385,9 @@ describe.skip('gdpr enforcement', function () { }]; 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); }); @@ -400,6 +407,7 @@ describe.skip('gdpr enforcement', function () { consentData.vendorData = staticConfig.consentData.getTCData; consentData.apiVersion = 2; consentData.gdprApplies = true; + let submodules = [{ submodule: { gvlid: 1, @@ -419,7 +427,7 @@ describe.skip('gdpr enforcement', function () { name: 'sampleUserId' } }] - sinon.assert.calledWith(nextFnSpy, expectedSubmodules, consentData); + sinon.assert.calledWith(nextFnSpy, expectedSubmodules, {...consentData, hasValidated: true}); }); }); @@ -458,7 +466,7 @@ describe.skip('gdpr enforcement', function () { config.resetConfig(); sandbox.restore(); }); - it.skip('should block bidder which does not have consent and allow bidder which has consent', function () { + it('should block bidder which does not have consent and allow bidder which has consent', function () { setEnforcementConfig({ gdpr: { rules: [{ @@ -708,7 +716,7 @@ describe.skip('gdpr enforcement', function () { expect(isAllowed).to.equal(false); }); - it('should return true when (enforcePurpose=false AND enforceVendor[p,v]===true AND vendorConsent[v]===true) OR (purposesLITransparency[p]===true)', function() { + it('should return true when (enforcePurpose=false AND enforceVendor[p,v]===true AND vendorConsent[v]===true) OR (purposesLITransparency[p]===true)', function () { // 'enforcePurpose' is 'false' and 'enforceVendor' is 'true' const gdprRule = createGdprRule('basicAds', false, true, []); From 1a0f5a7baa2b645f186430b1f2fd0dba0e8ac9c3 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Thu, 4 Jun 2020 19:46:38 +0530 Subject: [PATCH 10/20] make failing unit test case pass --- test/spec/modules/gdprEnforcement_spec.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index c6bcb12d442..419523b2f52 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -431,7 +431,7 @@ describe('gdpr enforcement', function () { }); }); - describe.skip('makeBidRequestsHook', function () { + describe('makeBidRequestsHook', function () { let sandbox; let adapterManagerStub; let emitEventSpy; @@ -442,7 +442,7 @@ describe('gdpr enforcement', function () { bids: [{ bidder: 'bidder_1' // has consent }, { - bidder: 'bidder_2' // doesn't have consent + bidder: 'bidder_2' // doesn't have consent, but liTransparency is true. Bidder remains active. }] }, { code: 'ad-unit-2', @@ -506,17 +506,19 @@ describe('gdpr enforcement', function () { code: 'ad-unit-1', mediaTypes: {}, bids: [ - sinon.match({ bidder: 'bidder_1' }) + sinon.match({ bidder: 'bidder_1' }), + sinon.match({ bidder: 'bidder_2' }) ] }, { code: 'ad-unit-2', mediaTypes: {}, - bids: [] + bids: [ + sinon.match({ bidder: 'bidder_2' }) + ] }], []); - expect(emitEventSpy.calledTwice).to.equal(true); - expect(logWarnSpy.calledTwice).to.equal(true); - sinon.assert.calledWith(emitEventSpy.firstCall, EVENTS.BIDDER_BLOCKED, 'bidder_2'); - sinon.assert.calledWith(emitEventSpy.secondCall, EVENTS.BIDDER_BLOCKED, 'bidder_3'); + expect(emitEventSpy.calledOnce).to.equal(true); + expect(logWarnSpy.calledOnce).to.equal(true); + sinon.assert.calledWith(emitEventSpy, EVENTS.BIDDER_BLOCKED, 'bidder_3'); }); it('should skip validation checks if GDPR version is not equal to "2"', function () { From ae9a237f33e937a8ff321091bcb8fd78b95ff92e Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Fri, 5 Jun 2020 17:34:25 +0530 Subject: [PATCH 11/20] deprecate allowAuctionWithouConsent with tcf 2 workflow --- modules/consentManagement.js | 22 ++++++--- test/spec/modules/consentManagement_spec.js | 52 ++++++++------------- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 53e97006bd1..fcf16f161f2 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -14,9 +14,12 @@ const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true; +export const allowAuction = { + value: DEFAULT_ALLOW_AUCTION_WO_CONSENT, + definedInConfig: false +} export let userCMP; export let consentTimeout; -export let allowAuction; export let gdprScope; export let staticConsentData; @@ -322,6 +325,13 @@ function processCmpData(consentObject, hookConfig) { // determine which set of checks to run based on cmpVersion let checkFn = (cmpVersion === 1) ? checkV1Data : (cmpVersion === 2) ? checkV2Data : null; + // Raise deprecation warning if 'allowAuctionWithoutConsent' is used with TCF 2. + if (allowAuction.definedInConfig && cmpVersion === 2) { + utils.logWarn(`consentManagement config is using deprecated property 'allowAuctionWithoutConsent' with TCF 2 setup. This property only works with TCF 1.1`); + } else if (!allowAuction.definedInConfig && cmpVersion === 1) { + utils.logInfo(`consentManagement config did not specify allowAuctionWithoutConsent. Using system default setting (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); + } + if (utils.isFn(checkFn)) { if (checkFn(consentObject)) { cmpFailed(`CMP returned unexpected value during lookup process.`, hookConfig, consentObject); @@ -352,7 +362,7 @@ function cmpFailed(errMsg, hookConfig, extraArgs) { clearTimeout(hookConfig.timer); // still set the consentData to undefined when there is a problem as per config options - if (allowAuction) { + if (allowAuction.value && cmpVersion === 1) { storeConsentData(undefined); } exitModule(errMsg, hookConfig, extraArgs); @@ -406,7 +416,7 @@ function exitModule(errMsg, hookConfig, extraArgs) { let nextFn = hookConfig.nextFn; if (errMsg) { - if (allowAuction) { + if (allowAuction.value && cmpVersion === 1) { utils.logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.', extraArgs); nextFn.apply(context, args); } else { @@ -460,10 +470,8 @@ export function setConsentConfig(config) { } if (typeof config.allowAuctionWithoutConsent === 'boolean') { - allowAuction = config.allowAuctionWithoutConsent; - } else { - allowAuction = DEFAULT_ALLOW_AUCTION_WO_CONSENT; - utils.logInfo(`consentManagement config did not specify allowAuctionWithoutConsent. Using system default setting (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); + allowAuction.value = config.allowAuctionWithoutConsent; + allowAuction.definedInConfig = true; } // if true, then gdprApplies should be set to true diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index c4f6fe70dd1..3ebebfef1ee 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -24,9 +24,8 @@ describe('consentManagement', function () { setConsentConfig({}); expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(10000); - expect(allowAuction).to.be.true; expect(gdprScope).to.be.equal(false); - sinon.assert.callCount(utils.logInfo, 4); + sinon.assert.callCount(utils.logInfo, 3); }); it('should exit consent manager if config is not an object', function () { @@ -58,7 +57,10 @@ describe('consentManagement', function () { setConsentConfig(allConfig); expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(7500); - expect(allowAuction).to.be.false; + expect(allowAuction).to.deep.equal({ + value: false, + definedInConfig: true + }); expect(gdprScope).to.be.true; }); @@ -110,7 +112,10 @@ describe('consentManagement', function () { expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(3333); - expect(allowAuction).to.be.equal(false); + expect(allowAuction).to.deep.equal({ + value: false, + definedInConfig: true + }); expect(gdprScope).to.be.equal(false); }); }); @@ -164,7 +169,10 @@ describe('consentManagement', function () { setConsentConfig(staticConfig); expect(userCMP).to.be.equal('static'); expect(consentTimeout).to.be.equal(0); // should always return without a timeout when config is used - expect(allowAuction).to.be.false; + expect(allowAuction).to.deep.equal({ + value: false, + definedInConfig: true + }); expect(staticConsentData).to.be.equal(staticConfig.consentData); }); @@ -244,7 +252,10 @@ describe('consentManagement', function () { setConsentConfig(staticConfig); expect(userCMP).to.be.equal('static'); expect(consentTimeout).to.be.equal(0); // should always return without a timeout when config is used - expect(allowAuction).to.be.false; + expect(allowAuction).to.deep.equal({ + value: false, + definedInConfig: true + }); expect(gdprScope).to.be.equal(false); expect(staticConsentData).to.be.equal(staticConfig.consentData); }); @@ -423,7 +434,6 @@ describe('consentManagement', function () { setConsentConfig(goodConfigWithAllowAuction); requestBidsHook(() => { let consent = gdprDataHandler.getConsentData(); - sinon.assert.notCalled(utils.logWarn); sinon.assert.notCalled(utils.logError); expect(consent.consentString).to.equal(tarConsentString); expect(consent.gdprApplies).to.be.true; @@ -626,7 +636,6 @@ describe('consentManagement', function () { didHookReturn = true; }, {}); let consent = gdprDataHandler.getConsentData(); - sinon.assert.notCalled(utils.logWarn); sinon.assert.notCalled(utils.logError); expect(didHookReturn).to.be.true; expect(consent.consentString).to.equal(testConsentData.tcString); @@ -634,7 +643,7 @@ describe('consentManagement', function () { expect(consent.apiVersion).to.equal(2); }); - it('throws an error when processCmpData check failed while config had allowAuction set to false', function () { + it('throws an error when processCmpData check fails + does not call requestBids callbcack even when allowAuction is true', function () { let testConsentData = {}; let bidsBackHandlerReturn = false; @@ -642,7 +651,7 @@ describe('consentManagement', function () { args[2](testConsentData); }); - setConsentConfig(goodConfigWithCancelAuction); + setConsentConfig(goodConfigWithAllowAuction); requestBidsHook(() => { didHookReturn = true; @@ -650,6 +659,7 @@ describe('consentManagement', function () { let consent = gdprDataHandler.getConsentData(); sinon.assert.calledOnce(utils.logError); + sinon.assert.notCalled(utils.logWarn); expect(didHookReturn).to.be.false; expect(bidsBackHandlerReturn).to.be.true; expect(consent).to.be.null; @@ -676,34 +686,12 @@ describe('consentManagement', function () { didHookReturn = true; }, {}); let consent = gdprDataHandler.getConsentData(); - sinon.assert.notCalled(utils.logWarn); sinon.assert.notCalled(utils.logError); expect(didHookReturn).to.be.true; expect(consent.consentString).to.equal(testConsentData.tcString); expect(consent.gdprApplies).to.be.true; expect(consent.apiVersion).to.equal(2); }); - - it('throws a warning + stores consentData + calls callback when processCmpData check failed while config had allowAuction set to true', function () { - let testConsentData = {}; - - cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { - args[2](testConsentData); - }); - - setConsentConfig(goodConfigWithAllowAuction); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); - let consent = gdprDataHandler.getConsentData(); - - sinon.assert.calledOnce(utils.logWarn); - expect(didHookReturn).to.be.true; - expect(consent.consentString).to.be.undefined; - expect(consent.gdprApplies).to.be.false; - expect(consent.apiVersion).to.equal(2); - }); }); }); }); From 82e80a8df5ee39f148850f994a3df7b4a568c319 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Fri, 5 Jun 2020 17:35:10 +0530 Subject: [PATCH 12/20] add extra checks for defaults --- modules/gdprEnforcement.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 6468e1484f0..7839c92e952 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -188,7 +188,7 @@ export function userIdHook(fn, submodules, consentData) { } /** - * Checks if a bidder is allowed. If it's not allowed, the bidder adapter won't send request to the endpoint. + * Checks if a bidder is allowed. If it's not allowed, the bidder adapter won't send request to their endpoint. * Enforces "purpose 2 (basic ads)" of TCF v2.0 spec * @param {Function} fn - Function reference to the original function. * @param {Array} adUnits @@ -226,7 +226,7 @@ const hasPurpose1 = (rule) => { return rule.purpose === TCF2.purpose1.name } const hasPurpose2 = (rule) => { return rule.purpose === TCF2.purpose2.name } /** - * A configuration function that initializes some module variables, as well as add hooks + * A configuration function that initializes some module variables, as well as adds hooks * @param {Object} config - GDPR enforcement config object */ export function setEnforcementConfig(config) { @@ -241,6 +241,14 @@ export function setEnforcementConfig(config) { purpose1Rule = find(enforcementRules, hasPurpose1); purpose2Rule = find(enforcementRules, hasPurpose2); + if (!purpose1Rule) { + purpose1Rule = DEFAULT_RULES[0]; + } + + if (!purpose2Rule) { + purpose2Rule = DEFAULT_RULES[1]; + } + if (purpose1Rule && !addedDeviceAccessHook) { addedDeviceAccessHook = true; validateStorageEnforcement.before(deviceAccessHook, 49); From 5311bac326b940e0db8258eafc95b9f5be867818 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Fri, 5 Jun 2020 17:39:26 +0530 Subject: [PATCH 13/20] remove tcf 2 test page --- integrationExamples/gpt/tcf2-test-page.html | 481 -------------------- 1 file changed, 481 deletions(-) delete mode 100644 integrationExamples/gpt/tcf2-test-page.html diff --git a/integrationExamples/gpt/tcf2-test-page.html b/integrationExamples/gpt/tcf2-test-page.html deleted file mode 100644 index 2850b5307b1..00000000000 --- a/integrationExamples/gpt/tcf2-test-page.html +++ /dev/null @@ -1,481 +0,0 @@ - - - - - - - - - - - - - - -

Prebid.js Test

-
Div-1
-
- -
- - - From 358a5560d618469f0d4026d7c6bcb6115340901f Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Mon, 8 Jun 2020 19:51:06 +0530 Subject: [PATCH 14/20] add strict gvlid check --- modules/gdprEnforcement.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 7839c92e952..1d631eb13f9 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -44,8 +44,6 @@ function getGvlid(bidderCode) { if (bidder && bidder.getSpec) { gvlid = bidder.getSpec().gvlid; } - } else { - utils.logWarn('Current module not found'); } return gvlid; } @@ -102,7 +100,7 @@ export function deviceAccessHook(fn, gvlid, moduleName, result) { gvlid = getGvlid(); } const curModule = moduleName || config.getCurrentBidder(); - let isAllowed = validateRules(purpose1Rule, consentData, curModule, gvlid); + let isAllowed = gvlid && validateRules(purpose1Rule, consentData, curModule, gvlid); if (isAllowed) { result.valid = true; fn.call(this, gvlid, moduleName, result); From d584a350a0a9f715287c169681b3805b807ef786 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Tue, 9 Jun 2020 19:47:04 +0530 Subject: [PATCH 15/20] add comments and shorten log messages --- modules/gdprEnforcement.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 1d631eb13f9..495f20be43f 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -49,7 +49,8 @@ function getGvlid(bidderCode) { } /** - * This function takes in a rule and consentData and validates against the consentData provided. If it returns true Prebid will allow the next call else it will log a warning + * This function takes in a rule and consentData and validates against the consentData provided. Depending on what it returns, + * the caller may decide to suppress a TCF-sensitive activity. * @param {Object} rule - enforcement rules set in config * @param {Object} consentData - gdpr consent data * @param {string=} currentModule - Bidder code of the current module @@ -64,13 +65,23 @@ export function validateRules(rule, consentData, currentModule, gvlId) { return true; } + // get data from the consent string const purposeConsent = utils.deepAccess(consentData, `vendorData.purpose.consents.${purposeId}`); const vendorConsent = utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlId}`); const liTransparency = utils.deepAccess(consentData, `vendorData.purpose.legitimateInterests.${purposeId}`); + /* + Since vendor exceptions have already been handled, the purpose as a whole is allowed if it's not being enforced + or the user has consented. Similar with vendors. + */ const purposeAllowed = rule.enforcePurpose === false || purposeConsent === true; const vendorAllowed = rule.enforceVendor === false || vendorConsent === true; + /* + Few if any vendors should be declaring Legitimate Interest for Device Access (Purpose 1), but some are claiming + LI for Basic Ads (Purpose 2). Prebid.js can't check to see who's declaring what legal basis, so if LI has been + established for Purpose 2, allow the auction to take place and let the server sort out the legal basis calculation. + */ if (purposeId === 2) { return (purposeAllowed && vendorAllowed) || (liTransparency === true); } @@ -105,7 +116,7 @@ export function deviceAccessHook(fn, gvlid, moduleName, result) { result.valid = true; fn.call(this, gvlid, moduleName, result); } else { - curModule && utils.logWarn(`User denied Permission for Device access for ${curModule}`); + curModule && utils.logWarn(`Device access denied for ${curModule} by TCF2`); result.valid = false; fn.call(this, gvlid, moduleName, result); } @@ -203,7 +214,7 @@ export function makeBidRequestsHook(fn, adUnits, ...args) { if (includes(disabledBidders, currBidder)) return false; const isAllowed = gvlId && validateRules(purpose2Rule, consentData, currBidder, gvlId); if (!isAllowed) { - utils.logWarn(`User blocked bidder: ${currBidder}. No bid request will be sent to their endpoint.`); + utils.logWarn(`TCF2 blocked auction for ${currBidder}`); events.emit(EVENTS.BIDDER_BLOCKED, currBidder); disabledBidders.push(currBidder); } @@ -212,7 +223,7 @@ export function makeBidRequestsHook(fn, adUnits, ...args) { }); fn.call(this, adUnits, ...args); } else { - utils.logInfo('Enforcing TCF2 only'); + // we don't enforce TCF1.1 strings fn.call(this, adUnits, ...args); } } else { @@ -230,7 +241,7 @@ const hasPurpose2 = (rule) => { return rule.purpose === TCF2.purpose2.name } export function setEnforcementConfig(config) { const rules = utils.deepAccess(config, 'gdpr.rules'); if (!rules) { - utils.logWarn('GDPR enforcement rules not defined, enforcing TCF2 Purpose 1 and Purpose 2'); + utils.logWarn('TCF2: enforcing P1 and P2'); enforcementRules = DEFAULT_RULES; } else { enforcementRules = rules; From 0b461fbdea0a192287b49e975d0c060eb35ded8f Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Tue, 9 Jun 2020 19:47:49 +0530 Subject: [PATCH 16/20] shorted log messages --- modules/consentManagement.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index fcf16f161f2..a5ed134420e 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -327,9 +327,9 @@ function processCmpData(consentObject, hookConfig) { // Raise deprecation warning if 'allowAuctionWithoutConsent' is used with TCF 2. if (allowAuction.definedInConfig && cmpVersion === 2) { - utils.logWarn(`consentManagement config is using deprecated property 'allowAuctionWithoutConsent' with TCF 2 setup. This property only works with TCF 1.1`); + utils.logWarn(`'allowAuctionWithoutConsent' ignored for TCF 2`); } else if (!allowAuction.definedInConfig && cmpVersion === 1) { - utils.logInfo(`consentManagement config did not specify allowAuctionWithoutConsent. Using system default setting (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); + utils.logInfo(`'allowAuctionWithoutConsent' using system default: (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); } if (utils.isFn(checkFn)) { @@ -417,7 +417,7 @@ function exitModule(errMsg, hookConfig, extraArgs) { if (errMsg) { if (allowAuction.value && cmpVersion === 1) { - utils.logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.', extraArgs); + utils.logWarn(errMsg + ` 'allowAuctionWithoutConsent' activated.`, extraArgs); nextFn.apply(context, args); } else { utils.logError(errMsg + ' Canceling auction as per consentManagement config.', extraArgs); From 32c06820110b0a969db1385844316beeeb221e00 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Tue, 9 Jun 2020 20:32:49 +0530 Subject: [PATCH 17/20] add unit tests for setEnforcementConfig --- modules/gdprEnforcement.js | 6 +- test/spec/modules/gdprEnforcement_spec.js | 96 +++++++++++++++++++++-- 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 495f20be43f..01f588ed369 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -31,10 +31,10 @@ const DEFAULT_RULES = [{ vendorExceptions: [] }]; -let purpose1Rule; -let purpose2Rule; +export let purpose1Rule; +export let purpose2Rule; let addedDeviceAccessHook = false; -let enforcementRules; +export let enforcementRules; function getGvlid(bidderCode) { let gvlid; diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 419523b2f52..4d563a6f4af 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -1,6 +1,4 @@ -/* eslint no-console: 0 */ - -import { deviceAccessHook, setEnforcementConfig, userSyncHook, userIdHook, makeBidRequestsHook, validateRules } from 'modules/gdprEnforcement.js'; +import { deviceAccessHook, setEnforcementConfig, userSyncHook, userIdHook, makeBidRequestsHook, validateRules, enforcementRules, purpose1Rule, purpose2Rule } from 'modules/gdprEnforcement.js'; import { config } from 'src/config.js'; import adapterManager, { gdprDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; @@ -373,7 +371,7 @@ describe('gdpr enforcement', function () { 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}); + sinon.assert.calledWith(nextFnSpy, submodules, { ...consentData, hasValidated: true }); }); it('should allow userId module if gdpr not in scope', function () { @@ -427,7 +425,7 @@ describe('gdpr enforcement', function () { name: 'sampleUserId' } }] - sinon.assert.calledWith(nextFnSpy, expectedSubmodules, {...consentData, hasValidated: true}); + sinon.assert.calledWith(nextFnSpy, expectedSubmodules, { ...consentData, hasValidated: true }); }); }); @@ -546,11 +544,10 @@ describe('gdpr enforcement', function () { sinon.assert.calledWith(nextFnSpy, sinon.match.array.deepEquals(MOCK_AD_UNITS), []); expect(emitEventSpy.notCalled).to.equal(true); expect(logWarnSpy.notCalled).to.equal(true); - expect(logInfoSpy.calledOnce).to.equal(true); }); }); - describe('validateRules(rule, consentData, currentModule, gvlId)', function () { + describe('validateRules', function () { const createGdprRule = (purposeName = 'storage', enforcePurpose = true, enforceVendor = true, vendorExceptions = []) => ({ purpose: purposeName, enforcePurpose: enforcePurpose, @@ -736,4 +733,89 @@ describe('gdpr enforcement', function () { }); }); }) + + describe('setEnforcementConfig', function () { + let sandbox; + const DEFAULT_RULES = [{ + purpose: 'storage', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + }, { + purpose: 'basicAds', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + }]; + beforeEach(function () { + sandbox = sinon.createSandbox(); + logWarnSpy = sandbox.spy(utils, 'logWarn'); + }); + afterEach(function () { + config.resetConfig(); + sandbox.restore(); + }); + + it('should enforce TCF2 Purpose1 and Purpose 2 if no "rules" found in the config', function () { + setEnforcementConfig({ + gdpr: { + cmpApi: 'iab', + allowAuctionWithoutConsent: true, + timeout: 5000 + } + }); + + expect(logWarnSpy.calledOnce).to.equal(true); + expect(enforcementRules).to.deep.equal(DEFAULT_RULES); + }); + + it('should enforce TCF2 Purpose 2 also if only Purpose 1 is defined in "rules"', function () { + const purpose1RuleDefinedInConfig = { + purpose: 'storage', + enforcePurpose: false, + enforceVendor: true, + vendorExceptions: ['bidderA'] + } + setEnforcementConfig({ + gdpr: { + rules: [purpose1RuleDefinedInConfig] + } + }); + + expect(purpose1Rule).to.deep.equal(purpose1RuleDefinedInConfig); + expect(purpose2Rule).to.deep.equal(DEFAULT_RULES[1]); + }); + + it('should enforce TCF2 Purpose 1 also if only Purpose 2 is defined in "rules"', function () { + const purpose2RuleDefinedInConfig = { + purpose: 'basicAds', + enforcePurpose: false, + enforceVendor: true, + vendorExceptions: ['bidderA'] + } + setEnforcementConfig({ + gdpr: { + rules: [purpose2RuleDefinedInConfig] + } + }); + + expect(purpose1Rule).to.deep.equal(DEFAULT_RULES[0]); + expect(purpose2Rule).to.deep.equal(purpose2RuleDefinedInConfig); + }); + + it('should use the "rules" defined in config if a definition found', function() { + const rules = [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: false + }, { + purpose: 'basicAds', + enforcePurpose: false, + enforceVendor: false + }] + setEnforcementConfig({gdpr: { rules }}); + + expect(enforcementRules).to.deep.equal(rules); + }); + }); }); From e7f14c0c43a7a012b8a89af824576dfac0a37f14 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Sun, 14 Jun 2020 23:48:55 -0400 Subject: [PATCH 18/20] add gvlid for alias and gvlMapping support --- modules/gdprEnforcement.js | 56 +++++++++------ src/adapterManager.js | 5 +- src/adapters/bidderFactory.js | 2 +- src/prebid.js | 4 +- test/spec/modules/gdprEnforcement_spec.js | 87 +++++++++++++++++++++++ 5 files changed, 127 insertions(+), 27 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 01f588ed369..c7ca931c06f 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -40,14 +40,30 @@ function getGvlid(bidderCode) { let gvlid; bidderCode = bidderCode || config.getCurrentBidder(); if (bidderCode) { - const bidder = adapterManager.getBidAdapter(bidderCode); - if (bidder && bidder.getSpec) { - gvlid = bidder.getSpec().gvlid; + const gvlMapping = config.getConfig('gvlMapping'); + if (gvlMapping && gvlMapping[bidderCode]) { + gvlid = gvlMapping[bidderCode]; + } else { + const bidder = adapterManager.getBidAdapter(bidderCode); + if (bidder && bidder.getSpec) { + gvlid = bidder.getSpec().gvlid; + } } } return gvlid; } +function getGvlidForUserIdModule(userIdModule) { + let gvlId; + const gvlMapping = config.getConfig('gvlMapping'); + if (gvlMapping && gvlMapping[userIdModule.name]) { + gvlId = gvlMapping[userIdModule.name]; + } else { + gvlId = userIdModule.gvlid; + } + return gvlId; +} + /** * This function takes in a rule and consentData and validates against the consentData provided. Depending on what it returns, * the caller may decide to suppress a TCF-sensitive activity. @@ -107,11 +123,15 @@ export function deviceAccessHook(fn, gvlid, moduleName, result) { const consentData = gdprDataHandler.getConsentData(); if (consentData && consentData.gdprApplies) { if (consentData.apiVersion === 2) { - if (!gvlid) { - gvlid = getGvlid(); + 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); + } else { + gvlid = getGvlid(moduleName); } - const curModule = moduleName || config.getCurrentBidder(); - let isAllowed = gvlid && validateRules(purpose1Rule, consentData, curModule, gvlid); + const curModule = moduleName || curBidder; + let isAllowed = validateRules(purpose1Rule, consentData, curModule, gvlid); if (isAllowed) { result.valid = true; fn.call(this, gvlid, moduleName, result); @@ -143,13 +163,9 @@ export function userSyncHook(fn, ...args) { if (consentData.apiVersion === 2) { const gvlid = getGvlid(); const curBidder = config.getCurrentBidder(); - if (gvlid) { - let isAllowed = validateRules(purpose1Rule, consentData, curBidder, gvlid); - if (isAllowed) { - fn.call(this, ...args); - } else { - utils.logWarn(`User sync not allowed for ${curBidder}`); - } + let isAllowed = validateRules(purpose1Rule, consentData, curBidder, gvlid); + if (isAllowed) { + fn.call(this, ...args); } else { utils.logWarn(`User sync not allowed for ${curBidder}`); } @@ -172,15 +188,11 @@ export function userIdHook(fn, submodules, consentData) { if (consentData && consentData.gdprApplies) { if (consentData.apiVersion === 2) { let userIdModules = submodules.map((submodule) => { - const gvlid = submodule.submodule.gvlid; + const gvlid = getGvlidForUserIdModule(submodule.submodule); const moduleName = submodule.submodule.name; - if (gvlid) { - let isAllowed = validateRules(purpose1Rule, consentData, moduleName, gvlid); - if (isAllowed) { - return submodule; - } else { - utils.logWarn(`User denied permission to fetch user id for ${moduleName} User id module`); - } + let isAllowed = validateRules(purpose1Rule, consentData, moduleName, gvlid); + if (isAllowed) { + return submodule; } else { utils.logWarn(`User denied permission to fetch user id for ${moduleName} User id module`); } diff --git a/src/adapterManager.js b/src/adapterManager.js index 2108bb7a4f6..06ccba9787e 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -426,7 +426,7 @@ adapterManager.registerBidAdapter = function (bidAdaptor, bidderCode, {supported } }; -adapterManager.aliasBidAdapter = function (bidderCode, alias) { +adapterManager.aliasBidAdapter = function (bidderCode, alias, options) { let existingAlias = _bidderRegistry[alias]; if (typeof existingAlias === 'undefined') { @@ -452,7 +452,8 @@ adapterManager.aliasBidAdapter = function (bidderCode, alias) { newAdapter.setBidderCode(alias); } else { let spec = bidAdaptor.getSpec(); - newAdapter = newBidder(Object.assign({}, spec, { code: alias })); + let gvlid = options && options.gvlid; + newAdapter = newBidder(Object.assign({}, spec, { code: alias, gvlid })); _aliasRegistry[alias] = bidderCode; } adapterManager.registerBidAdapter(newAdapter, alias, { diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 8dd351563be..a4a260432cf 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -154,7 +154,7 @@ export function registerBidder(spec) { if (Array.isArray(spec.aliases)) { spec.aliases.forEach(alias => { adapterManager.aliasRegistry[alias] = spec.code; - putBidder(Object.assign({}, spec, { code: alias })); + putBidder(Object.assign({}, spec, { code: alias, gvlid: undefined })); }); } } diff --git a/src/prebid.js b/src/prebid.js index 1710849ba92..093cd97ee71 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -666,10 +666,10 @@ $$PREBID_GLOBAL$$.enableAnalytics = function (config) { /** * @alias module:pbjs.aliasBidder */ -$$PREBID_GLOBAL$$.aliasBidder = function (bidderCode, alias) { +$$PREBID_GLOBAL$$.aliasBidder = function (bidderCode, alias, options) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.aliasBidder', arguments); if (bidderCode && alias) { - adapterManager.aliasBidAdapter(bidderCode, alias); + adapterManager.aliasBidAdapter(bidderCode, alias, options); } else { utils.logError('bidderCode and alias must be passed as arguments', '$$PREBID_GLOBAL$$.aliasBidder'); } diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 4d563a6f4af..675f824074a 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -92,15 +92,29 @@ describe('gdpr enforcement', function () { }) describe('deviceAccessHook', function () { + let adapterManagerStub; + + function getBidderSpec(gvlid) { + return { + getSpec: () => { + return { + gvlid + } + } + } + } + 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({ @@ -127,6 +141,8 @@ 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)); setEnforcementConfig({ gdpr: { rules: [{ @@ -149,6 +165,8 @@ describe('gdpr enforcement', function () { }); 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)); setEnforcementConfig({ gdpr: { rules: [{ @@ -170,6 +188,7 @@ describe('gdpr enforcement', function () { }); it('should allow device access when gdprApplies is false and hasDeviceAccess flag is true', function () { + adapterManagerStub.withArgs('appnexus').returns(getBidderSpec(1)); setEnforcementConfig({ gdpr: { rules: [{ @@ -194,6 +213,74 @@ describe('gdpr enforcement', function () { } sinon.assert.calledWith(nextFnSpy, 1, 'appnexus', result); }); + + it('should use gvlMapping set by publisher', function() { + config.setConfig({ + 'gvlMapping': { + 'appnexus': 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, 1, 'appnexus'); + expect(nextFnSpy.calledOnce).to.equal(true); + let result = { + hasEnforcementHook: true, + valid: true + } + sinon.assert.calledWith(nextFnSpy, 4, '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, 1, 'appnexus'); + expect(nextFnSpy.calledOnce).to.equal(true); + let result = { + hasEnforcementHook: true, + valid: true + } + sinon.assert.calledWith(nextFnSpy, 4, 'appnexus', result); + config.resetConfig(); + curBidderStub.restore(); + }); }); describe('userSyncHook', function () { From d0abaf077367cb8c8cd0d5ec07c62ebb04683af6 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen Date: Mon, 15 Jun 2020 20:11:17 +0530 Subject: [PATCH 19/20] remove gvlid check --- modules/gdprEnforcement.js | 12 ++-- test/spec/modules/gdprEnforcement_spec.js | 72 +++++++++++++++++++++-- 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index c7ca931c06f..0a32441c813 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -141,7 +141,7 @@ export function deviceAccessHook(fn, gvlid, moduleName, result) { fn.call(this, gvlid, moduleName, result); } } else { - utils.logInfo('Enforcing TCF2 only'); + // The module doesn't enforce TCF1.1 strings result.valid = true; fn.call(this, gvlid, moduleName, result); } @@ -170,7 +170,7 @@ export function userSyncHook(fn, ...args) { utils.logWarn(`User sync not allowed for ${curBidder}`); } } else { - utils.logInfo('Enforcing TCF2 only'); + // The module doesn't enforce TCF1.1 strings fn.call(this, ...args); } } else { @@ -200,7 +200,7 @@ export function userIdHook(fn, submodules, consentData) { }).filter(module => module) fn.call(this, userIdModules, {...consentData, hasValidated: true}); } else { - utils.logInfo('Enforcing TCF2 only'); + // The module doesn't enforce TCF1.1 strings fn.call(this, submodules, consentData); } } else { @@ -209,7 +209,7 @@ export function userIdHook(fn, submodules, consentData) { } /** - * Checks if a bidder is allowed. If it's not allowed, the bidder adapter won't send request to their endpoint. + * Checks if a bidder is allowed in Auction. * Enforces "purpose 2 (basic ads)" of TCF v2.0 spec * @param {Function} fn - Function reference to the original function. * @param {Array} adUnits @@ -224,7 +224,7 @@ export function makeBidRequestsHook(fn, adUnits, ...args) { const currBidder = bid.bidder; const gvlId = getGvlid(currBidder); if (includes(disabledBidders, currBidder)) return false; - const isAllowed = gvlId && validateRules(purpose2Rule, consentData, currBidder, gvlId); + const isAllowed = !!validateRules(purpose2Rule, consentData, currBidder, gvlId); if (!isAllowed) { utils.logWarn(`TCF2 blocked auction for ${currBidder}`); events.emit(EVENTS.BIDDER_BLOCKED, currBidder); @@ -235,7 +235,7 @@ export function makeBidRequestsHook(fn, adUnits, ...args) { }); fn.call(this, adUnits, ...args); } else { - // we don't enforce TCF1.1 strings + // The module doesn't enforce TCF1.1 strings fn.call(this, adUnits, ...args); } } else { diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 675f824074a..9b02f74f4bb 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -520,7 +520,7 @@ describe('gdpr enforcement', function () { let sandbox; let adapterManagerStub; let emitEventSpy; - let logInfoSpy; + const MOCK_AD_UNITS = [{ code: 'ad-unit-1', mediaTypes: {}, @@ -538,12 +538,12 @@ describe('gdpr enforcement', function () { bidder: 'bidder_3' }] }]; + beforeEach(function () { sandbox = sinon.createSandbox(); gdprDataHandlerStub = sandbox.stub(gdprDataHandler, 'getConsentData'); adapterManagerStub = sandbox.stub(adapterManager, 'getBidAdapter'); logWarnSpy = sandbox.spy(utils, 'logWarn'); - logInfoSpy = sandbox.spy(utils, 'logInfo'); nextFnSpy = sandbox.spy(); emitEventSpy = sandbox.spy(events, 'emit'); }); @@ -551,7 +551,8 @@ describe('gdpr enforcement', function () { config.resetConfig(); sandbox.restore(); }); - it('should block bidder which does not have consent and allow bidder which has consent', function () { + + it('should block bidder which does not have consent and allow bidder which has consent (liTransparency is established)', function () { setEnforcementConfig({ gdpr: { rules: [{ @@ -598,12 +599,71 @@ describe('gdpr enforcement', function () { code: 'ad-unit-2', mediaTypes: {}, bids: [ - sinon.match({ bidder: 'bidder_2' }) + 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. ] }], []); - expect(emitEventSpy.calledOnce).to.equal(true); + }); + + it('should block bidder which does not have consent and allow bidder which has consent (liTransparency is NOT established)', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'basicAds', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: ['bidder_3'] + }] + } + }); + 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); + 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 } + } + }); + + 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); - sinon.assert.calledWith(emitEventSpy, EVENTS.BIDDER_BLOCKED, 'bidder_3'); + expect(emitEventSpy.calledOnce).to.equal(true); + sinon.assert.calledWith(emitEventSpy, EVENTS.BIDDER_BLOCKED, 'bidder_2'); }); it('should skip validation checks if GDPR version is not equal to "2"', function () { From 45e9fb62e94c7325fdeaf959e4e3ed076c847156 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Wed, 17 Jun 2020 11:08:38 -0400 Subject: [PATCH 20/20] add support to add gvlid for aliases --- modules/appnexusBidAdapter.js | 15 ++++++++++++++- src/adapters/bidderFactory.js | 12 +++++++++--- test/spec/unit/core/bidderFactory_spec.js | 22 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 5e3b6a06011..7cdac473f74 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -45,7 +45,20 @@ const storage = getStorageManager(GVLID, BIDDER_CODE); export const spec = { code: BIDDER_CODE, gvlid: GVLID, - aliases: ['appnexusAst', 'brealtime', 'emxdigital', 'pagescience', 'defymedia', 'gourmetads', 'matomy', 'featureforward', 'oftmedia', 'districtm', 'adasta', 'beintoo'], + aliases: [ + { code: 'appnexusAst', gvlid: 32 }, + { code: 'brealtime' }, + { code: 'emxdigital', gvlid: 183 }, + { code: 'pagescience' }, + { code: 'defymedia' }, + { code: 'gourmetads' }, + { code: 'matomy' }, + { code: 'featureforward' }, + { code: 'oftmedia' }, + { code: 'districtm', gvlid: 144 }, + { code: 'adasta' }, + { code: 'beintoo', gvlid: 618 }, + ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index a4a260432cf..a1305b3c76b 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -9,7 +9,7 @@ import CONSTANTS from '../constants.json'; import events from '../events.js'; import includes from 'core-js-pure/features/array/includes.js'; import { ajax } from '../ajax.js'; -import { logWarn, logError, parseQueryStringParameters, delayExecution, parseSizesInput, getBidderRequest, flatten, uniques, timestamp, deepAccess, isArray } from '../utils.js'; +import { logWarn, logError, parseQueryStringParameters, delayExecution, parseSizesInput, getBidderRequest, flatten, uniques, timestamp, deepAccess, isArray, isPlainObject } from '../utils.js'; import { ADPOD } from '../mediaTypes.js'; import { getHook, hook } from '../hook.js'; import { getCoreStorageManager } from '../storageManager.js'; @@ -153,8 +153,14 @@ export function registerBidder(spec) { putBidder(spec); if (Array.isArray(spec.aliases)) { spec.aliases.forEach(alias => { - adapterManager.aliasRegistry[alias] = spec.code; - putBidder(Object.assign({}, spec, { code: alias, gvlid: undefined })); + let aliasCode = alias; + let gvlid; + if (isPlainObject(alias)) { + aliasCode = alias.code; + gvlid = alias.gvlid; + } + adapterManager.aliasRegistry[aliasCode] = spec.code; + putBidder(Object.assign({}, spec, { code: aliasCode, gvlid })); }); } } diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index cab0655a29d..501e82d0af9 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -645,6 +645,28 @@ describe('registerBidder', function () { expect(registerBidAdapterStub.secondCall.args[1]).to.equal('foo') expect(registerBidAdapterStub.thirdCall.args[1]).to.equal('bar') }); + + it('should register alias with their gvlid', function() { + const aliases = [ + { + code: 'foo', + gvlid: 1 + }, + { + code: 'bar', + gvlid: 2 + }, + { + code: 'baz' + } + ] + const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); + registerBidder(thisSpec); + + expect(registerBidAdapterStub.getCall(1).args[0].getSpec().gvlid).to.equal(1); + expect(registerBidAdapterStub.getCall(2).args[0].getSpec().gvlid).to.equal(2); + expect(registerBidAdapterStub.getCall(3).args[0].getSpec().gvlid).to.equal(undefined); + }) }) describe('validate bid response: ', function () {