From ea46c83d9795a1c78ec79ac4a2f0dcfe75312e6b Mon Sep 17 00:00:00 2001 From: VeronikaSolovei9 Date: Tue, 4 Jul 2023 14:52:25 -0700 Subject: [PATCH] UserSync activity --- endpoints/cookie_sync.go | 10 +++++++- endpoints/setuid.go | 33 +++++++++++++++++++------- endpoints/setuid_test.go | 32 +++++++++++++++++++++++++ usersync/chooser.go | 29 +++++++++++++++++------ usersync/chooser_test.go | 50 +++++++++++++++++++++++++++++++--------- 5 files changed, 127 insertions(+), 27 deletions(-) diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go index fa154bbcbff..7fdcb858790 100644 --- a/endpoints/cookie_sync.go +++ b/endpoints/cookie_sync.go @@ -147,6 +147,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}) { + return usersync.Request{}, privacy.Policies{}, err + } + } + syncTypeFilter, err := parseTypeFilter(request.FilterSettings) if err != nil { return usersync.Request{}, privacy.Policies{}, err @@ -171,7 +178,8 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr gdprPermissions: gdprPerms, ccpaParsedPolicy: ccpaParsedPolicy, }, - SyncTypeFilter: syncTypeFilter, + SyncTypeFilter: syncTypeFilter, + ActivityControl: activityControl, } return rx, privacyPolicies, nil } diff --git a/endpoints/setuid.go b/endpoints/setuid.go index a4d04749eae..4739b98788f 100644 --- a/endpoints/setuid.go +++ b/endpoints/setuid.go @@ -3,6 +3,8 @@ package endpoints import ( "context" "errors" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/privacy" "net/http" "net/url" "strconv" @@ -56,7 +58,7 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use query := r.URL.Query() - syncer, err := getSyncer(query, syncersByKey) + syncer, bidderName, err := getSyncer(query, syncersByKey) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) @@ -101,6 +103,21 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use return } + activities, activitiesErr := privacy.NewActivityControl(account.Privacy) + if activitiesErr != nil { + if errortypes.ContainsFatalError([]error{activitiesErr}) { + w.WriteHeader(http.StatusBadRequest) + return + } + } + + userSyncActivityAllowed := activities.Allow(privacy.ActivitySyncUser, + privacy.ScopedName{Scope: privacy.ScopeTypeBidder, Name: bidderName}) + if userSyncActivityAllowed == privacy.ActivityDeny { + w.WriteHeader(http.StatusUnavailableForLegalReasons) + return + } + tcf2Cfg := tcf2CfgBuilder(cfg.GDPR.TCF2, account.GDPR) if shouldReturn, status, body := preventSyncsGDPR(query.Get("gdpr"), query.Get("gdpr_consent"), gdprPermsBuilder, tcf2Cfg); shouldReturn { @@ -148,19 +165,19 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use }) } -func getSyncer(query url.Values, syncersByKey map[string]usersync.Syncer) (usersync.Syncer, error) { - key := query.Get("bidder") +func getSyncer(query url.Values, syncersByKey map[string]usersync.Syncer) (usersync.Syncer, string, error) { + bidderName := query.Get("bidder") - if key == "" { - return nil, errors.New(`"bidder" query param is required`) + if bidderName == "" { + return nil, "", errors.New(`"bidder" query param is required`) } - syncer, syncerExists := syncersByKey[key] + syncer, syncerExists := syncersByKey[bidderName] 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, bidderName, 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 609d85395fd..101b9c312f6 100644 --- a/endpoints/setuid_test.go +++ b/endpoints/setuid_test.go @@ -254,6 +254,34 @@ 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: nil, + expectedStatusCode: http.StatusBadRequest, + description: "Set uid for valid bidder with valid account provided with invalid user sync activity", + }, } analytics := analyticsConf.NewPBSAnalytics(&config.Analytics{}) @@ -740,6 +768,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/usersync/chooser.go b/usersync/chooser.go index 97fa1471b7e..1d3e310d6a1 100644 --- a/usersync/chooser.go +++ b/usersync/chooser.go @@ -1,5 +1,9 @@ package usersync +import ( + privacyActivity "github.com/prebid/prebid-server/privacy" +) + // Chooser determines which syncers are eligible for a given request. type Chooser interface { // Choose considers bidders to sync, filters the bidders, and returns the result of the @@ -23,11 +27,12 @@ func NewChooser(bidderSyncerLookup map[string]Syncer) Chooser { // Request specifies a user sync request. type Request struct { - Bidders []string - Cooperative Cooperative - Limit int - Privacy Privacy - SyncTypeFilter SyncTypeFilter + Bidders []string + Cooperative Cooperative + Limit int + Privacy Privacy + SyncTypeFilter SyncTypeFilter + ActivityControl privacyActivity.ActivityControl } // Cooperative specifies the settings for cooperative syncing for a given request, where bidders @@ -85,6 +90,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. @@ -120,7 +128,7 @@ func (c standardChooser) Choose(request Request, cookie *Cookie) Result { bidders := c.bidderChooser.choose(request.Bidders, c.biddersAvailable, request.Cooperative) for i := 0; i < len(bidders) && (limitDisabled || len(syncersChosen) < request.Limit); i++ { - 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.ActivityControl) biddersEvaluated = append(biddersEvaluated, evaluation) if evaluation.Status == StatusOK { @@ -131,7 +139,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, activityControl privacyActivity.ActivityControl) (Syncer, BidderEvaluation) { syncer, exists := c.bidderSyncerLookup[bidder] if !exists { return nil, BidderEvaluation{Status: StatusUnknownBidder, Bidder: bidder} @@ -151,6 +159,13 @@ func (c standardChooser) evaluate(bidder string, syncersSeen map[string]struct{} return nil, BidderEvaluation{Status: StatusAlreadySynced, Bidder: bidder, SyncerKey: syncer.Key()} } + userSyncActivityAllowed := activityControl.Allow(privacyActivity.ActivitySyncUser, + privacyActivity.ScopedName{Scope: privacyActivity.ScopeTypeBidder, Name: bidder}) + if userSyncActivityAllowed == privacyActivity.ActivityDeny { + return nil, BidderEvaluation{Status: StatusBlockedByPrivacy, Bidder: bidder, SyncerKey: syncer.Key()} + // from requirements: Debug message can be general "Bidder sync blocked for privacy reasons" - not done + } + 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 3b820b99f24..521041fb366 100644 --- a/usersync/chooser_test.go +++ b/usersync/chooser_test.go @@ -1,12 +1,13 @@ package usersync import ( - "testing" - "time" - + "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/util/ptrutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "testing" + "time" ) func TestNewChooser(t *testing.T) { @@ -241,14 +242,31 @@ func TestChooserEvaluate(t *testing.T) { cookieAlreadyHasSyncForA := Cookie{uids: map[string]uidWithExpiry{"keyA": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}} cookieAlreadyHasSyncForB := Cookie{uids: map[string]uidWithExpiry{"keyB": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}} + activityControl, activitiesErr := privacy.NewActivityControl(&config.AccountPrivacy{ + AllowActivities: config.AllowActivities{ + SyncUser: config.Activity{ + Default: ptrutil.ToPtr(true), + Rules: []config.ActivityRule{ + { + Allow: false, + Condition: config.ActivityCondition{ + ComponentName: []string{"bidder.a"}, + }, + }, + }, + }}, + }) + assert.NoError(t, activitiesErr) + testCases := []struct { - description string - givenBidder string - givenSyncersSeen map[string]struct{} - givenPrivacy Privacy - givenCookie Cookie - expectedSyncer Syncer - expectedEvaluation BidderEvaluation + description string + givenBidder string + givenSyncersSeen map[string]struct{} + givenPrivacy Privacy + givenCookie Cookie + givenActivityControl privacy.ActivityControl + expectedSyncer Syncer + expectedEvaluation BidderEvaluation }{ { description: "Valid", @@ -322,11 +340,21 @@ func TestChooserEvaluate(t *testing.T) { 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}, + givenCookie: cookieNeedsSync, + givenActivityControl: activityControl, + expectedSyncer: nil, + expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByPrivacy}, + }, } for _, test := range testCases { chooser, _ := NewChooser(bidderSyncerLookup).(standardChooser) - sync, evaluation := chooser.evaluate(test.givenBidder, test.givenSyncersSeen, syncTypeFilter, test.givenPrivacy, &test.givenCookie) + sync, evaluation := chooser.evaluate(test.givenBidder, test.givenSyncersSeen, syncTypeFilter, test.givenPrivacy, &test.givenCookie, test.givenActivityControl) assert.Equal(t, test.expectedSyncer, sync, test.description+":syncer") assert.Equal(t, test.expectedEvaluation, evaluation, test.description+":evaluation")