Skip to content

Commit ff71293

Browse files
bsardosachin-pubmatic
authored andcommitted
GDPR: Don't Call Bidder If It Lacks Purpose 2 Legal Basis (prebid#1851)
1 parent 5a34e65 commit ff71293

15 files changed

+372
-222
lines changed

config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,9 @@ type DisabledMetrics struct {
358358
// server establishes with bidder servers such as the number of connections
359359
// that were created or reused.
360360
AdapterConnectionMetrics bool `mapstructure:"adapter_connections_metrics"`
361+
362+
// True if we don't want to collect the per adapter GDPR request blocked metric
363+
AdapterGDPRRequestBlocked bool `mapstructure:"adapter_gdpr_request_blocked"`
361364
}
362365

363366
func (cfg *Metrics) validate(errs []error) []error {
@@ -728,6 +731,7 @@ func SetupViper(v *viper.Viper, filename string) {
728731
// no metrics configured by default (metrics{host|database|username|password})
729732
v.SetDefault("metrics.disabled_metrics.account_adapter_details", false)
730733
v.SetDefault("metrics.disabled_metrics.adapter_connections_metrics", true)
734+
v.SetDefault("metrics.disabled_metrics.adapter_gdpr_request_blocked", false)
731735
v.SetDefault("metrics.influxdb.host", "")
732736
v.SetDefault("metrics.influxdb.database", "")
733737
v.SetDefault("metrics.influxdb.username", "")

config/config_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ func TestDefaults(t *testing.T) {
135135
cmpInts(t, "metrics.influxdb.collection_rate_seconds", cfg.Metrics.Influxdb.MetricSendInterval, 20)
136136
cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, false)
137137
cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true)
138+
cmpBools(t, "adapter_gdpr_request_blocked", cfg.Metrics.Disabled.AdapterGDPRRequestBlocked, false)
138139
cmpStrings(t, "certificates_file", cfg.PemCertsFile, "")
139140
cmpBools(t, "stored_requests.filesystem.enabled", false, cfg.StoredRequests.Files.Enabled)
140141
cmpStrings(t, "stored_requests.filesystem.directorypath", "./stored_requests/data/by_id", cfg.StoredRequests.Files.Path)
@@ -197,6 +198,7 @@ metrics:
197198
disabled_metrics:
198199
account_adapter_details: true
199200
adapter_connections_metrics: true
201+
adapter_gdpr_request_blocked: true
200202
datacache:
201203
type: postgres
202204
filename: /usr/db/db.db
@@ -413,6 +415,7 @@ func TestFullConfig(t *testing.T) {
413415
cmpBools(t, "auto_gen_source_tid", cfg.AutoGenSourceTID, false)
414416
cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, true)
415417
cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true)
418+
cmpBools(t, "adapter_gdpr_request_blocked", cfg.Metrics.Disabled.AdapterGDPRRequestBlocked, true)
416419
cmpStrings(t, "certificates_file", cfg.PemCertsFile, "/etc/ssl/cert.pem")
417420
cmpStrings(t, "request_validation.ipv4_private_networks", cfg.RequestValidation.IPv4PrivateNetworks[0], "1.1.1.0/24")
418421
cmpStrings(t, "request_validation.ipv6_private_networks", cfg.RequestValidation.IPv6PrivateNetworks[0], "1111::/16")

endpoints/auction_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -441,8 +441,9 @@ func TestShouldUsersync(t *testing.T) {
441441
type auctionMockPermissions struct {
442442
allowBidderSync bool
443443
allowHostCookies bool
444-
allowGeo bool
445-
allowID bool
444+
allowBidRequest bool
445+
passGeo bool
446+
passID bool
446447
}
447448

448449
func (m *auctionMockPermissions) HostCookiesAllowed(ctx context.Context, gdprSignal gdpr.Signal, consent string) (bool, error) {
@@ -453,8 +454,8 @@ func (m *auctionMockPermissions) BidderSyncAllowed(ctx context.Context, bidder o
453454
return m.allowBidderSync, nil
454455
}
455456

456-
func (m *auctionMockPermissions) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string, weakVendorEnforcement bool) (allowGeo bool, allowID bool, err error) {
457-
return m.allowGeo, m.allowID, nil
457+
func (m *auctionMockPermissions) AuctionActivitiesAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string, weakVendorEnforcement bool) (allowBidRequest bool, passGeo bool, passID bool, err error) {
458+
return m.allowBidRequest, m.passGeo, m.passID, nil
458459
}
459460

460461
func TestBidSizeValidate(t *testing.T) {

endpoints/cookie_sync_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,6 @@ func (g *gdprPerms) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.Bi
254254
return ok, nil
255255
}
256256

257-
func (g *gdprPerms) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string, weakVendorEnforcement bool) (allowGeo bool, allowID bool, err error) {
258-
return true, true, nil
257+
func (g *gdprPerms) AuctionActivitiesAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string, weakVendorEnforcement bool) (allowBidRequest, passGeo bool, passID bool, err error) {
258+
return true, true, true, nil
259259
}

endpoints/setuid_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,8 +439,8 @@ func (g *mockPermsSetUID) BidderSyncAllowed(ctx context.Context, bidder openrtb_
439439
return false, nil
440440
}
441441

442-
func (g *mockPermsSetUID) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string, weakVendorEnforcement bool) (allowGeo bool, allowID bool, err error) {
443-
return g.personalInfoAllowed, g.personalInfoAllowed, nil
442+
func (g *mockPermsSetUID) AuctionActivitiesAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string, weakVendorEnforcement bool) (allowBidRequest bool, passGeo bool, passID bool, err error) {
443+
return g.personalInfoAllowed, g.personalInfoAllowed, g.personalInfoAllowed, nil
444444
}
445445

446446
func newFakeSyncer(familyName string) usersync.Usersyncer {

exchange/exchange.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
182182
usersyncIfAmbiguous := e.parseUsersyncIfAmbiguous(r.BidRequest)
183183

184184
// Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder
185-
bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(ctx, r, requestExt, e.gDPR, usersyncIfAmbiguous, e.privacyConfig, &r.Account)
185+
bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(ctx, r, requestExt, e.gDPR, e.me, usersyncIfAmbiguous, e.privacyConfig, &r.Account)
186186

187187
e.me.RecordRequestPrivacy(privacyLabels)
188188

exchange/exchange_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1771,7 +1771,7 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
17711771
me: metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames()),
17721772
cache: &wellBehavedCache{},
17731773
cacheTime: 0,
1774-
gDPR: gdpr.AlwaysFail{},
1774+
gDPR: &permissionsMock{allowAllBidders: true},
17751775
currencyConverter: currency.NewRateConverter(&http.Client{}, "", time.Duration(0)),
17761776
UsersyncIfAmbiguous: privacyConfig.GDPR.UsersyncIfAmbiguous,
17771777
privacyConfig: privacyConfig,

exchange/utils.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ func cleanOpenRTBRequests(ctx context.Context,
5656
req AuctionRequest,
5757
requestExt *openrtb_ext.ExtRequest,
5858
gDPR gdpr.Permissions,
59+
metricsEngine metrics.MetricsEngine,
5960
usersyncIfAmbiguous bool,
6061
privacyConfig config.Privacy,
61-
account *config.Account) (bidderRequests []BidderRequest, privacyLabels metrics.PrivacyLabels, errs []error) {
62+
account *config.Account) (allowedBidderRequests []BidderRequest, privacyLabels metrics.PrivacyLabels, errs []error) {
6263

6364
impsByBidder, err := splitImps(req.BidRequest.Imp)
6465
if err != nil {
@@ -71,9 +72,10 @@ func cleanOpenRTBRequests(ctx context.Context,
7172
return
7273
}
7374

74-
bidderRequests, errs = getAuctionBidderRequests(req, requestExt, impsByBidder, aliases)
75+
var allBidderRequests []BidderRequest
76+
allBidderRequests, errs = getAuctionBidderRequests(req, requestExt, impsByBidder, aliases)
7577

76-
if len(bidderRequests) == 0 {
78+
if len(allBidderRequests) == 0 {
7779
return
7880
}
7981

@@ -117,7 +119,10 @@ func cleanOpenRTBRequests(ctx context.Context,
117119
}
118120

119121
// bidder level privacy policies
120-
for _, bidderRequest := range bidderRequests {
122+
allowedBidderRequests = make([]BidderRequest, 0, len(allBidderRequests))
123+
for _, bidderRequest := range allBidderRequests {
124+
bidRequestAllowed := true
125+
121126
// CCPA
122127
privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String())
123128

@@ -133,17 +138,26 @@ func cleanOpenRTBRequests(ctx context.Context,
133138
}
134139
}
135140
var publisherID = req.LegacyLabels.PubID
136-
geo, id, err := gDPR.PersonalInfoAllowed(ctx, bidderRequest.BidderCoreName, publisherID, gdprSignal, consent, weakVendorEnforcement)
141+
bidReq, geo, id, err := gDPR.AuctionActivitiesAllowed(ctx, bidderRequest.BidderCoreName, publisherID, gdprSignal, consent, weakVendorEnforcement)
142+
bidRequestAllowed = bidReq
143+
137144
if err == nil {
138145
privacyEnforcement.GDPRGeo = !geo
139146
privacyEnforcement.GDPRID = !id
140147
} else {
141148
privacyEnforcement.GDPRGeo = true
142149
privacyEnforcement.GDPRID = true
143150
}
151+
152+
if !bidRequestAllowed {
153+
metricsEngine.RecordAdapterGDPRRequestBlocked(bidderRequest.BidderCoreName)
154+
}
144155
}
145156

146-
privacyEnforcement.Apply(bidderRequest.BidRequest)
157+
if bidRequestAllowed {
158+
privacyEnforcement.Apply(bidderRequest.BidRequest)
159+
allowedBidderRequests = append(allowedBidderRequests, bidderRequest)
160+
}
147161
}
148162

149163
return

exchange/utils_test.go

Lines changed: 129 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ import (
1414
"github.com/prebid/prebid-server/metrics"
1515
"github.com/prebid/prebid-server/openrtb_ext"
1616
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/mock"
1718
)
1819

1920
// permissionsMock mocks the Permissions interface for tests
20-
//
21-
// It only allows appnexus for GDPR consent
2221
type permissionsMock struct {
23-
personalInfoAllowed bool
24-
personalInfoAllowedError error
22+
allowAllBidders bool
23+
allowedBidders []openrtb_ext.BidderName
24+
passGeo bool
25+
passID bool
26+
activitiesError error
2527
}
2628

2729
func (p *permissionsMock) HostCookiesAllowed(ctx context.Context, gdpr gdpr.Signal, consent string) (bool, error) {
@@ -32,8 +34,18 @@ func (p *permissionsMock) BidderSyncAllowed(ctx context.Context, bidder openrtb_
3234
return true, nil
3335
}
3436

35-
func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdpr gdpr.Signal, consent string, weakVendorEnforcement bool) (allowGeo bool, allowID bool, err error) {
36-
return p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowedError
37+
func (p *permissionsMock) AuctionActivitiesAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdpr gdpr.Signal, consent string, weakVendorEnforcement bool) (allowBidRequest bool, passGeo bool, passID bool, err error) {
38+
if p.allowAllBidders {
39+
return true, p.passGeo, p.passID, p.activitiesError
40+
}
41+
42+
for _, allowedBidder := range p.allowedBidders {
43+
if bidder == allowedBidder {
44+
allowBidRequest = true
45+
}
46+
}
47+
48+
return allowBidRequest, p.passGeo, p.passID, p.activitiesError
3749
}
3850

3951
func assertReq(t *testing.T, bidderRequests []BidderRequest,
@@ -465,7 +477,9 @@ func TestCleanOpenRTBRequests(t *testing.T) {
465477
}
466478

467479
for _, test := range testCases {
468-
bidderRequests, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, nil)
480+
metricsMock := metrics.MetricsEngineMock{}
481+
permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
482+
bidderRequests, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, &permissions, &metricsMock, true, privacyConfig, nil)
469483
if test.hasError {
470484
assert.NotNil(t, err, "Error shouldn't be nil")
471485
} else {
@@ -620,7 +634,8 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
620634
context.Background(),
621635
auctionReq,
622636
nil,
623-
&permissionsMock{personalInfoAllowed: true},
637+
&permissionsMock{allowAllBidders: true, passGeo: true, passID: true},
638+
&metrics.MetricsEngineMock{},
624639
true,
625640
privacyConfig,
626641
nil)
@@ -681,7 +696,9 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) {
681696
Enforce: true,
682697
},
683698
}
684-
_, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, &reqExtStruct, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, nil)
699+
permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
700+
metrics := metrics.MetricsEngineMock{}
701+
_, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, &reqExtStruct, &permissions, &metrics, true, privacyConfig, nil)
685702

686703
assert.ElementsMatch(t, []error{test.expectError}, errs, test.description)
687704
}
@@ -721,7 +738,9 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) {
721738
UserSyncs: &emptyUsersync{},
722739
}
723740

724-
bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissionsMock{personalInfoAllowed: true}, true, config.Privacy{}, nil)
741+
permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
742+
metrics := metrics.MetricsEngineMock{}
743+
bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissions, &metrics, true, config.Privacy{}, nil)
725744
result := bidderRequests[0]
726745

727746
assert.Nil(t, errs)
@@ -828,7 +847,9 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) {
828847
UserSyncs: &emptyUsersync{},
829848
}
830849

831-
bidderRequests, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, &permissionsMock{}, true, config.Privacy{}, nil)
850+
permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
851+
metrics := metrics.MetricsEngineMock{}
852+
bidderRequests, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, &permissions, &metrics, true, config.Privacy{}, nil)
832853
if test.hasError == true {
833854
assert.NotNil(t, errs)
834855
assert.Len(t, bidderRequests, 0)
@@ -1409,7 +1430,9 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
14091430
},
14101431
}
14111432

1412-
results, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, nil)
1433+
permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
1434+
metrics := metrics.MetricsEngineMock{}
1435+
results, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissions, &metrics, true, privacyConfig, nil)
14131436
result := results[0]
14141437

14151438
assert.Nil(t, errs)
@@ -1424,7 +1447,7 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
14241447
}
14251448
}
14261449

1427-
func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
1450+
func TestCleanOpenRTBRequestsGDPRScrub(t *testing.T) {
14281451
tcf1Consent := "BONV8oqONXwgmADACHENAO7pqzAAppY"
14291452
tcf2Consent := "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA"
14301453
trueValue, falseValue := true, false
@@ -1624,7 +1647,8 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
16241647
context.Background(),
16251648
auctionReq,
16261649
nil,
1627-
&permissionsMock{personalInfoAllowed: !test.gdprScrub, personalInfoAllowedError: test.permissionsError},
1650+
&permissionsMock{allowAllBidders: true, passGeo: !test.gdprScrub, passID: !test.gdprScrub, activitiesError: test.permissionsError},
1651+
&metrics.MetricsEngineMock{},
16281652
test.userSyncIfAmbiguous,
16291653
privacyConfig,
16301654
nil)
@@ -1647,6 +1671,97 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
16471671
}
16481672
}
16491673

1674+
func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) {
1675+
testCases := []struct {
1676+
description string
1677+
gdprEnforced bool
1678+
gdprAllowedBidders []openrtb_ext.BidderName
1679+
expectedBidders []openrtb_ext.BidderName
1680+
expectedBlockedBidders []openrtb_ext.BidderName
1681+
}{
1682+
{
1683+
description: "gdpr enforced, one request allowed and one request blocked",
1684+
gdprEnforced: true,
1685+
gdprAllowedBidders: []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus},
1686+
expectedBidders: []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus},
1687+
expectedBlockedBidders: []openrtb_ext.BidderName{openrtb_ext.BidderRubicon},
1688+
},
1689+
{
1690+
description: "gdpr enforced, two requests allowed and no requests blocked",
1691+
gdprEnforced: true,
1692+
gdprAllowedBidders: []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon},
1693+
expectedBidders: []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon},
1694+
expectedBlockedBidders: []openrtb_ext.BidderName{},
1695+
},
1696+
{
1697+
description: "gdpr not enforced, two requests allowed and no requests blocked",
1698+
gdprEnforced: false,
1699+
gdprAllowedBidders: []openrtb_ext.BidderName{},
1700+
expectedBidders: []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon},
1701+
expectedBlockedBidders: []openrtb_ext.BidderName{},
1702+
},
1703+
}
1704+
1705+
for _, test := range testCases {
1706+
req := newBidRequest(t)
1707+
req.Regs = &openrtb2.Regs{
1708+
Ext: json.RawMessage(`{"gdpr":1}`),
1709+
}
1710+
req.Imp[0].Ext = json.RawMessage(`{"appnexus": {"placementId": 1}, "rubicon": {}}`)
1711+
1712+
privacyConfig := config.Privacy{
1713+
GDPR: config.GDPR{
1714+
Enabled: test.gdprEnforced,
1715+
UsersyncIfAmbiguous: true,
1716+
TCF2: config.TCF2{
1717+
Enabled: true,
1718+
},
1719+
},
1720+
}
1721+
1722+
accountConfig := config.Account{
1723+
GDPR: config.AccountGDPR{
1724+
Enabled: nil,
1725+
},
1726+
}
1727+
1728+
auctionReq := AuctionRequest{
1729+
BidRequest: req,
1730+
UserSyncs: &emptyUsersync{},
1731+
Account: accountConfig,
1732+
}
1733+
1734+
metricsMock := metrics.MetricsEngineMock{}
1735+
metricsMock.Mock.On("RecordAdapterGDPRRequestBlocked", mock.Anything).Return()
1736+
1737+
results, _, errs := cleanOpenRTBRequests(
1738+
context.Background(),
1739+
auctionReq,
1740+
nil,
1741+
&permissionsMock{allowedBidders: test.gdprAllowedBidders, passGeo: true, passID: true, activitiesError: nil},
1742+
&metricsMock,
1743+
true,
1744+
privacyConfig,
1745+
nil)
1746+
1747+
// extract bidder name from each request in the results
1748+
bidders := []openrtb_ext.BidderName{}
1749+
for _, req := range results {
1750+
bidders = append(bidders, req.BidderName)
1751+
}
1752+
1753+
assert.Empty(t, errs, test.description)
1754+
assert.ElementsMatch(t, bidders, test.expectedBidders, test.description)
1755+
1756+
for _, blockedBidder := range test.expectedBlockedBidders {
1757+
metricsMock.AssertCalled(t, "RecordAdapterGDPRRequestBlocked", blockedBidder)
1758+
}
1759+
for _, allowedBidder := range test.expectedBidders {
1760+
metricsMock.AssertNotCalled(t, "RecordAdapterGDPRRequestBlocked", allowedBidder)
1761+
}
1762+
}
1763+
}
1764+
16501765
// newAdapterAliasBidRequest builds a BidRequest with aliases
16511766
func newAdapterAliasBidRequest(t *testing.T) *openrtb2.BidRequest {
16521767
dnt := int8(1)

0 commit comments

Comments
 (0)