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

GDPR: host-level per-purpose vendor exceptions config #1893

Merged
merged 7 commits into from
Jul 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -951,9 +985,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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought: Can we maybe club the vendor ID, bidder name and vendor exception in a single object so that we don't need to pass these as separate arguments to all these methods? We can call it something like BidderPrivacyInfo or BidderGDPRInfo or BidderTCFInfo. Thoughts?

Don't have to be a part of this PR though.. can be a follow up

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I have this lined up to address when I refactor this file to allow for basic enforcement.

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