From 55f6ded2b3bd3160f0ebf49cc1a65382a3bd49e8 Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Wed, 3 Jun 2020 14:33:07 -0400 Subject: [PATCH] Enable full TCF2 support (#1302) * New config options * Enble TCF2 fields and logic * Resolves some PR comments * More tests * gofmt * Added enforcement tests for split GDPR/GDPRGeo * Testing tweaks * No longer ignore enforce purpose 1 on allowSync() * Removes Purpose 4 --- config/config.go | 29 +++ endpoints/auction_test.go | 5 +- endpoints/cookie_sync_test.go | 4 +- endpoints/setuid_test.go | 4 +- exchange/utils.go | 4 +- exchange/utils_test.go | 6 +- gdpr/gdpr.go | 2 +- gdpr/impl.go | 96 ++++++-- gdpr/impl_test.go | 390 ++++++++++++++++++++++++++++++- gdpr/vendorlist-fetching_test.go | 86 ++++++- go.mod | 3 +- go.sum | 6 +- privacy/enforcement.go | 19 +- privacy/enforcement_test.go | 108 ++++++--- 14 files changed, 677 insertions(+), 85 deletions(-) diff --git a/config/config.go b/config/config.go index 79a31db154a..5f19629d2db 100755 --- a/config/config.go +++ b/config/config.go @@ -142,6 +142,7 @@ type GDPR struct { Timeouts GDPRTimeouts `mapstructure:"timeouts_ms"` NonStandardPublishers []string `mapstructure:"non_standard_publishers,flow"` NonStandardPublisherMap map[string]int + TCF2 TCF2 `mapstructure:"tcf2"` AMPException bool `mapstructure:"amp_exception"` } @@ -165,6 +166,26 @@ func (t *GDPRTimeouts) ActiveTimeout() time.Duration { return time.Duration(t.ActiveVendorlistFetch) * time.Millisecond } +// 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 PurposeOneTreatement `mapstructure:"purpose_one_treatement"` +} + +// Making a purpose struct so purpose specific details can be added later. +type PurposeDetail struct { + Enabled bool `mapstructure:"enabled"` +} + +type PurposeOneTreatement struct { + Enabled bool `mapstructure:"enabled"` + AccessAllowed bool `mapstructure:"access_allowed"` +} + type CCPA struct { Enforce bool `mapstructure:"enforce"` } @@ -774,6 +795,14 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("gdpr.timeouts_ms.init_vendorlist_fetches", 0) v.SetDefault("gdpr.timeouts_ms.active_vendorlist_fetch", 0) v.SetDefault("gdpr.non_standard_publishers", []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.purpose4.enabled", true) + v.SetDefault("gdpr.tcf2.purpose7.enabled", true) + v.SetDefault("gdpr.tcf2.special_purpose1.enabled", true) + v.SetDefault("gdpr.tcf2.purpose_one_treatement.enabled", true) + v.SetDefault("gdpr.tcf2.purpose_one_treatement.access_allowed", true) v.SetDefault("gdpr.amp_exception", false) v.SetDefault("ccpa.enforce", false) v.SetDefault("currency_converter.fetch_url", "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json") diff --git a/endpoints/auction_test.go b/endpoints/auction_test.go index 3035a6d45fb..5e9e9639a9c 100644 --- a/endpoints/auction_test.go +++ b/endpoints/auction_test.go @@ -407,6 +407,7 @@ type auctionMockPermissions struct { allowBidderSync bool allowHostCookies bool allowPI bool + allowGeo bool } func (m *auctionMockPermissions) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) { @@ -417,8 +418,8 @@ func (m *auctionMockPermissions) BidderSyncAllowed(ctx context.Context, bidder o return m.allowBidderSync, nil } -func (m *auctionMockPermissions) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) { - return m.allowPI, nil +func (m *auctionMockPermissions) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) { + return m.allowPI, m.allowGeo, nil } func (m *auctionMockPermissions) AMPException() bool { diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index bb766aa92e7..824e32f1957 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -254,8 +254,8 @@ func (g *gdprPerms) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.Bi return ok, nil } -func (g *gdprPerms) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) { - return true, nil +func (g *gdprPerms) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) { + return true, true, nil } func (g *gdprPerms) AMPException() bool { diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go index 8499ac1ca5d..3f47b257d2e 100644 --- a/endpoints/setuid_test.go +++ b/endpoints/setuid_test.go @@ -437,8 +437,8 @@ func (g *mockPermsSetUID) BidderSyncAllowed(ctx context.Context, bidder openrtb_ return false, nil } -func (g *mockPermsSetUID) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) { - return g.allowPI, nil +func (g *mockPermsSetUID) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) { + return g.allowPI, g.allowPI, nil } func (g *mockPermsSetUID) AMPException() bool { diff --git a/exchange/utils.go b/exchange/utils.go index d961089c4cb..f602d1e8fba 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -60,10 +60,12 @@ func cleanOpenRTBRequests(ctx context.Context, coreBidder := resolveBidder(bidder.String(), aliases) var publisherID = labels.PubID - ok, err := gDPR.PersonalInfoAllowed(ctx, coreBidder, publisherID, consent) + ok, geo, err := gDPR.PersonalInfoAllowed(ctx, coreBidder, publisherID, consent) privacyEnforcement.GDPR = !ok && err == nil + privacyEnforcement.GDPRGeo = !geo && err == nil } else { privacyEnforcement.GDPR = false + privacyEnforcement.GDPRGeo = false } privacyEnforcement.Apply(bidReq, ampGDPRException) diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 53d6b85c243..acbf25ff691 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -24,11 +24,11 @@ func (p *permissionsMock) BidderSyncAllowed(ctx context.Context, bidder openrtb_ return true, nil } -func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) { +func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) { if bidder == "appnexus" { - return true, nil + return true, true, nil } - return false, nil + return false, false, nil } func (p *permissionsMock) AMPException() bool { diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go index 9390d942f80..0dfa12f5ebd 100644 --- a/gdpr/gdpr.go +++ b/gdpr/gdpr.go @@ -23,7 +23,7 @@ type Permissions interface { // Determines whether or not to send PI information to a bidder, or mask it out. // // If the consent string was nonsensical, the returned error will be an ErrorMalformedConsent. - PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) + PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) // Exposes the AMP execption flag AMPException() bool diff --git a/gdpr/impl.go b/gdpr/impl.go index 8743d7f2778..60db804aec6 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -2,11 +2,13 @@ package gdpr import ( "context" + "fmt" "github.com/prebid/go-gdpr/api" tcf1constants "github.com/prebid/go-gdpr/consentconstants" consentconstants "github.com/prebid/go-gdpr/consentconstants/tcf2" "github.com/prebid/go-gdpr/vendorconsent" + tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2" "github.com/prebid/go-gdpr/vendorlist" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/openrtb_ext" @@ -40,10 +42,10 @@ func (p *permissionsImpl) BidderSyncAllowed(ctx context.Context, bidder openrtb_ return false, nil } -func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) { +func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) { _, ok := p.cfg.NonStandardPublisherMap[PublisherID] if ok { - return true, nil + return true, true, nil } id, ok := p.vendorIDs[bidder] @@ -52,10 +54,10 @@ func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context, bidder openrt } if consent == "" { - return p.cfg.UsersyncIfAmbiguous, nil + return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil } - return false, nil + return false, false, nil } func (p *permissionsImpl) AMPException() bool { @@ -78,38 +80,104 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, consen } // InfoStorageAccess is the same across TCF 1 and TCF 2 + if parsedConsent.Version() == 2 { + if !p.cfg.TCF2.Purpose1.Enabled { + // We are not enforcing purpose 1 + return true, nil + } + consent, ok := parsedConsent.(tcf2.ConsentMetadata) + if !ok { + err := fmt.Errorf("Unable to access TCF2 parsed consent") + return false, err + } + return p.checkPurpose(consent, vendor, vendorID, consentconstants.InfoStorageAccess), nil + } if vendor.Purpose(consentconstants.InfoStorageAccess) && parsedConsent.PurposeAllowed(consentconstants.InfoStorageAccess) && parsedConsent.VendorConsent(vendorID) { return true, nil } return false, nil } -func (p *permissionsImpl) allowPI(ctx context.Context, vendorID uint16, consent string) (bool, error) { +func (p *permissionsImpl) allowPI(ctx context.Context, vendorID uint16, consent string) (bool, bool, error) { // If we're not given a consent string, respect the preferences in the app config. if consent == "" { - return p.cfg.UsersyncIfAmbiguous, nil + return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil } parsedConsent, vendor, err := p.parseVendor(ctx, vendorID, consent) if err != nil { - return false, err + return false, false, err } if vendor == nil { - return false, nil + return false, false, nil } if parsedConsent.Version() == 2 { - // Need to add the location special purpose once the library supports it. + if p.cfg.TCF2.Enabled { + return p.allowPITCF2(parsedConsent, vendor, vendorID) + } if (vendor.Purpose(consentconstants.InfoStorageAccess) || vendor.LegitimateInterest(consentconstants.InfoStorageAccess)) && parsedConsent.PurposeAllowed(consentconstants.InfoStorageAccess) && (vendor.Purpose(consentconstants.PersonalizationProfile) || vendor.LegitimateInterest(consentconstants.PersonalizationProfile)) && parsedConsent.PurposeAllowed(consentconstants.PersonalizationProfile) && parsedConsent.VendorConsent(vendorID) { - return true, nil + return true, true, nil } } else { if (vendor.Purpose(tcf1constants.InfoStorageAccess) || vendor.LegitimateInterest(tcf1constants.InfoStorageAccess)) && parsedConsent.PurposeAllowed(tcf1constants.InfoStorageAccess) && (vendor.Purpose(tcf1constants.AdSelectionDeliveryReporting) || vendor.LegitimateInterest(tcf1constants.AdSelectionDeliveryReporting)) && parsedConsent.PurposeAllowed(tcf1constants.AdSelectionDeliveryReporting) && parsedConsent.VendorConsent(vendorID) { - return true, nil + return true, true, nil } } - return false, nil + return false, false, nil +} + +func (p *permissionsImpl) allowPITCF2(parsedConsent api.VendorConsents, vendor api.Vendor, vendorID uint16) (allowPI bool, allowGeo bool, err error) { + consent, ok := parsedConsent.(tcf2.ConsentMetadata) + err = nil + allowPI = false + allowGeo = false + if !ok { + err = fmt.Errorf("Unable to access TCF2 parsed consent") + return + } + if p.cfg.TCF2.SpecialPurpose1.Enabled { + allowGeo = consent.SpecialFeatureOptIn(1) && vendor.SpecialPurpose(1) + } else { + allowGeo = true + } + // Set to true so any purpose check can flip it to false + allowPI = true + if p.cfg.TCF2.Purpose1.Enabled { + allowPI = allowPI && p.checkPurpose(consent, vendor, vendorID, consentconstants.InfoStorageAccess) + } + if p.cfg.TCF2.Purpose2.Enabled { + allowPI = allowPI && p.checkPurpose(consent, vendor, vendorID, consentconstants.BasicAdserving) + } + if p.cfg.TCF2.Purpose7.Enabled { + allowPI = allowPI && p.checkPurpose(consent, vendor, vendorID, consentconstants.AdPerformance) + } + return +} + +const pubRestrictNotAllowed = 0 +const pubRestrictRequireConsent = 1 +const pubRestrictRequireLegitInterest = 2 + +func (p *permissionsImpl) checkPurpose(consent tcf2.ConsentMetadata, vendor api.Vendor, vendorID uint16, purpose tcf1constants.Purpose) bool { + if purpose == consentconstants.InfoStorageAccess && p.cfg.TCF2.PurposeOneTreatment.Enabled && consent.PurposeOneTreatment() { + return p.cfg.TCF2.PurposeOneTreatment.AccessAllowed + } + if consent.CheckPubRestriction(uint8(purpose), pubRestrictNotAllowed, vendorID) { + return false + } + if consent.CheckPubRestriction(uint8(purpose), pubRestrictRequireConsent, vendorID) { + return vendor.PurposeStrict(purpose) && consent.PurposeAllowed(purpose) && consent.VendorConsent(vendorID) + } + if consent.CheckPubRestriction(uint8(purpose), pubRestrictRequireLegitInterest, vendorID) { + // Need LITransparency here + return vendor.LegitimateInterestStrict(purpose) && consent.PurposeLITransparency(purpose) && consent.VendorLegitInterest(vendorID) + } + purposeAllowed := vendor.Purpose(purpose) && consent.PurposeAllowed(purpose) && consent.VendorConsent(vendorID) + legitInterest := vendor.LegitimateInterest(purpose) && consent.PurposeLITransparency(purpose) && consent.VendorLegitInterest(vendorID) + + return purposeAllowed || legitInterest } func (p *permissionsImpl) parseVendor(ctx context.Context, vendorID uint16, consent string) (parsedConsent api.VendorConsents, vendor api.Vendor, err error) { @@ -146,8 +214,8 @@ func (a AlwaysAllow) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.B return true, nil } -func (a AlwaysAllow) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) { - return true, nil +func (a AlwaysAllow) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) { + return true, true, nil } func (a AlwaysAllow) AMPException() bool { diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index 8b89577d6c8..f05f25e87ea 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -10,6 +10,9 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/go-gdpr/vendorlist" + "github.com/prebid/go-gdpr/vendorlist2" + + "github.com/stretchr/testify/assert" ) func TestNoConsentButAllowByDefault(t *testing.T) { @@ -55,10 +58,10 @@ func TestNoConsentAndRejectByDefault(t *testing.T) { func TestAllowedSyncs(t *testing.T) { vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ 2: { - purposes: []uint8{1}, + purposes: []int{1}, }, 3: { - purposes: []uint8{1}, + purposes: []int{1}, }, }) perms := permissionsImpl{ @@ -91,10 +94,10 @@ func TestAllowedSyncs(t *testing.T) { func TestProhibitedPurposes(t *testing.T) { vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ 2: { - purposes: []uint8{1}, // cookie reads/writes + purposes: []int{1}, // cookie reads/writes }, 3: { - purposes: []uint8{3}, // ad personalization + purposes: []int{3}, // ad personalization }, }) perms := permissionsImpl{ @@ -127,10 +130,10 @@ func TestProhibitedPurposes(t *testing.T) { func TestProhibitedVendors(t *testing.T) { vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ 2: { - purposes: []uint8{1}, // cookie reads/writes + purposes: []int{1}, // cookie reads/writes }, 3: { - purposes: []uint8{3}, // ad personalization + purposes: []int{3}, // ad personalization }, }) perms := permissionsImpl{ @@ -179,10 +182,10 @@ func TestMalformedConsent(t *testing.T) { func TestAllowPersonalInfo(t *testing.T) { vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ 2: { - purposes: []uint8{1}, // cookie reads/writes + purposes: []int{1}, // cookie reads/writes }, 3: { - purposes: []uint8{1, 3}, // ad personalization + purposes: []int{1, 3}, // ad personalization }, }) perms := permissionsImpl{ @@ -204,21 +207,377 @@ func TestAllowPersonalInfo(t *testing.T) { } // PI needs both purposes to succeed - allowPI, err := perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") + allowPI, _, err := perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") assertNilErr(t, err) assertBoolsEqual(t, false, allowPI) - allowPI, err = perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderPubmatic, "", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") + allowPI, _, err = perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderPubmatic, "", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") assertNilErr(t, err) assertBoolsEqual(t, true, allowPI) // Assert that an item that otherwise would not be allowed PI access, gets approved because it is found in the GDPR.NonStandardPublishers array perms.cfg.NonStandardPublisherMap = map[string]int{"appNexusAppID": 1} - allowPI, err = perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") + allowPI, _, err = perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") assertNilErr(t, err) assertBoolsEqual(t, true, allowPI) } +var tcf2BasicPurposes = map[uint16]*purposes{ + 2: {purposes: []int{1}}, //cookie reads/writes + 6: {purposes: []int{1, 2, 4}}, // ad personalization + 8: {purposes: []int{1, 7}}, + 10: {purposes: []int{2, 4, 7}}, + 32: {purposes: []int{1, 2, 4, 7}}, +} +var tcf2LegitInterests = map[uint16]*purposes{ + 6: {purposes: []int{7}}, + 8: {purposes: []int{2, 4}}, +} +var tcf2SpecialPuproses = map[uint16]*purposes{ + 6: {purposes: []int{1}}, + 10: {purposes: []int{1}}, +} +var tcf2FlexPurposes = map[uint16]*purposes{ + 6: {purposes: []int{1, 2, 4, 7}}, +} +var tcf2Config = config.GDPR{ + HostVendorID: 2, + TCF2: config.TCF2{ + Enabled: true, + Purpose1: config.PurposeDetail{Enabled: true}, + Purpose2: config.PurposeDetail{Enabled: true}, + Purpose7: config.PurposeDetail{Enabled: true}, + SpecialPurpose1: config.PurposeDetail{Enabled: true}, + }, +} + +type tcf2TestDef struct { + description string + bidder openrtb_ext.BidderName + consent string + allowPI bool + allowGeo bool +} + +func TestAllowPersonalInfoTCF2(t *testing.T) { + vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 6, + openrtb_ext.BidderRubicon: 8, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 34: parseVendorListDataV2(t, vendorListData), + }), + }, + } + + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consensts to purposes and vendors 2, 6, 8 + // PI needs all purposes to succeed + testDefs := []tcf2TestDef{ + { + description: "Appnexus vendor test, insufficient purposes claimed", + bidder: openrtb_ext.BidderAppnexus, + consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", + allowPI: false, + allowGeo: false, + }, + { + description: "Pubmatic vendor test, flex purposes claimed", + bidder: openrtb_ext.BidderPubmatic, + consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", + allowPI: true, + allowGeo: true, + }, + { + description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", + bidder: openrtb_ext.BidderRubicon, + consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", + allowPI: true, + allowGeo: false, + }, + } + + for _, td := range testDefs { + allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description) + assert.EqualValuesf(t, td.allowPI, allowPI, "AllowPI failure on %s", td.description) + assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description) + } +} + +func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) { + vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 6, + openrtb_ext.BidderRubicon: 8, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 34: parseVendorListDataV2(t, vendorListData), + }), + }, + } + // Assert that an item that otherwise would not be allowed PI access, gets approved because it is found in the GDPR.NonStandardPublishers array + perms.cfg.NonStandardPublisherMap = map[string]int{"appNexusAppID": 1} + allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed") + assert.EqualValuesf(t, true, allowPI, "AllowPI failure") + assert.EqualValuesf(t, true, allowGeo, "AllowGeo failure") + +} + +func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) { + vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 32, + openrtb_ext.BidderRubicon: 8, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 15: parseVendorListDataV2(t, vendorListData), + }), + }, + } + + // COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA - vendors 1-10 legit interest only, + // Pub restriction on purpose 7, consent only ... no allowPI will pass, no Special purpose 1 consent + testDefs := []tcf2TestDef{ + { + description: "Appnexus vendor test, insufficient purposes claimed", + bidder: openrtb_ext.BidderAppnexus, + consent: "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA", + allowPI: false, + allowGeo: false, + }, + { + description: "Pubmatic vendor test, flex purposes claimed", + bidder: openrtb_ext.BidderPubmatic, + consent: "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA", + allowPI: false, + allowGeo: false, + }, + { + description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", + bidder: openrtb_ext.BidderRubicon, + consent: "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA", + allowPI: false, + allowGeo: false, + }, + } + + for _, td := range testDefs { + allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description) + assert.EqualValuesf(t, td.allowPI, allowPI, "AllowPI failure on %s", td.description) + assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description) + } +} + +func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) { + vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 10, + openrtb_ext.BidderRubicon: 8, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 34: parseVendorListDataV2(t, vendorListData), + }), + }, + } + perms.cfg.TCF2.PurposeOneTreatment.Enabled = true + perms.cfg.TCF2.PurposeOneTreatment.AccessAllowed = true + + // COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA Purpose one flag set + testDefs := []tcf2TestDef{ + { + description: "Appnexus vendor test, insufficient purposes claimed", + bidder: openrtb_ext.BidderAppnexus, + consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", + allowPI: false, + allowGeo: false, + }, + { + description: "Pubmatic vendor test, flex purposes claimed", + bidder: openrtb_ext.BidderPubmatic, + consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", + allowPI: true, + allowGeo: true, + }, + { + description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", + bidder: openrtb_ext.BidderRubicon, + consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", + allowPI: true, + allowGeo: false, + }, + } + + for _, td := range testDefs { + allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description) + assert.EqualValuesf(t, td.allowPI, allowPI, "AllowPI failure on %s", td.description) + assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description) + } +} + +func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { + vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 10, + openrtb_ext.BidderRubicon: 8, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 34: parseVendorListDataV2(t, vendorListData), + }), + }, + } + perms.cfg.TCF2.PurposeOneTreatment.Enabled = true + perms.cfg.TCF2.PurposeOneTreatment.AccessAllowed = false + + // COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA Purpose one flag set + testDefs := []tcf2TestDef{ + { + description: "Appnexus vendor test, insufficient purposes claimed", + bidder: openrtb_ext.BidderAppnexus, + consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", + allowPI: false, + allowGeo: false, + }, + { + description: "Pubmatic vendor test, flex purposes claimed", + bidder: openrtb_ext.BidderPubmatic, + consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", + allowPI: false, + allowGeo: true, + }, + { + description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", + bidder: openrtb_ext.BidderRubicon, + consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", + allowPI: false, + allowGeo: false, + }, + } + + for _, td := range testDefs { + allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description) + assert.EqualValuesf(t, td.allowPI, allowPI, "AllowPI failure on %s", td.description) + assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description) + } +} + +func TestAllowSyncTCF2(t *testing.T) { + vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 6, + openrtb_ext.BidderRubicon: 8, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 34: parseVendorListDataV2(t, vendorListData), + }), + }, + } + + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consensts to purposes and vendors 2, 6, 8 + allowSync, err := perms.HostCookiesAllowed(context.Background(), "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + assert.NoErrorf(t, err, "Error processing HostCookiesAllowed") + assert.EqualValuesf(t, true, allowSync, "HostCookiesAllowed failure") + + allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderRubicon, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + assert.NoErrorf(t, err, "Error processing BidderSyncAllowed") + assert.EqualValuesf(t, true, allowSync, "BidderSyncAllowed failure") +} + +func TestProhibitedPurposeSyncTCF2(t *testing.T) { + basicPurposes := tcf2BasicPurposes + basicPurposes[8] = &purposes{purposes: []int{7}} + vendorListData := mockVendorListDataTCF2(t, 2, basicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 6, + openrtb_ext.BidderRubicon: 8, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 34: parseVendorListDataV2(t, vendorListData), + }), + }, + } + perms.cfg.HostVendorID = 8 + + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consensts to purposes and vendors 2, 6, 8 + allowSync, err := perms.HostCookiesAllowed(context.Background(), "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + assert.NoErrorf(t, err, "Error processing HostCookiesAllowed") + assert.EqualValuesf(t, false, allowSync, "HostCookiesAllowed failure") + + allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderRubicon, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + assert.NoErrorf(t, err, "Error processing BidderSyncAllowed") + assert.EqualValuesf(t, false, allowSync, "BidderSyncAllowed failure") +} + +func TestProhibitedVendorSyncTCF2(t *testing.T) { + basicPurposes := tcf2BasicPurposes + basicPurposes[10] = &purposes{purposes: []int{1}} + vendorListData := mockVendorListDataTCF2(t, 2, basicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 6, + openrtb_ext.BidderRubicon: 8, + openrtb_ext.BidderOpenx: 10, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 34: parseVendorListDataV2(t, vendorListData), + }), + }, + } + perms.cfg.HostVendorID = 10 + + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consensts to purposes and vendors 2, 4, 6 + allowSync, err := perms.HostCookiesAllowed(context.Background(), "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + assert.NoErrorf(t, err, "Error processing HostCookiesAllowed") + assert.EqualValuesf(t, false, allowSync, "HostCookiesAllowed failure") + + allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderRubicon, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + assert.NoErrorf(t, err, "Error processing BidderSyncAllowed") + assert.EqualValuesf(t, false, allowSync, "BidderSyncAllowed failure") +} + func parseVendorListData(t *testing.T, data string) vendorlist.VendorList { t.Helper() parsed, err := vendorlist.ParseEagerly([]byte(data)) @@ -228,6 +587,15 @@ func parseVendorListData(t *testing.T, data string) vendorlist.VendorList { return parsed } +func parseVendorListDataV2(t *testing.T, data string) vendorlist.VendorList { + t.Helper() + parsed, err := vendorlist2.ParseEagerly([]byte(data)) + if err != nil { + t.Fatalf("Failed to parse vendor list data. %v", err) + } + return parsed +} + func listFetcher(lists map[uint16]vendorlist.VendorList) func(context.Context, uint16) (vendorlist.VendorList, error) { return func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { data, ok := lists[id] diff --git a/gdpr/vendorlist-fetching_test.go b/gdpr/vendorlist-fetching_test.go index 8197fa263bc..824f9178faa 100644 --- a/gdpr/vendorlist-fetching_test.go +++ b/gdpr/vendorlist-fetching_test.go @@ -15,12 +15,12 @@ import ( func TestVendorFetch(t *testing.T) { vendorListOne := mockVendorListData(t, 1, map[uint16]*purposes{ 32: { - purposes: []uint8{1, 2}, + purposes: []int{1, 2}, }, }) vendorListTwo := mockVendorListData(t, 2, map[uint16]*purposes{ 32: { - purposes: []uint8{1, 2, 3}, + purposes: []int{1, 2, 3}, }, }) server := httptest.NewServer(http.HandlerFunc(mockServer(2, map[int]string{ @@ -47,12 +47,12 @@ func TestVendorFetch(t *testing.T) { func TestLazyFetch(t *testing.T) { firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{ 32: { - purposes: []uint8{1, 2}, + purposes: []int{1, 2}, }, }) secondVendorList := mockVendorListData(t, 2, map[uint16]*purposes{ 3: { - purposes: []uint8{1}, + purposes: []int{1}, }, }) server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ @@ -73,7 +73,7 @@ func TestLazyFetch(t *testing.T) { func TestInitialTimeout(t *testing.T) { list := mockVendorListData(t, 1, map[uint16]*purposes{ 32: { - purposes: []uint8{1, 2}, + purposes: []int{1, 2}, }, }) server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ @@ -91,12 +91,12 @@ func TestInitialTimeout(t *testing.T) { func TestFetchThrottling(t *testing.T) { vendorListTwo := mockVendorListData(t, 2, map[uint16]*purposes{ 32: { - purposes: []uint8{1, 2}, + purposes: []int{1, 2}, }, }) vendorListThree := mockVendorListData(t, 3, map[uint16]*purposes{ 32: { - purposes: []uint8{1, 2}, + purposes: []int{1, 2}, }, }) server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ @@ -174,8 +174,8 @@ func mockServer(latestVersion int, responses map[int]string) func(http.ResponseW func mockVendorListData(t *testing.T, version uint16, vendors map[uint16]*purposes) string { type vendorContract struct { - ID uint16 `json:"id"` - Purposes []uint8 `json:"purposeIds"` + ID uint16 `json:"id"` + Purposes []int `json:"purposeIds"` } type vendorListContract struct { @@ -203,6 +203,72 @@ func mockVendorListData(t *testing.T, version uint16, vendors map[uint16]*purpos return string(data) } +type purposeMap map[uint16]*purposes + +func mockVendorListDataTCF2(t *testing.T, version uint16, basicPurposes purposeMap, legitInterests purposeMap, flexPurposes purposeMap, specialPurposes purposeMap) string { + type vendorContract struct { + ID uint16 `json:"id"` + Purposes []int `json:"purposes"` + LegIntPurposes []int `json:"legIntPurposes"` + FlexiblePurposes []int `json:"flexiblePurposes"` + SpecialPurposes []int `json:"specialPurposes"` + } + + type vendorListContract struct { + Version uint16 `json:"vendorListVersion"` + Vendors map[string]vendorContract `json:"vendors"` + } + + vendors := make(map[string]vendorContract, len(basicPurposes)) + for id, purpose := range basicPurposes { + sid := strconv.Itoa(int(id)) + vendor, ok := vendors[sid] + if !ok { + vendor = vendorContract{ID: id} + } + vendor.Purposes = purpose.purposes + vendors[sid] = vendor + } + + for id, purpose := range legitInterests { + sid := strconv.Itoa(int(id)) + vendor, ok := vendors[sid] + if !ok { + vendor = vendorContract{ID: id} + } + vendor.LegIntPurposes = purpose.purposes + vendors[sid] = vendor + } + + for id, purpose := range flexPurposes { + sid := strconv.Itoa(int(id)) + vendor, ok := vendors[sid] + if !ok { + vendor = vendorContract{ID: id} + } + vendor.FlexiblePurposes = purpose.purposes + vendors[sid] = vendor + } + + for id, purpose := range specialPurposes { + sid := strconv.Itoa(int(id)) + vendor, ok := vendors[sid] + if !ok { + vendor = vendorContract{ID: id} + } + vendor.SpecialPurposes = purpose.purposes + vendors[sid] = vendor + } + + obj := vendorListContract{ + Version: version, + Vendors: vendors, + } + data, err := json.Marshal(obj) + assertNilErr(t, err) + return string(data) +} + func testURLMaker(server *httptest.Server) func(uint16, uint8) string { url := server.URL return func(version uint16, TCFVer uint8) string { @@ -220,5 +286,5 @@ func testConfig() config.GDPR { } type purposes struct { - purposes []uint8 + purposes []int } diff --git a/go.mod b/go.mod index 8de6f10e4b9..0224057e464 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,8 @@ require ( github.com/onsi/ginkgo v1.10.1 // indirect github.com/onsi/gomega v1.7.0 // indirect github.com/pelletier/go-toml v1.2.0 // indirect - github.com/prebid/go-gdpr v0.7.0 + github.com/prebid/go-gdpr v0.8.2 + github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e // indirect diff --git a/go.sum b/go.sum index 176bacfc20a..5d941b89e90 100644 --- a/go.sum +++ b/go.sum @@ -70,8 +70,10 @@ github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prebid/go-gdpr v0.7.0 h1:m4E/FjUhTBMciDsd3lQlbzFyXLzNK+JQkFmInJpFAwc= -github.com/prebid/go-gdpr v0.7.0/go.mod h1:FPY0uxSrl9/Mz237LnPo3ge4aCG0wQ9FWf2b4WhwNn0= +github.com/prebid/go-gdpr v0.8.2 h1:mN2jKYZZpJkCYFQB/nDTJoPpuGYblOYP2UUzOzRggII= +github.com/prebid/go-gdpr v0.8.2/go.mod h1:FPY0uxSrl9/Mz237LnPo3ge4aCG0wQ9FWf2b4WhwNn0= +github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf h1:CcE+KN1tCtWKsUFH5IzdQxHIgP609VSIVe5Hywg2phs= +github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf/go.mod h1:k5xrl5ZpnumN1S2x8w8cMiFYsgRuVyAeFJz+BkSi+98= github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed h1:0dloFFFNNDG7c+8qtkYw2FdADrWy9s5cI8wHp6tK3Mg= github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= diff --git a/privacy/enforcement.go b/privacy/enforcement.go index 96d03ef4433..d302192ec3f 100644 --- a/privacy/enforcement.go +++ b/privacy/enforcement.go @@ -6,14 +6,15 @@ import ( // Enforcement represents the privacy policies to enforce for an OpenRTB bid request. type Enforcement struct { - CCPA bool - COPPA bool - GDPR bool + CCPA bool + COPPA bool + GDPR bool + GDPRGeo bool } // Any returns true if at least one privacy policy requires enforcement. func (e Enforcement) Any() bool { - return e.CCPA || e.COPPA || e.GDPR + return e.CCPA || e.COPPA || e.GDPR || e.GDPRGeo } // Apply cleans personally identifiable information from an OpenRTB bid request. @@ -45,7 +46,7 @@ func (e Enforcement) getGeoScrubStrategy() ScrubStrategyGeo { return ScrubStrategyGeoFull } - if e.GDPR || e.CCPA { + if e.GDPRGeo || e.CCPA { return ScrubStrategyGeoReducedPrecision } @@ -60,5 +61,11 @@ func (e Enforcement) getUserScrubStrategy(ampGDPRException bool) ScrubStrategyUs if e.GDPR && ampGDPRException { return ScrubStrategyUserNone } - return ScrubStrategyUserID + + // If no user scrubbing is needed, then return none, else scrub ID (COPPA checked above) + if e.CCPA || e.GDPR { + return ScrubStrategyUserID + } + + return ScrubStrategyUserNone } diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go index 25e08b5e80d..0e82648d4b9 100644 --- a/privacy/enforcement_test.go +++ b/privacy/enforcement_test.go @@ -17,27 +17,40 @@ func TestAny(t *testing.T) { { description: "All False", enforcement: Enforcement{ - CCPA: false, - COPPA: false, - GDPR: false, + CCPA: false, + COPPA: false, + GDPR: false, + GDPRGeo: false, }, expected: false, }, { description: "All True", enforcement: Enforcement{ - CCPA: true, - COPPA: true, - GDPR: true, + CCPA: true, + COPPA: true, + GDPR: true, + GDPRGeo: true, }, expected: true, }, { description: "Mixed", enforcement: Enforcement{ - CCPA: false, - COPPA: true, - GDPR: false, + CCPA: false, + COPPA: true, + GDPR: false, + GDPRGeo: false, + }, + expected: true, + }, + { + description: "GDPRGeo only", + enforcement: Enforcement{ + CCPA: false, + COPPA: false, + GDPR: false, + GDPRGeo: true, }, expected: true, }, @@ -62,9 +75,10 @@ func TestApply(t *testing.T) { { description: "All Enforced", enforcement: Enforcement{ - CCPA: true, - COPPA: true, - GDPR: true, + CCPA: true, + COPPA: true, + GDPR: true, + GDPRGeo: true, }, ampGDPRException: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, @@ -75,9 +89,10 @@ func TestApply(t *testing.T) { { description: "CCPA Only", enforcement: Enforcement{ - CCPA: true, - COPPA: false, - GDPR: false, + CCPA: true, + COPPA: false, + GDPR: false, + GDPRGeo: false, }, ampGDPRException: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -88,9 +103,10 @@ func TestApply(t *testing.T) { { description: "COPPA Only", enforcement: Enforcement{ - CCPA: false, - COPPA: true, - GDPR: false, + CCPA: false, + COPPA: true, + GDPR: false, + GDPRGeo: false, }, ampGDPRException: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, @@ -101,9 +117,10 @@ func TestApply(t *testing.T) { { description: "GDPR Only", enforcement: Enforcement{ - CCPA: false, - COPPA: false, - GDPR: true, + CCPA: false, + COPPA: false, + GDPR: true, + GDPRGeo: true, }, ampGDPRException: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -114,9 +131,10 @@ func TestApply(t *testing.T) { { description: "GDPR Only, ampGDPRException", enforcement: Enforcement{ - CCPA: false, - COPPA: false, - GDPR: true, + CCPA: false, + COPPA: false, + GDPR: true, + GDPRGeo: true, }, ampGDPRException: true, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -127,9 +145,10 @@ func TestApply(t *testing.T) { { description: "CCPA Only, ampGDPRException", enforcement: Enforcement{ - CCPA: true, - COPPA: false, - GDPR: false, + CCPA: true, + COPPA: false, + GDPR: false, + GDPRGeo: false, }, ampGDPRException: true, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -140,9 +159,10 @@ func TestApply(t *testing.T) { { description: "COPPA and GDPR, ampGDPRException", enforcement: Enforcement{ - CCPA: false, - COPPA: true, - GDPR: true, + CCPA: false, + COPPA: true, + GDPR: true, + GDPRGeo: true, }, ampGDPRException: true, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, @@ -150,6 +170,34 @@ func TestApply(t *testing.T) { expectedUser: ScrubStrategyUserIDAndDemographic, expectedUserGeo: ScrubStrategyGeoFull, }, + { + description: "GDPR Only, no Geo", + enforcement: Enforcement{ + CCPA: false, + COPPA: false, + GDPR: true, + GDPRGeo: false, + }, + ampGDPRException: false, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceGeo: ScrubStrategyGeoNone, + expectedUser: ScrubStrategyUserID, + expectedUserGeo: ScrubStrategyGeoNone, + }, + { + description: "GDPR Only, Geo only", + enforcement: Enforcement{ + CCPA: false, + COPPA: false, + GDPR: false, + GDPRGeo: true, + }, + ampGDPRException: false, + expectedDeviceIPv6: ScrubStrategyIPV6None, + expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, + expectedUser: ScrubStrategyUserNone, + expectedUserGeo: ScrubStrategyGeoReducedPrecision, + }, } for _, test := range testCases {