Skip to content

Commit

Permalink
GDPR: Don't Call Bidder If It Lacks Purpose 2 Legal Basis (prebid#1851)
Browse files Browse the repository at this point in the history
  • Loading branch information
bsardo authored and jizeyopera committed Oct 13, 2021
1 parent 84ae61e commit 61e83fe
Show file tree
Hide file tree
Showing 21 changed files with 470 additions and 247 deletions.
4 changes: 4 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,9 @@ type DisabledMetrics struct {
// server establishes with bidder servers such as the number of connections
// that were created or reused.
AdapterConnectionMetrics bool `mapstructure:"adapter_connections_metrics"`

// True if we don't want to collect the per adapter GDPR request blocked metric
AdapterGDPRRequestBlocked bool `mapstructure:"adapter_gdpr_request_blocked"`
}

func (cfg *Metrics) validate(errs []error) []error {
Expand Down Expand Up @@ -718,6 +721,7 @@ func SetupViper(v *viper.Viper, filename string) {
// no metrics configured by default (metrics{host|database|username|password})
v.SetDefault("metrics.disabled_metrics.account_adapter_details", false)
v.SetDefault("metrics.disabled_metrics.adapter_connections_metrics", true)
v.SetDefault("metrics.disabled_metrics.adapter_gdpr_request_blocked", false)
v.SetDefault("metrics.influxdb.host", "")
v.SetDefault("metrics.influxdb.database", "")
v.SetDefault("metrics.influxdb.username", "")
Expand Down
3 changes: 3 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ func TestDefaults(t *testing.T) {
cmpInts(t, "metrics.influxdb.collection_rate_seconds", cfg.Metrics.Influxdb.MetricSendInterval, 20)
cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, false)
cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true)
cmpBools(t, "adapter_gdpr_request_blocked", cfg.Metrics.Disabled.AdapterGDPRRequestBlocked, false)
cmpStrings(t, "certificates_file", cfg.PemCertsFile, "")
cmpBools(t, "stored_requests.filesystem.enabled", false, cfg.StoredRequests.Files.Enabled)
cmpStrings(t, "stored_requests.filesystem.directorypath", "./stored_requests/data/by_id", cfg.StoredRequests.Files.Path)
Expand Down Expand Up @@ -197,6 +198,7 @@ metrics:
disabled_metrics:
account_adapter_details: true
adapter_connections_metrics: true
adapter_gdpr_request_blocked: true
datacache:
type: postgres
filename: /usr/db/db.db
Expand Down Expand Up @@ -413,6 +415,7 @@ func TestFullConfig(t *testing.T) {
cmpBools(t, "auto_gen_source_tid", cfg.AutoGenSourceTID, false)
cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, true)
cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true)
cmpBools(t, "adapter_gdpr_request_blocked", cfg.Metrics.Disabled.AdapterGDPRRequestBlocked, true)
cmpStrings(t, "certificates_file", cfg.PemCertsFile, "/etc/ssl/cert.pem")
cmpStrings(t, "request_validation.ipv4_private_networks", cfg.RequestValidation.IPv4PrivateNetworks[0], "1.1.1.0/24")
cmpStrings(t, "request_validation.ipv6_private_networks", cfg.RequestValidation.IPv6PrivateNetworks[0], "1111::/16")
Expand Down
9 changes: 5 additions & 4 deletions endpoints/auction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,8 +441,9 @@ func TestShouldUsersync(t *testing.T) {
type auctionMockPermissions struct {
allowBidderSync bool
allowHostCookies bool
allowGeo bool
allowID bool
allowBidRequest bool
passGeo bool
passID bool
}

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

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) {
return m.allowGeo, m.allowID, nil
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) {
return m.allowBidRequest, m.passGeo, m.passID, nil
}

func TestBidSizeValidate(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions endpoints/cookie_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,6 @@ func (g *gdprPerms) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.Bi
return ok, nil
}

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) {
return true, true, nil
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) {
return true, true, true, nil
}
4 changes: 2 additions & 2 deletions endpoints/setuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,8 @@ func (g *mockPermsSetUID) BidderSyncAllowed(ctx context.Context, bidder openrtb_
return false, nil
}

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) {
return g.personalInfoAllowed, g.personalInfoAllowed, nil
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) {
return g.personalInfoAllowed, g.personalInfoAllowed, g.personalInfoAllowed, nil
}

func newFakeSyncer(familyName string) usersync.Usersyncer {
Expand Down
2 changes: 1 addition & 1 deletion exchange/exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
usersyncIfAmbiguous := e.parseUsersyncIfAmbiguous(r.BidRequest)

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

e.me.RecordRequestPrivacy(privacyLabels)

Expand Down
2 changes: 1 addition & 1 deletion exchange/exchange_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1770,7 +1770,7 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
me: metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames()),
cache: &wellBehavedCache{},
cacheTime: 0,
gDPR: gdpr.AlwaysFail{},
gDPR: &permissionsMock{allowAllBidders: true},
currencyConverter: currency.NewRateConverter(&http.Client{}, "", time.Duration(0)),
UsersyncIfAmbiguous: privacyConfig.GDPR.UsersyncIfAmbiguous,
privacyConfig: privacyConfig,
Expand Down
26 changes: 20 additions & 6 deletions exchange/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ func cleanOpenRTBRequests(ctx context.Context,
req AuctionRequest,
requestExt *openrtb_ext.ExtRequest,
gDPR gdpr.Permissions,
metricsEngine metrics.MetricsEngine,
usersyncIfAmbiguous bool,
privacyConfig config.Privacy,
account *config.Account) (bidderRequests []BidderRequest, privacyLabels metrics.PrivacyLabels, errs []error) {
account *config.Account) (allowedBidderRequests []BidderRequest, privacyLabels metrics.PrivacyLabels, errs []error) {

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

bidderRequests, errs = getAuctionBidderRequests(req, requestExt, impsByBidder, aliases)
var allBidderRequests []BidderRequest
allBidderRequests, errs = getAuctionBidderRequests(req, requestExt, impsByBidder, aliases)

if len(bidderRequests) == 0 {
if len(allBidderRequests) == 0 {
return
}

Expand Down Expand Up @@ -117,7 +119,10 @@ func cleanOpenRTBRequests(ctx context.Context,
}

// bidder level privacy policies
for _, bidderRequest := range bidderRequests {
allowedBidderRequests = make([]BidderRequest, 0, len(allBidderRequests))
for _, bidderRequest := range allBidderRequests {
bidRequestAllowed := true

// CCPA
privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String())

Expand All @@ -133,17 +138,26 @@ func cleanOpenRTBRequests(ctx context.Context,
}
}
var publisherID = req.LegacyLabels.PubID
geo, id, err := gDPR.PersonalInfoAllowed(ctx, bidderRequest.BidderCoreName, publisherID, gdprSignal, consent, weakVendorEnforcement)
bidReq, geo, id, err := gDPR.AuctionActivitiesAllowed(ctx, bidderRequest.BidderCoreName, publisherID, gdprSignal, consent, weakVendorEnforcement)
bidRequestAllowed = bidReq

if err == nil {
privacyEnforcement.GDPRGeo = !geo
privacyEnforcement.GDPRID = !id
} else {
privacyEnforcement.GDPRGeo = true
privacyEnforcement.GDPRID = true
}

if !bidRequestAllowed {
metricsEngine.RecordAdapterGDPRRequestBlocked(bidderRequest.BidderCoreName)
}
}

privacyEnforcement.Apply(bidderRequest.BidRequest)
if bidRequestAllowed {
privacyEnforcement.Apply(bidderRequest.BidRequest)
allowedBidderRequests = append(allowedBidderRequests, bidderRequest)
}
}

return
Expand Down
143 changes: 129 additions & 14 deletions exchange/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ import (
"github.com/prebid/prebid-server/metrics"
"github.com/prebid/prebid-server/openrtb_ext"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

// permissionsMock mocks the Permissions interface for tests
//
// It only allows appnexus for GDPR consent
type permissionsMock struct {
personalInfoAllowed bool
personalInfoAllowedError error
allowAllBidders bool
allowedBidders []openrtb_ext.BidderName
passGeo bool
passID bool
activitiesError error
}

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

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) {
return p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowedError
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) {
if p.allowAllBidders {
return true, p.passGeo, p.passID, p.activitiesError
}

for _, allowedBidder := range p.allowedBidders {
if bidder == allowedBidder {
allowBidRequest = true
}
}

return allowBidRequest, p.passGeo, p.passID, p.activitiesError
}

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

for _, test := range testCases {
bidderRequests, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, nil)
metricsMock := metrics.MetricsEngineMock{}
permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
bidderRequests, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, &permissions, &metricsMock, true, privacyConfig, nil)
if test.hasError {
assert.NotNil(t, err, "Error shouldn't be nil")
} else {
Expand Down Expand Up @@ -620,7 +634,8 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
context.Background(),
auctionReq,
nil,
&permissionsMock{personalInfoAllowed: true},
&permissionsMock{allowAllBidders: true, passGeo: true, passID: true},
&metrics.MetricsEngineMock{},
true,
privacyConfig,
nil)
Expand Down Expand Up @@ -681,7 +696,9 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) {
Enforce: true,
},
}
_, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, &reqExtStruct, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, nil)
permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
metrics := metrics.MetricsEngineMock{}
_, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, &reqExtStruct, &permissions, &metrics, true, privacyConfig, nil)

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

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

assert.Nil(t, errs)
Expand Down Expand Up @@ -828,7 +847,9 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) {
UserSyncs: &emptyUsersync{},
}

bidderRequests, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, &permissionsMock{}, true, config.Privacy{}, nil)
permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
metrics := metrics.MetricsEngineMock{}
bidderRequests, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, &permissions, &metrics, true, config.Privacy{}, nil)
if test.hasError == true {
assert.NotNil(t, errs)
assert.Len(t, bidderRequests, 0)
Expand Down Expand Up @@ -1409,7 +1430,9 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
},
}

results, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, nil)
permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
metrics := metrics.MetricsEngineMock{}
results, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissions, &metrics, true, privacyConfig, nil)
result := results[0]

assert.Nil(t, errs)
Expand All @@ -1424,7 +1447,7 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
}
}

func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
func TestCleanOpenRTBRequestsGDPRScrub(t *testing.T) {
tcf1Consent := "BONV8oqONXwgmADACHENAO7pqzAAppY"
tcf2Consent := "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA"
trueValue, falseValue := true, false
Expand Down Expand Up @@ -1624,7 +1647,8 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
context.Background(),
auctionReq,
nil,
&permissionsMock{personalInfoAllowed: !test.gdprScrub, personalInfoAllowedError: test.permissionsError},
&permissionsMock{allowAllBidders: true, passGeo: !test.gdprScrub, passID: !test.gdprScrub, activitiesError: test.permissionsError},
&metrics.MetricsEngineMock{},
test.userSyncIfAmbiguous,
privacyConfig,
nil)
Expand All @@ -1647,6 +1671,97 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
}
}

func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) {
testCases := []struct {
description string
gdprEnforced bool
gdprAllowedBidders []openrtb_ext.BidderName
expectedBidders []openrtb_ext.BidderName
expectedBlockedBidders []openrtb_ext.BidderName
}{
{
description: "gdpr enforced, one request allowed and one request blocked",
gdprEnforced: true,
gdprAllowedBidders: []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus},
expectedBidders: []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus},
expectedBlockedBidders: []openrtb_ext.BidderName{openrtb_ext.BidderRubicon},
},
{
description: "gdpr enforced, two requests allowed and no requests blocked",
gdprEnforced: true,
gdprAllowedBidders: []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon},
expectedBidders: []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon},
expectedBlockedBidders: []openrtb_ext.BidderName{},
},
{
description: "gdpr not enforced, two requests allowed and no requests blocked",
gdprEnforced: false,
gdprAllowedBidders: []openrtb_ext.BidderName{},
expectedBidders: []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon},
expectedBlockedBidders: []openrtb_ext.BidderName{},
},
}

for _, test := range testCases {
req := newBidRequest(t)
req.Regs = &openrtb2.Regs{
Ext: json.RawMessage(`{"gdpr":1}`),
}
req.Imp[0].Ext = json.RawMessage(`{"appnexus": {"placementId": 1}, "rubicon": {}}`)

privacyConfig := config.Privacy{
GDPR: config.GDPR{
Enabled: test.gdprEnforced,
UsersyncIfAmbiguous: true,
TCF2: config.TCF2{
Enabled: true,
},
},
}

accountConfig := config.Account{
GDPR: config.AccountGDPR{
Enabled: nil,
},
}

auctionReq := AuctionRequest{
BidRequest: req,
UserSyncs: &emptyUsersync{},
Account: accountConfig,
}

metricsMock := metrics.MetricsEngineMock{}
metricsMock.Mock.On("RecordAdapterGDPRRequestBlocked", mock.Anything).Return()

results, _, errs := cleanOpenRTBRequests(
context.Background(),
auctionReq,
nil,
&permissionsMock{allowedBidders: test.gdprAllowedBidders, passGeo: true, passID: true, activitiesError: nil},
&metricsMock,
true,
privacyConfig,
nil)

// extract bidder name from each request in the results
bidders := []openrtb_ext.BidderName{}
for _, req := range results {
bidders = append(bidders, req.BidderName)
}

assert.Empty(t, errs, test.description)
assert.ElementsMatch(t, bidders, test.expectedBidders, test.description)

for _, blockedBidder := range test.expectedBlockedBidders {
metricsMock.AssertCalled(t, "RecordAdapterGDPRRequestBlocked", blockedBidder)
}
for _, allowedBidder := range test.expectedBidders {
metricsMock.AssertNotCalled(t, "RecordAdapterGDPRRequestBlocked", allowedBidder)
}
}
}

// newAdapterAliasBidRequest builds a BidRequest with aliases
func newAdapterAliasBidRequest(t *testing.T) *openrtb2.BidRequest {
dnt := int8(1)
Expand Down
Loading

0 comments on commit 61e83fe

Please sign in to comment.