Repository: prebid/prebid-server
Pull request #1302 (2020-05-14 20:19:14)

Enable full TCF2 support


Patch:
diff --git a/config/config.go b/config/config.go
index 79a31db1..5f19629d 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/exchange/utils.go b/exchange/utils.go
index d961089c..f602d1e8 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/gdpr/gdpr.go b/gdpr/gdpr.go
index 9390d942..0dfa12f5 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 8743d7f2..60db804a 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/go.mod b/go.mod
index 8de6f10e..0224057e 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 176bacfc..5d941b89 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 96d03ef4..d302192e 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
 }


Test patch:
diff --git a/endpoints/auction_test.go b/endpoints/auction_test.go
index 3035a6d4..5e9e9639 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 bb766aa9..824e32f1 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 8499ac1c..3f47b257 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_test.go b/exchange/utils_test.go
index 53d6b85c..acbf25ff 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/impl_test.go b/gdpr/impl_test.go
index 8b89577d..f05f25e8 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 8197fa26..824f9178 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/privacy/enforcement_test.go b/privacy/enforcement_test.go
index 25e08b5e..0e82648d 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 {