/modules/gdprEnforcement.js

https://github.com/prebid/Prebid.js · JavaScript · 401 lines · 263 code · 37 blank · 101 comment · 77 complexity · a253c9fdc53bb6720de76ef0edaaf167 MD5 · raw file

  1. /**
  2. * This module gives publishers extra set of features to enforce individual purposes of TCF v2
  3. */
  4. import * as utils from '../src/utils.js';
  5. import { config } from '../src/config.js';
  6. import { hasDeviceAccess } from '../src/utils.js';
  7. import adapterManager, { gdprDataHandler } from '../src/adapterManager.js';
  8. import find from 'core-js-pure/features/array/find.js';
  9. import includes from 'core-js-pure/features/array/includes.js';
  10. import { registerSyncInner } from '../src/adapters/bidderFactory.js';
  11. import { getHook } from '../src/hook.js';
  12. import { validateStorageEnforcement } from '../src/storageManager.js';
  13. import events from '../src/events.js';
  14. import { EVENTS } from '../src/constants.json';
  15. const TCF2 = {
  16. 'purpose1': { id: 1, name: 'storage' },
  17. 'purpose2': { id: 2, name: 'basicAds' },
  18. 'purpose7': { id: 7, name: 'measurement' }
  19. }
  20. /*
  21. These rules would be used if `consentManagement.gdpr.rules` is undefined by the publisher.
  22. */
  23. const DEFAULT_RULES = [{
  24. purpose: 'storage',
  25. enforcePurpose: true,
  26. enforceVendor: true,
  27. vendorExceptions: []
  28. }, {
  29. purpose: 'basicAds',
  30. enforcePurpose: true,
  31. enforceVendor: true,
  32. vendorExceptions: []
  33. }];
  34. export let purpose1Rule;
  35. export let purpose2Rule;
  36. export let purpose7Rule;
  37. export let enforcementRules;
  38. const storageBlocked = [];
  39. const biddersBlocked = [];
  40. const analyticsBlocked = [];
  41. let addedDeviceAccessHook = false;
  42. // Helps in stubbing these functions in unit tests.
  43. export const internal = {
  44. getGvlidForBidAdapter,
  45. getGvlidForUserIdModule,
  46. getGvlidForAnalyticsAdapter
  47. };
  48. /**
  49. * Returns GVL ID for a Bid adapter / an USERID submodule / an Analytics adapter.
  50. * If modules of different types have the same moduleCode: For example, 'appnexus' is the code for both Bid adapter and Analytics adapter,
  51. * then, we assume that their GVL IDs are same. This function first checks if GVL ID is defined for a Bid adapter, if not found, tries to find User ID
  52. * submodule's GVL ID, if not found, tries to find Analytics adapter's GVL ID. In this process, as soon as it finds a GVL ID, it returns it
  53. * without going to the next check.
  54. * @param {{string|Object}} - module
  55. * @return {number} - GVL ID
  56. */
  57. export function getGvlid(module) {
  58. let gvlid = null;
  59. if (module) {
  60. // Check user defined GVL Mapping in pbjs.setConfig()
  61. const gvlMapping = config.getConfig('gvlMapping');
  62. // For USER ID Module, we pass the submodule object itself as the "module" parameter, this check is required to grab the module code
  63. const moduleCode = typeof module === 'string' ? module : module.name;
  64. // Return GVL ID from user defined gvlMapping
  65. if (gvlMapping && gvlMapping[moduleCode]) {
  66. gvlid = gvlMapping[moduleCode];
  67. return gvlid;
  68. }
  69. gvlid = internal.getGvlidForBidAdapter(moduleCode) || internal.getGvlidForUserIdModule(module) || internal.getGvlidForAnalyticsAdapter(moduleCode);
  70. }
  71. return gvlid;
  72. }
  73. /**
  74. * Returns GVL ID for a bid adapter. If the adapter does not have an associated GVL ID, it returns 'null'.
  75. * @param {string=} bidderCode - The 'code' property of the Bidder spec.
  76. * @return {number} GVL ID
  77. */
  78. function getGvlidForBidAdapter(bidderCode) {
  79. let gvlid = null;
  80. bidderCode = bidderCode || config.getCurrentBidder();
  81. if (bidderCode) {
  82. const bidder = adapterManager.getBidAdapter(bidderCode);
  83. if (bidder && bidder.getSpec) {
  84. gvlid = bidder.getSpec().gvlid;
  85. }
  86. }
  87. return gvlid;
  88. }
  89. /**
  90. * Returns GVL ID for an userId submodule. If an userId submodules does not have an associated GVL ID, it returns 'null'.
  91. * @param {Object} userIdModule
  92. * @return {number} GVL ID
  93. */
  94. function getGvlidForUserIdModule(userIdModule) {
  95. return (typeof userIdModule === 'object' ? userIdModule.gvlid : null);
  96. }
  97. /**
  98. * Returns GVL ID for an analytics adapter. If an analytics adapter does not have an associated GVL ID, it returns 'null'.
  99. * @param {string} code - 'provider' property on the analytics adapter config
  100. * @return {number} GVL ID
  101. */
  102. function getGvlidForAnalyticsAdapter(code) {
  103. return adapterManager.getAnalyticsAdapter(code) && (adapterManager.getAnalyticsAdapter(code).gvlid || null);
  104. }
  105. /**
  106. * This function takes in a rule and consentData and validates against the consentData provided. Depending on what it returns,
  107. * the caller may decide to suppress a TCF-sensitive activity.
  108. * @param {Object} rule - enforcement rules set in config
  109. * @param {Object} consentData - gdpr consent data
  110. * @param {string=} currentModule - Bidder code of the current module
  111. * @param {number=} gvlId - GVL ID for the module
  112. * @returns {boolean}
  113. */
  114. export function validateRules(rule, consentData, currentModule, gvlId) {
  115. const purposeId = TCF2[Object.keys(TCF2).filter(purposeName => TCF2[purposeName].name === rule.purpose)[0]].id;
  116. // return 'true' if vendor present in 'vendorExceptions'
  117. if (includes(rule.vendorExceptions || [], currentModule)) {
  118. return true;
  119. }
  120. // get data from the consent string
  121. const purposeConsent = utils.deepAccess(consentData, `vendorData.purpose.consents.${purposeId}`);
  122. const vendorConsent = utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlId}`);
  123. const liTransparency = utils.deepAccess(consentData, `vendorData.purpose.legitimateInterests.${purposeId}`);
  124. /*
  125. Since vendor exceptions have already been handled, the purpose as a whole is allowed if it's not being enforced
  126. or the user has consented. Similar with vendors.
  127. */
  128. const purposeAllowed = rule.enforcePurpose === false || purposeConsent === true;
  129. const vendorAllowed = rule.enforceVendor === false || vendorConsent === true;
  130. /*
  131. Few if any vendors should be declaring Legitimate Interest for Device Access (Purpose 1), but some are claiming
  132. LI for Basic Ads (Purpose 2). Prebid.js can't check to see who's declaring what legal basis, so if LI has been
  133. established for Purpose 2, allow the auction to take place and let the server sort out the legal basis calculation.
  134. */
  135. if (purposeId === 2) {
  136. return (purposeAllowed && vendorAllowed) || (liTransparency === true);
  137. }
  138. return purposeAllowed && vendorAllowed;
  139. }
  140. /**
  141. * This hook checks whether module has permission to access device or not. Device access include cookie and local storage
  142. * @param {Function} fn reference to original function (used by hook logic)
  143. * @param {Number=} gvlid gvlid of the module
  144. * @param {string=} moduleName name of the module
  145. */
  146. export function deviceAccessHook(fn, gvlid, moduleName, result) {
  147. result = Object.assign({}, {
  148. hasEnforcementHook: true
  149. });
  150. if (!hasDeviceAccess()) {
  151. utils.logWarn('Device access is disabled by Publisher');
  152. result.valid = false;
  153. fn.call(this, gvlid, moduleName, result);
  154. } else {
  155. const consentData = gdprDataHandler.getConsentData();
  156. if (consentData && consentData.gdprApplies) {
  157. if (consentData.apiVersion === 2) {
  158. const curBidder = config.getCurrentBidder();
  159. // 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
  160. if (curBidder && (curBidder != moduleName) && adapterManager.aliasRegistry[curBidder] === moduleName) {
  161. gvlid = getGvlid(curBidder);
  162. } else {
  163. gvlid = getGvlid(moduleName) || gvlid;
  164. }
  165. const curModule = moduleName || curBidder;
  166. let isAllowed = validateRules(purpose1Rule, consentData, curModule, gvlid);
  167. if (isAllowed) {
  168. result.valid = true;
  169. fn.call(this, gvlid, moduleName, result);
  170. } else {
  171. curModule && utils.logWarn(`TCF2 denied device access for ${curModule}`);
  172. result.valid = false;
  173. storageBlocked.push(curModule);
  174. fn.call(this, gvlid, moduleName, result);
  175. }
  176. } else {
  177. // The module doesn't enforce TCF1.1 strings
  178. result.valid = true;
  179. fn.call(this, gvlid, moduleName, result);
  180. }
  181. } else {
  182. result.valid = true;
  183. fn.call(this, gvlid, moduleName, result);
  184. }
  185. }
  186. }
  187. /**
  188. * This hook checks if a bidder has consent for user sync or not
  189. * @param {Function} fn reference to original function (used by hook logic)
  190. * @param {...any} args args
  191. */
  192. export function userSyncHook(fn, ...args) {
  193. const consentData = gdprDataHandler.getConsentData();
  194. if (consentData && consentData.gdprApplies) {
  195. if (consentData.apiVersion === 2) {
  196. const curBidder = config.getCurrentBidder();
  197. const gvlid = getGvlid(curBidder);
  198. let isAllowed = validateRules(purpose1Rule, consentData, curBidder, gvlid);
  199. if (isAllowed) {
  200. fn.call(this, ...args);
  201. } else {
  202. utils.logWarn(`User sync not allowed for ${curBidder}`);
  203. storageBlocked.push(curBidder);
  204. }
  205. } else {
  206. // The module doesn't enforce TCF1.1 strings
  207. fn.call(this, ...args);
  208. }
  209. } else {
  210. fn.call(this, ...args);
  211. }
  212. }
  213. /**
  214. * This hook checks if user id module is given consent or not
  215. * @param {Function} fn reference to original function (used by hook logic)
  216. * @param {Submodule[]} submodules Array of user id submodules
  217. * @param {Object} consentData GDPR consent data
  218. */
  219. export function userIdHook(fn, submodules, consentData) {
  220. if (consentData && consentData.gdprApplies) {
  221. if (consentData.apiVersion === 2) {
  222. let userIdModules = submodules.map((submodule) => {
  223. const gvlid = getGvlid(submodule.submodule);
  224. const moduleName = submodule.submodule.name;
  225. let isAllowed = validateRules(purpose1Rule, consentData, moduleName, gvlid);
  226. if (isAllowed) {
  227. return submodule;
  228. } else {
  229. utils.logWarn(`User denied permission to fetch user id for ${moduleName} User id module`);
  230. storageBlocked.push(moduleName);
  231. }
  232. return undefined;
  233. }).filter(module => module)
  234. fn.call(this, userIdModules, { ...consentData, hasValidated: true });
  235. } else {
  236. // The module doesn't enforce TCF1.1 strings
  237. fn.call(this, submodules, consentData);
  238. }
  239. } else {
  240. fn.call(this, submodules, consentData);
  241. }
  242. }
  243. /**
  244. * Checks if bidders are allowed in the auction.
  245. * Enforces "purpose 2 (Basic Ads)" of TCF v2.0 spec
  246. * @param {Function} fn - Function reference to the original function.
  247. * @param {Array<adUnits>} adUnits
  248. */
  249. export function makeBidRequestsHook(fn, adUnits, ...args) {
  250. const consentData = gdprDataHandler.getConsentData();
  251. if (consentData && consentData.gdprApplies) {
  252. if (consentData.apiVersion === 2) {
  253. adUnits.forEach(adUnit => {
  254. adUnit.bids = adUnit.bids.filter(bid => {
  255. const currBidder = bid.bidder;
  256. const gvlId = getGvlid(currBidder);
  257. if (includes(biddersBlocked, currBidder)) return false;
  258. const isAllowed = !!validateRules(purpose2Rule, consentData, currBidder, gvlId);
  259. if (!isAllowed) {
  260. utils.logWarn(`TCF2 blocked auction for ${currBidder}`);
  261. biddersBlocked.push(currBidder);
  262. }
  263. return isAllowed;
  264. });
  265. });
  266. fn.call(this, adUnits, ...args);
  267. } else {
  268. // The module doesn't enforce TCF1.1 strings
  269. fn.call(this, adUnits, ...args);
  270. }
  271. } else {
  272. fn.call(this, adUnits, ...args);
  273. }
  274. }
  275. /**
  276. * Checks if Analytics adapters are allowed to send data to their servers for furhter processing.
  277. * Enforces "purpose 7 (Measurement)" of TCF v2.0 spec
  278. * @param {Function} fn - Function reference to the original function.
  279. * @param {Array<AnalyticsAdapterConfig>} config - Configuration object passed to pbjs.enableAnalytics()
  280. */
  281. export function enableAnalyticsHook(fn, config) {
  282. const consentData = gdprDataHandler.getConsentData();
  283. if (consentData && consentData.gdprApplies) {
  284. if (consentData.apiVersion === 2) {
  285. if (!utils.isArray(config)) {
  286. config = [config]
  287. }
  288. config = config.filter(conf => {
  289. const analyticsAdapterCode = conf.provider;
  290. const gvlid = getGvlid(analyticsAdapterCode);
  291. const isAllowed = !!validateRules(purpose7Rule, consentData, analyticsAdapterCode, gvlid);
  292. if (!isAllowed) {
  293. analyticsBlocked.push(analyticsAdapterCode);
  294. utils.logWarn(`TCF2 blocked analytics adapter ${conf.provider}`);
  295. }
  296. return isAllowed;
  297. });
  298. fn.call(this, config);
  299. } else {
  300. // This module doesn't enforce TCF1.1 strings
  301. fn.call(this, config);
  302. }
  303. } else {
  304. fn.call(this, config);
  305. }
  306. }
  307. /**
  308. * Compiles the TCF2.0 enforcement results into an object, which is emitted as an event payload to "tcf2Enforcement" event.
  309. */
  310. function emitTCF2FinalResults() {
  311. // remove null and duplicate values
  312. const formatArray = function (arr) {
  313. return arr.filter((i, k) => i !== null && arr.indexOf(i) === k);
  314. }
  315. const tcf2FinalResults = {
  316. storageBlocked: formatArray(storageBlocked),
  317. biddersBlocked: formatArray(biddersBlocked),
  318. analyticsBlocked: formatArray(analyticsBlocked)
  319. };
  320. events.emit(EVENTS.TCF2_ENFORCEMENT, tcf2FinalResults);
  321. }
  322. events.on(EVENTS.AUCTION_END, emitTCF2FinalResults);
  323. /*
  324. Set of callback functions used to detect presence of a TCF rule, passed as the second argument to find().
  325. */
  326. const hasPurpose1 = (rule) => { return rule.purpose === TCF2.purpose1.name }
  327. const hasPurpose2 = (rule) => { return rule.purpose === TCF2.purpose2.name }
  328. const hasPurpose7 = (rule) => { return rule.purpose === TCF2.purpose7.name }
  329. /**
  330. * A configuration function that initializes some module variables, as well as adds hooks
  331. * @param {Object} config - GDPR enforcement config object
  332. */
  333. export function setEnforcementConfig(config) {
  334. const rules = utils.deepAccess(config, 'gdpr.rules');
  335. if (!rules) {
  336. utils.logWarn('TCF2: enforcing P1 and P2 by default');
  337. enforcementRules = DEFAULT_RULES;
  338. } else {
  339. enforcementRules = rules;
  340. }
  341. purpose1Rule = find(enforcementRules, hasPurpose1);
  342. purpose2Rule = find(enforcementRules, hasPurpose2);
  343. purpose7Rule = find(enforcementRules, hasPurpose7);
  344. if (!purpose1Rule) {
  345. purpose1Rule = DEFAULT_RULES[0];
  346. }
  347. if (!purpose2Rule) {
  348. purpose2Rule = DEFAULT_RULES[1];
  349. }
  350. if (purpose1Rule && !addedDeviceAccessHook) {
  351. addedDeviceAccessHook = true;
  352. validateStorageEnforcement.before(deviceAccessHook, 49);
  353. registerSyncInner.before(userSyncHook, 48);
  354. // Using getHook as user id and gdprEnforcement are both optional modules. Using import will auto include the file in build
  355. getHook('validateGdprEnforcement').before(userIdHook, 47);
  356. }
  357. if (purpose2Rule) {
  358. getHook('makeBidRequests').before(makeBidRequestsHook);
  359. }
  360. if (purpose7Rule) {
  361. getHook('enableAnalyticsCb').before(enableAnalyticsHook);
  362. }
  363. }
  364. config.getConfig('consentManagement', config => setEnforcementConfig(config.consentManagement));