Skip to content

Commit

Permalink
GDPR: host-level per-purpose vendor exceptions config (#1893)
Browse files Browse the repository at this point in the history
Co-authored-by: Scott Kay <noreply@syntaxnode.com>
  • Loading branch information
bsardo and SyntaxNode authored Jul 13, 2021
1 parent e87bec4 commit aff5f70
Show file tree
Hide file tree
Showing 5 changed files with 361 additions and 31 deletions.
67 changes: 59 additions & 8 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,20 +240,30 @@ func (t *GDPRTimeouts) ActiveTimeout() time.Duration {

// TCF2 defines the TCF2 specific configurations for GDPR
type TCF2 struct {
Enabled bool `mapstructure:"enabled"`
Purpose1 PurposeDetail `mapstructure:"purpose1"`
Purpose2 PurposeDetail `mapstructure:"purpose2"`
Purpose7 PurposeDetail `mapstructure:"purpose7"`
SpecialPurpose1 PurposeDetail `mapstructure:"special_purpose1"`
PurposeOneTreatment PurposeOneTreatment `mapstructure:"purpose_one_treatment"`
Enabled bool `mapstructure:"enabled"`
Purpose1 TCF2Purpose `mapstructure:"purpose1"`
Purpose2 TCF2Purpose `mapstructure:"purpose2"`
Purpose3 TCF2Purpose `mapstructure:"purpose3"`
Purpose4 TCF2Purpose `mapstructure:"purpose4"`
Purpose5 TCF2Purpose `mapstructure:"purpose5"`
Purpose6 TCF2Purpose `mapstructure:"purpose6"`
Purpose7 TCF2Purpose `mapstructure:"purpose7"`
Purpose8 TCF2Purpose `mapstructure:"purpose8"`
Purpose9 TCF2Purpose `mapstructure:"purpose9"`
Purpose10 TCF2Purpose `mapstructure:"purpose10"`
SpecialPurpose1 TCF2Purpose `mapstructure:"special_purpose1"`
PurposeOneTreatment TCF2PurposeOneTreatment `mapstructure:"purpose_one_treatment"`
}

// Making a purpose struct so purpose specific details can be added later.
type PurposeDetail struct {
type TCF2Purpose struct {
Enabled bool `mapstructure:"enabled"`
// Array of vendor exceptions that is used to create the hash table VendorExceptionMap so vendor names can be instantly accessed
VendorExceptions []openrtb_ext.BidderName `mapstructure:"vendor_exceptions"`
VendorExceptionMap map[openrtb_ext.BidderName]struct{}
}

type PurposeOneTreatment struct {
type TCF2PurposeOneTreatment struct {
Enabled bool `mapstructure:"enabled"`
AccessAllowed bool `mapstructure:"access_allowed"`
}
Expand Down Expand Up @@ -503,6 +513,30 @@ func New(v *viper.Viper) (*Configuration, error) {
c.GDPR.NonStandardPublisherMap[c.GDPR.EEACountries[i]] = s
}

// To look for a purpose's vendor exceptions in O(1) time, for each purpose we fill this hash table located in the
// VendorExceptions field of the GDPR.TCF2.PurposeX struct defined in this file
purposeConfigs := []*TCF2Purpose{
&c.GDPR.TCF2.Purpose1,
&c.GDPR.TCF2.Purpose2,
&c.GDPR.TCF2.Purpose3,
&c.GDPR.TCF2.Purpose4,
&c.GDPR.TCF2.Purpose5,
&c.GDPR.TCF2.Purpose6,
&c.GDPR.TCF2.Purpose7,
&c.GDPR.TCF2.Purpose8,
&c.GDPR.TCF2.Purpose9,
&c.GDPR.TCF2.Purpose10,
&c.GDPR.TCF2.SpecialPurpose1,
}
for c := 0; c < len(purposeConfigs); c++ {
purposeConfigs[c].VendorExceptionMap = make(map[openrtb_ext.BidderName]struct{})

for v := 0; v < len(purposeConfigs[c].VendorExceptions); v++ {
bidderName := purposeConfigs[c].VendorExceptions[v]
purposeConfigs[c].VendorExceptionMap[bidderName] = struct{}{}
}
}

// To look for a request's app_id in O(1) time, we fill this hash table located in the
// the BlacklistedApps field of the Configuration struct defined in this file
c.BlacklistedAppMap = make(map[string]bool)
Expand Down Expand Up @@ -957,9 +991,26 @@ func SetupViper(v *viper.Viper, filename string) {
v.SetDefault("gdpr.tcf2.enabled", true)
v.SetDefault("gdpr.tcf2.purpose1.enabled", true)
v.SetDefault("gdpr.tcf2.purpose2.enabled", true)
v.SetDefault("gdpr.tcf2.purpose3.enabled", true)
v.SetDefault("gdpr.tcf2.purpose4.enabled", true)
v.SetDefault("gdpr.tcf2.purpose5.enabled", true)
v.SetDefault("gdpr.tcf2.purpose6.enabled", true)
v.SetDefault("gdpr.tcf2.purpose7.enabled", true)
v.SetDefault("gdpr.tcf2.purpose8.enabled", true)
v.SetDefault("gdpr.tcf2.purpose9.enabled", true)
v.SetDefault("gdpr.tcf2.purpose10.enabled", true)
v.SetDefault("gdpr.tcf2.purpose1.vendor_exceptions", []openrtb_ext.BidderName{})
v.SetDefault("gdpr.tcf2.purpose2.vendor_exceptions", []openrtb_ext.BidderName{})
v.SetDefault("gdpr.tcf2.purpose3.vendor_exceptions", []openrtb_ext.BidderName{})
v.SetDefault("gdpr.tcf2.purpose4.vendor_exceptions", []openrtb_ext.BidderName{})
v.SetDefault("gdpr.tcf2.purpose5.vendor_exceptions", []openrtb_ext.BidderName{})
v.SetDefault("gdpr.tcf2.purpose6.vendor_exceptions", []openrtb_ext.BidderName{})
v.SetDefault("gdpr.tcf2.purpose7.vendor_exceptions", []openrtb_ext.BidderName{})
v.SetDefault("gdpr.tcf2.purpose8.vendor_exceptions", []openrtb_ext.BidderName{})
v.SetDefault("gdpr.tcf2.purpose9.vendor_exceptions", []openrtb_ext.BidderName{})
v.SetDefault("gdpr.tcf2.purpose10.vendor_exceptions", []openrtb_ext.BidderName{})
v.SetDefault("gdpr.tcf2.special_purpose1.enabled", true)
v.SetDefault("gdpr.tcf2.special_purpose1.vendor_exceptions", []openrtb_ext.BidderName{})
v.SetDefault("gdpr.amp_exception", false)
v.SetDefault("gdpr.eea_countries", []string{"ALA", "AUT", "BEL", "BGR", "HRV", "CYP", "CZE", "DNK", "EST",
"FIN", "FRA", "GUF", "DEU", "GIB", "GRC", "GLP", "GGY", "HUN", "ISL", "IRL", "IMN", "ITA", "JEY", "LVA",
Expand Down
89 changes: 89 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,30 @@ gdpr:
host_vendor_id: 15
default_value: "1"
non_standard_publishers: ["siteID","fake-site-id","appID","agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA"]
tcf2:
purpose1:
vendor_exceptions: ["foo1a", "foo1b"]
purpose2:
enabled: false
vendor_exceptions: ["foo2"]
purpose3:
vendor_exceptions: ["foo3"]
purpose4:
vendor_exceptions: ["foo4"]
purpose5:
vendor_exceptions: ["foo5"]
purpose6:
vendor_exceptions: ["foo6"]
purpose7:
vendor_exceptions: ["foo7"]
purpose8:
vendor_exceptions: ["foo8"]
purpose9:
vendor_exceptions: ["foo9"]
purpose10:
vendor_exceptions: ["foo10"]
special_purpose1:
vendor_exceptions: ["fooSP1"]
ccpa:
enforce: true
lmt:
Expand Down Expand Up @@ -378,6 +402,71 @@ func TestFullConfig(t *testing.T) {
cmpBools(t, "cfg.BlacklistedAppMap", cfg.BlacklistedAppMap[cfg.BlacklistedApps[i]], true)
}

//Assert purpose VendorExceptionMap hash tables were built correctly
expectedTCF2 := TCF2{
Enabled: true,
Purpose1: TCF2Purpose{
Enabled: true, // true by default
VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo1a"), openrtb_ext.BidderName("foo1b")},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo1a"): {}, openrtb_ext.BidderName("foo1b"): {}},
},
Purpose2: TCF2Purpose{
Enabled: false,
VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo2")},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo2"): {}},
},
Purpose3: TCF2Purpose{
Enabled: true, // true by default
VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo3")},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo3"): {}},
},
Purpose4: TCF2Purpose{
Enabled: true, // true by default
VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo4")},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo4"): {}},
},
Purpose5: TCF2Purpose{
Enabled: true, // true by default
VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo5")},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo5"): {}},
},
Purpose6: TCF2Purpose{
Enabled: true, // true by default
VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo6")},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo6"): {}},
},
Purpose7: TCF2Purpose{
Enabled: true, // true by default
VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo7")},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo7"): {}},
},
Purpose8: TCF2Purpose{
Enabled: true, // true by default
VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo8")},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo8"): {}},
},
Purpose9: TCF2Purpose{
Enabled: true, // true by default
VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo9")},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo9"): {}},
},
Purpose10: TCF2Purpose{
Enabled: true, // true by default
VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo10")},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo10"): {}},
},
SpecialPurpose1: TCF2Purpose{
Enabled: true, // true by default
VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("fooSP1")},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("fooSP1"): {}},
},
PurposeOneTreatment: TCF2PurposeOneTreatment{
Enabled: true, // true by default
AccessAllowed: true, // true by default
},
}
assert.Equal(t, expectedTCF2, cfg.GDPR.TCF2, "gdpr.tcf2")

cmpStrings(t, "currency_converter.fetch_url", cfg.CurrencyConverter.FetchURL, "https://currency.prebid.org")
cmpInts(t, "currency_converter.fetch_interval_seconds", cfg.CurrencyConverter.FetchIntervalSeconds, 1800)
cmpStrings(t, "recaptcha_secret", cfg.RecaptchaSecret, "asdfasdfasdfasdf")
Expand Down
15 changes: 15 additions & 0 deletions gdpr/gdpr.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"
"strconv"

"github.com/prebid/go-gdpr/consentconstants"
"github.com/prebid/go-gdpr/vendorlist"
"github.com/prebid/prebid-server/config"
"github.com/prebid/prebid-server/errortypes"
Expand Down Expand Up @@ -44,9 +45,23 @@ func NewPermissions(ctx context.Context, cfg config.GDPR, vendorIDs map[openrtb_
gdprDefaultValue = SignalNo
}

purposeConfigs := map[consentconstants.Purpose]config.TCF2Purpose{
1: cfg.TCF2.Purpose1,
2: cfg.TCF2.Purpose2,
3: cfg.TCF2.Purpose3,
4: cfg.TCF2.Purpose4,
5: cfg.TCF2.Purpose5,
6: cfg.TCF2.Purpose6,
7: cfg.TCF2.Purpose7,
8: cfg.TCF2.Purpose8,
9: cfg.TCF2.Purpose9,
10: cfg.TCF2.Purpose10,
}

permissionsImpl := &permissionsImpl{
cfg: cfg,
gdprDefaultValue: gdprDefaultValue,
purposeConfigs: purposeConfigs,
vendorIDs: vendorIDs,
fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
tcf2SpecVersion: newVendorListFetcher(ctx, cfg, client, vendorListURLMaker)},
Expand Down
45 changes: 34 additions & 11 deletions gdpr/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
type permissionsImpl struct {
cfg config.GDPR
gdprDefaultValue Signal
purposeConfigs map[consentconstants.Purpose]config.TCF2Purpose
vendorIDs map[openrtb_ext.BidderName]uint16
fetchVendorList map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error)
}
Expand All @@ -41,7 +42,7 @@ func (p *permissionsImpl) HostCookiesAllowed(ctx context.Context, gdprSignal Sig
return true, nil
}

return p.allowSync(ctx, uint16(p.cfg.HostVendorID), consent)
return p.allowSync(ctx, uint16(p.cfg.HostVendorID), consent, false)
}

func (p *permissionsImpl) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName, gdprSignal Signal, consent string) (bool, error) {
Expand All @@ -53,7 +54,8 @@ func (p *permissionsImpl) BidderSyncAllowed(ctx context.Context, bidder openrtb_

id, ok := p.vendorIDs[bidder]
if ok {
return p.allowSync(ctx, id, consent)
vendorException := p.isVendorException(consentconstants.Purpose(1), bidder)
return p.allowSync(ctx, id, consent, vendorException)
}

return false, nil
Expand All @@ -80,9 +82,9 @@ func (p *permissionsImpl) AuctionActivitiesAllowed(ctx context.Context,
}

if id, ok := p.vendorIDs[bidder]; ok {
return p.allowActivities(ctx, id, consent, weakVendorEnforcement)
return p.allowActivities(ctx, id, bidder, consent, weakVendorEnforcement)
} else if weakVendorEnforcement {
return p.allowActivities(ctx, 0, consent, weakVendorEnforcement)
return p.allowActivities(ctx, 0, bidder, consent, weakVendorEnforcement)
}

return p.defaultVendorPermissions()
Expand All @@ -104,7 +106,7 @@ func (p *permissionsImpl) normalizeGDPR(gdprSignal Signal) Signal {
return SignalYes
}

func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, consent string) (bool, error) {
func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, consent string, vendorException bool) (bool, error) {

if consent == "" {
return false, nil
Expand All @@ -127,10 +129,10 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, consen
err := fmt.Errorf("Unable to access TCF2 parsed consent")
return false, err
}
return p.checkPurpose(consentMeta, vendor, vendorID, tcf2ConsentConstants.InfoStorageAccess, false), nil
return p.checkPurpose(consentMeta, vendor, vendorID, tcf2ConsentConstants.InfoStorageAccess, vendorException, false), nil
}

func (p *permissionsImpl) allowActivities(ctx context.Context, vendorID uint16, consent string, weakVendorEnforcement bool) (allowBidRequest bool, passGeo bool, passID bool, err error) {
func (p *permissionsImpl) allowActivities(ctx context.Context, vendorID uint16, bidder openrtb_ext.BidderName, consent string, weakVendorEnforcement bool) (allowBidRequest bool, passGeo bool, passID bool, err error) {
parsedConsent, vendor, err := p.parseVendor(ctx, vendorID, consent)
if err != nil {
return false, false, false, err
Expand All @@ -156,17 +158,20 @@ func (p *permissionsImpl) allowActivities(ctx context.Context, vendorID uint16,
}

if p.cfg.TCF2.SpecialPurpose1.Enabled {
passGeo = consentMeta.SpecialFeatureOptIn(1) && (vendor.SpecialPurpose(1) || weakVendorEnforcement)
vendorException := p.isSpecialPurposeVendorException(bidder)
passGeo = vendorException || (consentMeta.SpecialFeatureOptIn(1) && (vendor.SpecialPurpose(1) || weakVendorEnforcement))
} else {
passGeo = true
}
if p.cfg.TCF2.Purpose2.Enabled {
allowBidRequest = p.checkPurpose(consentMeta, vendor, vendorID, consentconstants.Purpose(2), weakVendorEnforcement)
vendorException := p.isVendorException(consentconstants.Purpose(2), bidder)
allowBidRequest = p.checkPurpose(consentMeta, vendor, vendorID, consentconstants.Purpose(2), vendorException, weakVendorEnforcement)
} else {
allowBidRequest = true
}
for i := 2; i <= 10; i++ {
if p.checkPurpose(consentMeta, vendor, vendorID, consentconstants.Purpose(i), weakVendorEnforcement) {
vendorException := p.isVendorException(consentconstants.Purpose(i), bidder)
if p.checkPurpose(consentMeta, vendor, vendorID, consentconstants.Purpose(i), vendorException, weakVendorEnforcement) {
passID = true
break
}
Expand All @@ -175,18 +180,36 @@ func (p *permissionsImpl) allowActivities(ctx context.Context, vendorID uint16,
return
}

func (p *permissionsImpl) isVendorException(purpose consentconstants.Purpose, bidder openrtb_ext.BidderName) (vendorException bool) {
if _, ok := p.purposeConfigs[purpose].VendorExceptionMap[bidder]; ok {
vendorException = true
}
return
}

func (p *permissionsImpl) isSpecialPurposeVendorException(bidder openrtb_ext.BidderName) (vendorException bool) {
if _, ok := p.cfg.TCF2.SpecialPurpose1.VendorExceptionMap[bidder]; ok {
vendorException = true
}
return
}

const pubRestrictNotAllowed = 0
const pubRestrictRequireConsent = 1
const pubRestrictRequireLegitInterest = 2

func (p *permissionsImpl) checkPurpose(consent tcf2.ConsentMetadata, vendor api.Vendor, vendorID uint16, purpose consentconstants.Purpose, weakVendorEnforcement bool) bool {
func (p *permissionsImpl) checkPurpose(consent tcf2.ConsentMetadata, vendor api.Vendor, vendorID uint16, purpose consentconstants.Purpose, vendorException, weakVendorEnforcement bool) bool {
if purpose == tcf2ConsentConstants.InfoStorageAccess && p.cfg.TCF2.PurposeOneTreatment.Enabled && consent.PurposeOneTreatment() {
return p.cfg.TCF2.PurposeOneTreatment.AccessAllowed
}
if consent.CheckPubRestriction(uint8(purpose), pubRestrictNotAllowed, vendorID) {
return false
}

if vendorException {
return true
}

purposeAllowed := consent.PurposeAllowed(purpose) && (weakVendorEnforcement || (vendor.Purpose(purpose) && consent.VendorConsent(vendorID)))
legitInterest := consent.PurposeLITransparency(purpose) && (weakVendorEnforcement || (vendor.LegitimateInterest(purpose) && consent.VendorLegitInterest(vendorID)))

Expand Down
Loading

0 comments on commit aff5f70

Please sign in to comment.