From c9131abdbcb18aa620fd80b0978340cb1ddb897e Mon Sep 17 00:00:00 2001 From: Dmitry Savintsev Date: Thu, 22 Feb 2024 21:11:21 +0100 Subject: [PATCH 01/13] Add formatcheck Make target (#3480) Signed-off-by: Dmitry S --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 2d8aae6c78a..cf4ac52b515 100644 --- a/Makefile +++ b/Makefile @@ -33,3 +33,7 @@ image: # format runs format format: ./scripts/format.sh -f true + +# formatcheck runs format for diagnostics, without modifying the code +formatcheck: + ./scripts/format.sh -f false From b945d09c608ea27cf7bd94e337a1dc775b1cde3a Mon Sep 17 00:00:00 2001 From: Hendrick Musche <107099114+sag-henmus@users.noreply.github.com> Date: Fri, 23 Feb 2024 02:00:35 +0100 Subject: [PATCH 02/13] SeedingAlliance: Deprecate seatId in favor of accountId (#3486) --- adapters/seedingAlliance/seedingAlliance.go | 16 +- .../seedingAlliance/seedingAlliance_test.go | 25 +-- .../exemplary/banner_with_account.json | 142 +++++++++++++++++ .../banner_with_account_and_seat.json | 144 ++++++++++++++++++ openrtb_ext/imp_seedingAlliance.go | 5 +- static/bidder-params/seedingAlliance.json | 6 +- 6 files changed, 318 insertions(+), 20 deletions(-) create mode 100644 adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account.json create mode 100644 adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account_and_seat.json diff --git a/adapters/seedingAlliance/seedingAlliance.go b/adapters/seedingAlliance/seedingAlliance.go index 03b6853b975..af48133d7c2 100644 --- a/adapters/seedingAlliance/seedingAlliance.go +++ b/adapters/seedingAlliance/seedingAlliance.go @@ -33,11 +33,11 @@ func Builder(_ openrtb_ext.BidderName, config config.Adapter, server config.Serv } func (a *adapter) MakeRequests(request *openrtb2.BidRequest, extraRequestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { - var seatId string + var accountId string var err error for i := range request.Imp { - if seatId, err = getExtInfo(&request.Imp[i]); err != nil { + if accountId, err = getExtInfo(&request.Imp[i]); err != nil { return nil, []error{err} } } @@ -51,7 +51,7 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, extraRequestInfo *a return nil, []error{err} } - url, err := macros.ResolveMacros(a.endpoint, macros.EndpointTemplateParams{AccountID: seatId}) + url, err := macros.ResolveMacros(a.endpoint, macros.EndpointTemplateParams{AccountID: accountId}) if err != nil { return nil, []error{err} } @@ -146,7 +146,7 @@ func getExtInfo(imp *openrtb2.Imp) (string, error) { var ext adapters.ExtImpBidder var extSA openrtb_ext.ImpExtSeedingAlliance - seatID := "pbs" + accountId := "pbs" if err := json.Unmarshal(imp.Ext, &ext); err != nil { return "", fmt.Errorf("could not unmarshal adapters.ExtImpBidder: %w", err) @@ -159,8 +159,12 @@ func getExtInfo(imp *openrtb2.Imp) (string, error) { imp.TagID = extSA.AdUnitID if extSA.SeatID != "" { - seatID = extSA.SeatID + accountId = extSA.SeatID } - return seatID, nil + if extSA.AccountID != "" { + accountId = extSA.AccountID + } + + return accountId, nil } diff --git a/adapters/seedingAlliance/seedingAlliance_test.go b/adapters/seedingAlliance/seedingAlliance_test.go index 6d9460b875e..ae9b888e4b1 100644 --- a/adapters/seedingAlliance/seedingAlliance_test.go +++ b/adapters/seedingAlliance/seedingAlliance_test.go @@ -136,24 +136,27 @@ func TestGetMediaTypeForBid(t *testing.T) { func TestGetExtInfo(t *testing.T) { type args struct { - adUnitId string - seatId string + adUnitId string + seatId string + accountId string } tests := []struct { - name string - expectedAdUnitID string - expectedSeatID string - data args - wantErr bool + name string + expectedAdUnitID string + expectedAccountId string + data args + wantErr bool }{ {"regular case", "abc123", "pbs", args{adUnitId: "abc123"}, false}, {"nil case", "", "pbs", args{adUnitId: ""}, false}, {"unmarshal err case", "", "pbs", args{adUnitId: ""}, true}, {"seatId case", "abc123", "seat1", args{adUnitId: "abc123", seatId: "seat1"}, false}, + {"accountId case", "abc123", "account1", args{adUnitId: "abc123", accountId: "account1"}, false}, + {"accountId and seatId case", "abc123", "account1", args{adUnitId: "abc123", accountId: "account1", seatId: "seat1"}, false}, } for _, test := range tests { - extSA, err := json.Marshal(openrtb_ext.ImpExtSeedingAlliance{AdUnitID: test.data.adUnitId, SeatID: test.data.seatId}) + extSA, err := json.Marshal(openrtb_ext.ImpExtSeedingAlliance{AdUnitID: test.data.adUnitId, SeatID: test.data.seatId, AccountID: test.data.accountId}) if err != nil { t.Fatalf("unexpected error %v", err) } @@ -168,7 +171,7 @@ func TestGetExtInfo(t *testing.T) { } ortbImp := openrtb2.Imp{Ext: extBidder} - seatID, err := getExtInfo(&ortbImp) + accountId, err := getExtInfo(&ortbImp) if err != nil { if test.wantErr { continue @@ -180,8 +183,8 @@ func TestGetExtInfo(t *testing.T) { t.Fatalf("want: %v, got: %v", test.expectedAdUnitID, ortbImp.TagID) } - if test.expectedSeatID != seatID { - t.Fatalf("want: %v, got: %v", test.expectedSeatID, seatID) + if test.expectedAccountId != accountId { + t.Fatalf("want: %v, got: %v", test.expectedAccountId, accountId) } } } diff --git a/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account.json b/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account.json new file mode 100644 index 00000000000..fc3678ea34b --- /dev/null +++ b/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account.json @@ -0,0 +1,142 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "publisher": { + "id": "foo", + "name": "foo" + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "adUnitId": "example-tag-id", + "accountId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://mockup.seeding-alliance.de/?ssp=123", + "body": { + "cur": [ + "EUR" + ], + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "example-tag-id", + "ext": { + "bidder": { + "adUnitId": "example-tag-id", + "accountId": "123" + } + } + } + ], + "site": { + "publisher": { + "id": "foo", + "name": "foo" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "123", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "12341234", + "adm": "some-test-ad", + "adomain": [ + "domain.com" + ], + "iurl": "http://abc.com/cr?id=12341234", + "cid": "123", + "crid": "12341234", + "h": 250, + "w": 300, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "EUR" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "12341234", + "adomain": [ + "domain.com" + ], + "iurl": "http://abc.com/cr?id=12341234", + "cid": "123", + "crid": "12341234", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account_and_seat.json b/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account_and_seat.json new file mode 100644 index 00000000000..3f010e6075d --- /dev/null +++ b/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account_and_seat.json @@ -0,0 +1,144 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "publisher": { + "id": "foo", + "name": "foo" + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "adUnitId": "example-tag-id", + "seatId": "ignored", + "accountId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://mockup.seeding-alliance.de/?ssp=123", + "body": { + "cur": [ + "EUR" + ], + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "example-tag-id", + "ext": { + "bidder": { + "adUnitId": "example-tag-id", + "seatId": "ignored", + "accountId": "123" + } + } + } + ], + "site": { + "publisher": { + "id": "foo", + "name": "foo" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "123", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "12341234", + "adm": "some-test-ad", + "adomain": [ + "domain.com" + ], + "iurl": "http://abc.com/cr?id=12341234", + "cid": "123", + "crid": "12341234", + "h": 250, + "w": 300, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "EUR" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "12341234", + "adomain": [ + "domain.com" + ], + "iurl": "http://abc.com/cr?id=12341234", + "cid": "123", + "crid": "12341234", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/openrtb_ext/imp_seedingAlliance.go b/openrtb_ext/imp_seedingAlliance.go index 0f594d3c933..d383ad39d6e 100644 --- a/openrtb_ext/imp_seedingAlliance.go +++ b/openrtb_ext/imp_seedingAlliance.go @@ -1,6 +1,7 @@ package openrtb_ext type ImpExtSeedingAlliance struct { - AdUnitID string `json:"adUnitId"` - SeatID string `json:"seatId"` + AdUnitID string `json:"adUnitId"` + SeatID string `json:"seatId"` + AccountID string `json:"accountId"` } diff --git a/static/bidder-params/seedingAlliance.json b/static/bidder-params/seedingAlliance.json index 5fe463b6803..d72086230aa 100644 --- a/static/bidder-params/seedingAlliance.json +++ b/static/bidder-params/seedingAlliance.json @@ -11,7 +11,11 @@ }, "seatId": { "type": "string", - "description": "Seat ID" + "description": "Deprecated, please use accountId" + }, + "accountId": { + "type": "string", + "description": "Account ID of partner" } }, "required": [ From 1d96d891dfd7789dfd87ad37a7610f2adb31bb32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zdravko=20Kosanovi=C4=87?= <41286499+zkosanovic@users.noreply.github.com> Date: Fri, 23 Feb 2024 05:23:52 +0400 Subject: [PATCH 03/13] MinuteMedia: Add GPP macros (#3497) --- static/bidder-info/minutemedia.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/bidder-info/minutemedia.yaml b/static/bidder-info/minutemedia.yaml index efe58e57240..55f5a2b7cd3 100644 --- a/static/bidder-info/minutemedia.yaml +++ b/static/bidder-info/minutemedia.yaml @@ -14,5 +14,5 @@ capabilities: - video userSync: iframe: - url: https://pbs-cs.minutemedia-prebid.com/pbs-iframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + url: https://pbs-cs.minutemedia-prebid.com/pbs-iframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redirect={{.RedirectURL}} userMacro: "[PBS_UID]" From 14fcbb780bc649da67d927a834b425872de35ffb Mon Sep 17 00:00:00 2001 From: Aditya Mahendrakar Date: Thu, 22 Feb 2024 17:30:21 -0800 Subject: [PATCH 04/13] Update prebid.org url to https (#3529) --- static/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/index.html b/static/index.html index a3518ce27ae..ac79079f34f 100644 --- a/static/index.html +++ b/static/index.html @@ -3,7 +3,7 @@ Prebid Server - Prebid Server is a server-to-server proxy for Prebid.js users. + Prebid Server is a server-to-server proxy for Prebid.js users. The host is not responsible for the content or advertising delivered through this proxy. For more information, please contact prebid-server - at - prebid.org. From 17b7082a9d2d8fc2e0a6a15bc836e2e5227eeafe Mon Sep 17 00:00:00 2001 From: ahmadlob <109217988+ahmadlob@users.noreply.github.com> Date: Mon, 26 Feb 2024 20:46:39 +0200 Subject: [PATCH 05/13] Taboola: Fix gpp query param (#3515) --- static/bidder-info/taboola.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/bidder-info/taboola.yaml b/static/bidder-info/taboola.yaml index 436f746959a..0fccee145bc 100644 --- a/static/bidder-info/taboola.yaml +++ b/static/bidder-info/taboola.yaml @@ -13,8 +13,8 @@ capabilities: - native userSync: redirect: - url: https://trc.taboola.com/sg/ps/1/cm?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + url: "https://trc.taboola.com/sg/ps/1/cm?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redirect={{.RedirectURL}}" userMacro: "" iframe: - url: https://cdn.taboola.com/scripts/ps-sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + url: "https://cdn.taboola.com/scripts/ps-sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redirect={{.RedirectURL}}" userMacro: "" From 030da80c908945f4b23a17778c67c7a18a4ba0e6 Mon Sep 17 00:00:00 2001 From: pm-avinash-kapre <112699665+AvinashKapre@users.noreply.github.com> Date: Tue, 27 Feb 2024 22:00:58 +0530 Subject: [PATCH 06/13] Fix: Pubstack memory leak (#3541) --- analytics/pubstack/eventchannel/sender.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/analytics/pubstack/eventchannel/sender.go b/analytics/pubstack/eventchannel/sender.go index 951de4d414e..fe068b1555f 100644 --- a/analytics/pubstack/eventchannel/sender.go +++ b/analytics/pubstack/eventchannel/sender.go @@ -3,10 +3,11 @@ package eventchannel import ( "bytes" "fmt" - "github.com/golang/glog" "net/http" "net/url" "path" + + "github.com/golang/glog" ) type Sender = func(payload []byte) error @@ -26,6 +27,7 @@ func NewHttpSender(client *http.Client, endpoint string) Sender { if err != nil { return err } + resp.Body.Close() if resp.StatusCode != http.StatusOK { glog.Errorf("[pubstack] Wrong code received %d instead of %d", resp.StatusCode, http.StatusOK) From d13dfc58e8d0c5e4f9f5412ad433f54f3e558b22 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Tue, 27 Feb 2024 14:27:21 -0500 Subject: [PATCH 07/13] DSA: Bid response adrender, behalf & paid validations (#3523) --- dsa/validate.go | 102 ++++++--- dsa/validate_test.go | 308 ++++++++++++++++++++++++---- exchange/exchange.go | 8 +- exchange/exchange_test.go | 2 +- openrtb_ext/bid.go | 8 + openrtb_ext/regs.go | 4 +- openrtb_ext/request_wrapper_test.go | 14 +- 7 files changed, 363 insertions(+), 83 deletions(-) diff --git a/dsa/validate.go b/dsa/validate.go index f6ece66e132..034dae8b3cf 100644 --- a/dsa/validate.go +++ b/dsa/validate.go @@ -1,51 +1,103 @@ package dsa import ( + "errors" + "github.com/prebid/prebid-server/v2/exchange/entities" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) - "github.com/buger/jsonparser" +// Required values representing whether a DSA object is required +const ( + Required int8 = 2 // bid responses without DSA object will not be accepted + RequiredOnlinePlatform int8 = 3 // bid responses without DSA object will not be accepted, Publisher is Online Platform ) +// PubRender values representing publisher rendering intentions const ( - // Required - bid responses without DSA object will not be accepted - Required = 2 - // RequiredOnlinePlatform - bid responses without DSA object will not be accepted, Publisher is an Online Platform - RequiredOnlinePlatform = 3 + PubRenderCannotRender int8 = 0 // publisher can't render + PubRenderWillRender int8 = 2 // publisher will render +) + +// AdRender values representing buyer/advertiser rendering intentions +const ( + AdRenderWillRender int8 = 1 // buyer/advertiser will render +) + +var ( + ErrDsaMissing = errors.New("DSA object missing when required") + ErrBehalfTooLong = errors.New("DSA behalf exceeds limit of 100 chars") + ErrPaidTooLong = errors.New("DSA paid exceeds limit of 100 chars") + ErrNeitherWillRender = errors.New("DSA publisher and buyer both signal will not render") + ErrBothWillRender = errors.New("DSA publisher and buyer both signal will render") +) + +const ( + behalfMaxLength = 100 + paidMaxLength = 100 ) // Validate determines whether a given bid is valid from a DSA perspective. // A bid is considered valid unless the bid request indicates that a DSA object is required -// in bid responses and the object happens to be missing from the specified bid. -func Validate(req *openrtb_ext.RequestWrapper, bid *entities.PbsOrtbBid) (valid bool) { - if !dsaRequired(req) { - return true +// in bid responses and the object happens to be missing from the specified bid, or if the bid +// DSA object contents are invalid +func Validate(req *openrtb_ext.RequestWrapper, bid *entities.PbsOrtbBid) error { + reqDSA := getReqDSA(req) + bidDSA := getBidDSA(bid) + + if dsaRequired(reqDSA) && bidDSA == nil { + return ErrDsaMissing } - if bid == nil || bid.Bid == nil { - return false + if bidDSA == nil { + return nil + } + if len(bidDSA.Behalf) > behalfMaxLength { + return ErrBehalfTooLong } - _, dataType, _, err := jsonparser.Get(bid.Bid.Ext, "dsa") - if dataType == jsonparser.Object && err == nil { - return true - } else if err != nil && err != jsonparser.KeyPathNotFoundError { - return true + if len(bidDSA.Paid) > paidMaxLength { + return ErrPaidTooLong } - return false + if reqDSA != nil && reqDSA.PubRender != nil && bidDSA.AdRender != nil { + if *reqDSA.PubRender == PubRenderCannotRender && *bidDSA.AdRender != AdRenderWillRender { + return ErrNeitherWillRender + } + if *reqDSA.PubRender == PubRenderWillRender && *bidDSA.AdRender == AdRenderWillRender { + return ErrBothWillRender + } + } + return nil } // dsaRequired examines the bid request to determine if the dsarequired field indicates // that bid responses include a dsa object -func dsaRequired(req *openrtb_ext.RequestWrapper) bool { +func dsaRequired(dsa *openrtb_ext.ExtRegsDSA) bool { + if dsa == nil || dsa.Required == nil { + return false + } + return *dsa.Required == Required || *dsa.Required == RequiredOnlinePlatform +} + +// getReqDSA retrieves the DSA object from the request +func getReqDSA(req *openrtb_ext.RequestWrapper) *openrtb_ext.ExtRegsDSA { + if req == nil { + return nil + } regExt, err := req.GetRegExt() if regExt == nil || err != nil { - return false + return nil } - regsDSA := regExt.GetDSA() - if regsDSA == nil { - return false + return regExt.GetDSA() +} + +// getBidDSA retrieves the DSA object from the bid +func getBidDSA(bid *entities.PbsOrtbBid) *openrtb_ext.ExtBidDSA { + if bid == nil || bid.Bid == nil { + return nil } - if regsDSA.Required == Required || regsDSA.Required == RequiredOnlinePlatform { - return true + var bidExt openrtb_ext.ExtBid + if err := jsonutil.Unmarshal(bid.Bid.Ext, &bidExt); err != nil { + return nil } - return false + return bidExt.DSA } diff --git a/dsa/validate_test.go b/dsa/validate_test.go index 74d428c3e9e..9dfa32c6efe 100644 --- a/dsa/validate_test.go +++ b/dsa/validate_test.go @@ -2,23 +2,48 @@ package dsa import ( "encoding/json" + "strings" "testing" "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/exchange/entities" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/ptrutil" "github.com/stretchr/testify/assert" ) func TestValidate(t *testing.T) { + var ( + validBehalf = strings.Repeat("a", 100) + invalidBehalf = strings.Repeat("a", 101) + validPaid = strings.Repeat("a", 100) + invalidPaid = strings.Repeat("a", 101) + ) + tests := []struct { name string giveRequest *openrtb_ext.RequestWrapper giveBid *entities.PbsOrtbBid - wantValid bool + wantError error }{ { - name: "not_required", + name: "nil", + giveRequest: nil, + giveBid: nil, + wantError: nil, + }, + { + name: "request_nil", + giveRequest: nil, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa":{"behalf":"` + validBehalf + `","paid":"` + validPaid + `","adrender":1}}`), + }, + }, + wantError: nil, + }, + { + name: "not_required_and_bid_is_nil", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{ @@ -27,7 +52,23 @@ func TestValidate(t *testing.T) { }, }, giveBid: nil, - wantValid: true, + wantError: nil, + }, + { + name: "not_required_and_bid_dsa_is_valid", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 0,"pubrender":0}}`), + }, + }, + }, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa":{"behalf":"` + validBehalf + `","paid":"` + validPaid + `","adrender":1}}`), + }, + }, + wantError: nil, }, { name: "required_and_bid_is_nil", @@ -39,10 +80,10 @@ func TestValidate(t *testing.T) { }, }, giveBid: nil, - wantValid: false, + wantError: ErrDsaMissing, }, { - name: "required_and_bid.bid_is_nil", + name: "required_and_bid_dsa_has_invalid_behalf", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{ @@ -50,11 +91,15 @@ func TestValidate(t *testing.T) { }, }, }, - giveBid: &entities.PbsOrtbBid{}, - wantValid: false, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa":{"behalf":"` + invalidBehalf + `"}}`), + }, + }, + wantError: ErrBehalfTooLong, }, { - name: "required_and_bid.ext.dsa_not_present", + name: "required_and_bid_dsa_has_invalid_paid", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{ @@ -64,13 +109,61 @@ func TestValidate(t *testing.T) { }, giveBid: &entities.PbsOrtbBid{ Bid: &openrtb2.Bid{ - Ext: json.RawMessage(`{}`), + Ext: json.RawMessage(`{"dsa":{"paid":"` + invalidPaid + `"}}`), + }, + }, + wantError: ErrPaidTooLong, + }, + { + name: "required_and_neither_will_render", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 2,"pubrender": 0}}`), + }, + }, + }, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa":{"adrender": 0}}`), + }, + }, + wantError: ErrNeitherWillRender, + }, + { + name: "required_and_both_will_render", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 2,"pubrender": 2}}`), + }, }, }, - wantValid: false, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa":{"adrender": 1}}`), + }, + }, + wantError: ErrBothWillRender, }, { - name: "required_and_bid.ext.dsa_present", + name: "required_and_bid_dsa_is_valid", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 2,"pubrender": 0}}`), + }, + }, + }, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa":{"behalf":"` + validBehalf + `","paid":"` + validPaid + `","adrender":1}}`), + }, + }, + wantError: nil, + }, + { + name: "required_and_bid_dsa_is_valid_no_pubrender", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{ @@ -80,17 +173,37 @@ func TestValidate(t *testing.T) { }, giveBid: &entities.PbsOrtbBid{ Bid: &openrtb2.Bid{ - Ext: json.RawMessage(`{"dsa": {}}`), + Ext: json.RawMessage(`{"dsa":{"behalf":"` + validBehalf + `","paid":"` + validPaid + `","adrender":2}}`), + }, + }, + wantError: nil, + }, + { + name: "required_and_bid_dsa_is_valid_no_adrender", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 2, "pubrender": 0}}`), + }, + }, + }, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa":{"behalf":"` + validBehalf + `","paid":"` + validPaid + `"}}`), }, }, - wantValid: true, + wantError: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - valid := Validate(tt.giveRequest, tt.giveBid) - assert.Equal(t, tt.wantValid, valid) + err := Validate(tt.giveRequest, tt.giveBid) + if tt.wantError != nil { + assert.Equal(t, err, tt.wantError) + } else { + assert.NoError(t, err) + } }) } } @@ -98,55 +211,110 @@ func TestValidate(t *testing.T) { func TestDSARequired(t *testing.T) { tests := []struct { name string - giveRequest *openrtb_ext.RequestWrapper + giveReqDSA *openrtb_ext.ExtRegsDSA wantRequired bool }{ { - name: "not_required_and_reg.ext.dsa_is_nil", - giveRequest: &openrtb_ext.RequestWrapper{ - BidRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - Ext: json.RawMessage(`{}`), - }, - }, + name: "nil", + giveReqDSA: nil, + wantRequired: false, + }, + { + name: "nil_required", + giveReqDSA: &openrtb_ext.ExtRegsDSA{ + Required: nil, + }, + wantRequired: false, + }, + { + name: "not_required", + giveReqDSA: &openrtb_ext.ExtRegsDSA{ + Required: ptrutil.ToPtr[int8](0), + }, + wantRequired: false, + }, + { + name: "not_required_supported", + giveReqDSA: &openrtb_ext.ExtRegsDSA{ + Required: ptrutil.ToPtr[int8](1), }, wantRequired: false, }, { - name: "not_required_and_reg.ext.dsa_is_empty", + name: "required", + giveReqDSA: &openrtb_ext.ExtRegsDSA{ + Required: ptrutil.ToPtr[int8](2), + }, + wantRequired: true, + }, + { + name: "required_online_platform", + giveReqDSA: &openrtb_ext.ExtRegsDSA{ + Required: ptrutil.ToPtr[int8](3), + }, + wantRequired: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + required := dsaRequired(tt.giveReqDSA) + assert.Equal(t, tt.wantRequired, required) + }) + } +} + +func TestGetReqDSA(t *testing.T) { + tests := []struct { + name string + giveRequest *openrtb_ext.RequestWrapper + expectedDSA *openrtb_ext.ExtRegsDSA + }{ + { + name: "req_is_nil", + giveRequest: nil, + expectedDSA: nil, + }, + { + name: "bidrequest_is_nil", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: nil, + }, + expectedDSA: nil, + }, + { + name: "req.regs_is_nil", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - Ext: json.RawMessage(`{"dsa": {}}`), - }, + Regs: nil, }, }, - wantRequired: false, + expectedDSA: nil, }, { - name: "required_and_reg.ext.dsa_is_0", + name: "req.regs.ext_is_nil", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{ - Ext: json.RawMessage(`{"dsa": {"dsarequired": 0}}`), + Ext: nil, }, }, }, - wantRequired: false, + expectedDSA: nil, }, { - name: "required_and_reg.ext.dsa_is_1", + name: "req.regs.ext_is_empty", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{ - Ext: json.RawMessage(`{"dsa": {"dsarequired": 1}}`), + Ext: json.RawMessage(`{}`), }, }, }, - wantRequired: false, + expectedDSA: nil, }, { - name: "required_and_reg.ext.dsa_is_2", + name: "req.regs.ext_dsa_is_populated", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{ @@ -154,25 +322,75 @@ func TestDSARequired(t *testing.T) { }, }, }, - wantRequired: true, + expectedDSA: &openrtb_ext.ExtRegsDSA{ + Required: ptrutil.ToPtr[int8](2), + }, }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dsa := getReqDSA(tt.giveRequest) + assert.Equal(t, tt.expectedDSA, dsa) + }) + } +} + +func TestGetBidDSA(t *testing.T) { + tests := []struct { + name string + bid *entities.PbsOrtbBid + expectedDSA *openrtb_ext.ExtBidDSA + }{ { - name: "required_and_reg.ext.dsa_is_3", - giveRequest: &openrtb_ext.RequestWrapper{ - BidRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - Ext: json.RawMessage(`{"dsa": {"dsarequired": 3}}`), - }, + name: "bid_is_nil", + bid: nil, + expectedDSA: nil, + }, + { + name: "bid.bid_is_nil", + bid: &entities.PbsOrtbBid{ + Bid: nil, + }, + expectedDSA: nil, + }, + { + name: "bid.bid.ext_is_nil", + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: nil, }, }, - wantRequired: true, + expectedDSA: nil, + }, + { + name: "bid.bid.ext_is_empty", + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{}`), + }, + }, + expectedDSA: nil, + }, + { + name: "bid.bid.ext.dsa_is_populated", + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa": {"behalf":"test1","paid":"test2","adrender":1}}`), + }, + }, + expectedDSA: &openrtb_ext.ExtBidDSA{ + Behalf: "test1", + Paid: "test2", + AdRender: ptrutil.ToPtr[int8](1), + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - required := dsaRequired(tt.giveRequest) - assert.Equal(t, tt.wantRequired, required) + dsa := getBidDSA(tt.bid) + assert.Equal(t, tt.expectedDSA, dsa) }) } } diff --git a/exchange/exchange.go b/exchange/exchange.go index b9dae6725c6..41134104f37 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -1245,12 +1245,12 @@ func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCrea errs := make([]error, 0, 1) for _, bid := range bids { - if !dsa.Validate(bidRequest, bid) { - RequiredError := openrtb_ext.ExtBidderMessage{ + if err := dsa.Validate(bidRequest, bid); err != nil { + dsaMessage := openrtb_ext.ExtBidderMessage{ Code: errortypes.InvalidBidResponseDSAWarningCode, - Message: "bid response rejected: DSA object missing when required", + Message: fmt.Sprintf("bid rejected: %s", err.Error()), } - bidResponseExt.Warnings[adapter] = append(bidResponseExt.Warnings[adapter], RequiredError) + bidResponseExt.Warnings[adapter] = append(bidResponseExt.Warnings[adapter], dsaMessage) seatNonBids.addBid(bid, int(ResponseRejectedGeneral), adapter.String()) continue // Don't add bid to result diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index e484e21a42e..19c1eb67267 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -4758,7 +4758,7 @@ func TestMakeBidWithValidation(t *testing.T) { name: "One_of_two_bids_is_invalid_based_on_DSA_object_presence", givenBidRequestExt: json.RawMessage(`{"dsa": {"dsarequired": 2}}`), givenValidations: config.Validations{}, - givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{Ext: json.RawMessage(`{"dsa": {}}`)}}, {Bid: &openrtb2.Bid{}}}, + givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{Ext: json.RawMessage(`{"dsa": {"adrender":1}}`)}}, {Bid: &openrtb2.Bid{}}}, givenSeat: "pubmatic", expectedNumOfBids: 1, expectedNonBids: &nonBids{ diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go index 2e190389212..7d3dcbd70bf 100644 --- a/openrtb_ext/bid.go +++ b/openrtb_ext/bid.go @@ -7,6 +7,7 @@ import ( // ExtBid defines the contract for bidresponse.seatbid.bid[i].ext type ExtBid struct { + DSA *ExtBidDSA `json:"dsa,omitempty"` Prebid *ExtBidPrebid `json:"prebid,omitempty"` } @@ -83,6 +84,13 @@ type ExtBidPrebidEvents struct { Imp string `json:"imp,omitempty"` } +// ExtBidDSA defines the contract for bidresponse.seatbid.bid[i].ext.dsa +type ExtBidDSA struct { + AdRender *int8 `json:"adrender,omitempty"` + Behalf string `json:"behalf,omitempty"` + Paid string `json:"paid,omitempty"` +} + // BidType describes the allowed values for bidresponse.seatbid.bid[i].ext.prebid.type type BidType string diff --git a/openrtb_ext/regs.go b/openrtb_ext/regs.go index 8057ce2ccd7..eca5ff98e55 100644 --- a/openrtb_ext/regs.go +++ b/openrtb_ext/regs.go @@ -16,5 +16,7 @@ type ExtRegs struct { // ExtRegsDSA defines the contract for bidrequest.regs.ext.dsa type ExtRegsDSA struct { // Required should be a between 0 and 3 inclusive, see https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md - Required int8 `json:"dsarequired,omitempty"` + Required *int8 `json:"dsarequired,omitempty"` + // PubRender should be between 0 and 2 inclusive, see https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md + PubRender *int8 `json:"pubrender,omitempty"` } diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go index f04a51a4bdc..425229e54e9 100644 --- a/openrtb_ext/request_wrapper_test.go +++ b/openrtb_ext/request_wrapper_test.go @@ -2069,7 +2069,7 @@ func TestRebuildRegExt(t *testing.T) { { name: "req_regs_nil_-_dirty_and_different_-_change", request: openrtb2.BidRequest{}, - regExt: RegExt{dsa: &ExtRegsDSA{Required: 1}, dsaDirty: true, gdpr: ptrutil.ToPtr[int8](1), gdprDirty: true, usPrivacy: strA, usPrivacyDirty: true}, + regExt: RegExt{dsa: &ExtRegsDSA{Required: ptrutil.ToPtr[int8](1)}, dsaDirty: true, gdpr: ptrutil.ToPtr[int8](1), gdprDirty: true, usPrivacy: strA, usPrivacyDirty: true}, expectedRequest: openrtb2.BidRequest{ Regs: &openrtb2.Regs{ Ext: json.RawMessage(`{"dsa":{"dsarequired":1},"gdpr":1,"us_privacy":"a"}`), @@ -2085,7 +2085,7 @@ func TestRebuildRegExt(t *testing.T) { { name: "req_regs_ext_nil_-_dirty_and_different_-_change", request: openrtb2.BidRequest{Regs: &openrtb2.Regs{}}, - regExt: RegExt{dsa: &ExtRegsDSA{Required: 1}, dsaDirty: true, gdpr: ptrutil.ToPtr[int8](1), gdprDirty: true, usPrivacy: strA, usPrivacyDirty: true}, + regExt: RegExt{dsa: &ExtRegsDSA{Required: ptrutil.ToPtr[int8](1)}, dsaDirty: true, gdpr: ptrutil.ToPtr[int8](1), gdprDirty: true, usPrivacy: strA, usPrivacyDirty: true}, expectedRequest: openrtb2.BidRequest{ Regs: &openrtb2.Regs{ Ext: json.RawMessage(`{"dsa":{"dsarequired":1},"gdpr":1,"us_privacy":"a"}`), @@ -2101,13 +2101,13 @@ func TestRebuildRegExt(t *testing.T) { { name: "req_regs_dsa_populated_-_dirty_and_different-_change", request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}}, - regExt: RegExt{dsa: &ExtRegsDSA{Required: 2}, dsaDirty: true}, + regExt: RegExt{dsa: &ExtRegsDSA{Required: ptrutil.ToPtr[int8](2)}, dsaDirty: true}, expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":2}}`)}}, }, { name: "req_regs_dsa_populated_-_dirty_and_same_-_no_change", request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}}, - regExt: RegExt{dsa: &ExtRegsDSA{Required: 1}, dsaDirty: true}, + regExt: RegExt{dsa: &ExtRegsDSA{Required: ptrutil.ToPtr[int8](1)}, dsaDirty: true}, expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}}, }, { @@ -2215,7 +2215,7 @@ func TestRegExtUnmarshal(t *testing.T) { regExt: &RegExt{}, extJson: json.RawMessage(`{"dsa":{"dsarequired":1}}`), expectDSA: &ExtRegsDSA{ - Required: 1, + Required: ptrutil.ToPtr[int8](1), }, expectError: false, }, @@ -2224,7 +2224,7 @@ func TestRegExtUnmarshal(t *testing.T) { regExt: &RegExt{}, extJson: json.RawMessage(`{"dsa":{"dsarequired":""}}`), expectDSA: &ExtRegsDSA{ - Required: 0, + Required: ptrutil.ToPtr[int8](0), }, expectError: true, }, @@ -2299,7 +2299,7 @@ func TestRegExtGetDSASetDSA(t *testing.T) { assert.False(t, regExt.Dirty()) dsa := &ExtRegsDSA{ - Required: 2, + Required: ptrutil.ToPtr[int8](2), } regExt.SetDSA(dsa) assert.True(t, regExt.Dirty()) From 11decc200677979abc6f36763a38519dd8a626ae Mon Sep 17 00:00:00 2001 From: linux019 Date: Tue, 27 Feb 2024 21:51:35 +0200 Subject: [PATCH 08/13] Fix modules template and builder (#3534) Co-authored-by: oaleksieiev --- modules/generator/builder.tmpl | 17 +++++++++-------- modules/generator/buildergen.go | 21 +++++++++++---------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/modules/generator/builder.tmpl b/modules/generator/builder.tmpl index b7b78103dbe..db0cd1a4fb4 100644 --- a/modules/generator/builder.tmpl +++ b/modules/generator/builder.tmpl @@ -1,20 +1,21 @@ package modules - -{{if .}} import ( - {{- range .}} - {{.Vendor}}{{.Module | Title}} "github.com/prebid/prebid-server/v2/modules/{{.Vendor}}/{{.Module}}" +{{- range $vendor, $modules := .}} + {{- range $module := $modules}} + {{$vendor}}{{$module | Title}} "github.com/prebid/prebid-server/v2/modules/{{$vendor}}/{{$module}}" {{- end}} +{{- end}} ) -{{end}} // builders returns mapping between module name and its builder // vendor and module names are chosen based on the module directory name func builders() ModuleBuilders { return ModuleBuilders{ - {{- range .}} - "{{.Vendor}}": { - "{{.Module}}": {{.Vendor}}{{.Module | Title}}.Builder, + {{- range $vendor, $modules := .}} + "{{$vendor}}": { + {{- range $module := $modules}} + "{{$module}}": {{$vendor}}{{$module | Title}}.Builder, + {{- end}} }, {{- end}} } diff --git a/modules/generator/buildergen.go b/modules/generator/buildergen.go index b906682b7a3..219932420bd 100644 --- a/modules/generator/buildergen.go +++ b/modules/generator/buildergen.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "regexp" + "sort" "strings" "text/template" ) @@ -20,26 +21,26 @@ var ( outName = "builder.go" ) -type Module struct { - Vendor string - Module string -} - func main() { - var modules []Module + modules := make(map[string][]string) filepath.WalkDir("./", func(path string, d fs.DirEntry, err error) error { if !r.MatchString(path) { return nil } match := r.FindStringSubmatch(path) - modules = append(modules, Module{ - Vendor: match[1], - Module: match[2], - }) + vendorModules := modules[match[1]] + vendorModules = append(vendorModules, match[2]) + modules[match[1]] = vendorModules + return nil }) + for vendorName, names := range modules { + sort.Strings(names) + modules[vendorName] = names + } + funcMap := template.FuncMap{"Title": strings.Title} t, err := template.New(tmplName).Funcs(funcMap).ParseFiles(fmt.Sprintf("generator/%s", tmplName)) if err != nil { From 23eca7301cef4f64ff51184b43124ac4e67de3f7 Mon Sep 17 00:00:00 2001 From: bsardo <1168933+bsardo@users.noreply.github.com> Date: Fri, 23 Feb 2024 22:34:42 -0500 Subject: [PATCH 09/13] Add account config --- config/account.go | 7 +++++++ openrtb_ext/regs.go | 11 ++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/config/account.go b/config/account.go index ee131873e35..8ae19171e1b 100644 --- a/config/account.go +++ b/config/account.go @@ -335,10 +335,17 @@ func (m AccountModules) ModuleConfig(id string) (json.RawMessage, error) { type AccountPrivacy struct { AllowActivities *AllowActivities `mapstructure:"allowactivities" json:"allowactivities"` + DSA *AccountDSA `mapstructure:"dsa" json:"dsa"` IPv6Config IPv6 `mapstructure:"ipv6" json:"ipv6"` IPv4Config IPv4 `mapstructure:"ipv4" json:"ipv4"` } +// AccountDSA represents DSA configuration +type AccountDSA struct { + Default *openrtb_ext.ExtRegsDSA `mapstructure:"default" json:"default"` + GDPROnly bool `mapstructure:"gdpr_only" json:"gdpr_only"` +} + type IPv6 struct { AnonKeepBits int `mapstructure:"anon_keep_bits" json:"anon_keep_bits"` } diff --git a/openrtb_ext/regs.go b/openrtb_ext/regs.go index eca5ff98e55..8cd29158c6e 100644 --- a/openrtb_ext/regs.go +++ b/openrtb_ext/regs.go @@ -15,8 +15,17 @@ type ExtRegs struct { // ExtRegsDSA defines the contract for bidrequest.regs.ext.dsa type ExtRegsDSA struct { + // DataToPub should be between 0 and 2 inclusive, see https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md + DataToPub int8 `json:"datatopub,omitempty"` // Required should be a between 0 and 3 inclusive, see https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md Required *int8 `json:"dsarequired,omitempty"` // PubRender should be between 0 and 2 inclusive, see https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md - PubRender *int8 `json:"pubrender,omitempty"` + PubRender *int8 `json:"pubrender,omitempty"` + Transparency []ExtBidDSATransparency `json:"transparency,omitempty"` +} + +// ExtBidDSATransparency defines the contract for bidrequest.regs.ext.dsa.transparency +type ExtBidDSATransparency struct { + Domain string `json:"domain,omitempty"` + Params []int `json:"dsaparams,omitempty"` } From 9cddf2b9070dd3231b9d679ca1024d7655dce52e Mon Sep 17 00:00:00 2001 From: bsardo <1168933+bsardo@users.noreply.github.com> Date: Fri, 23 Feb 2024 22:35:29 -0500 Subject: [PATCH 10/13] Add DSA writer to insert default DSA in requests --- dsa/dsawriter.go | 35 ++++++++ dsa/dsawriter_test.go | 183 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 dsa/dsawriter.go create mode 100644 dsa/dsawriter_test.go diff --git a/dsa/dsawriter.go b/dsa/dsawriter.go new file mode 100644 index 00000000000..731e77cbf84 --- /dev/null +++ b/dsa/dsawriter.go @@ -0,0 +1,35 @@ +package dsa + +import ( + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +// DSAWriter is used to write the default DSA to the request (req.regs.ext.dsa) +type DSAWriter struct { + Config *config.AccountDSA + GDPRInScope bool +} + +// Write sets the default DSA object on the request at regs.ext.dsa if it is +// defined in the account config and it is not already present on the request +func (dw DSAWriter) Write(req *openrtb_ext.RequestWrapper) (err error) { + if req == nil { + return + } + if getReqDSA(req) != nil { + return + } + if dw.Config == nil || dw.Config.Default == nil { + return + } + if dw.Config.GDPROnly && !dw.GDPRInScope { + return + } + regExt, err := req.GetRegExt() + if err != nil { + return err + } + regExt.SetDSA(dw.Config.Default) + return +} diff --git a/dsa/dsawriter_test.go b/dsa/dsawriter_test.go new file mode 100644 index 00000000000..291e4a3ea7d --- /dev/null +++ b/dsa/dsawriter_test.go @@ -0,0 +1,183 @@ +package dsa + +import ( + "encoding/json" + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestWrite(t *testing.T) { + requestDSAJSON := json.RawMessage(`{"dsa":{"datatopub":1,"dsarequired":2,"pubrender":1,"transparency":[{"domain":"example1.com","dsaparams":[1,2,3]}]}}`) + defaultDSAJSON := json.RawMessage(`{"dsa":{"datatopub":2,"dsarequired":3,"pubrender":2,"transparency":[{"domain":"example2.com","dsaparams":[4,5,6]}]}}`) + defaultDSA := &openrtb_ext.ExtRegsDSA{ + DataToPub: 2, + Required: 3, + PubRender: 2, + Transparency: []openrtb_ext.ExtBidDSATransparency{ + { + Domain: "example2.com", + Params: []int{4, 5, 6}, + }, + }, + } + + tests := []struct { + name string + giveConfig *config.AccountDSA + giveGDPR bool + giveRequest *openrtb_ext.RequestWrapper + expectRequest *openrtb_ext.RequestWrapper + }{ + { + name: "request_nil", + }, + { + name: "config_nil", + giveConfig: nil, + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: nil, + }, + }, + }, + expectRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: nil, + }, + }, + }, + }, + { + name: "config_default_nil", + giveConfig: &config.AccountDSA{ + Default: nil, + }, + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: nil, + }, + }, + }, + expectRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: nil, + }, + }, + }, + }, + { + name: "request_dsa_present", + giveConfig: &config.AccountDSA{ + Default: defaultDSA, + }, + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: requestDSAJSON, + }, + }, + }, + expectRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: requestDSAJSON, + }, + }, + }, + }, + { + name: "config_default_present_with_gdpr_only_set_and_gdpr_in_scope", + giveConfig: &config.AccountDSA{ + Default: defaultDSA, + GDPROnly: true, + }, + giveGDPR: true, + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + expectRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: defaultDSAJSON, + }, + }, + }, + }, + { + name: "config_default_present_with_gdpr_only_set_and_gdpr_not_in_scope", + giveConfig: &config.AccountDSA{ + Default: defaultDSA, + GDPROnly: true, + }, + giveGDPR: false, + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + expectRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + }, + { + name: "config_default_present_with_gdpr_only_not_set_and_gdpr_in_scope", + giveConfig: &config.AccountDSA{ + Default: defaultDSA, + GDPROnly: false, + }, + giveGDPR: true, + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + expectRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: defaultDSAJSON, + }, + }, + }, + }, + { + name: "config_default_present_with_gdpr_only_not_set_and_gdpr_not_in_scope", + giveConfig: &config.AccountDSA{ + Default: defaultDSA, + GDPROnly: false, + }, + giveGDPR: false, + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + expectRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: defaultDSAJSON, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + writer := DSAWriter{ + Config: tt.giveConfig, + GDPRInScope: tt.giveGDPR, + } + err := writer.Write(tt.giveRequest) + + if tt.giveRequest != nil { + tt.giveRequest.RebuildRequest() + assert.Equal(t, tt.expectRequest.BidRequest, tt.giveRequest.BidRequest) + } else { + assert.Nil(t, tt.giveRequest) + } + assert.Nil(t, err) + }) + } +} From e0c174e4d3a1378ff1e4fc1c80eb45b6ad3a742a Mon Sep 17 00:00:00 2001 From: bsardo <1168933+bsardo@users.noreply.github.com> Date: Sun, 25 Feb 2024 10:17:53 -0500 Subject: [PATCH 11/13] Use DSA writer to insert default into each bidder request --- exchange/utils.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/exchange/utils.go b/exchange/utils.go index 676c015ae0e..6fb48f463c7 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -18,6 +18,7 @@ import ( "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/dsa" "github.com/prebid/prebid-server/v2/errortypes" "github.com/prebid/prebid-server/v2/firstpartydata" "github.com/prebid/prebid-server/v2/gdpr" @@ -154,6 +155,11 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, gdprPerms = rs.gdprPermsBuilder(auctionReq.TCF2Config, gdprRequestInfo) } + DSAWriter := dsa.DSAWriter{ + Config: auctionReq.Account.Privacy.DSA, + GDPRInScope: gdprEnforced, + } + // bidder level privacy policies for _, bidderRequest := range allBidderRequests { // fetchBids activity @@ -227,6 +233,10 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, privacy.ScrubTID(reqWrapper) } + if err := DSAWriter.Write(reqWrapper); err != nil { + errs = append(errs, err) + } + reqWrapper.RebuildRequest() bidderRequest.BidRequest = reqWrapper.BidRequest From a424d16da3693ad178901d20cb9b082c69fe09fc Mon Sep 17 00:00:00 2001 From: bsardo <1168933+bsardo@users.noreply.github.com> Date: Sun, 25 Feb 2024 10:24:08 -0500 Subject: [PATCH 12/13] Add DSA default JSON test with test framework update --- exchange/exchange_test.go | 1 + exchange/exchangetest/dsa-default.json | 219 +++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 exchange/exchangetest/dsa-default.json diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 19c1eb67267..bcce4947832 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -2181,6 +2181,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { }, DebugAllow: true, PriceFloors: config.AccountPriceFloors{Enabled: spec.AccountFloorsEnabled}, + Privacy: *spec.AccountPrivacy, Validations: spec.AccountConfigBidValidation, }, UserSyncs: mockIdFetcher(spec.IncomingRequest.Usersyncs), diff --git a/exchange/exchangetest/dsa-default.json b/exchange/exchangetest/dsa-default.json new file mode 100644 index 00000000000..339aa4014c0 --- /dev/null +++ b/exchange/exchangetest/dsa-default.json @@ -0,0 +1,219 @@ +{ + "accountPrivacy": { + "dsa": { + "default": { + "dsarequired": 2, + "pubrender": 1, + "datatopub": 2, + "transparency": [{ + "domain": "platform1domain.com", + "dsaparams": [1] + }, + { + "domain": "SSP2domain.com", + "dsaparams": [1, 2] + } + ] + } + } + }, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + }, + "audienceNetwork": { + "placementId": "some-placement" + } + } + } + } + }] + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + }], + "regs": { + "ext": { + "dsa": { + "dsarequired": 2, + "pubrender": 1, + "datatopub": 2, + "transparency": [{ + "domain": "platform1domain.com", + "dsaparams": [1] + }, + { + "domain": "SSP2domain.com", + "dsaparams": [1, 2] + } + ] + } + } + } + } + }, + "mockResponse": { + "pbsSeatBids": [{ + "pbsBids": [{ + "ortbBid": { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "ext": { + "someField": "someValue", + "origbidcpm": 0.3, + "dsa": { + "behalf": "Advertiser", + "paid": "Advertiser", + "transparency": [{ + "domain": "dsp1domain.com", + "dsaparams": [1, 2] + }], + "adrender": 1 + } + } + }, + "bidType": "video" + }], + "seat": "appnexus" + }] + } + }, + "audienceNetwork": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "placementId": "some-placement" + } + } + }], + "regs": { + "ext": { + "dsa": { + "dsarequired": 2, + "pubrender": 1, + "datatopub": 2, + "transparency": [{ + "domain": "platform1domain.com", + "dsaparams": [1] + }, + { + "domain": "SSP2domain.com", + "dsaparams": [1, 2] + } + ] + } + } + } + } + }, + "mockResponse": { + "pbsSeatBids": [{ + "pbsBids": [{ + "ortbBid": { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "ext": { + "someField": "someValue", + "origbidcpm": 0.3 + } + }, + "bidType": "video" + }], + "seat": "audienceNetwork" + }] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [{ + "seat": "appnexus", + "bid": [{ + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "ext": { + "someField": "someValue", + "origbidcpm": 0.3, + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "video" + }, + "dsa": { + "behalf": "Advertiser", + "paid": "Advertiser", + "transparency": [{ + "domain": "dsp1domain.com", + "dsaparams": [1, 2] + }], + "adrender": 1 + } + } + }] + }, { + "seat": "audienceNetwork", + "bid": [] + }] + } + } +} \ No newline at end of file From 3d0a754cb63941c3c0e9f0d7da1b966919eb64ea Mon Sep 17 00:00:00 2001 From: bsardo <1168933+bsardo@users.noreply.github.com> Date: Sun, 25 Feb 2024 11:50:44 -0500 Subject: [PATCH 13/13] Insert default into request before splitting into bidder requests --- exchange/exchange_test.go | 7 ++++--- exchange/gdpr.go | 6 ++++++ exchange/utils.go | 41 +++++++++++++++++++-------------------- exchange/utils_test.go | 2 ++ 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index bcce4947832..0ee873c42a5 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -978,6 +978,7 @@ func TestFloorsSignalling(t *testing.T) { Account: config.Account{DebugAllow: true, PriceFloors: config.AccountPriceFloors{Enabled: test.floorsEnable, MaxRule: 100, MaxSchemaDims: 5}}, UserSyncs: &emptyUsersync{}, HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{}) @@ -2170,7 +2171,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { impExtInfoMap[impID] = ImpExtInfo{} } - activityControl := privacy.NewActivityControl(spec.AccountPrivacy) + activityControl := privacy.NewActivityControl(&spec.AccountPrivacy) auctionRequest := &AuctionRequest{ BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &spec.IncomingRequest.OrtbRequest}, @@ -2181,7 +2182,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { }, DebugAllow: true, PriceFloors: config.AccountPriceFloors{Enabled: spec.AccountFloorsEnabled}, - Privacy: *spec.AccountPrivacy, + Privacy: spec.AccountPrivacy, Validations: spec.AccountConfigBidValidation, }, UserSyncs: mockIdFetcher(spec.IncomingRequest.Usersyncs), @@ -5485,7 +5486,7 @@ type exchangeSpec struct { FledgeEnabled bool `json:"fledge_enabled,omitempty"` MultiBid *multiBidSpec `json:"multiBid,omitempty"` Server exchangeServer `json:"server,omitempty"` - AccountPrivacy *config.AccountPrivacy `json:"accountPrivacy,omitempty"` + AccountPrivacy config.AccountPrivacy `json:"accountPrivacy,omitempty"` } type multiBidSpec struct { diff --git a/exchange/gdpr.go b/exchange/gdpr.go index 52fb860f5df..2f94eefdaef 100644 --- a/exchange/gdpr.go +++ b/exchange/gdpr.go @@ -35,3 +35,9 @@ func getConsent(req *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) (conse } return *ue.GetConsent(), nil } + +// enforceGDPR determines if GDPR should be enforced based on the request signal and whether the channel is enabled +func enforceGDPR(signal gdpr.Signal, defaultValue gdpr.Signal, channelEnabled bool) bool { + gdprApplies := signal == gdpr.SignalYes || (signal == gdpr.SignalAmbiguous && defaultValue == gdpr.SignalYes) + return gdprApplies && channelEnabled +} diff --git a/exchange/utils.go b/exchange/utils.go index 6fb48f463c7..4a2cc6510a7 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -83,8 +83,27 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, return } + gdprSignal, err := getGDPR(req) + if err != nil { + errs = append(errs, err) + } + channelEnabled := auctionReq.TCF2Config.ChannelEnabled(channelTypeMap[auctionReq.LegacyLabels.RType]) + gdprEnforced := enforceGDPR(gdprSignal, gdprDefaultValue, channelEnabled) + DSAWriter := dsa.DSAWriter{ + Config: auctionReq.Account.Privacy.DSA, + GDPRInScope: gdprEnforced, + } + if err := DSAWriter.Write(req); err != nil { + errs = append(errs, err) + } + req.RebuildRequest() + var allBidderRequests []BidderRequest - allBidderRequests, errs = getAuctionBidderRequests(auctionReq, requestExt, rs.bidderToSyncerKey, impsByBidder, aliases, rs.hostSChainNode) + var allBidderRequestErrs []error + allBidderRequests, allBidderRequestErrs = getAuctionBidderRequests(auctionReq, requestExt, rs.bidderToSyncerKey, impsByBidder, aliases, rs.hostSChainNode) + if allBidderRequestErrs != nil { + errs = append(errs, allBidderRequestErrs...) + } bidderNameToBidderReq := buildBidResponseRequest(req.BidRequest, bidderImpWithBidResp, aliases, auctionReq.BidderImpReplaceImpID) //this function should be executed after getAuctionBidderRequests @@ -104,16 +123,10 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, applyBidAdjustmentToFloor(allBidderRequests, bidAdjustmentFactors) } - gdprSignal, err := getGDPR(req) - if err != nil { - errs = append(errs, err) - } - consent, err := getConsent(req, gpp) if err != nil { errs = append(errs, err) } - gdprApplies := gdprSignal == gdpr.SignalYes || (gdprSignal == gdpr.SignalAmbiguous && gdprDefaultValue == gdpr.SignalYes) ccpaEnforcer, err := extractCCPA(req.BidRequest, rs.privacyConfig, &auctionReq.Account, aliases, channelTypeMap[auctionReq.LegacyLabels.RType], gpp) if err != nil { @@ -131,13 +144,8 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, privacyLabels.COPPAEnforced = coppa privacyLabels.LMTEnforced = lmt - var gdprEnforced bool var gdprPerms gdpr.Permissions = &gdpr.AlwaysAllow{} - if gdprApplies { - gdprEnforced = auctionReq.TCF2Config.ChannelEnabled(channelTypeMap[auctionReq.LegacyLabels.RType]) - } - if gdprEnforced { privacyLabels.GDPREnforced = true parsedConsent, err := vendorconsent.ParseString(consent) @@ -155,11 +163,6 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, gdprPerms = rs.gdprPermsBuilder(auctionReq.TCF2Config, gdprRequestInfo) } - DSAWriter := dsa.DSAWriter{ - Config: auctionReq.Account.Privacy.DSA, - GDPRInScope: gdprEnforced, - } - // bidder level privacy policies for _, bidderRequest := range allBidderRequests { // fetchBids activity @@ -233,10 +236,6 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, privacy.ScrubTID(reqWrapper) } - if err := DSAWriter.Write(reqWrapper); err != nil { - errs = append(errs, err) - } - reqWrapper.RebuildRequest() bidderRequest.BidRequest = reqWrapper.BidRequest diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 0c8153b6b11..71ab0fad989 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -3219,6 +3219,7 @@ func TestCleanOpenRTBRequestsBidAdjustment(t *testing.T) { BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, UserSyncs: &emptyUsersync{}, Account: accountConfig, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } gdprPermissionsBuilder := fakePermissionsBuilder{ permissions: &permissionsMock{ @@ -4666,6 +4667,7 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) { AnonKeepBits: 16, }, }}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), } bidderToSyncerKey := map[string]string{}