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

Add account.privacy.gdpr.eea-countries property #3212

Merged
merged 5 commits into from
Jun 21, 2024
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
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
Loading