Skip to content

Commit

Permalink
gdprEnforcement: check for vendor LI in addition to purpose LI
Browse files Browse the repository at this point in the history
  • Loading branch information
dgirardi committed Aug 16, 2023
1 parent 57f2ac8 commit 03370d3
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 219 deletions.
27 changes: 7 additions & 20 deletions modules/gdprEnforcement.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,27 +159,14 @@ export function validateRules(rule, consentData, currentModule, gvlId) {
if ((rule.vendorExceptions || []).includes(currentModule)) {
return true;
}
const vendorConsentRequred = !((gvlId === VENDORLESS_GVLID || (rule.softVendorExceptions || []).includes(currentModule)));

// get data from the consent string
const purposeConsent = deepAccess(consentData, `vendorData.purpose.consents.${purposeId}`);
const vendorConsent = vendorConsentRequred ? deepAccess(consentData, `vendorData.vendor.consents.${gvlId}`) : true;
const liTransparency = 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.
*/
const vendorConsentRequred = rule.enforceVendor && !((gvlId === VENDORLESS_GVLID || (rule.softVendorExceptions || []).includes(currentModule)));

let purposeAllowed = !rule.enforcePurpose || !!deepAccess(consentData, `vendorData.purpose.consents.${purposeId}`);
let vendorAllowed = !vendorConsentRequred || !!deepAccess(consentData, `vendorData.vendor.consents.${gvlId}`);

if (purposeId === 2) {
return (purposeAllowed && vendorAllowed) || (liTransparency === true);
purposeAllowed ||= !!deepAccess(consentData, `vendorData.purpose.legitimateInterests.${purposeId}`);
vendorAllowed ||= !!deepAccess(consentData, `vendorData.vendor.legitimateInterests.${gvlId}`);
}

return purposeAllowed && vendorAllowed;
Expand Down
268 changes: 69 additions & 199 deletions test/spec/modules/gdprEnforcement_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ describe('gdpr enforcement', function () {
config.resetConfig();
});

it('should block bidder which does not have consent and allow bidder which has consent (liTransparency is established)', function () {
it('should block bidder which does not have consent and allow bidder which has consent (LI is established)', function () {
setEnforcementConfig({
gdpr: {
rules: [{
Expand All @@ -363,12 +363,17 @@ describe('gdpr enforcement', function () {
}]
}
});
setupConsentData()
const cd = setupConsentData()
Object.assign(gvlids, {
bidder_1: 4,
bidder_2: 5,
});
['bidder_1', 'bidder_2', 'bidder_3'].forEach(bidder => expect(fetchBidsRule(activityParams(MODULE_TYPE_BIDDER, bidder))).to.not.exist);
Object.assign(cd.vendorData.vendor.legitimateInterests, {
4: true,
5: true,
});

['bidder_1', 'bidder_2'].forEach(bidder => expect(fetchBidsRule(activityParams(MODULE_TYPE_BIDDER, bidder))).to.not.exist);
});

it('should block bidder which does not have consent and allow bidder which has consent (liTransparency is NOT established)', function() {
Expand Down Expand Up @@ -396,39 +401,6 @@ describe('gdpr enforcement', function () {
expectAllow(allowed, fetchBidsRule(activityParams(MODULE_TYPE_BIDDER, bidder)));
})
});

it('should skip validation checks if GDPR version is not equal to "2"', function () {
setEnforcementConfig({
gdpr: {
rules: [{
purpose: 'storage',
enforcePurpose: true,
enforceVendor: true,
vendorExceptions: []
}]
}
});
const consent = setupConsentData();
consent.vendorData.purpose.consents['2'] = false;
consent.apiVersion = 1;
['bidder_1', 'bidder_2', 'bidder_3'].forEach(bidder => expect(fetchBidsRule(activityParams(MODULE_TYPE_BIDDER, bidder))).to.not.exist);
});

it('should skip validation if enforcePurpose is false', () => {
setEnforcementConfig({
gdpr: {
rules: [{
purpose: 'storage',
enforcePurpose: false,
enforceVendor: true,
vendorExceptions: []
}]
}
});
const consent = setupConsentData();
consent.vendorData.purpose.consents['2'] = false;
['bidder_1', 'bidder_2', 'bidder_3'].forEach(bidder => expect(fetchBidsRule(activityParams(MODULE_TYPE_BIDDER, bidder))).to.not.exist);
})
});

describe('reportAnalyticsRule', () => {
Expand Down Expand Up @@ -517,111 +489,13 @@ describe('gdpr enforcement', function () {
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('when the vendor has a softVendorException', () => {
const gdprRule = createGdprRule('storage', true, true, [], [vendorBlockedModule]);

Expand Down Expand Up @@ -661,71 +535,67 @@ describe('gdpr enforcement', function () {
})
})

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);
});
describe('validateRules', function () {
Object.entries({
'1 (which does not consider LI)': [1, 'storage', false],
'2 (which does consider LI)': [2, 'basicAds', true]
}).forEach(([t, [purposeNo, purpose, allowsLI]]) => {
describe(`for purpose ${t}`, () => {
Object.entries({
'enforcePurpose=true, enforceVendor=true': [true, true],
'enforcePurpose=true, enforceVendor=false': [true, false],
'enforcePurpose=false, enforceVendor=true': [false, true],
'enforcePurpose=false, enforceVendor=false': [false, false],
}).forEach(([t, [enforcePurpose, enforceVendor]]) => {
describe(`with ${t}`, () => {
let rule;
beforeEach(() => {
rule = createGdprRule(purpose, enforcePurpose, enforceVendor, []);
});

['consents', 'legitimateInterests'].forEach(ctype => {
Object.entries({
'purpose=true, vendor=true': [true, true, true],
'purpose=true, vendor=false': [true, false, !enforceVendor],
'purpose=false, vendor=true': [false, true, !enforcePurpose],
'purpose=false, vendor=false': [false, false, !enforcePurpose && !enforceVendor]
}).forEach(([t, [purposeConsent, vendorConsent, expected]]) => {
describe(`when ${ctype} for ${t}`, () => {
let consentData;
beforeEach(() => {
consentData = {
vendorData: {
purpose: {
[ctype]: {
[purposeNo]: purposeConsent,
}
},
vendor: {
[ctype]: {
123: vendorConsent,
}
}
}
}
});
if (allowsLI || ctype !== 'legitimateInterests') {
it(`should return ${expected}`, () => {
const allowed = validateRules(rule, consentData, 'mockVendor', 123);
expect(allowed).to.eql(expected);
})
} else if (enforceVendor || enforcePurpose) {
it(`should return false (LI is irrelevant)`, () => {
const allowed = validateRules(rule, consentData, 'mockVendor', 123);
expect(allowed).to.be.false;
})
}
})
})
})
})
})
})
})
});
})

Expand Down

0 comments on commit 03370d3

Please sign in to comment.