Skip to content

Commit

Permalink
UserSync activity (#2897)
Browse files Browse the repository at this point in the history
  • Loading branch information
VeronikaSolovei9 authored Jul 27, 2023
1 parent 9d72dbe commit 555c0a1
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 33 deletions.
15 changes: 15 additions & 0 deletions endpoints/cookie_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -172,6 +179,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
Privacy: usersyncPrivacy{
gdprPermissions: gdprPerms,
ccpaParsedPolicy: ccpaParsedPolicy,
activityControl: activityControl,
},
SyncTypeFilter: syncTypeFilter,
}
Expand Down Expand Up @@ -501,6 +509,7 @@ type usersyncPrivacyConfig struct {
type usersyncPrivacy struct {
gdprPermissions gdpr.Permissions
ccpaParsedPolicy ccpa.ParsedPolicy
activityControl privacy.ActivityControl
}

func (p usersyncPrivacy) GDPRAllowsHostCookie() bool {
Expand All @@ -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
}
92 changes: 90 additions & 2 deletions endpoints/cookie_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(`{` +
Expand Down Expand Up @@ -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 {
Expand All @@ -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())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"},
},
},
},
},
},
}
}
26 changes: 20 additions & 6 deletions endpoints/setuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions endpoints/setuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion privacy/enforcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions usersync/chooser.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,17 @@ 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.
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.
Expand Down Expand Up @@ -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()}
}
Expand Down
Loading

0 comments on commit 555c0a1

Please sign in to comment.