Skip to content

Commit

Permalink
DSA: Inject default in bid requests if not present (prebid#3540)
Browse files Browse the repository at this point in the history
  • Loading branch information
bsardo authored Apr 9, 2024
1 parent a35a668 commit b42841e
Show file tree
Hide file tree
Showing 15 changed files with 910 additions and 22 deletions.
5 changes: 5 additions & 0 deletions account/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ func GetAccount(ctx context.Context, cfg *config.Configuration, fetcher stored_r
Message: fmt.Sprintf("The prebid-server account config for account id \"%s\" is malformed. Please reach out to the prebid server host.", accountID),
}}
}
if err := config.UnpackDSADefault(account.Privacy.DSA); err != nil {
return nil, []error{&errortypes.MalformedAcct{
Message: fmt.Sprintf("The prebid-server account config DSA for account id \"%s\" is malformed. Please reach out to the prebid server host.", accountID),
}}
}

// Fill in ID if needed, so it can be left out of account definition
if len(account.ID) == 0 {
Expand Down
34 changes: 31 additions & 3 deletions account/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ import (
"github.com/prebid/prebid-server/v2/openrtb_ext"
"github.com/prebid/prebid-server/v2/stored_requests"
"github.com/prebid/prebid-server/v2/util/iputil"
"github.com/prebid/prebid-server/v2/util/ptrutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

var (
validDSA = `{\"dsarequired\":1,\"pubrender\":2,\"transparency\":[{\"domain\":\"test.com\"}]}`
invalidDSA = `{\"dsarequired\":\"invalid\",\"pubrender\":2,\"transparency\":[{\"domain\":\"test.com\"}]}`
)

var mockAccountData = map[string]json.RawMessage{
"valid_acct": json.RawMessage(`{"disabled":false}`),
"valid_acct_dsa": json.RawMessage(`{"disabled":false, "privacy": {"dsa": {"default": "` + validDSA + `"}}}`),
"invalid_acct_dsa": json.RawMessage(`{"disabled":false, "privacy": {"dsa": {"default": "` + invalidDSA + `"}}}`),
"invalid_acct_ipv6_ipv4": json.RawMessage(`{"disabled":false, "privacy": {"ipv6": {"anon_keep_bits": -32}, "ipv4": {"anon_keep_bits": -16}}}`),
"disabled_acct": json.RawMessage(`{"disabled":true}`),
"malformed_acct": json.RawMessage(`{"disabled":"invalid type"}`),
Expand All @@ -36,6 +44,16 @@ func (af mockAccountFetcher) FetchAccount(ctx context.Context, accountDefaultsJS
}

func TestGetAccount(t *testing.T) {
validDSA := &openrtb_ext.ExtRegsDSA{
Required: ptrutil.ToPtr[int8](1),
PubRender: ptrutil.ToPtr[int8](2),
Transparency: []openrtb_ext.ExtBidDSATransparency{
{
Domain: "test.com",
},
},
}

unknown := metrics.PublisherUnknown
testCases := []struct {
accountID string
Expand All @@ -44,7 +62,8 @@ func TestGetAccount(t *testing.T) {
// account_defaults.disabled
disabled bool
// checkDefaultIP indicates IPv6 and IPv6 should be set to default values
checkDefaultIP bool
wantDefaultIP bool
wantDSA *openrtb_ext.ExtRegsDSA
// expected error, or nil if account should be found
err error
}{
Expand All @@ -66,7 +85,13 @@ func TestGetAccount(t *testing.T) {
{accountID: "valid_acct", required: false, disabled: true, err: nil},
{accountID: "valid_acct", required: true, disabled: true, err: nil},

{accountID: "invalid_acct_ipv6_ipv4", required: true, disabled: false, err: nil, checkDefaultIP: true},
{accountID: "valid_acct_dsa", required: false, disabled: false, wantDSA: validDSA, err: nil},
{accountID: "valid_acct_dsa", required: true, disabled: false, wantDSA: validDSA, err: nil},
{accountID: "valid_acct_dsa", required: false, disabled: true, wantDSA: validDSA, err: nil},
{accountID: "valid_acct_dsa", required: true, disabled: true, wantDSA: validDSA, err: nil},

{accountID: "invalid_acct_ipv6_ipv4", required: true, disabled: false, err: nil, wantDefaultIP: true},
{accountID: "invalid_acct_dsa", required: false, disabled: false, err: &errortypes.MalformedAcct{}},

// pubID given and matches a host account explicitly disabled (Disabled: true on account json)
{accountID: "disabled_acct", required: false, disabled: false, err: &errortypes.AccountDisabled{}},
Expand Down Expand Up @@ -111,10 +136,13 @@ func TestGetAccount(t *testing.T) {
assert.Nil(t, account, "return account must be nil on error")
assert.IsType(t, test.err, errors[0], "error is of unexpected type")
}
if test.checkDefaultIP {
if test.wantDefaultIP {
assert.Equal(t, account.Privacy.IPv6Config.AnonKeepBits, iputil.IPv6DefaultMaskingBitSize, "ipv6 should be set to default value")
assert.Equal(t, account.Privacy.IPv4Config.AnonKeepBits, iputil.IPv4DefaultMaskingBitSize, "ipv4 should be set to default value")
}
if test.wantDSA != nil {
assert.Equal(t, test.wantDSA, account.Privacy.DSA.DefaultUnpacked)
}
})
}
}
Expand Down
8 changes: 8 additions & 0 deletions config/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ 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"`
PrivacySandbox PrivacySandbox `mapstructure:"privacysandbox" json:"privacysandbox"`
Expand All @@ -350,6 +351,13 @@ type CookieDeprecation struct {
TTLSec int `mapstructure:"ttl_sec"`
}

// AccountDSA represents DSA configuration
type AccountDSA struct {
Default string `mapstructure:"default" json:"default"`
DefaultUnpacked *openrtb_ext.ExtRegsDSA
GDPROnly bool `mapstructure:"gdpr_only" json:"gdpr_only"`
}

type IPv6 struct {
AnonKeepBits int `mapstructure:"anon_keep_bits" json:"anon_keep_bits"`
}
Expand Down
14 changes: 14 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,10 @@ func New(v *viper.Viper, bidderInfos BidderInfos, normalizeBidderName func(strin
return nil, err
}

if err := UnpackDSADefault(c.AccountDefaults.Privacy.DSA); err != nil {
return nil, fmt.Errorf("invalid default account DSA: %v", err)
}

// Update account defaults and generate base json for patch
c.AccountDefaults.CacheTTL = c.CacheURL.DefaultTTLs // comment this out to set explicitly in config

Expand Down Expand Up @@ -851,6 +855,14 @@ func (cfg *Configuration) MarshalAccountDefaults() error {
return err
}

// UnpackDSADefault validates the JSON DSA default object string by unmarshaling and maps it to a struct
func UnpackDSADefault(dsa *AccountDSA) error {
if dsa == nil || len(dsa.Default) == 0 {
return nil
}
return jsonutil.Unmarshal([]byte(dsa.Default), &dsa.DefaultUnpacked)
}

// AccountDefaultsJSON returns the precompiled JSON form of account_defaults
func (cfg *Configuration) AccountDefaultsJSON() json.RawMessage {
return cfg.accountDefaultsJSON
Expand Down Expand Up @@ -1151,6 +1163,8 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) {
v.SetDefault("account_defaults.privacy.privacysandbox.cookiedeprecation.ttl_sec", 604800)

v.SetDefault("account_defaults.events_enabled", false)
v.BindEnv("account_defaults.privacy.dsa.default")
v.BindEnv("account_defaults.privacy.dsa.gdpr_only")
v.SetDefault("account_defaults.privacy.ipv6.anon_keep_bits", 56)
v.SetDefault("account_defaults.privacy.ipv4.anon_keep_bits", 24)

Expand Down
89 changes: 89 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/prebid/go-gdpr/consentconstants"
"github.com/prebid/prebid-server/v2/openrtb_ext"
"github.com/prebid/prebid-server/v2/util/ptrutil"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -528,6 +529,9 @@ account_defaults:
anon_keep_bits: 50
ipv4:
anon_keep_bits: 20
dsa:
default: "{\"dsarequired\":3,\"pubrender\":1,\"datatopub\":2,\"transparency\":[{\"domain\":\"domain.com\",\"dsaparams\":[1]}]}"
gdpr_only: true
privacysandbox:
topicsdomain: "test.com"
cookiedeprecation:
Expand Down Expand Up @@ -662,6 +666,24 @@ func TestFullConfig(t *testing.T) {
cmpInts(t, "account_defaults.price_floors.fetch.max_age_sec", 6000, cfg.AccountDefaults.PriceFloors.Fetcher.MaxAge)
cmpInts(t, "account_defaults.price_floors.fetch.max_schema_dims", 10, cfg.AccountDefaults.PriceFloors.Fetcher.MaxSchemaDims)

// Assert the DSA was correctly unmarshalled and DefaultUnpacked was built correctly
expectedDSA := AccountDSA{
Default: "{\"dsarequired\":3,\"pubrender\":1,\"datatopub\":2,\"transparency\":[{\"domain\":\"domain.com\",\"dsaparams\":[1]}]}",
DefaultUnpacked: &openrtb_ext.ExtRegsDSA{
Required: ptrutil.ToPtr[int8](3),
PubRender: ptrutil.ToPtr[int8](1),
DataToPub: ptrutil.ToPtr[int8](2),
Transparency: []openrtb_ext.ExtBidDSATransparency{
{
Domain: "domain.com",
Params: []int{1},
},
},
},
GDPROnly: true,
}
assert.Equal(t, &expectedDSA, cfg.AccountDefaults.Privacy.DSA)

cmpBools(t, "account_defaults.events.enabled", true, cfg.AccountDefaults.Events.Enabled)

cmpInts(t, "account_defaults.privacy.ipv6.anon_keep_bits", 50, cfg.AccountDefaults.Privacy.IPv6Config.AnonKeepBits)
Expand Down Expand Up @@ -1856,3 +1878,70 @@ func TestTCF2FeatureOneVendorException(t *testing.T) {
assert.Equal(t, tt.wantIsVendorException, value, tt.description)
}
}

func TestUnpackDSADefault(t *testing.T) {
tests := []struct {
name string
giveDSA *AccountDSA
wantError bool
}{
{
name: "nil",
giveDSA: nil,
wantError: false,
},
{
name: "empty",
giveDSA: &AccountDSA{
Default: "",
},
wantError: false,
},
{
name: "empty_json",
giveDSA: &AccountDSA{
Default: "{}",
},
wantError: false,
},
{
name: "well_formed",
giveDSA: &AccountDSA{
Default: "{\"dsarequired\":3,\"pubrender\":1,\"datatopub\":2,\"transparency\":[{\"domain\":\"domain.com\",\"dsaparams\":[1]}]}",
},
wantError: false,
},
{
name: "well_formed_with_extra_fields",
giveDSA: &AccountDSA{
Default: "{\"unmappedkey\":\"unmappedvalue\",\"dsarequired\":3,\"pubrender\":1,\"datatopub\":2,\"transparency\":[{\"domain\":\"domain.com\",\"dsaparams\":[1]}]}",
},
wantError: false,
},
{
name: "invalid_type",
giveDSA: &AccountDSA{
Default: "{\"dsarequired\":\"invalid\",\"pubrender\":1,\"datatopub\":2,\"transparency\":[{\"domain\":\"domain.com\",\"dsaparams\":[1]}]}",
},
wantError: true,
},
{
name: "invalid_malformed_missing_colon",
giveDSA: &AccountDSA{
Default: "{\"dsarequired\"3,\"pubrender\":1,\"datatopub\":2,\"transparency\":[{\"domain\":\"domain.com\",\"dsaparams\":[1]}]}",
},
wantError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := UnpackDSADefault(tt.giveDSA)
if tt.wantError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
33 changes: 33 additions & 0 deletions dsa/writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dsa

import (
"github.com/prebid/prebid-server/v2/config"
"github.com/prebid/prebid-server/v2/openrtb_ext"
)

// Writer is used to write the default DSA to the request (req.regs.ext.dsa)
type Writer 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 Writer) Write(req *openrtb_ext.RequestWrapper) error {
if req == nil || getReqDSA(req) != nil {
return nil
}
if dw.Config == nil || dw.Config.DefaultUnpacked == nil {
return nil
}
if dw.Config.GDPROnly && !dw.GDPRInScope {
return nil
}
regExt, err := req.GetRegExt()
if err != nil {
return err
}
clonedDefaultUnpacked := dw.Config.DefaultUnpacked.Clone()
regExt.SetDSA(clonedDefaultUnpacked)
return nil
}
Loading

0 comments on commit b42841e

Please sign in to comment.