Skip to content

Commit fb8b58a

Browse files
authored
Core: TCF 2.3 Disclosed Vendors Support (#4442)
1 parent 8f76449 commit fb8b58a

82 files changed

Lines changed: 2756 additions & 905 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ public class AmpRequestFactory {
102102
private final DebugResolver debugResolver;
103103
private final JacksonMapper mapper;
104104
private final GeoLocationServiceWrapper geoLocationServiceWrapper;
105+
private final TcfDefinerService tcfDefinerService;
105106

106107
public AmpRequestFactory(Ortb2RequestFactory ortb2RequestFactory,
107108
StoredRequestProcessor storedRequestProcessor,
@@ -115,7 +116,8 @@ public AmpRequestFactory(Ortb2RequestFactory ortb2RequestFactory,
115116
AmpPrivacyContextFactory ampPrivacyContextFactory,
116117
DebugResolver debugResolver,
117118
JacksonMapper mapper,
118-
GeoLocationServiceWrapper geoLocationServiceWrapper) {
119+
GeoLocationServiceWrapper geoLocationServiceWrapper,
120+
TcfDefinerService tcfDefinerService) {
119121

120122
this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory);
121123
this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor);
@@ -130,6 +132,7 @@ public AmpRequestFactory(Ortb2RequestFactory ortb2RequestFactory,
130132
this.ampPrivacyContextFactory = Objects.requireNonNull(ampPrivacyContextFactory);
131133
this.mapper = Objects.requireNonNull(mapper);
132134
this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper);
135+
this.tcfDefinerService = Objects.requireNonNull(tcfDefinerService);
133136
}
134137

135138
/**
@@ -217,7 +220,7 @@ private Future<BidRequest> parseBidRequest(AuctionContext auctionContext, HttpRe
217220
return Future.succeededFuture(bidRequest);
218221
}
219222

220-
private static ConsentParam consentParamFromQueryStringParams(HttpRequestContext httpRequest) {
223+
private ConsentParam consentParamFromQueryStringParams(HttpRequestContext httpRequest) {
221224
final ConsentType specifiedConsentType = ConsentType.from(httpRequest.getQueryParams().get(CONSENT_TYPE_PARAM));
222225
final CaseInsensitiveMultiMap queryParams = httpRequest.getQueryParams();
223226

@@ -229,12 +232,12 @@ private static ConsentParam consentParamFromQueryStringParams(HttpRequestContext
229232
: toConsentParam(gdprConsentParam, GDPR_CONSENT_PARAM, specifiedConsentType);
230233
}
231234

232-
private static ConsentParam toConsentParam(String consent, String fromParam, ConsentType specifiedConsentType) {
235+
private ConsentParam toConsentParam(String consent, String fromParam, ConsentType specifiedConsentType) {
233236
return ConsentParam.of(
234237
consent,
235238
fromParam,
236239
specifiedConsentType,
237-
TcfDefinerService.isConsentStringValid(consent),
240+
tcfDefinerService.isConsentStringValid(consent),
238241
Ccpa.isValid(consent));
239242
}
240243

@@ -259,10 +262,10 @@ private static Site createSite(HttpRequestContext httpRequest) {
259262

260263
return !StringUtils.isAllBlank(accountId, canonicalUrl, domain)
261264
? Site.builder()
262-
.publisher(Publisher.builder().id(accountId).build())
263-
.page(canonicalUrl)
264-
.domain(domain)
265-
.build()
265+
.publisher(Publisher.builder().id(accountId).build())
266+
.page(canonicalUrl)
267+
.domain(domain)
268+
.build()
266269
: null;
267270
}
268271

@@ -278,9 +281,9 @@ private static User createUser(ConsentParam consentParam, String addtlConsent) {
278281

279282
final ExtUser extUser = consentedProvidersSettings != null
280283
? ExtUser.builder()
281-
.deprecatedConsentedProvidersSettings(consentedProvidersSettings)
282-
.consentedProvidersSettings(consentedProvidersSettings)
283-
.build()
284+
.deprecatedConsentedProvidersSettings(consentedProvidersSettings)
285+
.consentedProvidersSettings(consentedProvidersSettings)
286+
.build()
284287
: null;
285288

286289
return User.builder().consent(consent).ext(extUser).build();
@@ -301,12 +304,12 @@ private static Regs createRegs(ConsentParam consentParam,
301304

302305
return gdpr != null || usPrivacy != null || gppSid != null || gpp != null || gpc != null
303306
? Regs.builder()
304-
.gdpr(gdpr)
305-
.usPrivacy(usPrivacy)
306-
.gppSid(gppSid)
307-
.gpp(gpp)
308-
.ext(gpc != null ? ExtRegs.of(null, null, gpc, null) : null)
309-
.build()
307+
.gdpr(gdpr)
308+
.usPrivacy(usPrivacy)
309+
.gppSid(gppSid)
310+
.gpp(gpp)
311+
.ext(gpc != null ? ExtRegs.of(null, null, gpc, null) : null)
312+
.build()
310313
: null;
311314
}
312315

@@ -359,8 +362,8 @@ private GppSidExtraction gppSidFromQueryStringParams(HttpRequestContext httpRequ
359362
try {
360363
final List<Integer> gppSid = StringUtils.isNotBlank(gppSidParam)
361364
? Arrays.stream(gppSidParam.split(","))
362-
.map(Integer::valueOf)
363-
.toList()
365+
.map(Integer::valueOf)
366+
.toList()
364367
: null;
365368

366369
return GppSidExtraction.success(gppSid);

src/main/java/org/prebid/server/metric/MetricName.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ public enum MetricName {
104104
specified,
105105
opt_out("opt-out"),
106106
invalid,
107+
no_disclosed_vendors("no-disclosed-vendors"),
107108
in_geo("in-geo"),
108109
out_geo("out-geo"),
109110
unknown_geo("unknown-geo"),

src/main/java/org/prebid/server/metric/Metrics.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,10 @@ public void updatePrivacyTcfInvalidMetric() {
519519
privacy().tcf().incCounter(MetricName.invalid);
520520
}
521521

522+
public void updatePrivacyTcfNoDisclosedVendorsMetric() {
523+
privacy().tcf().incCounter(MetricName.no_disclosed_vendors);
524+
}
525+
522526
public void updatePrivacyTcfRequestsMetric(int version) {
523527
final UpdatableMetrics versionMetrics = privacy().tcf().fromVersion(version);
524528
versionMetrics.incCounter(MetricName.requests);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.prebid.server.privacy.gdpr;
2+
3+
import com.iabtcf.decoder.TCString;
4+
import org.prebid.server.settings.model.GdprConfig;
5+
6+
import java.time.Instant;
7+
import java.time.Month;
8+
import java.time.Year;
9+
import java.time.ZoneOffset;
10+
11+
public class DisclosedVendorsStrictness {
12+
13+
private static final Instant TCF_2_3_ENFORCEMENT_CUTOFF_DATE = Year.of(2026)
14+
.atMonth(Month.MARCH)
15+
.atDay(1)
16+
.atStartOfDay()
17+
.toInstant(ZoneOffset.UTC);
18+
19+
private final boolean strictnessEnabled;
20+
21+
public DisclosedVendorsStrictness(GdprConfig gdprConfig) {
22+
this.strictnessEnabled = gdprConfig == null || gdprConfig.isStrictDisclosedVendorsTreatment();
23+
}
24+
25+
public boolean isValid(TCString consent) {
26+
return !strictnessEnabled
27+
|| isCreatedBeforeTcfV2M3EnforcementCutoff(consent)
28+
|| !consent.getDisclosedVendors().isEmpty();
29+
}
30+
31+
private boolean isCreatedBeforeTcfV2M3EnforcementCutoff(TCString consent) {
32+
final Instant created = consent.getCreated();
33+
final Instant lastUpdated = consent.getLastUpdated();
34+
final Instant latest = lastUpdated.isAfter(created) ? lastUpdated : created;
35+
36+
return latest.isBefore(TCF_2_3_ENFORCEMENT_CUTOFF_DATE);
37+
}
38+
39+
public boolean isVendorDisclosed(TCString consent, Integer vendorId) {
40+
return !strictnessEnabled
41+
|| (vendorId != null
42+
&& (isCreatedBeforeTcfV2M3EnforcementCutoff(consent)
43+
|| consent.getDisclosedVendors().contains(vendorId)));
44+
}
45+
}

src/main/java/org/prebid/server/privacy/gdpr/Tcf2Service.java

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -358,17 +358,17 @@ private Purposes mergeAccountPurposes(AccountGdprConfig accountGdprConfig) {
358358

359359
return accountPurposes != null
360360
? Purposes.builder()
361-
.p1(mergeItem(accountPurposes.getP1(), defaultPurposes.getP1()))
362-
.p2(mergeItem(accountPurposes.getP2(), defaultPurposes.getP2()))
363-
.p3(mergeItem(accountPurposes.getP3(), defaultPurposes.getP3()))
364-
.p4(mergeItem(accountPurposes.getP4(), defaultPurposes.getP4()))
365-
.p5(mergeItem(accountPurposes.getP5(), defaultPurposes.getP5()))
366-
.p6(mergeItem(accountPurposes.getP6(), defaultPurposes.getP6()))
367-
.p7(mergeItem(accountPurposes.getP7(), defaultPurposes.getP7()))
368-
.p8(mergeItem(accountPurposes.getP8(), defaultPurposes.getP8()))
369-
.p9(mergeItem(accountPurposes.getP9(), defaultPurposes.getP9()))
370-
.p10(mergeItem(accountPurposes.getP10(), defaultPurposes.getP10()))
371-
.build()
361+
.p1(mergeItem(accountPurposes.getP1(), defaultPurposes.getP1()))
362+
.p2(mergeItem(accountPurposes.getP2(), defaultPurposes.getP2()))
363+
.p3(mergeItem(accountPurposes.getP3(), defaultPurposes.getP3()))
364+
.p4(mergeItem(accountPurposes.getP4(), defaultPurposes.getP4()))
365+
.p5(mergeItem(accountPurposes.getP5(), defaultPurposes.getP5()))
366+
.p6(mergeItem(accountPurposes.getP6(), defaultPurposes.getP6()))
367+
.p7(mergeItem(accountPurposes.getP7(), defaultPurposes.getP7()))
368+
.p8(mergeItem(accountPurposes.getP8(), defaultPurposes.getP8()))
369+
.p9(mergeItem(accountPurposes.getP9(), defaultPurposes.getP9()))
370+
.p10(mergeItem(accountPurposes.getP10(), defaultPurposes.getP10()))
371+
.build()
372372
: defaultPurposes;
373373
}
374374

@@ -379,9 +379,9 @@ private SpecialFeatures mergeAccountSpecialFeatures(AccountGdprConfig accountGdp
379379

380380
return accountSpecialFeatures != null
381381
? SpecialFeatures.builder()
382-
.sf1(mergeItem(accountSpecialFeatures.getSf1(), defaultSpecialFeatures.getSf1()))
383-
.sf2(mergeItem(accountSpecialFeatures.getSf2(), defaultSpecialFeatures.getSf2()))
384-
.build()
382+
.sf1(mergeItem(accountSpecialFeatures.getSf1(), defaultSpecialFeatures.getSf1()))
383+
.sf2(mergeItem(accountSpecialFeatures.getSf2(), defaultSpecialFeatures.getSf2()))
384+
.build()
385385
: defaultSpecialFeatures;
386386
}
387387

src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public class TcfDefinerService {
6060
private final boolean gdprEnabled;
6161
private final String gdprDefaultValue;
6262
private final boolean consentStringMeansInScope;
63+
private final DisclosedVendorsStrictness disclosedVendorsStrictness;
6364
private final Tcf2Service tcf2Service;
6465
private final Set<String> eeaCountries;
6566
private final GeoLocationServiceWrapper geoLocationServiceWrapper;
@@ -70,6 +71,7 @@ public class TcfDefinerService {
7071

7172
public TcfDefinerService(GdprConfig gdprConfig,
7273
Set<String> eeaCountries,
74+
DisclosedVendorsStrictness disclosedVendorsStrictness,
7375
Tcf2Service tcf2Service,
7476
GeoLocationServiceWrapper geoLocationServiceWrapper,
7577
BidderCatalog bidderCatalog,
@@ -81,6 +83,7 @@ public TcfDefinerService(GdprConfig gdprConfig,
8183
this.gdprDefaultValue = gdprConfig != null ? gdprConfig.getDefaultValue() : null;
8284
this.consentStringMeansInScope = gdprConfig != null
8385
&& BooleanUtils.isTrue(gdprConfig.getConsentStringMeansInScope());
86+
this.disclosedVendorsStrictness = Objects.requireNonNull(disclosedVendorsStrictness);
8487
this.tcf2Service = Objects.requireNonNull(tcf2Service);
8588
this.eeaCountries = Objects.requireNonNull(eeaCountries);
8689
this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper);
@@ -345,6 +348,15 @@ private TCStringParsingResult parseConsentString(String consentString, RequestLo
345348
return TCStringParsingResult.of(TCStringEmpty.create(), warnings);
346349
}
347350

351+
if (!disclosedVendorsStrictness.isValid(tcString)) {
352+
final String message = "Invalid TCF string: `disclosedVendors` list is empty.";
353+
warnings.add(message);
354+
logWarn(consentString, message, requestLogInfo);
355+
metrics.updatePrivacyTcfNoDisclosedVendorsMetric();
356+
357+
return TCStringParsingResult.of(TCStringEmpty.create(), warnings);
358+
}
359+
348360
return toValidResult(consentString, TCStringParsingResult.of(tcString, warnings));
349361
}
350362

@@ -417,10 +429,9 @@ private static boolean isConsentValid(TCString consent) {
417429
return consent != null && !(consent instanceof TCStringEmpty);
418430
}
419431

420-
public static boolean isConsentStringValid(String consentString) {
432+
public boolean isConsentStringValid(String consentString) {
421433
try {
422-
TCString.decode(consentString);
423-
return true;
434+
return disclosedVendorsStrictness.isValid(TCString.decode(consentString));
424435
} catch (RuntimeException e) {
425436
return false;
426437
}

src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ public static VendorIdResolver of(BidderCatalog bidderCatalog) {
2020
}
2121

2222
public Integer resolve(String aliasOrBidder) {
23-
return aliases != null ? aliases.resolveAliasVendorId(aliasOrBidder) : null;
23+
return aliases.resolveAliasVendorId(aliasOrBidder);
2424
}
2525
}

src/main/java/org/prebid/server/privacy/gdpr/model/TCStringEmpty.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ public int getVersion() {
2222

2323
@Override
2424
public Instant getCreated() {
25-
return null;
25+
return Instant.MAX;
2626
}
2727

2828
@Override
2929
public Instant getLastUpdated() {
30-
return null;
30+
return Instant.MAX;
3131
}
3232

3333
@Override

src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose01Strategy.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.prebid.server.privacy.gdpr.tcfstrategies.purpose;
22

3+
import org.prebid.server.privacy.gdpr.DisclosedVendorsStrictness;
34
import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction;
45
import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy;
56
import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy;
@@ -8,11 +9,16 @@
89

910
public class Purpose01Strategy extends PurposeStrategy {
1011

11-
public Purpose01Strategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy,
12+
public Purpose01Strategy(DisclosedVendorsStrictness disclosedVendorsStrictness,
13+
FullEnforcePurposeStrategy fullEnforcePurposeStrategy,
1214
BasicEnforcePurposeStrategy basicEnforcePurposeStrategy,
1315
NoEnforcePurposeStrategy noEnforcePurposeStrategy) {
1416

15-
super(fullEnforcePurposeStrategy, basicEnforcePurposeStrategy, noEnforcePurposeStrategy);
17+
super(
18+
disclosedVendorsStrictness,
19+
fullEnforcePurposeStrategy,
20+
basicEnforcePurposeStrategy,
21+
noEnforcePurposeStrategy);
1622
}
1723

1824
@Override

src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose02Strategy.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.prebid.server.privacy.gdpr.tcfstrategies.purpose;
22

3+
import org.prebid.server.privacy.gdpr.DisclosedVendorsStrictness;
34
import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction;
45
import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy;
56
import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy;
@@ -8,11 +9,16 @@
89

910
public class Purpose02Strategy extends PurposeStrategy {
1011

11-
public Purpose02Strategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy,
12+
public Purpose02Strategy(DisclosedVendorsStrictness disclosedVendorsStrictness,
13+
FullEnforcePurposeStrategy fullEnforcePurposeStrategy,
1214
BasicEnforcePurposeStrategy basicEnforcePurposeStrategy,
1315
NoEnforcePurposeStrategy noEnforcePurposeStrategy) {
1416

15-
super(fullEnforcePurposeStrategy, basicEnforcePurposeStrategy, noEnforcePurposeStrategy);
17+
super(
18+
disclosedVendorsStrictness,
19+
fullEnforcePurposeStrategy,
20+
basicEnforcePurposeStrategy,
21+
noEnforcePurposeStrategy);
1622
}
1723

1824
@Override

0 commit comments

Comments
 (0)