Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions amp/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"github.com/prebid/prebid-server/v3/privacy"
"github.com/prebid/prebid-server/v3/privacy/ccpa"
"github.com/prebid/prebid-server/v3/privacy/gdpr"
"github.com/prebid/prebid-server/v3/privacy/gpp"
"github.com/prebid/prebid-server/v3/util/stringutil"
)

// Params defines the parameters of an AMP request.
Expand All @@ -25,6 +27,7 @@ type Params struct {
ConsentType int64
Debug bool
GdprApplies *bool
GppSid string
Origin string
Size Size
Slot string
Expand All @@ -49,6 +52,7 @@ const (
ConsentTCF1 = 1
ConsentTCF2 = 2
ConsentUSPrivacy = 3
ConsentGPP = 4
)

// ReadPolicy returns a privacy writer in accordance to the query values consent, consent_type and gdpr_applies.
Expand Down Expand Up @@ -83,6 +87,11 @@ func ReadPolicy(ampParams Params, pbsConfigGDPREnabled bool) (privacy.PolicyWrit
// Log warning if CCPA string is invalid
warningMsg = fmt.Sprintf("Consent string '%s' is not a valid CCPA consent string.", ampParams.Consent)
}
case ConsentGPP:
if gppSidErr := validateGppSid(ampParams.GppSid); len(gppSidErr) > 0 {
warningMsg = gppSidErr
}
rv = gpp.ConsentWriter{Consent: ampParams.Consent, GppSid: ampParams.GppSid}
default:
if ccpa.ValidateConsent(ampParams.Consent) {
rv = ccpa.ConsentWriter{Consent: ampParams.Consent}
Expand Down Expand Up @@ -146,6 +155,20 @@ func validateTCf2ConsentString(consent string) string {
return ""
}

// validateGppSid validates that gpp_sid is a comma-separated list of integers
func validateGppSid(gppSid string) string {
if len(gppSid) == 0 {
return ""
}

_, err := stringutil.StrToInt8Slice(gppSid)
if err != nil {
return fmt.Sprintf("GPP SID '%s' is not a valid comma-separated list of integers.", gppSid)
}

return ""
}

// ParseParams parses the AMP parameters from a HTTP request.
func ParseParams(httpRequest *http.Request) (Params, error) {
query := httpRequest.URL.Query()
Expand All @@ -162,6 +185,7 @@ func ParseParams(httpRequest *http.Request) (Params, error) {
Consent: chooseConsent(query.Get("consent_string"), query.Get("gdpr_consent")),
ConsentType: parseInt(query.Get("consent_type")),
Debug: query.Get("debug") == "1",
GppSid: query.Get("gpp_sid"),
Origin: query.Get("__amp_source_origin"),
Size: Size{
Height: parseInt(query.Get("h")),
Expand Down
230 changes: 230 additions & 0 deletions amp/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package amp

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v3/errortypes"
"github.com/prebid/prebid-server/v3/privacy"
"github.com/prebid/prebid-server/v3/privacy/ccpa"
"github.com/prebid/prebid-server/v3/privacy/gdpr"
"github.com/prebid/prebid-server/v3/privacy/gpp"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -696,3 +698,231 @@ func TestParseGdprApplies(t *testing.T) {
assert.Equal(t, tc.expectRegsExtGdpr, parseGdprApplies(tc.inGdprApplies), tc.desc)
}
}

func TestParseParamsGppSid(t *testing.T) {
tests := []struct {
name string
url string
expectedSid string
}{
{
name: "gpp_sid present with single value",
url: "/openrtb2/amp?tag_id=1&gpp_sid=2",
expectedSid: "2",
},
{
name: "gpp_sid present with multiple values",
url: "/openrtb2/amp?tag_id=1&gpp_sid=2,4,6",
expectedSid: "2,4,6",
},
{
name: "gpp_sid absent",
url: "/openrtb2/amp?tag_id=1",
expectedSid: "",
},
{
name: "gpp_sid empty string",
url: "/openrtb2/amp?tag_id=1&gpp_sid=",
expectedSid: "",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
request := httptest.NewRequest("GET", test.url, nil)
params, err := ParseParams(request)
assert.NoError(t, err)
assert.Equal(t, test.expectedSid, params.GppSid)
})
}
}

func TestValidateGppSid(t *testing.T) {
tests := []struct {
name string
gppSid string
expectedErr string
}{
{
name: "valid single value",
gppSid: "2",
expectedErr: "",
},
{
name: "valid multiple values",
gppSid: "2,4,6",
expectedErr: "",
},
{
name: "empty string is valid",
gppSid: "",
expectedErr: "",
},
{
name: "invalid - contains letters",
gppSid: "2,abc,6",
expectedErr: "GPP SID '2,abc,6' is not a valid comma-separated list of integers.",
},
{
name: "invalid - malformed",
gppSid: "malformed",
expectedErr: "GPP SID 'malformed' is not a valid comma-separated list of integers.",
},
{
name: "invalid - special characters",
gppSid: "2;4;6",
expectedErr: "GPP SID '2;4;6' is not a valid comma-separated list of integers.",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := validateGppSid(test.gppSid)
assert.Equal(t, test.expectedErr, err)
})
}
}

func TestReadPolicyGPP(t *testing.T) {
tests := []struct {
name string
params Params
gdprEnabled bool
expectNilPolicy bool
expectWarning bool
warningContains string
}{
{
name: "GPP with valid gpp_sid",
params: Params{
Consent: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
ConsentType: ConsentGPP,
GppSid: "2,4,6",
},
gdprEnabled: true,
expectNilPolicy: false,
expectWarning: false,
},
{
name: "GPP with invalid gpp_sid",
params: Params{
Consent: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
ConsentType: ConsentGPP,
GppSid: "malformed",
},
gdprEnabled: true,
expectNilPolicy: false,
expectWarning: true,
warningContains: "not a valid comma-separated list of integers",
},
{
name: "GPP without gpp_sid",
params: Params{
Consent: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
ConsentType: ConsentGPP,
GppSid: "",
},
gdprEnabled: true,
expectNilPolicy: false,
expectWarning: false,
},
{
name: "GPP without consent string",
params: Params{
Consent: "",
ConsentType: ConsentGPP,
GppSid: "2,4",
},
gdprEnabled: true,
expectNilPolicy: true,
expectWarning: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
writer, warning := ReadPolicy(test.params, test.gdprEnabled)

if test.expectNilPolicy {
assert.IsType(t, privacy.NilPolicyWriter{}, writer)
} else {
assert.IsType(t, gpp.ConsentWriter{}, writer)
}

if test.expectWarning {
assert.Error(t, warning)
assert.Contains(t, warning.Error(), test.warningContains)
} else {
assert.NoError(t, warning)
}
})
}
}

func TestGppConsentWriterWrite(t *testing.T) {
tests := []struct {
name string
writer gpp.ConsentWriter
expectedGpp string
expectedGppSid []int8
expectedGppSidNil bool
}{
{
name: "write both gpp and gpp_sid",
writer: gpp.ConsentWriter{
Consent: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
GppSid: "2,4,6",
},
expectedGpp: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
expectedGppSid: []int8{2, 4, 6},
expectedGppSidNil: false,
},
{
name: "write only gpp string",
writer: gpp.ConsentWriter{
Consent: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
GppSid: "",
},
expectedGpp: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
expectedGppSid: nil,
expectedGppSidNil: true,
},
{
name: "invalid gpp_sid should result in nil GPPSID",
writer: gpp.ConsentWriter{
Consent: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
GppSid: "malformed",
},
expectedGpp: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
expectedGppSid: nil,
expectedGppSidNil: true,
},
{
name: "empty gpp string and empty gpp_sid",
writer: gpp.ConsentWriter{
Consent: "",
GppSid: "",
},
expectedGpp: "",
expectedGppSid: nil,
expectedGppSidNil: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := &openrtb2.BidRequest{}
err := test.writer.Write(req)

assert.NoError(t, err)
assert.NotNil(t, req.Regs)
assert.Equal(t, test.expectedGpp, req.Regs.GPP)

if test.expectedGppSidNil {
assert.Nil(t, req.Regs.GPPSID)
} else {
assert.Equal(t, test.expectedGppSid, req.Regs.GPPSID)
}
})
}
}
39 changes: 39 additions & 0 deletions privacy/gpp/consentwriter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package gpp

import (
"github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v3/util/stringutil"
)

// ConsentWriter implements the PolicyWriter interface for GPP.
type ConsentWriter struct {
Consent string
GppSid string
}

// Write mutates an OpenRTB bid request with the GPP consent.
func (c ConsentWriter) Write(req *openrtb2.BidRequest) error {
if req == nil {
return nil
}

if req.Regs == nil {
req.Regs = &openrtb2.Regs{}
}

// Set GPP consent string
if c.Consent != "" {
req.Regs.GPP = c.Consent
}

// Parse and set GPP SID
if c.GppSid != "" {
gppSID, err := stringutil.StrToInt8Slice(c.GppSid)
if err == nil {
req.Regs.GPPSID = gppSID
}
// If parsing fails, GPPSID remains nil (as per spec)
}

return nil
}
Loading
Loading