Skip to content

Commit

Permalink
Core: Add account.privacy.gdpr.eea-countries property (#3212)
Browse files Browse the repository at this point in the history
  • Loading branch information
CTMBNara authored Jun 21, 2024
1 parent 2a5a014 commit 13c9a37
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@
import org.prebid.server.util.ObjectUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
Expand Down Expand Up @@ -99,7 +101,7 @@ public Future<TcfContext> resolveTcfContext(Privacy privacy,

final Future<TcfContext> tcfContextFuture = !isGdprEnabled(accountGdprConfig, requestType)
? Future.succeededFuture(TcfContext.empty())
: prepareTcfContext(privacy, country, ipAddress, requestLogInfo, timeout, geoInfo);
: prepareTcfContext(privacy, country, ipAddress, accountGdprConfig, requestLogInfo, timeout, geoInfo);

return tcfContextFuture.map(this::updateTcfGeoMetrics);
}
Expand Down Expand Up @@ -185,6 +187,7 @@ private boolean isGdprEnabled(AccountGdprConfig accountGdprConfig, MetricName re
private Future<TcfContext> prepareTcfContext(Privacy privacy,
String country,
String ipAddress,
AccountGdprConfig accountGdprConfig,
RequestLogInfo requestLogInfo,
Timeout timeout,
GeoInfo geoInfo) {
Expand All @@ -195,7 +198,7 @@ private Future<TcfContext> prepareTcfContext(Privacy privacy,
final boolean consentValid = isConsentValid(consent);

final String effectiveIpAddress = maybeMaskIp(ipAddress, consent);
final Boolean inEea = isCountryInEea(country);
final Boolean inEea = isCountryInEea(country, accountGdprConfig);

final TcfContext defaultContext = TcfContext.builder()
.inGdprScope(inScopeOfGdpr(gdprDefaultValue))
Expand All @@ -218,7 +221,7 @@ private Future<TcfContext> prepareTcfContext(Privacy privacy,

return geoLocationServiceWrapper.doLookup(effectiveIpAddress, country, timeout)
.recover(ignored -> Future.succeededFuture(geoInfo))
.map(lookupResult -> enrichWithGeoInfo(defaultContext, lookupResult, country));
.map(lookupResult -> enrichWithGeoInfo(defaultContext, lookupResult, country, accountGdprConfig));
}

private String maybeMaskIp(String ipAddress, TCString consent) {
Expand All @@ -240,9 +243,13 @@ private static boolean shouldMaskIp(TCString consent) {
return isConsentValid(consent) && consent.getVersion() == 2 && !consent.getSpecialFeatureOptIns().contains(1);
}

private TcfContext enrichWithGeoInfo(TcfContext defaultTcfContext, GeoInfo geoInfo, String defaultCountry) {
private TcfContext enrichWithGeoInfo(TcfContext defaultTcfContext,
GeoInfo geoInfo,
String defaultCountry,
AccountGdprConfig accountGdprConfig) {

final String country = ObjectUtil.getIfNotNullOrDefault(geoInfo, GeoInfo::getCountry, () -> defaultCountry);
final Boolean inEea = isCountryInEea(country);
final Boolean inEea = isCountryInEea(country, accountGdprConfig);
final boolean inScope = inScopeOfGdpr(inEea);

return defaultTcfContext.toBuilder()
Expand All @@ -252,8 +259,19 @@ private TcfContext enrichWithGeoInfo(TcfContext defaultTcfContext, GeoInfo geoIn
.build();
}

private Boolean isCountryInEea(String country) {
return country != null ? eeaCountries.contains(country) : null;
private Boolean isCountryInEea(String country, AccountGdprConfig accountGdprConfig) {
final Set<String> publisherEeaCountries = Optional.ofNullable(accountGdprConfig)
.map(AccountGdprConfig::getEeaCountries)
.map(TcfDefinerService::eeaCountries)
.orElse(eeaCountries);
return country != null ? publisherEeaCountries.contains(country) : null;
}

private static Set<String> eeaCountries(String eeaCountriesAsString) {
return Arrays.stream(eeaCountriesAsString.split(","))
.map(StringUtils::strip)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
}

private TcfContext updateTcfGeoMetrics(TcfContext tcfContext) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.prebid.server.settings.model;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Value;
Expand All @@ -13,6 +14,9 @@ public class AccountGdprConfig {
@JsonProperty("enabled")
Boolean enabled;

@JsonAlias("eea-countries")
String eeaCountries;

@JsonProperty("channel-enabled")
EnabledForRequestType enabledForRequestType;

Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ host-cookie:
max-cookie-size-bytes: 4096
gdpr:
enabled: true
eea-countries: at,bg,be,cy,cz,dk,ee,fi,fr,de,gr,hu,ie,it,lv,lt,lu,mt,nl,pl,pt,ro,sk,si,es,se,gb,is,no,li,ai,aw,pt,bm,aq,io,vg,ic,ky,fk,re,mw,gp,gf,yt,pf,tf,gl,pt,ms,an,bq,cw,sx,nc,pn,sh,pm,gs,tc,uk,wf,ch
eea-countries: at,bg,be,cy,cz,dk,ee,fi,fr,de,gr,hu,ie,it,lv,lt,lu,mt,nl,pl,pt,ro,sk,si,es,se,gb,is,no,li,ai,aw,pt,bm,aq,io,vg,ic,ky,fk,re,mw,gp,gf,yt,pf,tf,gl,pt,ms,an,bq,cw,sx,nc,pn,sh,pm,gs,tc,uk,wf
vendorlist:
default-timeout-ms: 2000
v2:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.prebid.server.functional.model.ChannelType
class AccountGdprConfig {

Boolean enabled
String eeaCountries
Map<ChannelType, Boolean> channelEnabled
Map<Purpose, PurposeConfig> purposes
Map<SpecialFeature, SpecialFeatureConfig> specialFeatures
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ enum Country {

USA("USA","US"),
CAN("CAN","CA"),
BULGARIA("BGR","BG"),
MULTIPLE("*","*")

@JsonValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import java.time.Instant
import static org.prebid.server.functional.model.ChannelType.PBJS
import static org.prebid.server.functional.model.ChannelType.WEB
import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA
import static org.prebid.server.functional.model.pricefloors.Country.CAN
import static org.prebid.server.functional.model.pricefloors.Country.USA
import static org.prebid.server.functional.model.request.auction.Prebid.Channel
import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REJECTED_BY_PRIVACY
import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID
Expand Down Expand Up @@ -367,4 +370,116 @@ class GdprAuctionSpec extends PrivacyBaseSpec {
where:
tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V3]
}

def "PBS should apply gdpr and emit metrics when host and device.geo.country contains same eea-country"() {
given: "Valid consent string"
def validConsentString = new TcfConsent.Builder()
.setPurposesLITransparency(BASIC_ADS)
.setVendorLegitimateInterest([GENERIC_VENDOR_ID])
.build()

and: "Gpdr bid request with override country"
def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString).tap {
device.geo.country = BULGARIA
}

and: "Save account config into DB"
accountDao.save(getAccountWithGdpr(bidRequest.app.publisher.id,
new AccountGdprConfig(enabled: true, eeaCountries: null)))

and: "Flush metrics"
flushMetrics(privacyPbsService)

when: "PBS processes auction request"
privacyPbsService.sendAuctionRequest(bidRequest)

then: "PBs should increment metrics when eea-country matched"
def metricsRequest = privacyPbsService.sendCollectedMetricsRequest()
assert metricsRequest["privacy.tcf.v2.in-geo"] == 1
assert !metricsRequest["privacy.tcf.v2.out-geo"]
}

def "PBS should apply gdpr and not emit metrics when host and device.geo.country doesn't contain same eea-country"() {
given: "Valid consent string"
def validConsentString = new TcfConsent.Builder()
.setPurposesLITransparency(BASIC_ADS)
.setVendorLegitimateInterest([GENERIC_VENDOR_ID])
.build()

and: "Gpdr bid request with override country"
def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString).tap {
device.geo.country = USA
}

and: "Save account config into DB"
accountDao.save(getAccountWithGdpr(bidRequest.app.publisher.id,
new AccountGdprConfig(enabled: true, eeaCountries: null)))

and: "Flush metrics"
flushMetrics(privacyPbsService)

when: "PBS processes auction request"
privacyPbsService.sendAuctionRequest(bidRequest)

then: "PBs should increment metrics when eea-country doens't matched"
def metricsRequest = privacyPbsService.sendCollectedMetricsRequest()
assert !metricsRequest["privacy.tcf.v2.in-geo"]
assert metricsRequest["privacy.tcf.v2.out-geo"] == 1
}

def "PBS should apply gdpr and emit metrics when account and device.geo.country contains same eea-country"() {
given: "Valid consent string"
def validConsentString = new TcfConsent.Builder()
.setPurposesLITransparency(BASIC_ADS)
.setVendorLegitimateInterest([GENERIC_VENDOR_ID])
.build()

and: "Gpdr bid request with override country"
def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString).tap {
device.geo.country = USA
}

and: "Save account config into DB"
accountDao.save(getAccountWithGdpr(bidRequest.app.publisher.id,
new AccountGdprConfig(enabled: true, eeaCountries: USA.ISOAlpha2)))

and: "Flush metrics"
flushMetrics(privacyPbsService)

when: "PBS processes auction request"
privacyPbsService.sendAuctionRequest(bidRequest)

then: "PBs should increment metrics when eea-country matched"
def metricsRequest = privacyPbsService.sendCollectedMetricsRequest()
assert metricsRequest["privacy.tcf.v2.in-geo"] == 1
assert !metricsRequest["privacy.tcf.v2.out-geo"]
}

def "PBS should apply gdpr and not emit metrics when account and device.geo.country doesn't contain same eea-country"() {
given: "Valid consent string"
def validConsentString = new TcfConsent.Builder()
.setPurposesLITransparency(BASIC_ADS)
.setVendorLegitimateInterest([GENERIC_VENDOR_ID])
.build()

and: "Gpdr bid request with override country"
def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString).tap {
device.geo.country = USA
}

and: "Save account config into DB"
accountDao.save(getAccountWithGdpr(bidRequest.app.publisher.id,
new AccountGdprConfig(enabled: true, eeaCountries: CAN.ISOAlpha2)))

and: "Flush metrics"
flushMetrics(privacyPbsService)

when: "PBS processes auction request"
privacyPbsService.sendAuctionRequest(bidRequest)

then: "PBs shouldn't increment metrics when eea-country matched"
def metricsRequest = privacyPbsService.sendCollectedMetricsRequest()
assert !metricsRequest["privacy.tcf.v2.in-geo"]
assert metricsRequest["privacy.tcf.v2.out-geo"] == 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import static org.prebid.server.functional.model.config.PurposeEnforcement.FULL
import static org.prebid.server.functional.model.config.PurposeEnforcement.NO
import static org.prebid.server.functional.model.mock.services.vendorlist.VendorListResponse.getDefaultVendorListResponse
import static org.prebid.server.functional.model.pricefloors.Country.USA
import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA
import static org.prebid.server.functional.model.request.amp.ConsentType.GPP
import static org.prebid.server.functional.model.request.amp.ConsentType.TCF_2
import static org.prebid.server.functional.model.request.amp.ConsentType.US_PRIVACY
Expand Down Expand Up @@ -91,14 +92,16 @@ abstract class PrivacyBaseSpec extends BaseSpec {
"gdpr.vendorlist.v3.retry-policy.exponential-backoff.max-delay-millis": EXPONENTIAL_BACKOFF_MAX_DELAY as String,
"gdpr.vendorlist.v3.retry-policy.exponential-backoff.factor" : Long.MAX_VALUE as String]

private static final Map<String, String> GDPR_EEA_COUNTRY = ["gdpr.eea-countries": "$BULGARIA.ISOAlpha2, SK, VK" as String]

protected static final String VENDOR_LIST_PATH = "/app/prebid-server/data/vendorlist-v{VendorVersion}/{VendorVersion}.json"
protected static final String VALID_VALUE_FOR_GPC_HEADER = "1"
protected static final GppConsent SIMPLE_GPC_DISALLOW_LOGIC = new UsNatV1Consent.Builder().setGpc(true).build()
protected static final VendorList vendorListResponse = new VendorList(networkServiceContainer)

@Shared
protected final PrebidServerService privacyPbsService = pbsServiceFactory.getService(GDPR_VENDOR_LIST_CONFIG +
GENERIC_COOKIE_SYNC_CONFIG + GENERIC_VENDOR_CONFIG + RETRY_POLICY_EXPONENTIAL_CONFIG)
GENERIC_COOKIE_SYNC_CONFIG + GENERIC_VENDOR_CONFIG + RETRY_POLICY_EXPONENTIAL_CONFIG + GDPR_EEA_COUNTRY)

protected static final Map<String, String> PBS_CONFIG = OPENX_CONFIG + OPENX_COOKIE_SYNC_CONFIG +
GENERIC_COOKIE_SYNC_CONFIG + GDPR_VENDOR_LIST_CONFIG + SETTING_CONFIG + GENERIC_VENDOR_CONFIG
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,45 @@ public void resolveTcfContextShouldConsiderPresenceOfConsentStringAsInScope() {
verify(metrics).updatePrivacyTcfGeoMetric(2, null);
}

@Test
public void resolveTcfContextShouldUseEeaListFromAccountConfig() {
// given
final GdprConfig gdprConfig = GdprConfig.builder()
.enabled(true)
.consentStringMeansInScope(true)
.build();

target = new TcfDefinerService(
gdprConfig,
singleton(EEA_COUNTRY),
tcf2Service,
geoLocationServiceWrapper,
bidderCatalog,
ipAddressHelper,
metrics);

final String vendorConsent = "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA";

// when
final Future<TcfContext> result = target.resolveTcfContext(
Privacy.builder().consentString(vendorConsent).build(),
"country",
null,
AccountGdprConfig.builder().eeaCountries("").build(),
null,
null,
null,
null);

// then
assertThat(result).isSucceeded();
assertThat(result.result())
.extracting(TcfContext::getInEea)
.isEqualTo(false);

verify(metrics).updatePrivacyTcfGeoMetric(2, false);
}

@Test
public void resolveTcfContextShouldReturnGdprFromCountryWhenGdprFromRequestIsNotValidAndGeoLookupSkipped() {
// given
Expand Down

0 comments on commit 13c9a37

Please sign in to comment.