Skip to content

Commit

Permalink
UserSync activity
Browse files Browse the repository at this point in the history
  • Loading branch information
VeronikaSolovei9 committed Jul 6, 2023
1 parent 23fffa1 commit ea46c83
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 27 deletions.
10 changes: 9 additions & 1 deletion endpoints/cookie_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down
33 changes: 25 additions & 8 deletions endpoints/setuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
32 changes: 32 additions & 0 deletions endpoints/setuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down Expand Up @@ -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)
Expand Down
29 changes: 22 additions & 7 deletions usersync/chooser.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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}
Expand All @@ -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()}
}
Expand Down
50 changes: 39 additions & 11 deletions usersync/chooser_test.go
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit ea46c83

Please sign in to comment.