Skip to content

Commit

Permalink
Allow bidders to skip sync for GDPR/GPP (prebid#3265)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexBVolcy authored Nov 27, 2023
1 parent 020a7dd commit df072ff
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 16 deletions.
8 changes: 8 additions & 0 deletions config/bidderinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ type Syncer struct {

// SupportCORS identifies if CORS is supported for the user syncing endpoints.
SupportCORS *bool `yaml:"supportCors" mapstructure:"support_cors"`

// SkipWhen allows bidders to specify when they don't want to sync
SkipWhen *SkipWhen `yaml:"skipwhen" mapstructure:"skipwhen"`
}

type SkipWhen struct {
GDPR bool `yaml:"gdpr" mapstructure:"gdpr"`
GPPSID []string `yaml:"gpp_sid" mapstructure:"gpp_sid"`
}

// SyncerEndpoint specifies the configuration of the URL returned by the /cookie_sync endpoint
Expand Down
9 changes: 8 additions & 1 deletion endpoints/cookie_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func NewCookieSyncEndpoint(
}

return &cookieSyncEndpoint{
chooser: usersync.NewChooser(syncersByBidder, bidderHashSet),
chooser: usersync.NewChooser(syncersByBidder, bidderHashSet, config.BidderInfos),
config: config,
privacyConfig: usersyncPrivacyConfig{
gdprConfig: config.GDPR,
Expand Down Expand Up @@ -180,8 +180,10 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma
ccpaParsedPolicy: ccpaParsedPolicy,
activityControl: activityControl,
activityRequest: privacy.NewRequestFromPolicies(privacyPolicies),
gdprSignal: gdprSignal,
},
SyncTypeFilter: syncTypeFilter,
GPPSID: request.GPPSID,
}
return rx, privacyMacros, nil
}
Expand Down Expand Up @@ -554,6 +556,7 @@ type usersyncPrivacy struct {
ccpaParsedPolicy ccpa.ParsedPolicy
activityControl privacy.ActivityControl
activityRequest privacy.ActivityRequest
gdprSignal gdpr.Signal
}

func (p usersyncPrivacy) GDPRAllowsHostCookie() bool {
Expand All @@ -577,3 +580,7 @@ func (p usersyncPrivacy) ActivityAllowsUserSync(bidder string) bool {
privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidder},
p.activityRequest)
}

func (p usersyncPrivacy) GDPRInScope() bool {
return p.gdprSignal == gdpr.SignalYes
}
71 changes: 62 additions & 9 deletions endpoints/cookie_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,18 @@ func TestNewCookieSyncEndpoint(t *testing.T) {
analytics = MockAnalyticsRunner{}
fetcher = FakeAccountsFetcher{}
bidders = map[string]openrtb_ext.BidderName{"bidderA": openrtb_ext.BidderName("bidderA"), "bidderB": openrtb_ext.BidderName("bidderB")}
bidderInfo = map[string]config.BidderInfo{"bidderA": {}, "bidderB": {}}
biddersKnown = map[string]struct{}{"bidderA": {}, "bidderB": {}}
)

endpoint := NewCookieSyncEndpoint(
syncersByBidder,
&config.Configuration{
UserSync: configUserSync,
HostCookie: configHostCookie,
GDPR: configGDPR,
CCPA: config.CCPA{Enforce: configCCPAEnforce},
UserSync: configUserSync,
HostCookie: configHostCookie,
GDPR: configGDPR,
CCPA: config.CCPA{Enforce: configCCPAEnforce},
BidderInfos: bidderInfo,
},
gdprPermsBuilder,
tcf2ConfigBuilder,
Expand All @@ -66,12 +68,13 @@ func TestNewCookieSyncEndpoint(t *testing.T) {
result := endpoint.(*cookieSyncEndpoint)

expected := &cookieSyncEndpoint{
chooser: usersync.NewChooser(syncersByBidder, biddersKnown),
chooser: usersync.NewChooser(syncersByBidder, biddersKnown, bidderInfo),
config: &config.Configuration{
UserSync: configUserSync,
HostCookie: configHostCookie,
GDPR: configGDPR,
CCPA: config.CCPA{Enforce: configCCPAEnforce},
UserSync: configUserSync,
HostCookie: configHostCookie,
GDPR: configGDPR,
CCPA: config.CCPA{Enforce: configCCPAEnforce},
BidderInfos: bidderInfo,
},
privacyConfig: usersyncPrivacyConfig{
gdprConfig: configGDPR,
Expand Down Expand Up @@ -571,11 +574,13 @@ func TestCookieSyncParseRequest(t *testing.T) {
gdprPermissions: &fakePermissions{},
ccpaParsedPolicy: expectedCCPAParsedPolicy,
activityRequest: privacy.NewRequestFromPolicies(privacy.Policies{GPPSID: []int8{2}}),
gdprSignal: 1,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Redirect: usersync.NewSpecificBidderFilter([]string{"b"}, usersync.BidderFilterModeExclude),
},
GPPSID: "2",
},
},
{
Expand Down Expand Up @@ -611,6 +616,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
gdprPermissions: &fakePermissions{},
ccpaParsedPolicy: expectedCCPAParsedPolicy,
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: 1,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand All @@ -628,6 +634,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
Privacy: usersyncPrivacy{
gdprPermissions: &fakePermissions{},
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: -1,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand Down Expand Up @@ -655,6 +662,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
Privacy: usersyncPrivacy{
gdprPermissions: &fakePermissions{},
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: -1,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand Down Expand Up @@ -682,6 +690,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
Privacy: usersyncPrivacy{
gdprPermissions: &fakePermissions{},
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: -1,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand Down Expand Up @@ -709,6 +718,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
Privacy: usersyncPrivacy{
gdprPermissions: &fakePermissions{},
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: -1,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand Down Expand Up @@ -736,6 +746,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
Privacy: usersyncPrivacy{
gdprPermissions: &fakePermissions{},
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: -1,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand Down Expand Up @@ -763,6 +774,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
Privacy: usersyncPrivacy{
gdprPermissions: &fakePermissions{},
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: -1,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand Down Expand Up @@ -790,6 +802,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
Privacy: usersyncPrivacy{
gdprPermissions: &fakePermissions{},
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: -1,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand All @@ -807,6 +820,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
Privacy: usersyncPrivacy{
gdprPermissions: &fakePermissions{},
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: -1,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand All @@ -826,6 +840,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
Privacy: usersyncPrivacy{
gdprPermissions: &fakePermissions{},
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: -1,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand Down Expand Up @@ -866,6 +881,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
Privacy: usersyncPrivacy{
gdprPermissions: &fakePermissions{},
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: 0,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand All @@ -892,6 +908,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
Privacy: usersyncPrivacy{
gdprPermissions: &fakePermissions{},
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: -1,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand Down Expand Up @@ -937,6 +954,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
Privacy: usersyncPrivacy{
gdprPermissions: &fakePermissions{},
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: -1,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand Down Expand Up @@ -969,6 +987,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
Privacy: usersyncPrivacy{
gdprPermissions: &fakePermissions{},
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: -1,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand Down Expand Up @@ -1001,6 +1020,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
Privacy: usersyncPrivacy{
gdprPermissions: &fakePermissions{},
activityRequest: emptyActivityPoliciesRequest,
gdprSignal: 0,
},
SyncTypeFilter: usersync.SyncTypeFilter{
IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
Expand Down Expand Up @@ -1974,6 +1994,39 @@ func TestCookieSyncActivityControlIntegration(t *testing.T) {
}
}

func TestUsersyncPrivacyGDPRInScope(t *testing.T) {
testCases := []struct {
description string
givenGdprSignal gdpr.Signal
expected bool
}{
{
description: "GDPR Signal Yes",
givenGdprSignal: gdpr.SignalYes,
expected: true,
},
{
description: "GDPR Signal No",
givenGdprSignal: gdpr.SignalNo,
expected: false,
},
{
description: "GDPR Signal Ambigious",
givenGdprSignal: gdpr.SignalAmbiguous,
expected: false,
},
}

for _, test := range testCases {
privacy := usersyncPrivacy{
gdprSignal: test.givenGdprSignal,
}

result := privacy.GDPRInScope()
assert.Equal(t, test.expected, result, test.description)
}
}

func TestCombineErrors(t *testing.T) {
testCases := []struct {
description string
Expand Down
26 changes: 23 additions & 3 deletions usersync/chooser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package usersync
import (
"strings"

"github.com/prebid/prebid-server/v2/config"
"github.com/prebid/prebid-server/v2/openrtb_ext"
)

Expand All @@ -14,7 +15,7 @@ type Chooser interface {
}

// NewChooser returns a new instance of the standard chooser implementation.
func NewChooser(bidderSyncerLookup map[string]Syncer, biddersKnown map[string]struct{}) Chooser {
func NewChooser(bidderSyncerLookup map[string]Syncer, biddersKnown map[string]struct{}, bidderInfo map[string]config.BidderInfo) Chooser {
bidders := make([]string, 0, len(bidderSyncerLookup))

for k := range bidderSyncerLookup {
Expand All @@ -27,6 +28,7 @@ func NewChooser(bidderSyncerLookup map[string]Syncer, biddersKnown map[string]st
bidderChooser: standardBidderChooser{shuffler: randomShuffler{}},
normalizeValidBidderName: openrtb_ext.NormalizeBidderName,
biddersKnown: biddersKnown,
bidderInfo: bidderInfo,
}
}

Expand All @@ -37,6 +39,7 @@ type Request struct {
Limit int
Privacy Privacy
SyncTypeFilter SyncTypeFilter
GPPSID string
Debug bool
}

Expand Down Expand Up @@ -92,13 +95,17 @@ const (
// StatusBlockedByPrivacy specifies a bidder sync url is not allowed by privacy activities
StatusBlockedByPrivacy

// StatusBlockedByRegulationScope specifies the bidder chose to not sync given GDPR being in scope or because of a GPPSID
StatusBlockedByRegulationScope

// StatusUnconfiguredBidder refers to a bidder who hasn't been configured to have a syncer key, but is known by Prebid Server
StatusUnconfiguredBidder
)

// Privacy determines which privacy policies will be enforced for a user sync request.
type Privacy interface {
GDPRAllowsHostCookie() bool
GDPRInScope() bool
GDPRAllowsBidderSync(bidder string) bool
CCPAAllowsBidderSync(bidder string) bool
ActivityAllowsUserSync(bidder string) bool
Expand All @@ -111,6 +118,7 @@ type standardChooser struct {
bidderChooser bidderChooser
normalizeValidBidderName func(name string) (openrtb_ext.BidderName, bool)
biddersKnown map[string]struct{}
bidderInfo map[string]config.BidderInfo
}

// Choose randomly selects user syncers which are permitted by the user's privacy settings and
Expand All @@ -136,7 +144,7 @@ func (c standardChooser) Choose(request Request, cookie *Cookie) Result {
if _, ok := biddersSeen[bidders[i]]; ok {
continue
}
syncer, evaluation := c.evaluate(bidders[i], syncersSeen, request.SyncTypeFilter, request.Privacy, cookie)
syncer, evaluation := c.evaluate(bidders[i], syncersSeen, request.SyncTypeFilter, request.Privacy, cookie, request.GPPSID)

biddersEvaluated = append(biddersEvaluated, evaluation)
if evaluation.Status == StatusOK {
Expand All @@ -148,7 +156,7 @@ func (c standardChooser) Choose(request Request, cookie *Cookie) Result {
return Result{Status: StatusOK, BiddersEvaluated: biddersEvaluated, SyncersChosen: syncersChosen}
}

func (c standardChooser) evaluate(bidder string, syncersSeen map[string]struct{}, syncTypeFilter SyncTypeFilter, privacy Privacy, cookie *Cookie) (Syncer, BidderEvaluation) {
func (c standardChooser) evaluate(bidder string, syncersSeen map[string]struct{}, syncTypeFilter SyncTypeFilter, privacy Privacy, cookie *Cookie, GPPSID string) (Syncer, BidderEvaluation) {
bidderNormalized, exists := c.normalizeValidBidderName(bidder)
if !exists {
return nil, BidderEvaluation{Status: StatusUnknownBidder, Bidder: bidder}
Expand Down Expand Up @@ -186,5 +194,17 @@ func (c standardChooser) evaluate(bidder string, syncersSeen map[string]struct{}
return nil, BidderEvaluation{Status: StatusBlockedByPrivacy, Bidder: bidder, SyncerKey: syncer.Key()}
}

if privacy.GDPRInScope() && c.bidderInfo[bidder].Syncer != nil && c.bidderInfo[bidder].Syncer.SkipWhen != nil && c.bidderInfo[bidder].Syncer.SkipWhen.GDPR {
return nil, BidderEvaluation{Status: StatusBlockedByRegulationScope, Bidder: bidder, SyncerKey: syncer.Key()}
}

if c.bidderInfo[bidder].Syncer != nil && c.bidderInfo[bidder].Syncer.SkipWhen != nil {
for _, gppSID := range c.bidderInfo[bidder].Syncer.SkipWhen.GPPSID {
if gppSID == GPPSID {
return nil, BidderEvaluation{Status: StatusBlockedByRegulationScope, Bidder: bidder, SyncerKey: syncer.Key()}
}
}
}

return syncer, BidderEvaluation{Status: StatusOK, Bidder: bidder, SyncerKey: syncer.Key()}
}
Loading

0 comments on commit df072ff

Please sign in to comment.