Skip to content

Commit

Permalink
CCPA Phase 1: AMP Endpoint (#1125)
Browse files Browse the repository at this point in the history
  • Loading branch information
SyntaxNode authored Dec 6, 2019
1 parent 5c43c19 commit d64c98b
Show file tree
Hide file tree
Showing 9 changed files with 452 additions and 16 deletions.
43 changes: 27 additions & 16 deletions endpoints/openrtb2/amp_auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import (
"github.com/prebid/prebid-server/exchange"
"github.com/prebid/prebid-server/openrtb_ext"
"github.com/prebid/prebid-server/pbsmetrics"
"github.com/prebid/prebid-server/privacy"
"github.com/prebid/prebid-server/privacy/ccpa"
"github.com/prebid/prebid-server/privacy/gdpr"
"github.com/prebid/prebid-server/stored_requests"
"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
"github.com/prebid/prebid-server/usersync"
Expand Down Expand Up @@ -379,22 +382,19 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope
req.Imp[0].TagID = slot
}

//In the AMP endpoint the consent string found in the http.Request query overrides that of the prebid query
queryConsentString := httpRequest.FormValue("gdpr_consent")
if queryConsentString != "" {
jsonMsg := json.RawMessage(`{"consent":"` + queryConsentString + `"}`)
// If nil, initialize
if req.User == nil {
req.User = &openrtb.User{Ext: jsonMsg}
} else if req.User.Ext == nil {
req.User.Ext = jsonMsg
} else { // req.User.Ext != nil, keep whatever is in there and only substitute the consent string
var parserErr error
req.User.Ext, parserErr = jsonparser.Set(req.User.Ext, []byte(`"`+queryConsentString+`"`), "consent")
if parserErr != nil {
return parserErr
}
}
gdprConsent := getQueryParam(httpRequest, "gdpr_consent")
ccpaValue := getQueryParam(httpRequest, "us_privacy")
privacyPolicies := privacy.Policies{
GDPR: gdpr.Policy{
Consent: gdprConsent,
},
CCPA: ccpa.Policy{
Value: ccpaValue,
},
}

if err := privacyPolicies.Write(req); err != nil {
return err
}

if timeout, err := strconv.ParseInt(httpRequest.FormValue("timeout"), 10, 64); err == nil {
Expand All @@ -404,6 +404,17 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope
return nil
}

func getQueryParam(httpRequest *http.Request, name string) string {
values, ok := httpRequest.URL.Query()[name]

if !ok || len(values) == 0 {
return ""
}

// return first value of the query param, matching the behavior of httpRequest.FormValue
return values[0]
}

func makeFormatReplacement(overrideWidth uint64, overrideHeight uint64, width uint64, height uint64, multisize string) []openrtb.Format {
if overrideWidth != 0 && overrideHeight != 0 {
return []openrtb.Format{{
Expand Down
96 changes: 96 additions & 0 deletions endpoints/openrtb2/amp_auction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,102 @@ func TestWidthOnly(t *testing.T) {
}.execute(t)
}

func TestCCPAPresent(t *testing.T) {
req, err := getTestBidRequest(false, false, "", "digitrustId")
if err != nil {
t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err)
}

reqStored := map[string]json.RawMessage{
"1": json.RawMessage(req),
}

theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})

exchange := &mockAmpExchange{}

endpoint, _ := NewAmpEndpoint(
exchange,
newParamsValidator(t),
&mockAmpStoredReqFetcher{reqStored},
empty_fetcher.EmptyFetcher{},
&config.Configuration{MaxRequestSize: maxSize},
theMetrics,
analyticsConf.NewPBSAnalytics(&config.Analytics{}),
map[string]string{},
[]byte{},
openrtb_ext.BidderMap,
)

usPrivacy := "1YYN"
httpReq := httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1&us_privacy="+usPrivacy, nil)
httpRecorder := httptest.NewRecorder()
endpoint(httpRecorder, httpReq, nil)

// Assert our bidRequest was valid
if !assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", httpRecorder.Code, httpRecorder.Body.String()) {
return
}
// Assert our bidRequest had a valid "Regs" field
if !assert.NotNil(t, exchange.lastRequest.Regs) {
return
}
// Assert our bidRequest had a valid "Regs.Ext" field
if !assert.NotNil(t, exchange.lastRequest.Regs.Ext) {
return
}

var regs openrtb_ext.ExtRegs
err = json.Unmarshal(exchange.lastRequest.Regs.Ext, &regs)
assert.NoError(t, err)
assert.Equal(t, usPrivacy, regs.USPrivacy)
}

func TestCCPANotPresent(t *testing.T) {
req, err := getTestBidRequest(false, false, "", "digitrustId")
if err != nil {
t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err)
}

reqStored := map[string]json.RawMessage{
"1": json.RawMessage(req),
}

theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})

exchange := &mockAmpExchange{}

endpoint, _ := NewAmpEndpoint(
exchange,
newParamsValidator(t),
&mockAmpStoredReqFetcher{reqStored},
empty_fetcher.EmptyFetcher{},
&config.Configuration{MaxRequestSize: maxSize},
theMetrics,
analyticsConf.NewPBSAnalytics(&config.Analytics{}),
map[string]string{},
[]byte{},
openrtb_ext.BidderMap,
)

httpReq := httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1", nil)
httpRecorder := httptest.NewRecorder()
endpoint(httpRecorder, httpReq, nil)

// Assert our bidRequest was valid
if !assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", httpRecorder.Code, httpRecorder.Body.String()) {
return
}

// Assert CCPA Signal Not Found
if exchange.lastRequest.Regs != nil && exchange.lastRequest.Regs.Ext != nil {
var regs openrtb_ext.ExtRegs
err = json.Unmarshal(exchange.lastRequest.Regs.Ext, &regs)
assert.NoError(t, err)
assert.Empty(t, regs.USPrivacy)
}
}

type formatOverrideSpec struct {
width uint64
height uint64
Expand Down
3 changes: 3 additions & 0 deletions openrtb_ext/regs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ type ExtRegs struct {
// GDPR should be "1" if the caller believes the user is subject to GDPR laws, "0" if not, and undefined
// if it's unknown. For more info on this parameter, see: https://iabtechlab.com/wp-content/uploads/2018/02/OpenRTB_Advisory_GDPR_2018-02.pdf
GDPR *int8 `json:"gdpr,omitempty"`

// USPrivacy should be a four character string, see: https://iabtechlab.com/wp-content/uploads/2019/11/OpenRTB-Extension-U.S.-Privacy-IAB-Tech-Lab.pdf
USPrivacy string `json:"us_privacy,omitempty"`
}
33 changes: 33 additions & 0 deletions privacy/ccpa/ccpa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package ccpa

import (
"encoding/json"

"github.com/buger/jsonparser"
"github.com/mxmCherry/openrtb"
)

// Policy represents the CCPA regulation for an OpenRTB bid request.
type Policy struct {
Value string
}

// Write mutates an OpenRTB bid request with the context of the CCPA policy.
func (p Policy) Write(req *openrtb.BidRequest) error {
if p.Value == "" {
return nil
}

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

if req.Regs.Ext == nil {
req.Regs.Ext = json.RawMessage(`{"us_privacy":"` + p.Value + `"}`)
return nil
}

var err error
req.Regs.Ext, err = jsonparser.Set(req.Regs.Ext, []byte(`"`+p.Value+`"`), "us_privacy")
return err
}
74 changes: 74 additions & 0 deletions privacy/ccpa/ccpa_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package ccpa

import (
"encoding/json"
"testing"

"github.com/mxmCherry/openrtb"
"github.com/stretchr/testify/assert"
)

func TestWrite(t *testing.T) {
testCases := []struct {
description string
policy Policy
request *openrtb.BidRequest
expected *openrtb.BidRequest
expectedError bool
}{
{
description: "Disabled",
policy: Policy{Value: ""},
request: &openrtb.BidRequest{},
expected: &openrtb.BidRequest{},
},
{
description: "Enabled With Nil Request Regs Object",
policy: Policy{Value: "anyValue"},
request: &openrtb.BidRequest{},
expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
Ext: json.RawMessage(`{"us_privacy":"anyValue"}`)}},
},
{
description: "Enabled With Nil Request Regs Ext Object",
policy: Policy{Value: "anyValue"},
request: &openrtb.BidRequest{Regs: &openrtb.Regs{}},
expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
Ext: json.RawMessage(`{"us_privacy":"anyValue"}`)}},
},
{
description: "Enabled With Existing Request Regs Ext Object - Doesn't Overwrite",
policy: Policy{Value: "anyValue"},
request: &openrtb.BidRequest{Regs: &openrtb.Regs{
Ext: json.RawMessage(`{"existing":"any"}`)}},
expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
Ext: json.RawMessage(`{"existing":"any","us_privacy":"anyValue"}`)}},
},
{
description: "Enabled With Existing Request Regs Ext Object - Overwrites",
policy: Policy{Value: "anyValue"},
request: &openrtb.BidRequest{Regs: &openrtb.Regs{
Ext: json.RawMessage(`{"existing":"any","us_privacy":"toBeOverwritten"}`)}},
expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
Ext: json.RawMessage(`{"existing":"any","us_privacy":"anyValue"}`)}},
},
{
description: "Enabled With Existing Malformed Request Regs Ext Object",
policy: Policy{Value: "anyValue"},
request: &openrtb.BidRequest{Regs: &openrtb.Regs{
Ext: json.RawMessage(`malformed`)}},
expectedError: true,
},
}

for _, test := range testCases {
err := test.policy.Write(test.request)

if test.expectedError {
assert.Error(t, err, test.description)
} else {
assert.NoError(t, err, test.description)
assert.Equal(t, test.expected, test.request, test.description)
}
}
}
33 changes: 33 additions & 0 deletions privacy/gdpr/gdpr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package gdpr

import (
"encoding/json"

"github.com/buger/jsonparser"
"github.com/mxmCherry/openrtb"
)

// Policy represents the GDPR regulation of an OpenRTB bid request.
type Policy struct {
Consent string
}

// Write mutates an OpenRTB bid request with the context of the GDPR policy.
func (p Policy) Write(req *openrtb.BidRequest) error {
if p.Consent == "" {
return nil
}

if req.User == nil {
req.User = &openrtb.User{}
}

if req.User.Ext == nil {
req.User.Ext = json.RawMessage(`{"consent":"` + p.Consent + `"}`)
return nil
}

var err error
req.User.Ext, err = jsonparser.Set(req.User.Ext, []byte(`"`+p.Consent+`"`), "consent")
return err
}
74 changes: 74 additions & 0 deletions privacy/gdpr/gdpr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package gdpr

import (
"encoding/json"
"testing"

"github.com/mxmCherry/openrtb"
"github.com/stretchr/testify/assert"
)

func TestWrite(t *testing.T) {
testCases := []struct {
description string
policy Policy
request *openrtb.BidRequest
expected *openrtb.BidRequest
expectedError bool
}{
{
description: "Disabled",
policy: Policy{Consent: ""},
request: &openrtb.BidRequest{},
expected: &openrtb.BidRequest{},
},
{
description: "Enabled With Nil Request User Object",
policy: Policy{Consent: "anyValue"},
request: &openrtb.BidRequest{},
expected: &openrtb.BidRequest{User: &openrtb.User{
Ext: json.RawMessage(`{"consent":"anyValue"}`)}},
},
{
description: "Enabled With Nil Request User Ext Object",
policy: Policy{Consent: "anyValue"},
request: &openrtb.BidRequest{User: &openrtb.User{}},
expected: &openrtb.BidRequest{User: &openrtb.User{
Ext: json.RawMessage(`{"consent":"anyValue"}`)}},
},
{
description: "Enabled With Existing Request User Ext Object - Doesn't Overwrite",
policy: Policy{Consent: "anyValue"},
request: &openrtb.BidRequest{User: &openrtb.User{
Ext: json.RawMessage(`{"existing":"any"}`)}},
expected: &openrtb.BidRequest{User: &openrtb.User{
Ext: json.RawMessage(`{"existing":"any","consent":"anyValue"}`)}},
},
{
description: "Enabled With Existing Request User Ext Object - Overwrites",
policy: Policy{Consent: "anyValue"},
request: &openrtb.BidRequest{User: &openrtb.User{
Ext: json.RawMessage(`{"existing":"any","consent":"toBeOverwritten"}`)}},
expected: &openrtb.BidRequest{User: &openrtb.User{
Ext: json.RawMessage(`{"existing":"any","consent":"anyValue"}`)}},
},
{
description: "Enabled With Existing Malformed Request User Ext Object",
policy: Policy{Consent: "anyValue"},
request: &openrtb.BidRequest{User: &openrtb.User{
Ext: json.RawMessage(`malformed`)}},
expectedError: true,
},
}

for _, test := range testCases {
err := test.policy.Write(test.request)

if test.expectedError {
assert.Error(t, err, test.description)
} else {
assert.NoError(t, err, test.description)
assert.Equal(t, test.expected, test.request, test.description)
}
}
}
Loading

0 comments on commit d64c98b

Please sign in to comment.