Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gdprEnforcement: check for vendor LI in addition to purpose LI #10367

Merged
merged 1 commit into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice work!

}

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