diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go index ef3c40e8815..49ef70744ff 100644 --- a/endpoints/cookie_sync.go +++ b/endpoints/cookie_sync.go @@ -149,6 +149,13 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr } } + activityControl, activitiesErr := privacy.NewActivityControl(account.Privacy) + if activitiesErr != nil { + if errortypes.ContainsFatalError([]error{activitiesErr}) { + activityControl = privacy.ActivityControl{} + } + } + syncTypeFilter, err := parseTypeFilter(request.FilterSettings) if err != nil { return usersync.Request{}, privacy.Policies{}, err @@ -172,6 +179,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr Privacy: usersyncPrivacy{ gdprPermissions: gdprPerms, ccpaParsedPolicy: ccpaParsedPolicy, + activityControl: activityControl, }, SyncTypeFilter: syncTypeFilter, } @@ -501,6 +509,7 @@ type usersyncPrivacyConfig struct { type usersyncPrivacy struct { gdprPermissions gdpr.Permissions ccpaParsedPolicy ccpa.ParsedPolicy + activityControl privacy.ActivityControl } func (p usersyncPrivacy) GDPRAllowsHostCookie() bool { @@ -517,3 +526,9 @@ func (p usersyncPrivacy) CCPAAllowsBidderSync(bidder string) bool { enforce := p.ccpaParsedPolicy.CanEnforce() && p.ccpaParsedPolicy.ShouldEnforce(bidder) return !enforce } + +func (p usersyncPrivacy) ActivityAllowsUserSync(bidder string) bool { + activityResult := p.activityControl.Allow(privacy.ActivitySyncUser, + privacy.ScopedName{Scope: privacy.ScopeTypeBidder, Name: bidder}) + return activityResult == privacy.ActivityAllow +} diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index a69a2cdb819..e9d812b8baf 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -499,6 +499,7 @@ func TestCookieSyncParseRequest(t *testing.T) { expectedPrivacy privacy.Policies expectedRequest usersync.Request }{ + { description: "Complete Request - includes GPP string with EU TCF V2", givenBody: strings.NewReader(`{` + @@ -973,6 +974,38 @@ func TestCookieSyncParseRequest(t *testing.T) { expectedError: errCookieSyncAccountBlocked.Error(), givenAccountRequired: true, }, + + { + description: "Account Defaults - Invalid Activities", + givenBody: strings.NewReader(`{` + + `"bidders":["a", "b"],` + + `"account":"ValidAccountInvalidActivities"` + + `}`), + givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "0"}, + givenCCPAEnabled: true, + givenConfig: config.UserSync{ + Cooperative: config.UserSyncCooperative{ + EnabledByDefault: false, + PriorityGroups: [][]string{{"a", "b", "c"}}, + }, + }, + expectedPrivacy: privacy.Policies{}, + expectedRequest: usersync.Request{ + Bidders: []string{"a", "b"}, + Cooperative: usersync.Cooperative{ + Enabled: false, + PriorityGroups: [][]string{{"a", "b", "c"}}, + }, + Limit: 0, + Privacy: usersyncPrivacy{ + gdprPermissions: &fakePermissions{}, + }, + SyncTypeFilter: usersync.SyncTypeFilter{ + IFrame: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude), + Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude), + }, + }, + }, } for _, test := range testCases { @@ -997,8 +1030,9 @@ func TestCookieSyncParseRequest(t *testing.T) { ccpaEnforce: test.givenCCPAEnabled, }, accountsFetcher: FakeAccountsFetcher{AccountData: map[string]json.RawMessage{ - "TestAccount": json.RawMessage(`{"cookie_sync": {"default_limit": 20, "max_limit": 30, "default_coop_sync": true}}`), - "DisabledAccount": json.RawMessage(`{"disabled":true}`), + "TestAccount": json.RawMessage(`{"cookie_sync": {"default_limit": 20, "max_limit": 30, "default_coop_sync": true}}`), + "DisabledAccount": json.RawMessage(`{"disabled":true}`), + "ValidAccountInvalidActivities": json.RawMessage(`{"privacy":{"allowactivities":{"syncUser":{"rules":[{"condition":{"componentName": ["bidderA.bidderB.bidderC"]}}]}}}}`), }}, } assert.NoError(t, endpoint.config.MarshalAccountDefaults()) @@ -1871,6 +1905,41 @@ func TestUsersyncPrivacyCCPAAllowsBidderSync(t *testing.T) { } } +func TestCookieSyncActivityControlIntegration(t *testing.T) { + testCases := []struct { + name string + bidderName string + allow bool + expectedResult bool + }{ + { + name: "activity_is_allowed", + bidderName: "bidderA", + allow: true, + expectedResult: true, + }, + { + name: "activity_is_denied", + bidderName: "bidderA", + allow: false, + expectedResult: false, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + privacyConfig := getDefaultActivityConfig(test.bidderName, test.allow) + activities, err := privacy.NewActivityControl(privacyConfig) + assert.NoError(t, err) + up := usersyncPrivacy{ + activityControl: activities, + } + actualResult := up.ActivityAllowsUserSync(test.bidderName) + assert.Equal(t, test.expectedResult, actualResult) + }) + } +} + func TestCombineErrors(t *testing.T) { testCases := []struct { description string @@ -2031,3 +2100,22 @@ func (p *fakePermissions) AuctionActivitiesAllowed(ctx context.Context, bidderCo AllowBidRequest: true, }, nil } + +func getDefaultActivityConfig(componentName string, allow bool) *config.AccountPrivacy { + return &config.AccountPrivacy{ + AllowActivities: config.AllowActivities{ + SyncUser: config.Activity{ + Default: ptrutil.ToPtr(true), + Rules: []config.ActivityRule{ + { + Allow: allow, + Condition: config.ActivityCondition{ + ComponentName: []string{componentName}, + ComponentType: []string{"bidder"}, + }, + }, + }, + }, + }, + } +} diff --git a/endpoints/setuid.go b/endpoints/setuid.go index 25fb246593a..8f691389758 100644 --- a/endpoints/setuid.go +++ b/endpoints/setuid.go @@ -18,6 +18,7 @@ import ( "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/metrics" + "github.com/prebid/prebid-server/privacy" gppPrivacy "github.com/prebid/prebid-server/privacy/gpp" "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/usersync" @@ -58,7 +59,7 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use query := r.URL.Query() - syncer, err := getSyncer(query, syncersByBidder) + syncer, bidderName, err := getSyncer(query, syncersByBidder) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) @@ -103,6 +104,20 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use return } + activities, activitiesErr := privacy.NewActivityControl(account.Privacy) + if activitiesErr != nil { + if errortypes.ContainsFatalError([]error{activitiesErr}) { + activities = privacy.ActivityControl{} + } + } + + userSyncActivityAllowed := activities.Allow(privacy.ActivitySyncUser, + privacy.ScopedName{Scope: privacy.ScopeTypeBidder, Name: bidderName}) + if userSyncActivityAllowed == privacy.ActivityDeny { + w.WriteHeader(http.StatusUnavailableForLegalReasons) + return + } + gdprRequestInfo, err := extractGDPRInfo(query) if err != nil { // Only exit if non-warning @@ -178,7 +193,6 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use // first and the 'gdpr' and 'gdpr_consent' query params second. If found in both, throws a // warning. Can also throw a parsing or validation error func extractGDPRInfo(query url.Values) (reqInfo gdpr.RequestInfo, err error) { - reqInfo, err = parseGDPRFromGPP(query) if err != nil { return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err @@ -306,19 +320,19 @@ func parseConsentFromGppStr(gppQueryValue string) (string, error) { return gdprConsent, nil } -func getSyncer(query url.Values, syncersByBidder map[string]usersync.Syncer) (usersync.Syncer, error) { +func getSyncer(query url.Values, syncersByBidder map[string]usersync.Syncer) (usersync.Syncer, string, error) { bidder := query.Get("bidder") if bidder == "" { - return nil, errors.New(`"bidder" query param is required`) + return nil, "", errors.New(`"bidder" query param is required`) } syncer, syncerExists := syncersByBidder[bidder] if !syncerExists { - return nil, errors.New("The bidder name provided is not supported by Prebid Server") + return nil, "", errors.New("The bidder name provided is not supported by Prebid Server") } - return syncer, nil + return syncer, bidder, nil } // getResponseFormat reads the format query parameter or falls back to the syncer's default. diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go index 65bc26559da..05030cc7cf7 100644 --- a/endpoints/setuid_test.go +++ b/endpoints/setuid_test.go @@ -277,6 +277,35 @@ func TestSetUIDEndpoint(t *testing.T) { expectedBody: "account is disabled, please reach out to the prebid server host", description: "Set uid for valid bidder with valid disabled account provided", }, + { + uri: "/setuid?bidder=pubmatic&uid=123&account=valid_acct_with_valid_activities_usersync_enabled", + syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"}, + existingSyncs: nil, + gdprAllowsHostCookies: true, + expectedSyncs: map[string]string{"pubmatic": "123"}, + expectedStatusCode: http.StatusOK, + expectedHeaders: map[string]string{"Content-Type": "text/html", "Content-Length": "0"}, + description: "Set uid for valid bidder with valid account provided with user sync allowed activity", + }, + { + uri: "/setuid?bidder=pubmatic&uid=123&account=valid_acct_with_valid_activities_usersync_disabled", + syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"}, + existingSyncs: nil, + gdprAllowsHostCookies: true, + expectedSyncs: nil, + expectedStatusCode: http.StatusUnavailableForLegalReasons, + description: "Set uid for valid bidder with valid account provided with user sync disallowed activity", + }, + { + uri: "/setuid?bidder=pubmatic&uid=123&account=valid_acct_with_invalid_activities", + syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"}, + existingSyncs: nil, + gdprAllowsHostCookies: true, + expectedSyncs: map[string]string{"pubmatic": "123"}, + expectedStatusCode: http.StatusOK, + expectedHeaders: map[string]string{"Content-Type": "text/html", "Content-Length": "0"}, + description: "Set uid for valid bidder with valid account provided with invalid user sync activity", + }, } analytics := analyticsConf.NewPBSAnalytics(&config.Analytics{}) @@ -1354,6 +1383,10 @@ func doRequest(req *http.Request, analytics analytics.PBSAnalyticsModule, metric "disabled_acct": json.RawMessage(`{"disabled":true}`), "malformed_acct": json.RawMessage(`{"disabled":"malformed"}`), "invalid_json_acct": json.RawMessage(`{"}`), + + "valid_acct_with_valid_activities_usersync_enabled": json.RawMessage(`{"privacy":{"allowactivities":{"syncUser":{"default": true}}}}`), + "valid_acct_with_valid_activities_usersync_disabled": json.RawMessage(`{"privacy":{"allowactivities":{"syncUser":{"default": false}}}}`), + "valid_acct_with_invalid_activities": json.RawMessage(`{"privacy":{"allowactivities":{"syncUser":{"rules":[{"condition":{"componentName": ["bidderA.bidderB.bidderC"]}}]}}}}`), }} endpoint := NewSetUIDEndpoint(&cfg, syncersByBidder, gdprPermsBuilder, tcf2ConfigBuilder, analytics, fakeAccountsFetcher, metrics) diff --git a/privacy/enforcer.go b/privacy/enforcer.go index a8685e29276..bcd9e948377 100644 --- a/privacy/enforcer.go +++ b/privacy/enforcer.go @@ -31,7 +31,7 @@ func NewActivityControl(privacyConf *config.AccountPrivacy) (ActivityControl, er var err error if privacyConf == nil { - return ac, err + return ac, nil } plans := make(map[Activity]ActivityPlan) diff --git a/usersync/chooser.go b/usersync/chooser.go index 97fa1471b7e..3f478049066 100644 --- a/usersync/chooser.go +++ b/usersync/chooser.go @@ -85,6 +85,9 @@ const ( // StatusDuplicate specifies the bidder is a duplicate or shared a syncer key with another bidder choice. StatusDuplicate + + // StatusBlockedByPrivacy specifies a bidder sync url is not allowed by privacy activities + StatusBlockedByPrivacy ) // Privacy determines which privacy policies will be enforced for a user sync request. @@ -92,6 +95,7 @@ type Privacy interface { GDPRAllowsHostCookie() bool GDPRAllowsBidderSync(bidder string) bool CCPAAllowsBidderSync(bidder string) bool + ActivityAllowsUserSync(bidder string) bool } // standardChooser implements the user syncer algorithm per official Prebid specification. @@ -151,6 +155,11 @@ func (c standardChooser) evaluate(bidder string, syncersSeen map[string]struct{} return nil, BidderEvaluation{Status: StatusAlreadySynced, Bidder: bidder, SyncerKey: syncer.Key()} } + userSyncActivityAllowed := privacy.ActivityAllowsUserSync(bidder) + if !userSyncActivityAllowed { + return nil, BidderEvaluation{Status: StatusBlockedByPrivacy, Bidder: bidder, SyncerKey: syncer.Key()} + } + if !privacy.GDPRAllowsBidderSync(bidder) { return nil, BidderEvaluation{Status: StatusBlockedByGDPR, Bidder: bidder, SyncerKey: syncer.Key()} } diff --git a/usersync/chooser_test.go b/usersync/chooser_test.go index 0071a2e36b0..adc217ae8f5 100644 --- a/usersync/chooser_test.go +++ b/usersync/chooser_test.go @@ -1,12 +1,11 @@ package usersync import ( - "testing" - "time" - "github.com/prebid/prebid-server/privacy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "testing" + "time" ) func TestNewChooser(t *testing.T) { @@ -66,7 +65,7 @@ func TestChooserChoose(t *testing.T) { { description: "Cookie Opt Out", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 0, }, givenChosenBidders: []string{"a"}, @@ -80,7 +79,7 @@ func TestChooserChoose(t *testing.T) { { description: "GDPR Host Cookie Not Allowed", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: false, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: false, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 0, }, givenChosenBidders: []string{"a"}, @@ -94,7 +93,7 @@ func TestChooserChoose(t *testing.T) { { description: "No Bidders", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 0, }, givenChosenBidders: []string{}, @@ -108,7 +107,7 @@ func TestChooserChoose(t *testing.T) { { description: "One Bidder - Sync", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 0, }, givenChosenBidders: []string{"a"}, @@ -122,7 +121,7 @@ func TestChooserChoose(t *testing.T) { { description: "One Bidder - No Sync", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 0, }, givenChosenBidders: []string{"c"}, @@ -136,7 +135,7 @@ func TestChooserChoose(t *testing.T) { { description: "Many Bidders - All Sync - Limit Disabled With 0", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 0, }, givenChosenBidders: []string{"a", "b"}, @@ -150,7 +149,7 @@ func TestChooserChoose(t *testing.T) { { description: "Many Bidders - All Sync - Limit Disabled With Negative Value", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: -1, }, givenChosenBidders: []string{"a", "b"}, @@ -164,7 +163,7 @@ func TestChooserChoose(t *testing.T) { { description: "Many Bidders - Limited Sync", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 1, }, givenChosenBidders: []string{"a", "b"}, @@ -178,7 +177,7 @@ func TestChooserChoose(t *testing.T) { { description: "Many Bidders - Limited Sync - Disqualified Syncers Don't Count Towards Limit", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 1, }, givenChosenBidders: []string{"c", "a", "b"}, @@ -192,7 +191,7 @@ func TestChooserChoose(t *testing.T) { { description: "Many Bidders - Some Sync, Some Don't", givenRequest: Request{ - Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, Limit: 0, }, givenChosenBidders: []string{"a", "c"}, @@ -254,7 +253,7 @@ func TestChooserEvaluate(t *testing.T) { description: "Valid", givenBidder: "a", givenSyncersSeen: map[string]struct{}{}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, givenCookie: cookieNeedsSync, expectedSyncer: fakeSyncerA, expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, @@ -263,7 +262,7 @@ func TestChooserEvaluate(t *testing.T) { description: "Unknown Bidder", givenBidder: "unknown", givenSyncersSeen: map[string]struct{}{}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, givenCookie: cookieNeedsSync, expectedSyncer: nil, expectedEvaluation: BidderEvaluation{Bidder: "unknown", Status: StatusUnknownBidder}, @@ -272,7 +271,7 @@ func TestChooserEvaluate(t *testing.T) { description: "Duplicate Syncer", givenBidder: "a", givenSyncersSeen: map[string]struct{}{"keyA": {}}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, givenCookie: cookieNeedsSync, expectedSyncer: nil, expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusDuplicate}, @@ -281,7 +280,7 @@ func TestChooserEvaluate(t *testing.T) { description: "Incompatible Kind", givenBidder: "b", givenSyncersSeen: map[string]struct{}{}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, givenCookie: cookieNeedsSync, expectedSyncer: nil, expectedEvaluation: BidderEvaluation{Bidder: "b", SyncerKey: "keyB", Status: StatusTypeNotSupported}, @@ -290,7 +289,7 @@ func TestChooserEvaluate(t *testing.T) { description: "Already Synced", givenBidder: "a", givenSyncersSeen: map[string]struct{}{}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, givenCookie: cookieAlreadyHasSyncForA, expectedSyncer: nil, expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusAlreadySynced}, @@ -299,7 +298,7 @@ func TestChooserEvaluate(t *testing.T) { description: "Different Bidder Already Synced", givenBidder: "a", givenSyncersSeen: map[string]struct{}{}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, givenCookie: cookieAlreadyHasSyncForB, expectedSyncer: fakeSyncerA, expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, @@ -308,7 +307,7 @@ func TestChooserEvaluate(t *testing.T) { description: "Blocked By GDPR", givenBidder: "a", givenSyncersSeen: map[string]struct{}{}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: false, ccpaAllowsBidderSync: true}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: false, ccpaAllowsBidderSync: true, activityAllowUserSync: true}, givenCookie: cookieNeedsSync, expectedSyncer: nil, expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByGDPR}, @@ -317,11 +316,20 @@ func TestChooserEvaluate(t *testing.T) { description: "Blocked By CCPA", givenBidder: "a", givenSyncersSeen: map[string]struct{}{}, - givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: false}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: false, activityAllowUserSync: true}, givenCookie: cookieNeedsSync, expectedSyncer: nil, expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByCCPA}, }, + { + description: "Blocked By activity control", + givenBidder: "a", + givenSyncersSeen: map[string]struct{}{}, + givenPrivacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: false}, + givenCookie: cookieNeedsSync, + expectedSyncer: nil, + expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByPrivacy}, + }, } for _, test := range testCases { @@ -373,9 +381,10 @@ func (fakeSyncer) GetSync(syncTypes []SyncType, privacyPolicies privacy.Policies } type fakePrivacy struct { - gdprAllowsHostCookie bool - gdprAllowsBidderSync bool - ccpaAllowsBidderSync bool + gdprAllowsHostCookie bool + gdprAllowsBidderSync bool + ccpaAllowsBidderSync bool + activityAllowUserSync bool } func (p fakePrivacy) GDPRAllowsHostCookie() bool { @@ -389,3 +398,7 @@ func (p fakePrivacy) GDPRAllowsBidderSync(bidder string) bool { func (p fakePrivacy) CCPAAllowsBidderSync(bidder string) bool { return p.ccpaAllowsBidderSync } + +func (p fakePrivacy) ActivityAllowsUserSync(bidder string) bool { + return p.activityAllowUserSync +}