diff --git a/config/bidderinfo.go b/config/bidderinfo.go index 409393c2b0c..d3c874f706b 100644 --- a/config/bidderinfo.go +++ b/config/bidderinfo.go @@ -125,6 +125,9 @@ type Syncer struct { // SupportCORS identifies if CORS is supported for the user syncing endpoints. SupportCORS *bool `yaml:"supportCors" mapstructure:"support_cors"` + // Enabled signifies whether a bidder is enabled/disabled for user sync + Enabled *bool `yaml:"enabled" mapstructure:"enabled"` + // SkipWhen allows bidders to specify when they don't want to sync SkipWhen *SkipWhen `yaml:"skipwhen" mapstructure:"skipwhen"` } diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go index 6d69aa8241e..ef32e4048d7 100644 --- a/endpoints/cookie_sync.go +++ b/endpoints/cookie_sync.go @@ -492,6 +492,8 @@ func getDebugMessage(status usersync.Status) string { return "No sync config" case usersync.StatusTypeNotSupported: return "Type not supported" + case usersync.StatusBlockedByDisabledUsersync: + return "Sync disabled by config" } return "" } diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index 050e137ffed..401c6796237 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -1640,6 +1640,7 @@ func TestCookieSyncHandleResponse(t *testing.T) { {Bidder: "Bidder4", Status: usersync.StatusBlockedByPrivacy}, {Bidder: "Bidder5", Status: usersync.StatusTypeNotSupported}, {Bidder: "Bidder6", Status: usersync.StatusBlockedByUserOptOut}, + {Bidder: "Bidder7", Status: usersync.StatusBlockedByDisabledUsersync}, {Bidder: "BidderA", Status: usersync.StatusDuplicate, SyncerKey: "syncerB"}, } @@ -1730,7 +1731,7 @@ func TestCookieSyncHandleResponse(t *testing.T) { givenCookieHasSyncs: true, givenDebug: true, givenSyncersChosen: []usersync.SyncerChoice{}, - expectedJSON: `{"status":"ok","bidder_status":[],"debug":[{"bidder":"Bidder1","error":"Already in sync"},{"bidder":"Bidder2","error":"Unsupported bidder"},{"bidder":"Bidder3","error":"No sync config"},{"bidder":"Bidder4","error":"Rejected by privacy"},{"bidder":"Bidder5","error":"Type not supported"},{"bidder":"Bidder6","error":"Status blocked by user opt out"},{"bidder":"BidderA","error":"Duplicate bidder synced as syncerB"}]}` + "\n", + expectedJSON: `{"status":"ok","bidder_status":[],"debug":[{"bidder":"Bidder1","error":"Already in sync"},{"bidder":"Bidder2","error":"Unsupported bidder"},{"bidder":"Bidder3","error":"No sync config"},{"bidder":"Bidder4","error":"Rejected by privacy"},{"bidder":"Bidder5","error":"Type not supported"},{"bidder":"Bidder6","error":"Status blocked by user opt out"},{"bidder":"Bidder7","error":"Sync disabled by config"},{"bidder":"BidderA","error":"Duplicate bidder synced as syncerB"}]}` + "\n", expectedAnalytics: analytics.CookieSyncObject{Status: 200, BidderStatus: []*analytics.CookieSyncBidder{}}, }, } diff --git a/usersync/chooser.go b/usersync/chooser.go index 68acf195ffe..d8bf731f693 100644 --- a/usersync/chooser.go +++ b/usersync/chooser.go @@ -100,6 +100,9 @@ const ( // StatusUnconfiguredBidder refers to a bidder who hasn't been configured to have a syncer key, but is known by Prebid Server StatusUnconfiguredBidder + + // StatusBlockedByDisabledUsersync refers to a bidder who won't be synced because it's been disabled in its config by the host + StatusBlockedByDisabledUsersync ) // Privacy determines which privacy policies will be enforced for a user sync request. @@ -194,6 +197,10 @@ func (c standardChooser) evaluate(bidder string, syncersSeen map[string]struct{} return nil, BidderEvaluation{Status: StatusBlockedByPrivacy, Bidder: bidder, SyncerKey: syncer.Key()} } + if c.bidderInfo[bidder].Syncer != nil && c.bidderInfo[bidder].Syncer.Enabled != nil && !*c.bidderInfo[bidder].Syncer.Enabled { + return nil, BidderEvaluation{Status: StatusBlockedByDisabledUsersync, 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()} } diff --git a/usersync/chooser_test.go b/usersync/chooser_test.go index 5b67361d341..abf49b7f7c2 100644 --- a/usersync/chooser_test.go +++ b/usersync/chooser_test.go @@ -6,6 +6,7 @@ import ( "github.com/prebid/prebid-server/v2/config" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/ptrutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -69,6 +70,8 @@ func TestChooserChoose(t *testing.T) { cooperativeConfig := Cooperative{Enabled: true} + usersyncDisabled := ptrutil.ToPtr(false) + testCases := []struct { description string givenRequest Request @@ -343,6 +346,28 @@ func TestChooserChoose(t *testing.T) { SyncersChosen: []SyncerChoice{{Bidder: "AppNexus", Syncer: fakeSyncerA}}, }, }, + { + description: "Disabled Usersync", + givenRequest: Request{ + Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, + Limit: 0, + }, + givenChosenBidders: []string{"a"}, + givenCookie: Cookie{}, + givenBidderInfo: map[string]config.BidderInfo{ + "a": { + Syncer: &config.Syncer{ + Enabled: usersyncDisabled, + }, + }, + }, + bidderNamesLookup: normalizedBidderNamesLookup, + expected: Result{ + Status: StatusOK, + BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByDisabledUsersync}}, + SyncersChosen: []SyncerChoice{}, + }, + }, { description: "Regulation Scope GDPR", givenRequest: Request{ @@ -442,6 +467,8 @@ func TestChooserEvaluate(t *testing.T) { cookieAlreadyHasSyncForA := Cookie{uids: map[string]UIDEntry{"keyA": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}} cookieAlreadyHasSyncForB := Cookie{uids: map[string]UIDEntry{"keyB": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}} + usersyncDisabled := ptrutil.ToPtr(false) + testCases := []struct { description string givenBidder string @@ -600,6 +627,25 @@ func TestChooserEvaluate(t *testing.T) { expectedSyncer: nil, expectedEvaluation: BidderEvaluation{Bidder: "unconfigured", Status: StatusUnconfiguredBidder}, }, + { + description: "Disabled Usersync", + givenBidder: "a", + normalisedBidderName: "a", + givenSyncersSeen: map[string]struct{}{}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, + givenCookie: cookieNeedsSync, + givenBidderInfo: map[string]config.BidderInfo{ + "a": { + Syncer: &config.Syncer{ + Enabled: usersyncDisabled, + }, + }, + }, + givenSyncTypeFilter: syncTypeFilter, + normalizedBidderNamesLookup: normalizedBidderNamesLookup, + expectedSyncer: nil, + expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByDisabledUsersync}, + }, { description: "Blocked By Regulation Scope - GDPR", givenBidder: "a",