Skip to content

GDPR: host-level per-purpose enforce vendor signals config #1921

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

Merged
merged 2 commits into from
Jul 15, 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
13 changes: 12 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ type TCF2 struct {

// Making a purpose struct so purpose specific details can be added later.
type TCF2Purpose struct {
Enabled bool `mapstructure:"enabled"`
Enabled bool `mapstructure:"enabled"`
EnforceVendors bool `mapstructure:"enforce_vendors"`
// 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{}
Expand Down Expand Up @@ -999,6 +1000,16 @@ func SetupViper(v *viper.Viper, filename string) {
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.enforce_vendors", true)
v.SetDefault("gdpr.tcf2.purpose2.enforce_vendors", true)
v.SetDefault("gdpr.tcf2.purpose3.enforce_vendors", true)
v.SetDefault("gdpr.tcf2.purpose4.enforce_vendors", true)
v.SetDefault("gdpr.tcf2.purpose5.enforce_vendors", true)
v.SetDefault("gdpr.tcf2.purpose6.enforce_vendors", true)
v.SetDefault("gdpr.tcf2.purpose7.enforce_vendors", true)
v.SetDefault("gdpr.tcf2.purpose8.enforce_vendors", true)
v.SetDefault("gdpr.tcf2.purpose9.enforce_vendors", true)
v.SetDefault("gdpr.tcf2.purpose10.enforce_vendors", 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{})
Expand Down
97 changes: 95 additions & 2 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,81 @@ func TestDefaults(t *testing.T) {
cmpStrings(t, "stored_requests.filesystem.directorypath", "./stored_requests/data/by_id", cfg.StoredRequests.Files.Path)
cmpBools(t, "auto_gen_source_tid", cfg.AutoGenSourceTID, true)
cmpBools(t, "generate_bid_id", cfg.GenerateBidID, false)
cmpBools(t, "gdpr.tcf2.purpose_one_treatment.enabled", true, cfg.GDPR.TCF2.PurposeOneTreatment.Enabled)
cmpBools(t, "gdpr.tcf2.purpose_one_treatment.access_allowed", true, cfg.GDPR.TCF2.PurposeOneTreatment.AccessAllowed)

//Assert purpose VendorExceptionMap hash tables were built correctly
expectedTCF2 := TCF2{
Enabled: true,
Purpose1: TCF2Purpose{
Enabled: true,
EnforceVendors: true,
VendorExceptions: []openrtb_ext.BidderName{},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
},
Purpose2: TCF2Purpose{
Enabled: true,
EnforceVendors: true,
VendorExceptions: []openrtb_ext.BidderName{},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
},
Purpose3: TCF2Purpose{
Enabled: true,
EnforceVendors: true,
VendorExceptions: []openrtb_ext.BidderName{},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
},
Purpose4: TCF2Purpose{
Enabled: true,
EnforceVendors: true,
VendorExceptions: []openrtb_ext.BidderName{},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
},
Purpose5: TCF2Purpose{
Enabled: true,
EnforceVendors: true,
VendorExceptions: []openrtb_ext.BidderName{},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
},
Purpose6: TCF2Purpose{
Enabled: true,
EnforceVendors: true,
VendorExceptions: []openrtb_ext.BidderName{},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
},
Purpose7: TCF2Purpose{
Enabled: true,
EnforceVendors: true,
VendorExceptions: []openrtb_ext.BidderName{},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
},
Purpose8: TCF2Purpose{
Enabled: true,
EnforceVendors: true,
VendorExceptions: []openrtb_ext.BidderName{},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
},
Purpose9: TCF2Purpose{
Enabled: true,
EnforceVendors: true,
VendorExceptions: []openrtb_ext.BidderName{},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
},
Purpose10: TCF2Purpose{
Enabled: true,
EnforceVendors: true,
VendorExceptions: []openrtb_ext.BidderName{},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
},
SpecialPurpose1: TCF2Purpose{
Enabled: true,
VendorExceptions: []openrtb_ext.BidderName{},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
},
PurposeOneTreatment: TCF2PurposeOneTreatment{
Enabled: true,
AccessAllowed: true,
},
}
assert.Equal(t, expectedTCF2, cfg.GDPR.TCF2, "gdpr.tcf2")
}

var fullConfig = []byte(`
Expand All @@ -149,25 +222,35 @@ gdpr:
non_standard_publishers: ["siteID","fake-site-id","appID","agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA"]
tcf2:
purpose1:
enforce_vendors: false
vendor_exceptions: ["foo1a", "foo1b"]
purpose2:
enabled: false
enforce_vendors: false
vendor_exceptions: ["foo2"]
purpose3:
enforce_vendors: false
vendor_exceptions: ["foo3"]
purpose4:
enforce_vendors: false
vendor_exceptions: ["foo4"]
purpose5:
enforce_vendors: false
vendor_exceptions: ["foo5"]
purpose6:
enforce_vendors: false
vendor_exceptions: ["foo6"]
purpose7:
enforce_vendors: false
vendor_exceptions: ["foo7"]
purpose8:
enforce_vendors: false
vendor_exceptions: ["foo8"]
purpose9:
enforce_vendors: false
vendor_exceptions: ["foo9"]
purpose10:
enforce_vendors: false
vendor_exceptions: ["foo10"]
special_purpose1:
vendor_exceptions: ["fooSP1"]
Expand Down Expand Up @@ -407,51 +490,61 @@ func TestFullConfig(t *testing.T) {
Enabled: true,
Purpose1: TCF2Purpose{
Enabled: true, // true by default
EnforceVendors: false,
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,
EnforceVendors: 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
EnforceVendors: false,
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
EnforceVendors: false,
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
EnforceVendors: false,
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
EnforceVendors: false,
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
EnforceVendors: false,
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
EnforceVendors: false,
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
EnforceVendors: false,
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
EnforceVendors: false,
VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo10")},
VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo10"): {}},
},
Expand Down
36 changes: 34 additions & 2 deletions gdpr/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ func (p *permissionsImpl) checkPurpose(consent tcf2.ConsentMetadata, vendor api.
return true
}

purposeAllowed := consent.PurposeAllowed(purpose) && (weakVendorEnforcement || (vendor.Purpose(purpose) && consent.VendorConsent(vendorID)))
legitInterest := consent.PurposeLITransparency(purpose) && (weakVendorEnforcement || (vendor.LegitimateInterest(purpose) && consent.VendorLegitInterest(vendorID)))
purposeAllowed := p.consentEstablished(consent, vendor, vendorID, purpose, weakVendorEnforcement)
legitInterest := p.legitInterestEstablished(consent, vendor, vendorID, purpose, weakVendorEnforcement)
Copy link
Contributor

Choose a reason for hiding this comment

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

Functions consentEstablished and legitInterestEstablished contain the exact same logic. Do you think we can reduce, if not now in the future, to a single function?

func (p *permissionsImpl) checkIfAllowed(consent, weakVendorEnforcement, purposeEnforceVendors, vendorPurposeAndConsent bool) bool {
	if !consent { 
		return false
	}
	if weakVendorEnforcement {
		return true
	}
	if !purposeEnforceVendors {
		return true
	}
	if vendorPurposeAndConsent {
		return true
	}
	return false
}
gdpr/impl.go

Therefore:

201   func (p *permissionsImpl) checkPurpose(consent tcf2.ConsentMetadata, vendor api.Vendor, vendorID uint16, purpose consentconstants.Purpose, vendorException, weakVendorEnforcement bool) bool {
202       if purpose == tcf2ConsentConstants.InfoStorageAccess && p.cfg.TCF2.PurposeOneTreatment.Enabled && consent.PurposeOneTreatment() {
203           return p.cfg.TCF2.PurposeOneTreatment.AccessAllowed
204       }
205       if consent.CheckPubRestriction(uint8(purpose), pubRestrictNotAllowed, vendorID) {
206           return false
207       }
208  
209       if vendorException {
210           return true
211       }
212  
213 -     purposeAllowed := p.consentEstablished(consent, vendor, vendorID, purpose, weakVendorEnforcement)
    +     purposeAllowed := checkIfAllowed(
    +                           consent.PurposeAllowed(purpose),
    +                           weakVendorEnforcement,
    +                           p.purposeConfigs[purpose].EnforceVendors,
    +                           vendor.Purpose(purpose) && consent.VendorConsent(vendorID),
    +                       )
214 -     legitInterest := p.legitInterestEstablished(consent, vendor, vendorID, purpose, weakVendorEnforcement)
    +     legitInterest := checkIfAllowed(
    +                           consent.PurposeLITransparency(purpose),
    +                           weakVendorEnforcement,
    +                           p.purposeConfigs[purpose].EnforceVendors,
    +                           vendor.LegitimateInterest(purpose) && consent.VendorLegitInterest(vendorID),
    +                       )
215  
216       if consent.CheckPubRestriction(uint8(purpose), pubRestrictRequireConsent, vendorID) {
217           return purposeAllowed
218       }
219       if consent.CheckPubRestriction(uint8(purpose), pubRestrictRequireLegitInterest, vendorID) {
220           // Need LITransparency here
221           return legitInterest
222       }
223  
224       return purposeAllowed || legitInterest
225   }
gdpr/impl.go

Implement it only if you believe this could be more readable or practical. I'm good either way

Copy link
Collaborator Author

@bsardo bsardo Jul 15, 2021

Choose a reason for hiding this comment

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

I'm on the fence; this is a very good point. On the flip side, I like having the logic separate for consent established and legit interest established because it helps us verify we have sufficient test coverage (we want to make sure we exercise each of the conditions for both consent and legit interest). I’d like to punt on this decision and reevaluate your suggestion in a future PR that involves overhauling impl.go and impl_test.go. I’ll note it in that refactor story so I don’t forget.


if consent.CheckPubRestriction(uint8(purpose), pubRestrictRequireConsent, vendorID) {
return purposeAllowed
Expand All @@ -224,6 +224,38 @@ func (p *permissionsImpl) checkPurpose(consent tcf2.ConsentMetadata, vendor api.
return purposeAllowed || legitInterest
}

func (p *permissionsImpl) consentEstablished(consent tcf2.ConsentMetadata, vendor api.Vendor, vendorID uint16, purpose consentconstants.Purpose, weakVendorEnforcement bool) bool {
if !consent.PurposeAllowed(purpose) {
return false
}
if weakVendorEnforcement {
return true
}
if !p.purposeConfigs[purpose].EnforceVendors {
return true
}
if vendor.Purpose(purpose) && consent.VendorConsent(vendorID) {
return true
}
return false
}

func (p *permissionsImpl) legitInterestEstablished(consent tcf2.ConsentMetadata, vendor api.Vendor, vendorID uint16, purpose consentconstants.Purpose, weakVendorEnforcement bool) bool {
if !consent.PurposeLITransparency(purpose) {
return false
}
if weakVendorEnforcement {
return true
}
if !p.purposeConfigs[purpose].EnforceVendors {
return true
}
if vendor.LegitimateInterest(purpose) && consent.VendorLegitInterest(vendorID) {
return true
}
return false
}

func (p *permissionsImpl) parseVendor(ctx context.Context, vendorID uint16, consent string) (parsedConsent api.VendorConsents, vendor api.Vendor, err error) {
parsedConsent, err = vendorconsent.ParseString(consent)
if err != nil {
Expand Down
Loading