Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GPP in the cookie_sync endpoint #2757

Merged
merged 5 commits into from May 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
115 changes: 83 additions & 32 deletions endpoints/cookie_sync.go
Expand Up @@ -12,6 +12,8 @@ import (

"github.com/golang/glog"
"github.com/julienschmidt/httprouter"
gpplib "github.com/prebid/go-gpp"
gppConstants "github.com/prebid/go-gpp/constants"
accountService "github.com/prebid/prebid-server/account"
"github.com/prebid/prebid-server/analytics"
"github.com/prebid/prebid-server/config"
Expand All @@ -25,6 +27,7 @@ import (
gppPrivacy "github.com/prebid/prebid-server/privacy/gpp"
"github.com/prebid/prebid-server/stored_requests"
"github.com/prebid/prebid-server/usersync"
stringutil "github.com/prebid/prebid-server/util/stringutil"
)

var (
Expand Down Expand Up @@ -125,40 +128,12 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
return usersync.Request{}, privacy.Policies{}, combineErrors(fetchErrs)
}

var gdprString string
if request.GDPR != nil {
gdprString = strconv.Itoa(*request.GDPR)
}
gdprSignal, err := gdpr.StrSignalParse(gdprString)
if err != nil {
return usersync.Request{}, privacy.Policies{}, err
}

if request.GDPRConsent == "" {
if gdprSignal == gdpr.SignalYes {
return usersync.Request{}, privacy.Policies{}, errCookieSyncGDPRConsentMissing
}

if gdprSignal == gdpr.SignalAmbiguous && gdpr.SignalNormalize(gdprSignal, c.privacyConfig.gdprConfig.DefaultValue) == gdpr.SignalYes {
return usersync.Request{}, privacy.Policies{}, errCookieSyncGDPRConsentMissingSignalAmbiguous
}
}

request = c.setLimit(request, account.CookieSync)
request = c.setCooperativeSync(request, account.CookieSync)

privacyPolicies := privacy.Policies{
GDPR: gdprPrivacy.Policy{
Signal: gdprString,
Consent: request.GDPRConsent,
},
CCPA: ccpa.Policy{
Consent: request.USPrivacy,
},
GPP: gppPrivacy.Policy{
Consent: request.GPP,
RawSID: request.GPPSid,
},
privacyPolicies, gdprSignal, err := extractPrivacyPolicies(request, c.privacyConfig.gdprConfig.DefaultValue)
if err != nil {
return usersync.Request{}, privacy.Policies{}, err
}

ccpaParsedPolicy := ccpa.ParsedPolicy{}
Expand All @@ -178,7 +153,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
}

gdprRequestInfo := gdpr.RequestInfo{
Consent: request.GDPRConsent,
Consent: privacyPolicies.GDPR.Consent,
GDPRSignal: gdprSignal,
}

Expand All @@ -201,6 +176,82 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
return rx, privacyPolicies, nil
}

func extractPrivacyPolicies(request cookieSyncRequest, usersyncDefaultGDPRValue string) (privacy.Policies, gdpr.Signal, error) {
// GDPR
gppSID, err := stringutil.StrToInt8Slice(request.GPPSid)
if err != nil {
return privacy.Policies{}, gdpr.SignalNo, err
}

gdprSignal, gdprString, err := extractGDPRSignal(request.GDPR, gppSID)
if err != nil {
return privacy.Policies{}, gdpr.SignalNo, err
}

var gpp gpplib.GppContainer
if len(request.GPP) > 0 {
var err error
gpp, err = gpplib.Parse(request.GPP)
if err != nil {
return privacy.Policies{}, gdpr.SignalNo, err
}
}

gdprConsent := request.GDPRConsent
if i := gppPrivacy.IndexOfSID(gpp, gppConstants.SectionTCFEU2); i >= 0 {
gdprConsent = gpp.Sections[i].GetValue()
}

if gdprConsent == "" {
if gdprSignal == gdpr.SignalYes {
return privacy.Policies{}, gdpr.SignalNo, errCookieSyncGDPRConsentMissing
}

if gdprSignal == gdpr.SignalAmbiguous && gdpr.SignalNormalize(gdprSignal, usersyncDefaultGDPRValue) == gdpr.SignalYes {
return privacy.Policies{}, gdpr.SignalNo, errCookieSyncGDPRConsentMissingSignalAmbiguous
}
}

// CCPA
ccpaString, err := ccpa.SelectCCPAConsent(request.USPrivacy, gpp, gppSID)
if err != nil {
return privacy.Policies{}, gdpr.SignalNo, err
}

return privacy.Policies{
GDPR: gdprPrivacy.Policy{
Signal: gdprString,
Consent: gdprConsent,
},
CCPA: ccpa.Policy{
Consent: ccpaString,
},
GPP: gppPrivacy.Policy{
Consent: request.GPP,
RawSID: request.GPPSid,
},
}, gdprSignal, nil
}

func extractGDPRSignal(requestGDPR *int, gppSID []int8) (gdpr.Signal, string, error) {
if len(gppSID) > 0 {
if gppPrivacy.IsSIDInList(gppSID, gppConstants.SectionTCFEU2) {
return gdpr.SignalYes, strconv.Itoa(int(gdpr.SignalYes)), nil
}
return gdpr.SignalNo, strconv.Itoa(int(gdpr.SignalNo)), nil
}

if requestGDPR == nil {
return gdpr.SignalAmbiguous, "", nil
}

gdprSignal, err := gdpr.IntSignalParse(*requestGDPR)
if err != nil {
return gdpr.SignalAmbiguous, strconv.Itoa(*requestGDPR), err
}
return gdprSignal, strconv.Itoa(*requestGDPR), nil
}

func (c *cookieSyncEndpoint) writeParseRequestErrorMetrics(err error) {
switch err {
case errCookieSyncAccountBlocked:
Expand Down
189 changes: 185 additions & 4 deletions endpoints/cookie_sync_test.go
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"testing/iotest"
Expand All @@ -23,6 +24,7 @@ import (
gdprPrivacy "github.com/prebid/prebid-server/privacy/gdpr"
gppPrivacy "github.com/prebid/prebid-server/privacy/gpp"
"github.com/prebid/prebid-server/usersync"
"github.com/prebid/prebid-server/util/ptrutil"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
Expand Down Expand Up @@ -303,6 +305,185 @@ func TestCookieSyncHandle(t *testing.T) {
}
}

func TestExtractGDPRSignal(t *testing.T) {
type testInput struct {
requestGDPR *int
gppSID []int8
}
type testOutput struct {
gdprSignal gdpr.Signal
gdprString string
err error
}
testCases := []struct {
desc string
in testInput
expected testOutput
}{
{
desc: "SectionTCFEU2 is listed in GPP_SID array, expect SignalYes and nil error",
in: testInput{
requestGDPR: nil,
gppSID: []int8{2},
},
expected: testOutput{
gdprSignal: gdpr.SignalYes,
gdprString: strconv.Itoa(int(gdpr.SignalYes)),
err: nil,
},
},
{
desc: "SectionTCFEU2 is not listed in GPP_SID array, expect SignalNo and nil error",
in: testInput{
requestGDPR: nil,
gppSID: []int8{6},
},
expected: testOutput{
gdprSignal: gdpr.SignalNo,
gdprString: strconv.Itoa(int(gdpr.SignalNo)),
err: nil,
},
},
{
desc: "Empty GPP_SID array and nil requestGDPR value, expect SignalAmbiguous and nil error",
in: testInput{
requestGDPR: nil,
gppSID: []int8{},
},
expected: testOutput{
gdprSignal: gdpr.SignalAmbiguous,
gdprString: "",
err: nil,
},
},
{
desc: "Empty GPP_SID array and non-nil requestGDPR value that could not be successfully parsed, expect SignalAmbiguous and parse error",
in: testInput{
requestGDPR: ptrutil.ToPtr(2),
gppSID: nil,
},
expected: testOutput{
gdprSignal: gdpr.SignalAmbiguous,
gdprString: "2",
err: &errortypes.BadInput{"GDPR signal should be integer 0 or 1"},
},
},
{
desc: "Empty GPP_SID array and non-nil requestGDPR value that could be successfully parsed, expect SignalYes and nil error",
in: testInput{
requestGDPR: ptrutil.ToPtr(1),
gppSID: nil,
},
expected: testOutput{
gdprSignal: gdpr.SignalYes,
gdprString: "1",
err: nil,
},
},
}
for _, tc := range testCases {
// run
outSignal, outGdprStr, outErr := extractGDPRSignal(tc.in.requestGDPR, tc.in.gppSID)
// assertions
assert.Equal(t, tc.expected.gdprSignal, outSignal, tc.desc)
assert.Equal(t, tc.expected.gdprString, outGdprStr, tc.desc)
assert.Equal(t, tc.expected.err, outErr, tc.desc)
}
}

func TestExtractPrivacyPolicies(t *testing.T) {
type testInput struct {
request cookieSyncRequest
usersyncDefaultGDPRValue string
}
type testOutput struct {
policies privacy.Policies
gdprSignal gdpr.Signal
err error
}
testCases := []struct {
desc string
in testInput
expected testOutput
}{
{
desc: "request GPP string is malformed, expect empty policies, signal No and error",
in: testInput{
request: cookieSyncRequest{GPP: "malformedGPPString"},
},
expected: testOutput{
policies: privacy.Policies{},
gdprSignal: gdpr.SignalNo,
err: errors.New("error parsing GPP header, header must have type=3"),
},
},
{
desc: "Malformed GPPSid string",
in: testInput{
request: cookieSyncRequest{
GPP: "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
GPPSid: "malformed",
USPrivacy: "1YYY",
},
},
expected: testOutput{
policies: privacy.Policies{},
gdprSignal: gdpr.SignalNo,
err: &strconv.NumError{"ParseInt", "malformed", strconv.ErrSyntax},
},
},
{
desc: "request USPrivacy string is different from the one in the GPP string, expect empty policies, signalNo and error",
in: testInput{
request: cookieSyncRequest{
GPP: "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
GPPSid: "6",
USPrivacy: "1YYY",
},
},
expected: testOutput{
policies: privacy.Policies{},
gdprSignal: gdpr.SignalNo,
err: errors.New("request.us_privacy consent does not match uspv1"),
},
},
{
desc: "no issues extracting privacy policies from request GPP and request GPPSid strings",
in: testInput{
request: cookieSyncRequest{
GPP: "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
GPPSid: "6",
},
},
expected: testOutput{
policies: privacy.Policies{
GDPR: gdprPrivacy.Policy{
Signal: "0",
Consent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
},
CCPA: ccpa.Policy{
Consent: "1YNN",
},
GPP: gppPrivacy.Policy{
Consent: "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
RawSID: "6",
},
},
gdprSignal: gdpr.SignalNo,
err: nil,
},
},
}
for _, tc := range testCases {
// run
outPolicies, outSignal, outErr := extractPrivacyPolicies(tc.in.request, tc.in.usersyncDefaultGDPRValue)
// assertions
assert.Equal(t, tc.expected.policies, outPolicies, tc.desc)
assert.Equal(t, tc.expected.gdprSignal, outSignal, tc.desc)
assert.Equal(t, tc.expected.err, outErr, tc.desc)
}
}

func TestCookieSyncParseRequest(t *testing.T) {
expectedCCPAParsedPolicy, _ := ccpa.Policy{Consent: "1NYN"}.Parse(map[string]struct{}{})

Expand All @@ -318,13 +499,13 @@ func TestCookieSyncParseRequest(t *testing.T) {
expectedRequest usersync.Request
}{
{
description: "Complete Request",
description: "Complete Request - includes GPP string with EU TCF V2",
givenBody: strings.NewReader(`{` +
`"bidders":["a", "b"],` +
`"gdpr":1,` +
`"gdpr_consent":"anyGDPRConsent",` +
`"us_privacy":"1NYN",` +
`"gpp":"anyGPPString",` +
`"gpp":"DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",` +
`"gpp_sid":"2",` +
`"limit":42,` +
`"coopSync":true,` +
Expand All @@ -341,13 +522,13 @@ func TestCookieSyncParseRequest(t *testing.T) {
expectedPrivacy: privacy.Policies{
GDPR: gdprPrivacy.Policy{
Signal: "1",
Consent: "anyGDPRConsent",
Consent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
},
CCPA: ccpa.Policy{
Consent: "1NYN",
},
GPP: gppPrivacy.Policy{
Consent: "anyGPPString",
Consent: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
RawSID: "2",
},
},
Expand Down