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

Privacy Sandbox: support testing label header #3381

Merged
merged 32 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ef67df2
Privacy Sandbox: support testing label header #3297
pm-nilesh-chate Jan 3, 2024
6305606
udpate device.ext.cdep
pm-nilesh-chate Jan 3, 2024
187d6a3
move config auction to root lvl
pm-nilesh-chate Jan 18, 2024
dd3244e
refactor + new ut
pm-nilesh-chate Jan 19, 2024
067330a
add ut for secCookieDeprecation
pm-nilesh-chate Jan 19, 2024
2894864
refactor as per review
pm-nilesh-chate Jan 22, 2024
0ecfdec
cookie sync refactor as per review comments
pm-nilesh-chate Jan 22, 2024
96141e7
prc
pm-nilesh-chate Jan 22, 2024
94e020c
add cdep len validation tests
pm-nilesh-chate Jan 25, 2024
fcf8b00
fix: use warning for cdep len check
pm-nilesh-chate Jan 25, 2024
644f484
refactor as per review
pm-nilesh-chate Jan 30, 2024
445bb75
typo fix
pm-nilesh-chate Jan 30, 2024
cf2b466
revert typo
pm-nilesh-chate Jan 30, 2024
2a844f6
address nitpick pr reviews
pm-nilesh-chate Feb 5, 2024
1b1ca06
add Partitioned cookie type
pm-nilesh-chate Feb 8, 2024
e443ea5
refactor: validateOrFillCDep
pm-nilesh-chate Feb 13, 2024
3625a9b
pr comments
pm-nilesh-chate Feb 16, 2024
05cdf32
pr comment
pm-nilesh-chate Feb 16, 2024
344de67
seperate setCookieDeprecationHeader tests
pm-nilesh-chate Feb 16, 2024
7767c3d
Merge remote-tracking branch 'upstream-vanilla/master'
pm-nilesh-chate Feb 20, 2024
f9969ab
prc
pm-nilesh-chate Feb 22, 2024
cb5ff78
Merge remote-tracking branch 'upstream-vanilla/master'
pm-nilesh-chate Feb 22, 2024
dabc8bb
prc
pm-nilesh-chate Feb 22, 2024
fdc2829
prc
pm-nilesh-chate Feb 22, 2024
829c797
typo
pm-nilesh-chate Feb 22, 2024
990faea
fix TestAmpBadRequests
pm-nilesh-chate Feb 26, 2024
5d5cf55
Revert "fix TestAmpBadRequests"
pm-nilesh-chate Feb 26, 2024
0d83607
prc test case names
pm-nilesh-chate Feb 26, 2024
b6068d4
update testname validateOrFillCDep
pm-nilesh-chate Feb 26, 2024
c7bee8e
tn
pm-nilesh-chate Feb 26, 2024
a09ead0
prc
pm-nilesh-chate Feb 28, 2024
b4e1d3c
prc
pm-nilesh-chate Feb 29, 2024
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
1 change: 1 addition & 0 deletions config/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ type AccountPrivacy struct {
AllowActivities *AllowActivities `mapstructure:"allowactivities" json:"allowactivities"`
IPv6Config IPv6 `mapstructure:"ipv6" json:"ipv6"`
IPv4Config IPv4 `mapstructure:"ipv4" json:"ipv4"`
PrivacySandbox PrivacySandbox `mapstructure:"privacysandbox" json:"privacysandbox"`
}

type IPv6 struct {
Expand Down
9 changes: 9 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1285,3 +1285,12 @@ type TmaxAdjustments struct {
// PBS won't send a request to the bidder if the bidder tmax calculated is less than the BidderResponseDurationMin value
BidderResponseDurationMin uint `mapstructure:"bidder_response_duration_min_ms"`
}

type PrivacySandbox struct {
CookieDeprecation CookieDeprecation `mapstructure:"cookiedeprecation"`
}

type CookieDeprecation struct {
Enabled bool `mapstructure:"enabled"`
TTLSec int `mapstructure:"ttlsec"`
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved
}
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved
43 changes: 35 additions & 8 deletions endpoints/cookie_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"strconv"
"strings"
"time"

"github.com/golang/glog"
"github.com/julienschmidt/httprouter"
Expand All @@ -29,8 +30,11 @@ import (
"github.com/prebid/prebid-server/v2/usersync"
"github.com/prebid/prebid-server/v2/util/jsonutil"
stringutil "github.com/prebid/prebid-server/v2/util/stringutil"
"github.com/prebid/prebid-server/v2/util/timeutil"
)

const receiveCookieDeprecation = "receive-cookie-deprecation"

var (
errCookieSyncOptOut = errors.New("User has opted out")
errCookieSyncBody = errors.New("Failed to read request body")
Expand Down Expand Up @@ -73,6 +77,7 @@ func NewCookieSyncEndpoint(
metrics: metrics,
pbsAnalytics: analyticsRunner,
accountsFetcher: accountsFetcher,
time: &timeutil.RealTime{},
}
}

Expand All @@ -83,10 +88,12 @@ type cookieSyncEndpoint struct {
metrics metrics.MetricsEngine
pbsAnalytics analytics.Runner
accountsFetcher stored_requests.AccountFetcher
time timeutil.Time
}

func (c *cookieSyncEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
request, privacyMacros, err := c.parseRequest(r)
request, privacyMacros, account, err := c.parseRequest(r)
c.setCookieDeprecationHeader(w, r, account)
if err != nil {
c.writeParseRequestErrorMetrics(err)
c.handleError(w, err, http.StatusBadRequest)
Expand All @@ -113,32 +120,32 @@ func (c *cookieSyncEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ ht
}
}

func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, macros.UserSyncPrivacy, error) {
func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, macros.UserSyncPrivacy, *config.Account, error) {
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
return usersync.Request{}, macros.UserSyncPrivacy{}, errCookieSyncBody
return usersync.Request{}, macros.UserSyncPrivacy{}, nil, errCookieSyncBody
}

request := cookieSyncRequest{}
if err := jsonutil.UnmarshalValid(body, &request); err != nil {
return usersync.Request{}, macros.UserSyncPrivacy{}, fmt.Errorf("JSON parsing failed: %s", err.Error())
return usersync.Request{}, macros.UserSyncPrivacy{}, nil, fmt.Errorf("JSON parsing failed: %s", err.Error())
}

if request.Account == "" {
request.Account = metrics.PublisherUnknown
}
account, fetchErrs := accountService.GetAccount(context.Background(), c.config, c.accountsFetcher, request.Account, c.metrics)
if len(fetchErrs) > 0 {
return usersync.Request{}, macros.UserSyncPrivacy{}, combineErrors(fetchErrs)
return usersync.Request{}, macros.UserSyncPrivacy{}, nil, combineErrors(fetchErrs)
}

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

privacyMacros, gdprSignal, privacyPolicies, err := extractPrivacyPolicies(request, c.privacyConfig.gdprConfig.DefaultValue)
if err != nil {
return usersync.Request{}, macros.UserSyncPrivacy{}, err
return usersync.Request{}, macros.UserSyncPrivacy{}, account, err
}

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

syncTypeFilter, err := parseTypeFilter(request.FilterSettings)
if err != nil {
return usersync.Request{}, macros.UserSyncPrivacy{}, err
return usersync.Request{}, macros.UserSyncPrivacy{}, account, err
}

gdprRequestInfo := gdpr.RequestInfo{
Expand Down Expand Up @@ -185,7 +192,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma
SyncTypeFilter: syncTypeFilter,
GPPSID: request.GPPSID,
}
return rx, privacyMacros, nil
return rx, privacyMacros, account, nil
}

func extractPrivacyPolicies(request cookieSyncRequest, usersyncDefaultGDPRValue string) (macros.UserSyncPrivacy, gdpr.Signal, privacy.Policies, error) {
Expand Down Expand Up @@ -455,11 +462,31 @@ func (c *cookieSyncEndpoint) handleResponse(w http.ResponseWriter, tf usersync.S
})

w.Header().Set("Content-Type", "application/json; charset=utf-8")

enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
enc.Encode(response)
}

func (c *cookieSyncEndpoint) setCookieDeprecationHeader(w http.ResponseWriter, r *http.Request, account *config.Account) {
if rcd, err := r.Cookie(receiveCookieDeprecation); err == nil && rcd != nil {
return
}
if account == nil || !account.Privacy.PrivacySandbox.CookieDeprecation.Enabled {
return
}
http.SetCookie(w, &http.Cookie{
Name: receiveCookieDeprecation,
Value: "1",
Secure: true,
HttpOnly: true,
Path: "/",
SameSite: http.SameSiteNoneMode,
// Partition: "",
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved
Expires: c.time.Now().Add(time.Second * time.Duration(account.Privacy.PrivacySandbox.CookieDeprecation.TTLSec)),
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved
})
}

func mapBidderStatusToAnalytics(from []cookieSyncResponseBidder) []*analytics.CookieSyncBidder {
to := make([]*analytics.CookieSyncBidder, len(from))
for i, b := range from {
Expand Down
124 changes: 114 additions & 10 deletions endpoints/cookie_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"testing/iotest"
"time"

"github.com/prebid/prebid-server/v2/analytics"
"github.com/prebid/prebid-server/v2/config"
Expand All @@ -28,6 +30,15 @@ import (
"github.com/stretchr/testify/mock"
)

// fakeTime implements the Time interface
type fakeTime struct {
time time.Time
}

func (ft *fakeTime) Now() time.Time {
return ft.time
}

func TestNewCookieSyncEndpoint(t *testing.T) {
var (
syncersByBidder = map[string]usersync.Syncer{"a": &MockSyncer{}}
Expand Down Expand Up @@ -111,14 +122,17 @@ func TestCookieSyncHandle(t *testing.T) {
cookieWithSyncs.Sync("foo", "anyID")

testCases := []struct {
description string
givenCookie *usersync.Cookie
givenBody io.Reader
givenChooserResult usersync.Result
expectedStatusCode int
expectedBody string
setMetricsExpectations func(*metrics.MetricsEngineMock)
setAnalyticsExpectations func(*MockAnalyticsRunner)
description string
givenCookie *usersync.Cookie
givenChromeDeprecationCookie bool
givenBody io.Reader
givenChooserResult usersync.Result
givenAccountData map[string]json.RawMessage
expectedStatusCode int
expectedBody string
expectedCookieDeprecationHeader bool
setMetricsExpectations func(*metrics.MetricsEngineMock)
setAnalyticsExpectations func(*MockAnalyticsRunner)
}{
{
description: "Request With Cookie",
Expand Down Expand Up @@ -285,6 +299,79 @@ func TestCookieSyncHandle(t *testing.T) {
a.On("LogCookieSyncObject", &expected).Once()
},
},
{
description: "CookieDeprecation enabled for account, receive-cookie-deprecation cookie not present in request, expect cookie deprecation header set in response",
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved
givenCookie: cookieWithSyncs,
givenBody: strings.NewReader(`{"account": "testAccount"}`),
givenChooserResult: usersync.Result{
Status: usersync.StatusOK,
BiddersEvaluated: []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusAlreadySynced}},
SyncersChosen: []usersync.SyncerChoice{{Bidder: "a", Syncer: &syncer}},
},
givenAccountData: map[string]json.RawMessage{
"testAccount": json.RawMessage(`{"id":"1","privacy":{"privacysandbox":{"cookiedeprecation":{"enabled":true,"ttlsec":86400}}}}`),
},
expectedStatusCode: 200,
expectedBody: `{"status":"ok","bidder_status":[` +
`{"bidder":"a","no_cookie":true,"usersync":{"url":"aURL","type":"redirect","supportCORS":true}}` +
`]}` + "\n",
expectedCookieDeprecationHeader: true,
setMetricsExpectations: func(m *metrics.MetricsEngineMock) {
m.On("RecordCookieSync", metrics.CookieSyncOK).Once()
m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncAlreadySynced).Once()
},
setAnalyticsExpectations: func(a *MockAnalyticsRunner) {
expected := analytics.CookieSyncObject{
Status: 200,
Errors: nil,
BidderStatus: []*analytics.CookieSyncBidder{
{
BidderCode: "a",
NoCookie: true,
UsersyncInfo: &analytics.UsersyncInfo{URL: "aURL", Type: "redirect", SupportCORS: true},
},
},
}
a.On("LogCookieSyncObject", &expected).Once()
},
},
{
description: "CookieDeprecation enabled for account, receive-cookie-deprecation cookie present in request, cookie deprecation header not set in response",
givenCookie: cookieWithSyncs,
givenChromeDeprecationCookie: true,
givenBody: strings.NewReader(`{"account": "testAccount"}`),
givenChooserResult: usersync.Result{
Status: usersync.StatusOK,
BiddersEvaluated: []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusAlreadySynced}},
SyncersChosen: []usersync.SyncerChoice{{Bidder: "a", Syncer: &syncer}},
},
givenAccountData: map[string]json.RawMessage{
"testAccount": json.RawMessage(`{"id":"1","privacy":{"privacysandbox":{"cookiedeprecation":{"enabled":true,"ttlsec":86400}}}}`),
},
expectedStatusCode: 200,
expectedBody: `{"status":"ok","bidder_status":[` +
`{"bidder":"a","no_cookie":true,"usersync":{"url":"aURL","type":"redirect","supportCORS":true}}` +
`]}` + "\n",
expectedCookieDeprecationHeader: false,
setMetricsExpectations: func(m *metrics.MetricsEngineMock) {
m.On("RecordCookieSync", metrics.CookieSyncOK).Once()
m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncAlreadySynced).Once()
},
setAnalyticsExpectations: func(a *MockAnalyticsRunner) {
expected := analytics.CookieSyncObject{
Status: 200,
Errors: nil,
BidderStatus: []*analytics.CookieSyncBidder{
{
BidderCode: "a",
NoCookie: true,
UsersyncInfo: &analytics.UsersyncInfo{URL: "aURL", Type: "redirect", SupportCORS: true},
},
},
}
a.On("LogCookieSyncObject", &expected).Once()
},
},
}

for _, test := range testCases {
Expand All @@ -294,7 +381,9 @@ func TestCookieSyncHandle(t *testing.T) {
mockAnalytics := MockAnalyticsRunner{}
test.setAnalyticsExpectations(&mockAnalytics)

fakeAccountFetcher := FakeAccountsFetcher{}
fakeAccountFetcher := FakeAccountsFetcher{
AccountData: test.givenAccountData,
}

gdprPermsBuilder := fakePermissionsBuilder{
permissions: &fakePermissions{},
Expand All @@ -310,6 +399,10 @@ func TestCookieSyncHandle(t *testing.T) {
request.AddCookie(httpCookie)
}

if test.givenChromeDeprecationCookie {
request.AddCookie(&http.Cookie{Name: receiveCookieDeprecation, Value: "1"})
}

writer := httptest.NewRecorder()

endpoint := cookieSyncEndpoint{
Expand All @@ -329,13 +422,24 @@ func TestCookieSyncHandle(t *testing.T) {
metrics: &mockMetrics,
pbsAnalytics: &mockAnalytics,
accountsFetcher: &fakeAccountFetcher,
time: &fakeTime{time: time.Now()},
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved
}
assert.NoError(t, endpoint.config.MarshalAccountDefaults())

endpoint.Handle(writer, request, nil)

assert.Equal(t, test.expectedStatusCode, writer.Code, test.description+":status_code")
assert.Equal(t, test.expectedBody, writer.Body.String(), test.description+":body")

gotCookie := writer.Header().Get("Set-Cookie")
if test.expectedCookieDeprecationHeader {
wantCookieTTL := endpoint.time.Now().Add(time.Second * time.Duration(86400)).UTC().Format(http.TimeFormat)
wantCookie := fmt.Sprintf("receive-cookie-deprecation=1; Path=/; Expires=%v; HttpOnly; Secure; SameSite=None", wantCookieTTL)
assert.Equal(t, wantCookie, gotCookie, ":set_cookie_deprecation_header")
} else {
assert.Equal(t, gotCookie, "")
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved
}

mockMetrics.AssertExpectations(t)
mockAnalytics.AssertExpectations(t)
}
Expand Down Expand Up @@ -1060,7 +1164,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
}},
}
assert.NoError(t, endpoint.config.MarshalAccountDefaults())
request, privacyPolicies, err := endpoint.parseRequest(httpRequest)
request, privacyPolicies, _, err := endpoint.parseRequest(httpRequest)

if test.expectedError == "" {
assert.NoError(t, err, test.description+":err")
Expand Down
23 changes: 16 additions & 7 deletions endpoints/openrtb2/amp_auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,22 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
return
}

// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
deps.setFieldsImplicitly(r, reqWrapper, account)

hasStoredResponses := len(storedAuctionResponses) > 0
errs := deps.validateRequest(reqWrapper, true, hasStoredResponses, storedBidResponses, false)
errL = append(errL, errs...)
ao.Errors = append(ao.Errors, errs...)
if errortypes.ContainsFatalError(errs) {
w.WriteHeader(http.StatusBadRequest)
for _, err := range errortypes.FatalOnly(errs) {
w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", err.Error())))
}
labels.RequestStatus = metrics.RequestStatusBadInput
return
}

pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved
tcf2Config := gdpr.NewTCF2Config(deps.cfg.GDPR.TCF2, account.GDPR)

activityControl = privacy.NewActivityControl(&account.Privacy)
Expand Down Expand Up @@ -482,9 +498,6 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr
// move to using the request wrapper
req = &openrtb_ext.RequestWrapper{BidRequest: reqNormal}

// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
deps.setFieldsImplicitly(httpRequest, req)

pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved
// Need to ensure cache and targeting are turned on
e = initAmpTargetingAndCache(req)
if errs = append(errs, e...); errortypes.ContainsFatalError(errs) {
Expand All @@ -496,10 +509,6 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr
return
}

hasStoredResponses := len(storedAuctionResponses) > 0
e = deps.validateRequest(req, true, hasStoredResponses, storedBidResponses, false)
errs = append(errs, e...)

return
}

Expand Down
2 changes: 1 addition & 1 deletion endpoints/openrtb2/amp_auction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1204,7 +1204,7 @@ func TestInitAmpTargetingAndCache(t *testing.T) {

func TestQueryParamOverrides(t *testing.T) {
requests := map[string]json.RawMessage{
"1": json.RawMessage(validRequest(t, "site.json")),
"1": json.RawMessage(validRequest(t, "site-amp.json")),
}

endpoint, _ := NewAmpEndpoint(
Expand Down
Loading
Loading