From 8c78ce54e39d6d8f7ff82f2a7f83b2e27b341081 Mon Sep 17 00:00:00 2001 From: Danylo Date: Thu, 26 Mar 2026 13:17:21 +0100 Subject: [PATCH 1/5] Ignore LI for purposes 3, 4, 5 and 6. --- .../purpose/typestrategies/EnforcePurposeStrategy.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java index ffb1e355276..fa64ccdf447 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java @@ -43,6 +43,10 @@ protected boolean isAllowedByLegitimateInterest(PurposeCode purpose, boolean isEnforceVendor, TCString tcString) { + switch (purpose) { + case THREE, FOUR, FIVE, SIX: return false; + } + final IntIterable purposesConsent = tcString.getPurposesLITransparency(); final IntIterable vendorConsent = tcString.getVendorLegitimateInterest(); From 36cf442d8cd3a66faea06dc4375699cd521589a9 Mon Sep 17 00:00:00 2001 From: Danylo Date: Thu, 26 Mar 2026 13:34:37 +0100 Subject: [PATCH 2/5] Ignore LI for purposes 3, 4, 5 and 6 in `NoEnforcePurposeStrategy` --- .../typestrategies/EnforcePurposeStrategy.java | 15 +++++++++++++-- .../typestrategies/NoEnforcePurposeStrategy.java | 5 ++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java index fa64ccdf447..d9729d991d6 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java @@ -2,15 +2,26 @@ import com.iabtcf.decoder.TCString; import com.iabtcf.utils.IntIterable; +import org.apache.commons.collections4.SetUtils; import org.prebid.server.privacy.gdpr.model.VendorPermission; import org.prebid.server.privacy.gdpr.model.VendorPermissionWithGvl; import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import java.util.Collection; +import java.util.Set; import java.util.stream.Stream; public abstract class EnforcePurposeStrategy { + protected static final Set LI_SUPPORTED_PURPOSES = SetUtils.difference( + Set.of(PurposeCode.values()), + Set.of( + PurposeCode.THREE, + PurposeCode.FOUR, + PurposeCode.FIVE, + PurposeCode.SIX, + PurposeCode.UNKNOWN)); + public abstract Stream allowedByTypeStrategy( PurposeCode purpose, TCString vendorConsent, @@ -43,8 +54,8 @@ protected boolean isAllowedByLegitimateInterest(PurposeCode purpose, boolean isEnforceVendor, TCString tcString) { - switch (purpose) { - case THREE, FOUR, FIVE, SIX: return false; + if (!LI_SUPPORTED_PURPOSES.contains(purpose)) { + return false; } final IntIterable purposesConsent = tcString.getPurposesLITransparency(); diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/NoEnforcePurposeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/NoEnforcePurposeStrategy.java index 5b14f9a8a7e..092ebf8f77b 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/NoEnforcePurposeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/NoEnforcePurposeStrategy.java @@ -1,6 +1,7 @@ package org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies; import com.iabtcf.decoder.TCString; +import com.iabtcf.utils.BitSetIntIterable; import com.iabtcf.utils.IntIterable; import org.prebid.server.privacy.gdpr.model.VendorPermission; import org.prebid.server.privacy.gdpr.model.VendorPermissionWithGvl; @@ -18,7 +19,9 @@ public Stream allowedByTypeStrategy(PurposeCode purpose, boolean isEnforceVendors) { final IntIterable vendorConsent = tcString.getVendorConsent(); - final IntIterable vendorLIConsent = tcString.getVendorLegitimateInterest(); + final IntIterable vendorLIConsent = LI_SUPPORTED_PURPOSES.contains(purpose) + ? tcString.getVendorLegitimateInterest() + : BitSetIntIterable.EMPTY; final Stream allowedVendorPermissions = toVendorPermissions(vendorsForPurpose) .filter(vendorPermission -> vendorPermission.getVendorId() != null) From 574aed7bbcb63f6f09b3fddf1b1292720f34fe77 Mon Sep 17 00:00:00 2001 From: Danylo Date: Fri, 27 Mar 2026 01:15:50 +0100 Subject: [PATCH 3/5] Refactor --- .../server/settings/model/GdprConfig.java | 2 +- .../config/PrivacyServiceConfiguration.java | 7 +++-- .../spring/config/ServiceConfiguration.java | 26 +++++++++---------- .../config/bidder/TaboolaConfiguration.java | 5 ++-- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/prebid/server/settings/model/GdprConfig.java b/src/main/java/org/prebid/server/settings/model/GdprConfig.java index 80d4abd9cfb..10487677d82 100644 --- a/src/main/java/org/prebid/server/settings/model/GdprConfig.java +++ b/src/main/java/org/prebid/server/settings/model/GdprConfig.java @@ -17,7 +17,7 @@ public class GdprConfig { @JsonProperty("host-vendor-id") - String hostVendorId; + Integer hostVendorId; Boolean enabled; diff --git a/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java index 601de9e5f11..0cdc87ae7d3 100644 --- a/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java @@ -181,11 +181,10 @@ TcfDefinerService tcfDefinerService( } @Bean - HostVendorTcfDefinerService hostVendorTcfDefinerService( - TcfDefinerService tcfDefinerService, - @Value("${gdpr.host-vendor-id:#{null}}") Integer hostVendorId) { + HostVendorTcfDefinerService hostVendorTcfDefinerService(TcfDefinerService tcfDefinerService, + GdprConfig gdprConfig) { - return new HostVendorTcfDefinerService(tcfDefinerService, hostVendorId); + return new HostVendorTcfDefinerService(tcfDefinerService, gdprConfig.getHostVendorId()); } @Bean diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 64a8dc7614a..f86547199a0 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -112,6 +112,7 @@ import org.prebid.server.privacy.gdpr.TcfDefinerService; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.BidValidationEnforcement; +import org.prebid.server.settings.model.GdprConfig; import org.prebid.server.spring.config.model.CacheDefaultTtlProperties; import org.prebid.server.spring.config.model.ExternalConversionProperties; import org.prebid.server.spring.config.model.HttpClientCircuitBreakerProperties; @@ -161,17 +162,16 @@ public class ServiceConfiguration { private double logSamplingRate; @Bean - CoreCacheService cacheService( - CacheConfigurationProperties cacheConfigurationProperties, - @Value("${auction.cache.expected-request-time-ms}") long expectedCacheTimeMs, - @Value("${pbc.api.key:#{null}}") String apiKey, - @Value("${datacenter-region:#{null}}") String datacenterRegion, - VastModifier vastModifier, - EventsService eventsService, - HttpClient httpClient, - Metrics metrics, - Clock clock, - JacksonMapper mapper) { + CoreCacheService cacheService(CacheConfigurationProperties cacheConfigurationProperties, + @Value("${auction.cache.expected-request-time-ms}") long expectedCacheTimeMs, + @Value("${pbc.api.key:#{null}}") String apiKey, + @Value("${datacenter-region:#{null}}") String datacenterRegion, + VastModifier vastModifier, + EventsService eventsService, + HttpClient httpClient, + Metrics metrics, + Clock clock, + JacksonMapper mapper) { final String scheme = cacheConfigurationProperties.getScheme(); final String host = cacheConfigurationProperties.getHost(); @@ -354,8 +354,8 @@ Ortb2ImplicitParametersResolver ortb2ImplicitParametersResolver( @Value("${auction.ad-server-currency}") String adServerCurrency, @Value("${auction.blocklisted-apps}") String blocklistedAppsString, @Value("${external-url}") String externalUrl, - @Value("${gdpr.host-vendor-id:#{null}}") Integer hostVendorId, @Value("${datacenter-region}") String datacenterRegion, + GdprConfig gdprConfig, BidderCatalog bidderCatalog, ImplicitParametersExtractor implicitParametersExtractor, TimeoutResolver timeoutResolver, @@ -371,7 +371,7 @@ Ortb2ImplicitParametersResolver ortb2ImplicitParametersResolver( adServerCurrency, splitToList(blocklistedAppsString), externalUrl, - hostVendorId, + gdprConfig.getHostVendorId(), datacenterRegion, bidderCatalog, implicitParametersExtractor, diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TaboolaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TaboolaConfiguration.java index ef917b09bc5..065c889bf8e 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/TaboolaConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/TaboolaConfiguration.java @@ -3,6 +3,7 @@ import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.taboola.TaboolaBidder; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.settings.model.GdprConfig; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; @@ -29,14 +30,14 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps taboolaBidderDeps(BidderConfigurationProperties taboolaConfigurationProperties, - @Value("${gdpr.host-vendor-id:#{null}}") Integer hostVendorId, @NotBlank @Value("${external-url}") String externalUrl, + GdprConfig gdprConfig, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(taboolaConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new TaboolaBidder(config.getEndpoint(), hostVendorId, mapper)) + .bidderCreator(config -> new TaboolaBidder(config.getEndpoint(), gdprConfig.getHostVendorId(), mapper)) .assemble(); } } From 0e352a24e7f5cc68d45c39cba62a45035268e6a1 Mon Sep 17 00:00:00 2001 From: Danylo Date: Fri, 27 Mar 2026 02:20:54 +0100 Subject: [PATCH 4/5] Add support for `disclosedVendors` property. --- .../server/privacy/gdpr/Tcf2Service.java | 14 ++++++-- .../privacy/gdpr/TcfDefinerService.java | 34 +++++++++++++++++-- .../server/privacy/gdpr/VendorIdResolver.java | 2 +- .../EnforcePurposeStrategy.java | 3 +- 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/prebid/server/privacy/gdpr/Tcf2Service.java b/src/main/java/org/prebid/server/privacy/gdpr/Tcf2Service.java index 3134f33e91d..409e178867a 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/Tcf2Service.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/Tcf2Service.java @@ -99,12 +99,19 @@ private Future> permissionsForInternal(Collection disclosedVendors = vendorPermissions.stream() + .filter(permission -> TcfDefinerService.isVendorDisclosed(tcfConsent, permission.getVendorId())) + .toList(); + if (disclosedVendors.isEmpty()) { + return Future.succeededFuture(vendorPermissions); + } + final Purposes mergedPurposes = mergeAccountPurposes(accountGdprConfig); final PurposeOneTreatmentInterpretation mergedPurposeOneTreatmentInterpretation = mergePurposeOneTreatmentInterpretation(accountGdprConfig); final VendorPermissionsByType vendorPermissionsByType = - toVendorPermissionsByType(vendorPermissions, accountGdprConfig); + toVendorPermissionsByType(disclosedVendors, accountGdprConfig); return versionedVendorListService.forConsent(tcfConsent) .compose(vendorGvlPermissions -> processSupportedPurposeStrategies( @@ -120,8 +127,9 @@ private Future> permissionsForInternal(Collection enforcePurpose4IfRequired(mergedPurposes, vendorPermissionsByType)) .map(ignored -> processSupportedSpecialFeatureStrategies( tcfConsent, - vendorPermissions, - mergeAccountSpecialFeatures(accountGdprConfig))); + disclosedVendors, + mergeAccountSpecialFeatures(accountGdprConfig))) + .map(vendorPermissions); } private static VendorPermissionsByType toVendorPermissionsByType( diff --git a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java index 0d86272d378..2776957f362 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java @@ -29,6 +29,10 @@ import org.prebid.server.settings.model.GdprConfig; import org.prebid.server.util.ObjectUtil; +import java.time.Instant; +import java.time.Month; +import java.time.Year; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -56,6 +60,11 @@ public class TcfDefinerService { new ConditionalLogger("undefined_corrupt_consent", logger); private static final String GDPR_ENABLED = "1"; + private static final Instant MARCH_01_2026 = Year.of(2026) + .atMonth(Month.MARCH) + .atDay(1) + .atStartOfDay() + .toInstant(ZoneOffset.UTC); private final boolean gdprEnabled; private final String gdprDefaultValue; @@ -345,6 +354,11 @@ private TCStringParsingResult parseConsentString(String consentString, RequestLo return TCStringParsingResult.of(TCStringEmpty.create(), warnings); } + if (!isDisclosedVendorsValid(tcString)) { + warnings.add("Invalid TCF string: `disclosedVendors` list is empty."); + return TCStringParsingResult.of(TCStringEmpty.create(), warnings); + } + return toValidResult(consentString, TCStringParsingResult.of(tcString, warnings)); } @@ -417,10 +431,26 @@ private static boolean isConsentValid(TCString consent) { return consent != null && !(consent instanceof TCStringEmpty); } + private static boolean isDisclosedVendorsValid(TCString consent) { + return isCreatedBeforeMarch01Y2026(consent) || !consent.getDisclosedVendors().isEmpty(); + } + + private static boolean isCreatedBeforeMarch01Y2026(TCString consent) { + final Instant created = consent.getCreated(); + final Instant lastUpdated = consent.getLastUpdated(); + final Instant latest = lastUpdated.isAfter(created) ? lastUpdated : created; + + return latest.isBefore(MARCH_01_2026); + } + + public static boolean isVendorDisclosed(TCString consent, Integer vendorId) { + return vendorId != null + && (isCreatedBeforeMarch01Y2026(consent) || consent.getDisclosedVendors().contains(vendorId)); + } + public static boolean isConsentStringValid(String consentString) { try { - TCString.decode(consentString); - return true; + return isDisclosedVendorsValid(TCString.decode(consentString)); } catch (RuntimeException e) { return false; } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java b/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java index d8d360fa823..a1f32122327 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java @@ -20,6 +20,6 @@ public static VendorIdResolver of(BidderCatalog bidderCatalog) { } public Integer resolve(String aliasOrBidder) { - return aliases != null ? aliases.resolveAliasVendorId(aliasOrBidder) : null; + return aliases.resolveAliasVendorId(aliasOrBidder); } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java index d9729d991d6..c301c2850ec 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java @@ -19,8 +19,7 @@ public abstract class EnforcePurposeStrategy { PurposeCode.THREE, PurposeCode.FOUR, PurposeCode.FIVE, - PurposeCode.SIX, - PurposeCode.UNKNOWN)); + PurposeCode.SIX)); public abstract Stream allowedByTypeStrategy( PurposeCode purpose, From 8cbecc75be3d17fba9ec9591136dee84a7a49f0e Mon Sep 17 00:00:00 2001 From: Danylo Date: Fri, 27 Mar 2026 12:09:21 +0100 Subject: [PATCH 5/5] Fix NPE --- .../org/prebid/server/privacy/gdpr/model/TCStringEmpty.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/prebid/server/privacy/gdpr/model/TCStringEmpty.java b/src/main/java/org/prebid/server/privacy/gdpr/model/TCStringEmpty.java index 4225e32319f..01b77925b32 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/model/TCStringEmpty.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/model/TCStringEmpty.java @@ -22,12 +22,12 @@ public int getVersion() { @Override public Instant getCreated() { - return null; + return Instant.MAX; } @Override public Instant getLastUpdated() { - return null; + return Instant.MAX; } @Override