diff --git a/config/config.go b/config/config.go index 7fc77855810..9e6b1370128 100755 --- a/config/config.go +++ b/config/config.go @@ -159,6 +159,11 @@ type GDPR struct { TCF1 TCF1 `mapstructure:"tcf1"` TCF2 TCF2 `mapstructure:"tcf2"` AMPException bool `mapstructure:"amp_exception"` + // EEACountries (EEA = European Economic Area) are a list of countries where we should assume GDPR applies. + // If the gdpr flag is unset in a request, but geo.country is set, we will assume GDPR applies if and only + // if the country matches one on this list. If both the GDPR flag and country are not set, we default + // to UsersyncIfAmbiguous + EEACountries []string `mapstructure:"eea_countries"` } func (cfg *GDPR) validate(errs configErrors) configErrors { @@ -903,6 +908,10 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("gdpr.tcf2.purpose_one_treatement.enabled", true) v.SetDefault("gdpr.tcf2.purpose_one_treatement.access_allowed", true) v.SetDefault("gdpr.amp_exception", false) + v.SetDefault("gdpr.eea_countries", []string{"ALA", "AUT", "BEL", "BGR", "HRV", "CYP", "CZE", "DNK", "EST", + "FIN", "FRA", "GUF", "DEU", "GIB", "GRC", "GLP", "GGY", "HUN", "ISL", "IRL", "IMN", "ITA", "JEY", "LVA", + "LIE", "LTU", "LUX", "MLT", "MTQ", "MYT", "NLD", "NOR", "POL", "PRT", "REU", "ROU", "BLM", "MAF", "SPM", + "SVK", "SVN", "ESP", "SWE", "GBR"}) v.SetDefault("ccpa.enforce", false) v.SetDefault("lmt.enforce", true) v.SetDefault("currency_converter.fetch_url", "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json") diff --git a/exchange/exchange.go b/exchange/exchange.go index cf5ec9cc000..53f4a7a3e1f 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -55,6 +55,7 @@ type exchange struct { UsersyncIfAmbiguous bool defaultTTLs config.DefaultTTLs privacyConfig config.Privacy + eeaCountries map[string]struct{} } // Container to pass out response ext data from the GetAllBids goroutines back into the main thread @@ -75,6 +76,10 @@ type bidResponseWrapper struct { func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *config.Configuration, metricsEngine pbsmetrics.MetricsEngine, infos adapters.BidderInfos, gDPR gdpr.Permissions, currencyConverter *currencies.RateConverter) Exchange { e := new(exchange) + var s struct{} + for _, c := range cfg.GDPR.EEACountries { + e.eeaCountries[c] = s + } e.adapterMap = newAdapterMap(client, cfg, infos, metricsEngine) e.cache = cache e.cacheTime = time.Duration(cfg.CacheURL.ExpectedTimeMillis) * time.Millisecond @@ -121,9 +126,27 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque e.me.RecordImps(impLabels) } + // Make our best guess if GDPR applies + usersyncIfAmbiguous := e.UsersyncIfAmbiguous + var geo *openrtb.Geo = nil + if bidRequest.User != nil && bidRequest.User.Geo != nil { + geo = bidRequest.User.Geo + } else if bidRequest.Device != nil && bidRequest.Device.Geo != nil { + geo = bidRequest.Device.Geo + } + if geo != nil { + // If we have a country set, and it is on the list, we assume GDPR applies if not set on the request. + // Otherwise we assume it does not apply as long as it appears "valid" (is 3 characters long). + if _, found := e.eeaCountries[strings.ToUpper(geo.Country)]; found { + usersyncIfAmbiguous = false + } else if len(geo.Country) == 3 { + // The country field is formatted properly as a three character country code + usersyncIfAmbiguous = true + } + } // Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder blabels := make(map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels) - cleanRequests, aliases, privacyLabels, errs := cleanOpenRTBRequests(ctx, bidRequest, requestExt, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.privacyConfig) + cleanRequests, aliases, privacyLabels, errs := cleanOpenRTBRequests(ctx, bidRequest, requestExt, usersyncs, blabels, labels, e.gDPR, usersyncIfAmbiguous, e.privacyConfig) e.me.RecordRequestPrivacy(privacyLabels) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 545f04fd0ef..aad448f397f 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -909,6 +909,9 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { LMT: config.LMT{ Enforce: spec.EnforceLMT, }, + GDPR: config.GDPR{ + UsersyncIfAmbiguous: !spec.AssumeGDPRApplies, + }, } ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, privacyConfig) @@ -1026,15 +1029,22 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] } } + var s struct{} + eeac := make(map[string]struct{}) + for _, c := range []string{"FIN", "FRA", "GUF"} { + eeac[c] = s + } + return &exchange{ adapterMap: adapters, me: metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.BidderList()), cache: &wellBehavedCache{}, cacheTime: 0, - gDPR: gdpr.AlwaysAllow{}, + gDPR: gdpr.AlwaysFail{}, currencyConverter: currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)), - UsersyncIfAmbiguous: false, + UsersyncIfAmbiguous: privacyConfig.GDPR.UsersyncIfAmbiguous, privacyConfig: privacyConfig, + eeaCountries: eeac, } } @@ -1882,12 +1892,13 @@ func TestUpdateHbPbCatDur(t *testing.T) { } type exchangeSpec struct { - IncomingRequest exchangeRequest `json:"incomingRequest"` - OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"` - Response exchangeResponse `json:"response,omitempty"` - EnforceCCPA bool `json:"enforceCcpa"` - EnforceLMT bool `json:"enforceLmt"` - DebugLog *DebugLog `json:"debuglog,omitempty"` + IncomingRequest exchangeRequest `json:"incomingRequest"` + OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"` + Response exchangeResponse `json:"response,omitempty"` + EnforceCCPA bool `json:"enforceCcpa"` + EnforceLMT bool `json:"enforceLmt"` + AssumeGDPRApplies bool `json:"assume_gdpr_applies"` + DebugLog *DebugLog `json:"debuglog,omitempty"` } type exchangeRequest struct { diff --git a/exchange/exchangetest/gdpr-geo-eu-off-device.json b/exchange/exchangetest/gdpr-geo-eu-off-device.json new file mode 100644 index 00000000000..fc655de8162 --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-eu-off-device.json @@ -0,0 +1,64 @@ +{ + "assume_gdpr_applies": false, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + }], + "user": { + "buyeruid": "some-buyer-id" + }, + "device": { + "geo": { + "country": "FRA" + } + } +} + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + }], + "user": { + }, + "device": { + "geo": { + "country": "FRA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-eu-off.json b/exchange/exchangetest/gdpr-geo-eu-off.json new file mode 100644 index 00000000000..27a030f11fc --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-eu-off.json @@ -0,0 +1,60 @@ +{ + "assume_gdpr_applies": false, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "FRA" + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + }], + "user": { + "geo": { + "country": "FRA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-eu-on.json b/exchange/exchangetest/gdpr-geo-eu-on.json new file mode 100644 index 00000000000..4ec42fc6c70 --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-eu-on.json @@ -0,0 +1,60 @@ +{ + "assume_gdpr_applies": true, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "FRA" + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + }], + "user": { + "geo": { + "country": "FRA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-usa-off.json b/exchange/exchangetest/gdpr-geo-usa-off.json new file mode 100644 index 00000000000..d56c9318a56 --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-usa-off.json @@ -0,0 +1,61 @@ +{ + "assume_gdpr_applies": false, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "USA" + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "USA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-usa-on.json b/exchange/exchangetest/gdpr-geo-usa-on.json new file mode 100644 index 00000000000..f922be9ea4e --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-usa-on.json @@ -0,0 +1,61 @@ +{ + "assume_gdpr_applies": true, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "USA" + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "USA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/gdpr/impl.go b/gdpr/impl.go index 2deddc7b2ba..2fbd9c5a07c 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -228,3 +228,22 @@ func (a AlwaysAllow) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext func (a AlwaysAllow) AMPException() bool { return false } + +// Exporting to allow for easy test setups +type AlwaysFail struct{} + +func (a AlwaysFail) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) { + return false, nil +} + +func (a AlwaysFail) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName, consent string) (bool, error) { + return false, nil +} + +func (a AlwaysFail) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { + return false, false, false, nil +} + +func (a AlwaysFail) AMPException() bool { + return false +}