Repository: prebid/prebid-server
Pull request #1851 (2021-05-14 17:39:37)

GDPR: Don't Call Bidder If It Lacks Purpose 2 Legal Basis
**Refactor changes:**
- changed the name of the GDPR method `PersonalInfoAllowed` to `AuctionActivitiesAllowed` since that method now determines whether a bid request is allowed in addition to whether personal info is allowed.
- changed the name of `allowID` and `allowGeo` to `passID` and `passGeo` because they sound more like activities.
- removed obselete tests `TestAllowPersonalInfoTCF2PurposeOneTrue` and `TestAllowPersonalInfoTCF2PurposeOneFalse` in `impl_test.go` that correspond to obselete code deleted in #1839

**Logic changes:**
- added logic in `cleanOpenRTBRequests` that filters out any blocked requests just before returning
- created a new test for `cleanOpenRTBRequests` that specifically tests the bid request functionality and left the existing GDPR test for testing PI scrubbing.
- added logic in allowActivitiesTCF2 to determine whether to block the request
- added a new test for `allowActivitiesTCF2` that specifically tests the bid request functionality and left the existing test `TestAllowActivitiesTCF2` for testing PI.
- if GDPR applies and there is no consent string or there is an error parsing the consent string, the bid request is not allowed IAW spec requirement 17 in the Prebid Server section

bsardo: I did not add an E2E JSON framework test inside the exchange package to verify bid requests are blocked. It would require some changes to the test framework, particularly making the GDPR permissions mock configurable via new fields set in the JSON spec. I figured the coverage at the UT level was sufficient but I can add this if others feel it is necessary.

Patch:
diff --git a/adapters/bidder.go b/adapters/bidder.go
index fe40184e..2e5ec83c 100644
--- a/adapters/bidder.go
+++ b/adapters/bidder.go
@@ -136,7 +136,8 @@ func (r *RequestData) SetBasicAuth(username string, password string) {
 }
 
 type ExtraRequestInfo struct {
-	PbsEntryPoint metrics.RequestType
+	PbsEntryPoint              metrics.RequestType
+	GlobalPrivacyControlHeader string
 }
 
 type Builder func(openrtb_ext.BidderName, config.Adapter) (Bidder, error)
diff --git a/config/config.go b/config/config.go
index 280bdbf0..afb4ac8d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -353,6 +353,9 @@ type DisabledMetrics struct {
 	// server establishes with bidder servers such as the number of connections
 	// that were created or reused.
 	AdapterConnectionMetrics bool `mapstructure:"adapter_connections_metrics"`
+
+	// True if we don't want to collect the per adapter GDPR request blocked metric
+	AdapterGDPRRequestBlocked bool `mapstructure:"adapter_gdpr_request_blocked"`
 }
 
 func (cfg *Metrics) validate(errs []error) []error {
@@ -718,6 +721,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	// no metrics configured by default (metrics{host|database|username|password})
 	v.SetDefault("metrics.disabled_metrics.account_adapter_details", false)
 	v.SetDefault("metrics.disabled_metrics.adapter_connections_metrics", true)
+	v.SetDefault("metrics.disabled_metrics.adapter_gdpr_request_blocked", false)
 	v.SetDefault("metrics.influxdb.host", "")
 	v.SetDefault("metrics.influxdb.database", "")
 	v.SetDefault("metrics.influxdb.username", "")
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 6767151f..862311da 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -182,13 +182,16 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 		return
 	}
 
+	secGPC := r.Header.Get("Sec-GPC")
+
 	auctionRequest := exchange.AuctionRequest{
-		BidRequest:   req,
-		Account:      *account,
-		UserSyncs:    usersyncs,
-		RequestType:  labels.RType,
-		StartTime:    start,
-		LegacyLabels: labels,
+		BidRequest:                 req,
+		Account:                    *account,
+		UserSyncs:                  usersyncs,
+		RequestType:                labels.RType,
+		StartTime:                  start,
+		LegacyLabels:               labels,
+		GlobalPrivacyControlHeader: secGPC,
 	}
 
 	response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 1cb7985c..8913e907 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -173,14 +173,17 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		return
 	}
 
+	secGPC := r.Header.Get("Sec-GPC")
+
 	auctionRequest := exchange.AuctionRequest{
-		BidRequest:   req,
-		Account:      *account,
-		UserSyncs:    usersyncs,
-		RequestType:  labels.RType,
-		StartTime:    start,
-		LegacyLabels: labels,
-		Warnings:     warnings,
+		BidRequest:                 req,
+		Account:                    *account,
+		UserSyncs:                  usersyncs,
+		RequestType:                labels.RType,
+		StartTime:                  start,
+		LegacyLabels:               labels,
+		Warnings:                   warnings,
+		GlobalPrivacyControlHeader: secGPC,
 	}
 
 	response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index 2ab3bbb0..84f0699e 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -274,13 +274,16 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 		return
 	}
 
+	secGPC := r.Header.Get("Sec-GPC")
+
 	auctionRequest := exchange.AuctionRequest{
-		BidRequest:   bidReq,
-		Account:      *account,
-		UserSyncs:    usersyncs,
-		RequestType:  labels.RType,
-		StartTime:    start,
-		LegacyLabels: labels,
+		BidRequest:                 bidReq,
+		Account:                    *account,
+		UserSyncs:                  usersyncs,
+		RequestType:                labels.RType,
+		StartTime:                  start,
+		LegacyLabels:               labels,
+		GlobalPrivacyControlHeader: secGPC,
 	}
 
 	response, err := deps.ex.HoldAuction(ctx, auctionRequest, &debugLog)
diff --git a/exchange/bidder.go b/exchange/bidder.go
index 59ef92cb..c6e25874 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -137,6 +137,19 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb2.B
 		return nil, errs
 	}
 
+	if reqInfo.GlobalPrivacyControlHeader == "1" {
+		for i := 0; i < len(reqData); i++ {
+			if reqData[i].Headers != nil {
+				reqHeader := reqData[i].Headers.Clone()
+				reqHeader.Add("Sec-GPC", reqInfo.GlobalPrivacyControlHeader)
+				reqData[i].Headers = reqHeader
+			} else {
+				reqData[i].Headers = http.Header{}
+				reqData[i].Headers.Add("Sec-GPC", reqInfo.GlobalPrivacyControlHeader)
+			}
+		}
+	}
+
 	// Make any HTTP requests in parallel.
 	// If the bidder only needs to make one, save some cycles by just using the current one.
 	responseChannel := make(chan *httpCallInfo, len(reqData))
diff --git a/exchange/exchange.go b/exchange/exchange.go
index eaff759f..0d3d93cf 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -123,12 +123,13 @@ func NewExchange(adapters map[openrtb_ext.BidderName]adaptedBidder, cache prebid
 // AuctionRequest holds the bid request for the auction
 // and all other information needed to process that request
 type AuctionRequest struct {
-	BidRequest  *openrtb2.BidRequest
-	Account     config.Account
-	UserSyncs   IdFetcher
-	RequestType metrics.RequestType
-	StartTime   time.Time
-	Warnings    []error
+	BidRequest                 *openrtb2.BidRequest
+	Account                    config.Account
+	UserSyncs                  IdFetcher
+	RequestType                metrics.RequestType
+	StartTime                  time.Time
+	Warnings                   []error
+	GlobalPrivacyControlHeader string
 
 	// LegacyLabels is included here for temporary compatability with cleanOpenRTBRequests
 	// in HoldAuction until we get to factoring it away. Do not use for anything new.
@@ -178,7 +179,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 	usersyncIfAmbiguous := e.parseUsersyncIfAmbiguous(r.BidRequest)
 
 	// Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder
-	bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(ctx, r, requestExt, e.gDPR, usersyncIfAmbiguous, e.privacyConfig, &r.Account)
+	bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(ctx, r, requestExt, e.gDPR, e.me, usersyncIfAmbiguous, e.privacyConfig, &r.Account)
 
 	e.me.RecordRequestPrivacy(privacyLabels)
 
@@ -193,7 +194,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 	// Get currency rates conversions for the auction
 	conversions := e.currencyConverter.Rates()
 
-	adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, r.Account.DebugAllow)
+	adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, r.Account.DebugAllow, r.GlobalPrivacyControlHeader)
 
 	var auc *auction
 	var cacheErrs []error
@@ -402,7 +403,8 @@ func (e *exchange) getAllBids(
 	bidderRequests []BidderRequest,
 	bidAdjustments map[string]float64,
 	conversions currency.Conversions,
-	accountDebugAllowed bool) (
+	accountDebugAllowed bool,
+	globalPrivacyControlHeader string) (
 	map[openrtb_ext.BidderName]*pbsOrtbSeatBid,
 	map[openrtb_ext.BidderName]*seatResponseExtra, bool) {
 	// Set up pointers to the bid results
@@ -433,6 +435,7 @@ func (e *exchange) getAllBids(
 			}
 			var reqInfo adapters.ExtraRequestInfo
 			reqInfo.PbsEntryPoint = bidderRequest.BidderLabels.RType
+			reqInfo.GlobalPrivacyControlHeader = globalPrivacyControlHeader
 			bids, err := e.adapterMap[bidderRequest.BidderCoreName].requestBid(ctx, bidderRequest.BidRequest, bidderRequest.BidderName, adjustmentFactor, conversions, &reqInfo, accountDebugAllowed)
 
 			// Add in time reporting
diff --git a/exchange/utils.go b/exchange/utils.go
index e367facb..3d5e2374 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -56,9 +56,10 @@ func cleanOpenRTBRequests(ctx context.Context,
 	req AuctionRequest,
 	requestExt *openrtb_ext.ExtRequest,
 	gDPR gdpr.Permissions,
+	metricsEngine metrics.MetricsEngine,
 	usersyncIfAmbiguous bool,
 	privacyConfig config.Privacy,
-	account *config.Account) (bidderRequests []BidderRequest, privacyLabels metrics.PrivacyLabels, errs []error) {
+	account *config.Account) (allowedBidderRequests []BidderRequest, privacyLabels metrics.PrivacyLabels, errs []error) {
 
 	impsByBidder, err := splitImps(req.BidRequest.Imp)
 	if err != nil {
@@ -71,9 +72,10 @@ func cleanOpenRTBRequests(ctx context.Context,
 		return
 	}
 
-	bidderRequests, errs = getAuctionBidderRequests(req, requestExt, impsByBidder, aliases)
+	var allBidderRequests []BidderRequest
+	allBidderRequests, errs = getAuctionBidderRequests(req, requestExt, impsByBidder, aliases)
 
-	if len(bidderRequests) == 0 {
+	if len(allBidderRequests) == 0 {
 		return
 	}
 
@@ -117,7 +119,10 @@ func cleanOpenRTBRequests(ctx context.Context,
 	}
 
 	// bidder level privacy policies
-	for _, bidderRequest := range bidderRequests {
+	allowedBidderRequests = make([]BidderRequest, 0, len(allBidderRequests))
+	for _, bidderRequest := range allBidderRequests {
+		bidRequestAllowed := true
+
 		// CCPA
 		privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String())
 
@@ -133,7 +138,9 @@ func cleanOpenRTBRequests(ctx context.Context,
 				}
 			}
 			var publisherID = req.LegacyLabels.PubID
-			geo, id, err := gDPR.PersonalInfoAllowed(ctx, bidderRequest.BidderCoreName, publisherID, gdprSignal, consent, weakVendorEnforcement)
+			bidReq, geo, id, err := gDPR.AuctionActivitiesAllowed(ctx, bidderRequest.BidderCoreName, publisherID, gdprSignal, consent, weakVendorEnforcement)
+			bidRequestAllowed = bidReq
+
 			if err == nil {
 				privacyEnforcement.GDPRGeo = !geo
 				privacyEnforcement.GDPRID = !id
@@ -141,9 +148,16 @@ func cleanOpenRTBRequests(ctx context.Context,
 				privacyEnforcement.GDPRGeo = true
 				privacyEnforcement.GDPRID = true
 			}
+
+			if !bidRequestAllowed {
+				metricsEngine.RecordAdapterGDPRRequestBlocked(bidderRequest.BidderCoreName)
+			}
 		}
 
-		privacyEnforcement.Apply(bidderRequest.BidRequest)
+		if bidRequestAllowed {
+			privacyEnforcement.Apply(bidderRequest.BidRequest)
+			allowedBidderRequests = append(allowedBidderRequests, bidderRequest)
+		}
 	}
 
 	return
diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go
index 2b450160..ffd5ced4 100644
--- a/gdpr/gdpr.go
+++ b/gdpr/gdpr.go
@@ -25,7 +25,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, gdprSignal Signal, consent string, weakVendorEnforcement bool) (allowGeo bool, allowID bool, err error)
+	AuctionActivitiesAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal Signal, consent string, weakVendorEnforcement bool) (allowBidReq bool, passGeo bool, passID bool, err error)
 }
 
 // Versions of the GDPR TCF technical specification.
diff --git a/gdpr/impl.go b/gdpr/impl.go
index 22b4dbe3..a91a9308 100644
--- a/gdpr/impl.go
+++ b/gdpr/impl.go
@@ -58,35 +58,35 @@ func (p *permissionsImpl) BidderSyncAllowed(ctx context.Context, bidder openrtb_
 	return false, nil
 }
 
-func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context,
+func (p *permissionsImpl) AuctionActivitiesAllowed(ctx context.Context,
 	bidder openrtb_ext.BidderName,
 	PublisherID string,
 	gdprSignal Signal,
 	consent string,
-	weakVendorEnforcement bool) (allowGeo bool, allowID bool, err error) {
+	weakVendorEnforcement bool) (allowBidRequest bool, passGeo bool, passID bool, err error) {
 	if _, ok := p.cfg.NonStandardPublisherMap[PublisherID]; ok {
-		return true, true, nil
+		return true, true, true, nil
 	}
 
 	gdprSignal = p.normalizeGDPR(gdprSignal)
 
 	if gdprSignal == SignalNo {
-		return true, true, nil
+		return true, true, true, nil
 	}
 
 	if consent == "" && gdprSignal == SignalYes {
-		return false, false, nil
+		return false, false, false, nil
 	}
 
 	if id, ok := p.vendorIDs[bidder]; ok {
-		return p.allowPI(ctx, id, consent, weakVendorEnforcement)
+		return p.allowActivities(ctx, id, consent, weakVendorEnforcement)
 	}
 
 	return p.defaultVendorPermissions()
 }
 
-func (p *permissionsImpl) defaultVendorPermissions() (allowGeo bool, allowID bool, err error) {
-	return false, false, nil
+func (p *permissionsImpl) defaultVendorPermissions() (allowBidRequest bool, passGeo bool, passID bool, err error) {
+	return false, false, false, nil
 }
 
 func (p *permissionsImpl) normalizeGDPR(gdprSignal Signal) Signal {
@@ -135,48 +135,51 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, consen
 	return false, nil
 }
 
-func (p *permissionsImpl) allowPI(ctx context.Context, vendorID uint16, consent string, weakVendorEnforcement bool) (allowGeo bool, allowID bool, err error) {
+func (p *permissionsImpl) allowActivities(ctx context.Context, vendorID uint16, consent string, weakVendorEnforcement bool) (allowBidRequest bool, passGeo bool, passID bool, err error) {
 	parsedConsent, vendor, err := p.parseVendor(ctx, vendorID, consent)
 	if err != nil {
-		return false, false, err
+		return false, false, false, err
 	}
 
 	if vendor == nil {
-		return false, false, nil
+		return false, false, false, nil
 	}
 
 	if parsedConsent.Version() == 2 {
 		if p.cfg.TCF2.Enabled {
-			return p.allowPITCF2(parsedConsent, vendor, vendorID, weakVendorEnforcement)
+			return p.allowActivitiesTCF2(parsedConsent, vendor, vendorID, weakVendorEnforcement)
 		}
 		if (vendor.Purpose(consentconstants.InfoStorageAccess) || vendor.LegitimateInterest(consentconstants.InfoStorageAccess) || weakVendorEnforcement) && parsedConsent.PurposeAllowed(consentconstants.InfoStorageAccess) && (vendor.Purpose(consentconstants.PersonalizationProfile) || vendor.LegitimateInterest(consentconstants.PersonalizationProfile) || weakVendorEnforcement) && parsedConsent.PurposeAllowed(consentconstants.PersonalizationProfile) && (parsedConsent.VendorConsent(vendorID) || weakVendorEnforcement) {
-			return true, true, nil
+			return true, 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, true, nil
+			return true, true, true, nil
 		}
 	}
-	return false, false, nil
+	return true, false, false, nil
 }
 
-func (p *permissionsImpl) allowPITCF2(parsedConsent api.VendorConsents, vendor api.Vendor, vendorID uint16, weakVendorEnforcement bool) (allowGeo bool, allowID bool, err error) {
+func (p *permissionsImpl) allowActivitiesTCF2(parsedConsent api.VendorConsents, vendor api.Vendor, vendorID uint16, weakVendorEnforcement bool) (allowBidRequest bool, passGeo bool, passID bool, err error) {
 	consent, ok := parsedConsent.(tcf2.ConsentMetadata)
-	err = nil
-	allowGeo = false
-	allowID = 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) || weakVendorEnforcement)
+		passGeo = consent.SpecialFeatureOptIn(1) && (vendor.SpecialPurpose(1) || weakVendorEnforcement)
+	} else {
+		passGeo = true
+	}
+	if p.cfg.TCF2.Purpose2.Enabled {
+		allowBidRequest = p.checkPurpose(consent, vendor, vendorID, tcf1constants.Purpose(2), weakVendorEnforcement)
 	} else {
-		allowGeo = true
+		allowBidRequest = true
 	}
 	for i := 2; i <= 10; i++ {
 		if p.checkPurpose(consent, vendor, vendorID, tcf1constants.Purpose(i), weakVendorEnforcement) {
-			allowID = true
+			passID = true
 			break
 		}
 	}
@@ -254,21 +257,6 @@ 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, gdprSignal Signal, consent string, weakVendorEnforcement bool) (allowGeo bool, allowID bool, err error) {
-	return true, true, nil
-}
-
-// Exporting to allow for easy test setups
-type AlwaysFail struct{}
-
-func (a AlwaysFail) HostCookiesAllowed(ctx context.Context, gdprSignal Signal, consent string) (bool, error) {
-	return false, nil
-}
-
-func (a AlwaysFail) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName, gdprSignal Signal, consent string) (bool, error) {
-	return false, nil
-}
-
-func (a AlwaysFail) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal Signal, consent string, weakVendorEnforcement bool) (allowGeo bool, allowID bool, err error) {
-	return false, false, nil
+func (a AlwaysAllow) AuctionActivitiesAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal Signal, consent string, weakVendorEnforcement bool) (allowBidRequest bool, passGeo bool, passID bool, err error) {
+	return true, true, true, nil
 }
diff --git a/metrics/config/metrics.go b/metrics/config/metrics.go
index c1f726e9..2c65a475 100644
--- a/metrics/config/metrics.go
+++ b/metrics/config/metrics.go
@@ -244,6 +244,13 @@ func (me *MultiMetricsEngine) RecordRequestPrivacy(privacy metrics.PrivacyLabels
 	}
 }
 
+// RecordAdapterGDPRRequestBlocked across all engines
+func (me *MultiMetricsEngine) RecordAdapterGDPRRequestBlocked(adapter openrtb_ext.BidderName) {
+	for _, thisME := range *me {
+		thisME.RecordAdapterGDPRRequestBlocked(adapter)
+	}
+}
+
 // DummyMetricsEngine is a Noop metrics engine in case no metrics are configured. (may also be useful for tests)
 type DummyMetricsEngine struct{}
 
@@ -350,3 +357,7 @@ func (me *DummyMetricsEngine) RecordTimeoutNotice(success bool) {
 // RecordRequestPrivacy as a noop
 func (me *DummyMetricsEngine) RecordRequestPrivacy(privacy metrics.PrivacyLabels) {
 }
+
+// RecordAdapterGDPRRequestBlocked as a noop
+func (me *DummyMetricsEngine) RecordAdapterGDPRRequestBlocked(adapter openrtb_ext.BidderName) {
+}
diff --git a/metrics/go_metrics.go b/metrics/go_metrics.go
index ac7ed069..93659db3 100644
--- a/metrics/go_metrics.go
+++ b/metrics/go_metrics.go
@@ -75,18 +75,19 @@ type Metrics struct {
 
 // AdapterMetrics houses the metrics for a particular adapter
 type AdapterMetrics struct {
-	NoCookieMeter     metrics.Meter
-	ErrorMeters       map[AdapterError]metrics.Meter
-	NoBidMeter        metrics.Meter
-	GotBidsMeter      metrics.Meter
-	RequestTimer      metrics.Timer
-	PriceHistogram    metrics.Histogram
-	BidsReceivedMeter metrics.Meter
-	PanicMeter        metrics.Meter
-	MarkupMetrics     map[openrtb_ext.BidType]*MarkupDeliveryMetrics
-	ConnCreated       metrics.Counter
-	ConnReused        metrics.Counter
-	ConnWaitTime      metrics.Timer
+	NoCookieMeter      metrics.Meter
+	ErrorMeters        map[AdapterError]metrics.Meter
+	NoBidMeter         metrics.Meter
+	GotBidsMeter       metrics.Meter
+	RequestTimer       metrics.Timer
+	PriceHistogram     metrics.Histogram
+	BidsReceivedMeter  metrics.Meter
+	PanicMeter         metrics.Meter
+	MarkupMetrics      map[openrtb_ext.BidType]*MarkupDeliveryMetrics
+	ConnCreated        metrics.Counter
+	ConnReused         metrics.Counter
+	ConnWaitTime       metrics.Timer
+	GDPRRequestBlocked metrics.Meter
 }
 
 type MarkupDeliveryMetrics struct {
@@ -306,6 +307,9 @@ func makeBlankAdapterMetrics(disabledMetrics config.DisabledMetrics) *AdapterMet
 		newAdapter.ConnReused = metrics.NilCounter{}
 		newAdapter.ConnWaitTime = &metrics.NilTimer{}
 	}
+	if !disabledMetrics.AdapterGDPRRequestBlocked {
+		newAdapter.GDPRRequestBlocked = blankMeter
+	}
 	for _, err := range AdapterErrors() {
 		newAdapter.ErrorMeters[err] = blankMeter
 	}
@@ -350,6 +354,7 @@ func registerAdapterMetrics(registry metrics.Registry, adapterOrAccount string,
 		am.BidsReceivedMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("%[1]s.%[2]s.bids_received", adapterOrAccount, exchange), registry)
 	}
 	am.PanicMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("%[1]s.%[2]s.requests.panic", adapterOrAccount, exchange), registry)
+	am.GDPRRequestBlocked = metrics.GetOrRegisterMeter(fmt.Sprintf("%[1]s.%[2]s.gdpr_request_blocked", adapterOrAccount, exchange), registry)
 }
 
 func makeDeliveryMetrics(registry metrics.Registry, prefix string, bidType openrtb_ext.BidType) *MarkupDeliveryMetrics {
@@ -704,6 +709,20 @@ func (me *Metrics) RecordRequestPrivacy(privacy PrivacyLabels) {
 	return
 }
 
+func (me *Metrics) RecordAdapterGDPRRequestBlocked(adapterName openrtb_ext.BidderName) {
+	if me.MetricsDisabled.AdapterGDPRRequestBlocked {
+		return
+	}
+
+	am, ok := me.AdapterMetrics[adapterName]
+	if !ok {
+		glog.Errorf("Trying to log adapter GDPR request blocked metric for %s: adapter not found", string(adapterName))
+		return
+	}
+
+	am.GDPRRequestBlocked.Mark(1)
+}
+
 func doMark(bidder openrtb_ext.BidderName, meters map[openrtb_ext.BidderName]metrics.Meter) {
 	met, ok := meters[bidder]
 	if ok {
diff --git a/metrics/metrics.go b/metrics/metrics.go
index 62de3afa..7cb5f0f1 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -358,4 +358,5 @@ type MetricsEngine interface {
 	RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration)
 	RecordTimeoutNotice(sucess bool)
 	RecordRequestPrivacy(privacy PrivacyLabels)
+	RecordAdapterGDPRRequestBlocked(adapterName openrtb_ext.BidderName)
 }
diff --git a/metrics/metrics_mock.go b/metrics/metrics_mock.go
index c852eb47..261002b3 100644
--- a/metrics/metrics_mock.go
+++ b/metrics/metrics_mock.go
@@ -140,3 +140,8 @@ func (me *MetricsEngineMock) RecordTimeoutNotice(success bool) {
 func (me *MetricsEngineMock) RecordRequestPrivacy(privacy PrivacyLabels) {
 	me.Called(privacy)
 }
+
+// RecordAdapterGDPRRequestBlocked mock
+func (me *MetricsEngineMock) RecordAdapterGDPRRequestBlocked(adapterName openrtb_ext.BidderName) {
+	me.Called(adapterName)
+}
diff --git a/metrics/prometheus/preload.go b/metrics/prometheus/preload.go
index f4dfe434..621ad808 100644
--- a/metrics/prometheus/preload.go
+++ b/metrics/prometheus/preload.go
@@ -178,6 +178,12 @@ func preloadLabelValues(m *Metrics) {
 		sourceLabel:  sourceValues,
 		versionLabel: tcfVersionsAsString(),
 	})
+
+	if !m.metricsDisabled.AdapterGDPRRequestBlocked {
+		preloadLabelValuesForCounter(m.adapterGDPRBlockedRequests, map[string][]string{
+			adapterLabel: adapterValues,
+		})
+	}
 }
 
 func preloadLabelValuesForCounter(counter *prometheus.CounterVec, labelsWithValues map[string][]string) {
diff --git a/metrics/prometheus/prometheus.go b/metrics/prometheus/prometheus.go
index 1e384c0e..2c7361d0 100644
--- a/metrics/prometheus/prometheus.go
+++ b/metrics/prometheus/prometheus.go
@@ -48,17 +48,18 @@ type Metrics struct {
 	privacyTCF                   *prometheus.CounterVec
 
 	// Adapter Metrics
-	adapterBids               *prometheus.CounterVec
-	adapterCookieSync         *prometheus.CounterVec
-	adapterErrors             *prometheus.CounterVec
-	adapterPanics             *prometheus.CounterVec
-	adapterPrices             *prometheus.HistogramVec
-	adapterRequests           *prometheus.CounterVec
-	adapterRequestsTimer      *prometheus.HistogramVec
-	adapterUserSync           *prometheus.CounterVec
-	adapterReusedConnections  *prometheus.CounterVec
-	adapterCreatedConnections *prometheus.CounterVec
-	adapterConnectionWaitTime *prometheus.HistogramVec
+	adapterBids                *prometheus.CounterVec
+	adapterCookieSync          *prometheus.CounterVec
+	adapterErrors              *prometheus.CounterVec
+	adapterPanics              *prometheus.CounterVec
+	adapterPrices              *prometheus.HistogramVec
+	adapterRequests            *prometheus.CounterVec
+	adapterRequestsTimer       *prometheus.HistogramVec
+	adapterUserSync            *prometheus.CounterVec
+	adapterReusedConnections   *prometheus.CounterVec
+	adapterCreatedConnections  *prometheus.CounterVec
+	adapterConnectionWaitTime  *prometheus.HistogramVec
+	adapterGDPRBlockedRequests *prometheus.CounterVec
 
 	// Account Metrics
 	accountRequests *prometheus.CounterVec
@@ -283,6 +284,13 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet
 		"Count of total requests to Prebid Server where the LMT flag was set by source",
 		[]string{sourceLabel})
 
+	if !metrics.metricsDisabled.AdapterGDPRRequestBlocked {
+		metrics.adapterGDPRBlockedRequests = newCounter(cfg, metrics.Registry,
+			"adapter_gdpr_requests_blocked",
+			"Count of total bidder requests blocked due to unsatisfied GDPR purpose 2 legal basis",
+			[]string{adapterLabel})
+	}
+
 	metrics.adapterBids = newCounter(cfg, metrics.Registry,
 		"adapter_bids",
 		"Count of bids labeled by adapter and markup delivery type (adm or nurl).",
@@ -691,3 +699,13 @@ func (m *Metrics) RecordRequestPrivacy(privacy metrics.PrivacyLabels) {
 		}).Inc()
 	}
 }
+
+func (m *Metrics) RecordAdapterGDPRRequestBlocked(adapterName openrtb_ext.BidderName) {
+	if m.metricsDisabled.AdapterGDPRRequestBlocked {
+		return
+	}
+
+	m.adapterGDPRBlockedRequests.With(prometheus.Labels{
+		adapterLabel: string(adapterName),
+	}).Inc()
+}
diff --git a/static/bidder-info/smaato.yaml b/static/bidder-info/smaato.yaml
index db3e61e5..b73edb23 100644
--- a/static/bidder-info/smaato.yaml
+++ b/static/bidder-info/smaato.yaml
@@ -1,5 +1,6 @@
 maintainer:
   email: "prebid@smaato.com"
+gvlVendorID: 82
 capabilities:
   app:
     mediaTypes:


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index 43ee8fa2..1d4c00a5 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -135,6 +135,7 @@ func TestDefaults(t *testing.T) {
 	cmpInts(t, "metrics.influxdb.collection_rate_seconds", cfg.Metrics.Influxdb.MetricSendInterval, 20)
 	cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, false)
 	cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true)
+	cmpBools(t, "adapter_gdpr_request_blocked", cfg.Metrics.Disabled.AdapterGDPRRequestBlocked, false)
 	cmpStrings(t, "certificates_file", cfg.PemCertsFile, "")
 	cmpBools(t, "stored_requests.filesystem.enabled", false, cfg.StoredRequests.Files.Enabled)
 	cmpStrings(t, "stored_requests.filesystem.directorypath", "./stored_requests/data/by_id", cfg.StoredRequests.Files.Path)
@@ -197,6 +198,7 @@ metrics:
   disabled_metrics:
     account_adapter_details: true
     adapter_connections_metrics: true
+    adapter_gdpr_request_blocked: true
 datacache:
   type: postgres
   filename: /usr/db/db.db
@@ -413,6 +415,7 @@ func TestFullConfig(t *testing.T) {
 	cmpBools(t, "auto_gen_source_tid", cfg.AutoGenSourceTID, false)
 	cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, true)
 	cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true)
+	cmpBools(t, "adapter_gdpr_request_blocked", cfg.Metrics.Disabled.AdapterGDPRRequestBlocked, true)
 	cmpStrings(t, "certificates_file", cfg.PemCertsFile, "/etc/ssl/cert.pem")
 	cmpStrings(t, "request_validation.ipv4_private_networks", cfg.RequestValidation.IPv4PrivateNetworks[0], "1.1.1.0/24")
 	cmpStrings(t, "request_validation.ipv6_private_networks", cfg.RequestValidation.IPv6PrivateNetworks[0], "1111::/16")
diff --git a/endpoints/auction_test.go b/endpoints/auction_test.go
index f8b4f9d9..2062589e 100644
--- a/endpoints/auction_test.go
+++ b/endpoints/auction_test.go
@@ -441,8 +441,9 @@ func TestShouldUsersync(t *testing.T) {
 type auctionMockPermissions struct {
 	allowBidderSync  bool
 	allowHostCookies bool
-	allowGeo         bool
-	allowID          bool
+	allowBidRequest  bool
+	passGeo          bool
+	passID           bool
 }
 
 func (m *auctionMockPermissions) HostCookiesAllowed(ctx context.Context, gdprSignal gdpr.Signal, consent string) (bool, error) {
@@ -453,8 +454,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, gdprSignal gdpr.Signal, consent string, weakVendorEnforcement bool) (allowGeo bool, allowID bool, err error) {
-	return m.allowGeo, m.allowID, nil
+func (m *auctionMockPermissions) AuctionActivitiesAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string, weakVendorEnforcement bool) (allowBidRequest bool, passGeo bool, passID bool, err error) {
+	return m.allowBidRequest, m.passGeo, m.passID, nil
 }
 
 func TestBidSizeValidate(t *testing.T) {
diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go
index bfd6c507..561b4ed7 100644
--- a/endpoints/cookie_sync_test.go
+++ b/endpoints/cookie_sync_test.go
@@ -254,6 +254,6 @@ 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, gdprSignal gdpr.Signal, consent string, weakVendorEnforcement bool) (allowGeo bool, allowID bool, err error) {
-	return true, true, nil
+func (g *gdprPerms) AuctionActivitiesAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string, weakVendorEnforcement bool) (allowBidRequest, passGeo bool, passID bool, err error) {
+	return true, true, true, nil
 }
diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go
index 45ea2490..caeb09a8 100644
--- a/endpoints/setuid_test.go
+++ b/endpoints/setuid_test.go
@@ -439,8 +439,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, gdprSignal gdpr.Signal, consent string, weakVendorEnforcement bool) (allowGeo bool, allowID bool, err error) {
-	return g.personalInfoAllowed, g.personalInfoAllowed, nil
+func (g *mockPermsSetUID) AuctionActivitiesAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string, weakVendorEnforcement bool) (allowBidRequest bool, passGeo bool, passID bool, err error) {
+	return g.personalInfoAllowed, g.personalInfoAllowed, g.personalInfoAllowed, nil
 }
 
 func newFakeSyncer(familyName string) usersync.Usersyncer {
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index 6db249ec..3140930d 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -183,6 +183,85 @@ func TestRequestBidRemovesSensitiveHeaders(t *testing.T) {
 	assert.ElementsMatch(t, seatBid.httpCalls, expectedHttpCalls)
 }
 
+func TestSetGPCHeader(t *testing.T) {
+	server := httptest.NewServer(mockHandler(200, "getBody", "responseJson"))
+	defer server.Close()
+
+	requestHeaders := http.Header{}
+	requestHeaders.Add("Content-Type", "application/json")
+
+	bidderImpl := &goodSingleBidder{
+		httpRequest: &adapters.RequestData{
+			Method:  "POST",
+			Uri:     server.URL,
+			Body:    []byte("requestJson"),
+			Headers: requestHeaders,
+		},
+		bidResponse: &adapters.BidderResponse{
+			Bids: []*adapters.TypedBid{},
+		},
+	}
+
+	debugInfo := &config.DebugInfo{Allow: true}
+	ctx := context.Background()
+	ctx = context.WithValue(ctx, DebugContextKey, true)
+
+	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus, debugInfo)
+	currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
+	seatBid, errs := bidder.requestBid(ctx, &openrtb2.BidRequest{}, "test", 1, currencyConverter.Rates(), &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}, true)
+
+	expectedHttpCall := []*openrtb_ext.ExtHttpCall{
+		{
+			Uri:            server.URL,
+			RequestBody:    "requestJson",
+			RequestHeaders: map[string][]string{"Content-Type": {"application/json"}, "Sec-Gpc": {"1"}},
+			ResponseBody:   "responseJson",
+			Status:         200,
+		},
+	}
+
+	assert.Empty(t, errs)
+	assert.ElementsMatch(t, seatBid.httpCalls, expectedHttpCall)
+}
+
+func TestSetGPCHeaderNil(t *testing.T) {
+	server := httptest.NewServer(mockHandler(200, "getBody", "responseJson"))
+	defer server.Close()
+
+	bidderImpl := &goodSingleBidder{
+		httpRequest: &adapters.RequestData{
+			Method:  "POST",
+			Uri:     server.URL,
+			Body:    []byte("requestJson"),
+			Headers: nil,
+		},
+		bidResponse: &adapters.BidderResponse{
+			Bids: []*adapters.TypedBid{},
+		},
+	}
+
+	debugInfo := &config.DebugInfo{Allow: true}
+	ctx := context.Background()
+	ctx = context.WithValue(ctx, DebugContextKey, true)
+
+	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus, debugInfo)
+	currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
+	seatBid, errs := bidder.requestBid(ctx, &openrtb2.BidRequest{}, "test", 1, currencyConverter.Rates(), &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}, true)
+
+	expectedHttpCall := []*openrtb_ext.ExtHttpCall{
+		{
+			Uri:            server.URL,
+			RequestBody:    "requestJson",
+			RequestHeaders: map[string][]string{"Sec-Gpc": {"1"}},
+			ResponseBody:   "responseJson",
+			Status:         200,
+		},
+	}
+
+	assert.Empty(t, errs)
+	assert.ElementsMatch(t, seatBid.httpCalls, expectedHttpCall)
+}
+
 // TestMultiBidder makes sure all the requests get sent, and the responses processed.
 // Because this is done in parallel, it should be run under the race detector.
 func TestMultiBidder(t *testing.T) {
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index c18f4533..c64b3935 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -1770,7 +1770,7 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
 		me:                  metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames()),
 		cache:               &wellBehavedCache{},
 		cacheTime:           0,
-		gDPR:                gdpr.AlwaysFail{},
+		gDPR:                &permissionsMock{allowAllBidders: true},
 		currencyConverter:   currency.NewRateConverter(&http.Client{}, "", time.Duration(0)),
 		UsersyncIfAmbiguous: privacyConfig.GDPR.UsersyncIfAmbiguous,
 		privacyConfig:       privacyConfig,
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index 9f6041e9..50636d35 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -14,14 +14,16 @@ import (
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 )
 
 // permissionsMock mocks the Permissions interface for tests
-//
-// It only allows appnexus for GDPR consent
 type permissionsMock struct {
-	personalInfoAllowed      bool
-	personalInfoAllowedError error
+	allowAllBidders bool
+	allowedBidders  []openrtb_ext.BidderName
+	passGeo         bool
+	passID          bool
+	activitiesError error
 }
 
 func (p *permissionsMock) HostCookiesAllowed(ctx context.Context, gdpr gdpr.Signal, consent string) (bool, error) {
@@ -32,8 +34,18 @@ 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, gdpr gdpr.Signal, consent string, weakVendorEnforcement bool) (allowGeo bool, allowID bool, err error) {
-	return p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowedError
+func (p *permissionsMock) AuctionActivitiesAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdpr gdpr.Signal, consent string, weakVendorEnforcement bool) (allowBidRequest bool, passGeo bool, passID bool, err error) {
+	if p.allowAllBidders {
+		return true, p.passGeo, p.passID, p.activitiesError
+	}
+
+	for _, allowedBidder := range p.allowedBidders {
+		if bidder == allowedBidder {
+			allowBidRequest = true
+		}
+	}
+
+	return allowBidRequest, p.passGeo, p.passID, p.activitiesError
 }
 
 func assertReq(t *testing.T, bidderRequests []BidderRequest,
@@ -465,7 +477,9 @@ func TestCleanOpenRTBRequests(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		bidderRequests, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, nil)
+		metricsMock := metrics.MetricsEngineMock{}
+		permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
+		bidderRequests, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, &permissions, &metricsMock, true, privacyConfig, nil)
 		if test.hasError {
 			assert.NotNil(t, err, "Error shouldn't be nil")
 		} else {
@@ -620,7 +634,8 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 			context.Background(),
 			auctionReq,
 			nil,
-			&permissionsMock{personalInfoAllowed: true},
+			&permissionsMock{allowAllBidders: true, passGeo: true, passID: true},
+			&metrics.MetricsEngineMock{},
 			true,
 			privacyConfig,
 			nil)
@@ -681,7 +696,9 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) {
 				Enforce: true,
 			},
 		}
-		_, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, &reqExtStruct, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, nil)
+		permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
+		metrics := metrics.MetricsEngineMock{}
+		_, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, &reqExtStruct, &permissions, &metrics, true, privacyConfig, nil)
 
 		assert.ElementsMatch(t, []error{test.expectError}, errs, test.description)
 	}
@@ -721,7 +738,9 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) {
 			UserSyncs:  &emptyUsersync{},
 		}
 
-		bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissionsMock{personalInfoAllowed: true}, true, config.Privacy{}, nil)
+		permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
+		metrics := metrics.MetricsEngineMock{}
+		bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissions, &metrics, true, config.Privacy{}, nil)
 		result := bidderRequests[0]
 
 		assert.Nil(t, errs)
@@ -828,7 +847,9 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) {
 			UserSyncs:  &emptyUsersync{},
 		}
 
-		bidderRequests, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, &permissionsMock{}, true, config.Privacy{}, nil)
+		permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
+		metrics := metrics.MetricsEngineMock{}
+		bidderRequests, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, &permissions, &metrics, true, config.Privacy{}, nil)
 		if test.hasError == true {
 			assert.NotNil(t, errs)
 			assert.Len(t, bidderRequests, 0)
@@ -1409,7 +1430,9 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
 			},
 		}
 
-		results, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, nil)
+		permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
+		metrics := metrics.MetricsEngineMock{}
+		results, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissions, &metrics, true, privacyConfig, nil)
 		result := results[0]
 
 		assert.Nil(t, errs)
@@ -1424,7 +1447,7 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
 	}
 }
 
-func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
+func TestCleanOpenRTBRequestsGDPRScrub(t *testing.T) {
 	tcf1Consent := "BONV8oqONXwgmADACHENAO7pqzAAppY"
 	tcf2Consent := "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA"
 	trueValue, falseValue := true, false
@@ -1624,7 +1647,8 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			context.Background(),
 			auctionReq,
 			nil,
-			&permissionsMock{personalInfoAllowed: !test.gdprScrub, personalInfoAllowedError: test.permissionsError},
+			&permissionsMock{allowAllBidders: true, passGeo: !test.gdprScrub, passID: !test.gdprScrub, activitiesError: test.permissionsError},
+			&metrics.MetricsEngineMock{},
 			test.userSyncIfAmbiguous,
 			privacyConfig,
 			nil)
@@ -1647,6 +1671,97 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 	}
 }
 
+func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) {
+	testCases := []struct {
+		description            string
+		gdprEnforced           bool
+		gdprAllowedBidders     []openrtb_ext.BidderName
+		expectedBidders        []openrtb_ext.BidderName
+		expectedBlockedBidders []openrtb_ext.BidderName
+	}{
+		{
+			description:            "gdpr enforced, one request allowed and one request blocked",
+			gdprEnforced:           true,
+			gdprAllowedBidders:     []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus},
+			expectedBidders:        []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus},
+			expectedBlockedBidders: []openrtb_ext.BidderName{openrtb_ext.BidderRubicon},
+		},
+		{
+			description:            "gdpr enforced, two requests allowed and no requests blocked",
+			gdprEnforced:           true,
+			gdprAllowedBidders:     []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon},
+			expectedBidders:        []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon},
+			expectedBlockedBidders: []openrtb_ext.BidderName{},
+		},
+		{
+			description:            "gdpr not enforced, two requests allowed and no requests blocked",
+			gdprEnforced:           false,
+			gdprAllowedBidders:     []openrtb_ext.BidderName{},
+			expectedBidders:        []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon},
+			expectedBlockedBidders: []openrtb_ext.BidderName{},
+		},
+	}
+
+	for _, test := range testCases {
+		req := newBidRequest(t)
+		req.Regs = &openrtb2.Regs{
+			Ext: json.RawMessage(`{"gdpr":1}`),
+		}
+		req.Imp[0].Ext = json.RawMessage(`{"appnexus": {"placementId": 1}, "rubicon": {}}`)
+
+		privacyConfig := config.Privacy{
+			GDPR: config.GDPR{
+				Enabled:             test.gdprEnforced,
+				UsersyncIfAmbiguous: true,
+				TCF2: config.TCF2{
+					Enabled: true,
+				},
+			},
+		}
+
+		accountConfig := config.Account{
+			GDPR: config.AccountGDPR{
+				Enabled: nil,
+			},
+		}
+
+		auctionReq := AuctionRequest{
+			BidRequest: req,
+			UserSyncs:  &emptyUsersync{},
+			Account:    accountConfig,
+		}
+
+		metricsMock := metrics.MetricsEngineMock{}
+		metricsMock.Mock.On("RecordAdapterGDPRRequestBlocked", mock.Anything).Return()
+
+		results, _, errs := cleanOpenRTBRequests(
+			context.Background(),
+			auctionReq,
+			nil,
+			&permissionsMock{allowedBidders: test.gdprAllowedBidders, passGeo: true, passID: true, activitiesError: nil},
+			&metricsMock,
+			true,
+			privacyConfig,
+			nil)
+
+		// extract bidder name from each request in the results
+		bidders := []openrtb_ext.BidderName{}
+		for _, req := range results {
+			bidders = append(bidders, req.BidderName)
+		}
+
+		assert.Empty(t, errs, test.description)
+		assert.ElementsMatch(t, bidders, test.expectedBidders, test.description)
+
+		for _, blockedBidder := range test.expectedBlockedBidders {
+			metricsMock.AssertCalled(t, "RecordAdapterGDPRRequestBlocked", blockedBidder)
+		}
+		for _, allowedBidder := range test.expectedBidders {
+			metricsMock.AssertNotCalled(t, "RecordAdapterGDPRRequestBlocked", allowedBidder)
+		}
+	}
+}
+
 // newAdapterAliasBidRequest builds a BidRequest with aliases
 func newAdapterAliasBidRequest(t *testing.T) *openrtb2.BidRequest {
 	dnt := int8(1)
diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go
index cc08aedd..3b974ffa 100644
--- a/gdpr/impl_test.go
+++ b/gdpr/impl_test.go
@@ -169,7 +169,7 @@ func TestMalformedConsent(t *testing.T) {
 	assertBoolsEqual(t, false, sync)
 }
 
-func TestAllowPersonalInfo(t *testing.T) {
+func TestAllowActivities(t *testing.T) {
 	bidderAllowedByConsent := openrtb_ext.BidderAppnexus
 	bidderBlockedByConsent := openrtb_ext.BidderRubicon
 	consent := "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA"
@@ -181,7 +181,7 @@ func TestAllowPersonalInfo(t *testing.T) {
 		userSyncIfAmbiguous   bool
 		gdpr                  Signal
 		consent               string
-		allowID               bool
+		passID                bool
 		weakVendorEnforcement bool
 	}{
 		{
@@ -191,7 +191,7 @@ func TestAllowPersonalInfo(t *testing.T) {
 			userSyncIfAmbiguous: false,
 			gdpr:                SignalYes,
 			consent:             consent,
-			allowID:             true,
+			passID:              true,
 		},
 		{
 			description:         "Allow PI - known vendor with No GDPR",
@@ -199,7 +199,7 @@ func TestAllowPersonalInfo(t *testing.T) {
 			userSyncIfAmbiguous: false,
 			gdpr:                SignalNo,
 			consent:             consent,
-			allowID:             true,
+			passID:              true,
 		},
 		{
 			description:         "Allow PI - known vendor with Yes GDPR",
@@ -207,7 +207,7 @@ func TestAllowPersonalInfo(t *testing.T) {
 			userSyncIfAmbiguous: false,
 			gdpr:                SignalYes,
 			consent:             consent,
-			allowID:             true,
+			passID:              true,
 		},
 		{
 			description:         "PI allowed according to host setting UserSyncIfAmbiguous true - known vendor with ambiguous GDPR and empty consent",
@@ -215,7 +215,7 @@ func TestAllowPersonalInfo(t *testing.T) {
 			userSyncIfAmbiguous: true,
 			gdpr:                SignalAmbiguous,
 			consent:             "",
-			allowID:             true,
+			passID:              true,
 		},
 		{
 			description:         "PI allowed according to host setting UserSyncIfAmbiguous true - known vendor with ambiguous GDPR and non-empty consent",
@@ -223,7 +223,7 @@ func TestAllowPersonalInfo(t *testing.T) {
 			userSyncIfAmbiguous: true,
 			gdpr:                SignalAmbiguous,
 			consent:             consent,
-			allowID:             true,
+			passID:              true,
 		},
 		{
 			description:         "PI allowed according to host setting UserSyncIfAmbiguous false - known vendor with ambiguous GDPR and empty consent",
@@ -231,7 +231,7 @@ func TestAllowPersonalInfo(t *testing.T) {
 			userSyncIfAmbiguous: false,
 			gdpr:                SignalAmbiguous,
 			consent:             "",
-			allowID:             false,
+			passID:              false,
 		},
 		{
 			description:         "PI allowed according to host setting UserSyncIfAmbiguous false - known vendor with ambiguous GDPR and non-empty consent",
@@ -239,7 +239,7 @@ func TestAllowPersonalInfo(t *testing.T) {
 			userSyncIfAmbiguous: false,
 			gdpr:                SignalAmbiguous,
 			consent:             consent,
-			allowID:             true,
+			passID:              true,
 		},
 		{
 			description:         "Don't allow PI - known vendor with Yes GDPR and empty consent",
@@ -247,7 +247,7 @@ func TestAllowPersonalInfo(t *testing.T) {
 			userSyncIfAmbiguous: false,
 			gdpr:                SignalYes,
 			consent:             "",
-			allowID:             false,
+			passID:              false,
 		},
 		{
 			description:         "Don't allow PI - default vendor with Yes GDPR and non-empty consent",
@@ -255,7 +255,7 @@ func TestAllowPersonalInfo(t *testing.T) {
 			userSyncIfAmbiguous: false,
 			gdpr:                SignalYes,
 			consent:             consent,
-			allowID:             false,
+			passID:              false,
 		},
 	}
 
@@ -286,10 +286,10 @@ func TestAllowPersonalInfo(t *testing.T) {
 	for _, tt := range tests {
 		perms.cfg.UsersyncIfAmbiguous = tt.userSyncIfAmbiguous
 
-		_, allowID, err := perms.PersonalInfoAllowed(context.Background(), tt.bidderName, tt.publisherID, tt.gdpr, tt.consent, tt.weakVendorEnforcement)
+		_, _, passID, err := perms.AuctionActivitiesAllowed(context.Background(), tt.bidderName, tt.publisherID, tt.gdpr, tt.consent, tt.weakVendorEnforcement)
 
 		assert.Nil(t, err, tt.description)
-		assert.Equal(t, tt.allowID, allowID, tt.description)
+		assert.Equal(t, tt.passID, passID, tt.description)
 	}
 }
 
@@ -347,12 +347,13 @@ type tcf2TestDef struct {
 	description           string
 	bidder                openrtb_ext.BidderName
 	consent               string
-	allowGeo              bool
-	allowID               bool
+	allowBid              bool
+	passGeo               bool
+	passID                bool
 	weakVendorEnforcement bool
 }
 
-func TestAllowPersonalInfoTCF2(t *testing.T) {
+func TestAllowActivitiesTCF2(t *testing.T) {
 	vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34())
 	perms := permissionsImpl{
 		cfg: tcf2Config,
@@ -372,36 +373,39 @@ func TestAllowPersonalInfoTCF2(t *testing.T) {
 	}
 
 	// COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consents 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",
-			allowGeo:    false,
-			allowID:     false,
+			allowBid:    false,
+			passGeo:     false,
+			passID:      false,
 		},
 		{
 			description:           "Appnexus vendor test, insufficient purposes claimed, basic enforcement",
 			bidder:                openrtb_ext.BidderAppnexus,
 			consent:               "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA",
-			allowGeo:              true,
-			allowID:               true,
+			allowBid:              true,
+			passGeo:               true,
+			passID:                true,
 			weakVendorEnforcement: true,
 		},
 		{
 			description: "Pubmatic vendor test, flex purposes claimed",
 			bidder:      openrtb_ext.BidderPubmatic,
 			consent:     "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA",
-			allowGeo:    true,
-			allowID:     true,
+			allowBid:    true,
+			passGeo:     true,
+			passID:      true,
 		},
 		{
 			description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed",
 			bidder:      openrtb_ext.BidderRubicon,
 			consent:     "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA",
-			allowGeo:    false,
-			allowID:     true,
+			allowBid:    true,
+			passGeo:     false,
+			passID:      true,
 		},
 		{
 			// This requires publisher restrictions on any claimed purposes, 2-10. Vendor must declare all claimed purposes
@@ -410,20 +414,22 @@ func TestAllowPersonalInfoTCF2(t *testing.T) {
 			description: "OpenX vendor test, Specific purposes/LIs claimed, no geo claimed, Publisher restrictions apply",
 			bidder:      openrtb_ext.BidderOpenx,
 			consent:     "CPAavcCPAavcCAGABCFRBKCsAP_AAH_AAAqIHFNf_X_fb3_j-_59_9t0eY1f9_7_v-0zjgeds-8Nyd_X_L8X5mM7vB36pq4KuR4Eu3LBAQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XT_ZKY79_____7__-_____7_f__-__3_vp9V---wOJAIMBAUAgAEMAAQIFCIQAAQhiQAAAABBCIBQJIAEqgAWVwEdoIEACAxAQgQAgBBQgwCAAQAAJKAgBACwQCAAiAQAAgAEAIAAEIAILACQEAAAEAJCAAiACECAgiAAg5DAgIgCCAFABAAAuJDACAMooASBAPGQGAAKAAqACGAEwALgAjgBlgDUAHZAPsA_ACMAFLAK2AbwBMQCbAFogLYAYEAw8BkQDOQGeAM-EQHwAVABWAC4AIYAZAAywBqADZAHYAPwAgABGAClgFPANYAdUA-QCGwEOgIvASIAmwBOwCkQFyAMCAYSAw8Bk4DOQGfCQAYADgBzgN_CQTgAEAALgAoACoAGQAOAAeABAACIAFQAMIAaABqADyAIYAigBMgCqAKwAWAAuABvADmAHoAQ0AiACJgEsAS4AmgBSgC3AGGAMgAZcA1ADVAGyAO8AewA-IB9gH6AQAAjABQQClgFPAL8AYoA1gBtADcAG8AOIAegA-QCGwEOgIqAReAkQBMQCZQE2AJ2AUOApEBYoC2AFyALvAYEAwYBhIDDQGHgMiAZIAycBlwDOQGfANIAadA1gDWQoAEAYQaBIACoAKwAXABDADIAGWANQAbIA7AB-AEAAIKARgApYBT4C0ALSAawA3gB1QD5AIbAQ6Ai8BIgCbAE7AKRAXIAwIBhIDDwGMAMnAZyAzwBnwcAEAA4Bv4qA2ABQAFQAQwAmABcAEcAMsAagA7AB-AEYAKXAWgBaQDeAJBATEAmwBTYC2AFyAMCAYeAyIBnIDPAGfANyHQWQAFwAUABUADIAHAAQAAiABdADAAMYAaABqADwAH0AQwBFACZAFUAVgAsABcADEAGYAN4AcwA9ACGAERAJYAmABNACjAFKALEAW4AwwBkADKAGiANQAbIA3wB3gD2gH2AfoBGACVAFBAKeAWKAtAC0gFzALyAX4AxQBuADiQHTAdQA9ACGwEOgIiAReAkEBIgCbAE7AKHAU0AqwBYsC2ALZAXAAuQBdoC7wGEgMNAYeAxIBjADHgGSAMnAZUAywBlwDOQGfANEgaQBpIDSwGnANYAbGPABAIqAb-QgZgALAAoABkAEQALgAYgBDACYAFUALgAYgAzABvAD0AI4AWIAygBqADfAHfAPsA_ACMAFBAKGAU-AtAC0gF-AMUAdQA9ACQQEiAJsAU0AsUBaMC2ALaAXAAuQBdoDDwGJAMiAZOAzkBngDPgGiANJAaWA4AlAyAAQAAsACgAGQAOAAigBgAGIAPAAiABMACqAFwAMQAZgA2gCGgEQARIAowBSgC3AGEAMoAaoA2QB3gD8AIwAU-AtAC0gGKANwAcQA6gCHQEXgJEATYAsUBbAC7QGHgMiAZOAywBnIDPAGfANIAawA4AmACARUA38pBBAAXABQAFQAMgAcABAACKAGAAYwA0ADUAHkAQwBFACYAFIAKoAWAAuABiADMAHMAQwAiABRgClAFiALcAZQA0QBqgDZAHfAPsA_ACMAFBAKGAVsAuYBeQDaAG4APQAh0BF4CRAE2AJ2AUOApoBWwCxQFsALgAXIAu0BhoDDwGMAMiAZIAycBlwDOQGeAM-gaQBpMDWANZAbGVABAA-Ab-A.YAAAAAAAAAAA",
-			allowGeo:    false,
-			allowID:     true,
+			allowBid:    true,
+			passGeo:     false,
+			passID:      true,
 		},
 	}
 
 	for _, td := range testDefs {
-		allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", SignalYes, td.consent, td.weakVendorEnforcement)
-		assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description)
-		assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description)
-		assert.EqualValuesf(t, td.allowID, allowID, "AllowGeo failure on %s", td.description)
+		allowBid, passGeo, passID, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidder, "", SignalYes, td.consent, td.weakVendorEnforcement)
+		assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description)
+		assert.EqualValuesf(t, td.allowBid, allowBid, "AllowBid failure on %s", td.description)
+		assert.EqualValuesf(t, td.passGeo, passGeo, "PassGeo failure on %s", td.description)
+		assert.EqualValuesf(t, td.passID, passID, "PassID failure on %s", td.description)
 	}
 }
 
-func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) {
+func TestAllowActivitiesWhitelistTCF2(t *testing.T) {
 	vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34())
 	perms := permissionsImpl{
 		cfg: tcf2Config,
@@ -441,13 +447,13 @@ func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) {
 	}
 	// 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]struct{}{"appNexusAppID": {}}
-	allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", SignalYes, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", false)
-	assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed")
-	assert.EqualValuesf(t, true, allowGeo, "AllowGeo failure")
-	assert.EqualValuesf(t, true, allowID, "AllowID failure")
+	_, passGeo, passID, err := perms.AuctionActivitiesAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", SignalYes, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", false)
+	assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed")
+	assert.EqualValuesf(t, true, passGeo, "PassGeo failure")
+	assert.EqualValuesf(t, true, passID, "PassID failure")
 }
 
-func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) {
+func TestAllowActivitiesTCF2PubRestrict(t *testing.T) {
 	vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34())
 	perms := permissionsImpl{
 		cfg: tcf2Config,
@@ -471,135 +477,30 @@ func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) {
 			description: "Appnexus vendor test, insufficient purposes claimed",
 			bidder:      openrtb_ext.BidderAppnexus,
 			consent:     "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA",
-			allowGeo:    false,
-			allowID:     false,
+			passGeo:     false,
+			passID:      false,
 		},
 		{
 			description: "Pubmatic vendor test, flex purposes claimed",
 			bidder:      openrtb_ext.BidderPubmatic,
 			consent:     "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA",
-			allowGeo:    false,
-			allowID:     false,
+			passGeo:     false,
+			passID:      false,
 		},
 		{
 			description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed",
 			bidder:      openrtb_ext.BidderRubicon,
 			consent:     "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA",
-			allowGeo:    false,
-			allowID:     true,
+			passGeo:     false,
+			passID:      true,
 		},
 	}
 
 	for _, td := range testDefs {
-		allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", SignalYes, td.consent, td.weakVendorEnforcement)
-		assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description)
-		assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description)
-		assert.EqualValuesf(t, td.allowID, allowID, "AllowID failure on %s", td.description)
-	}
-}
-
-func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) {
-	vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34())
-	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){
-			tcf1SpecVersion: nil,
-			tcf2SpecVersion: 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",
-			allowGeo:    false,
-			allowID:     false,
-		},
-		{
-			description: "Pubmatic vendor test, flex purposes claimed",
-			bidder:      openrtb_ext.BidderPubmatic,
-			consent:     "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA",
-			allowGeo:    true,
-			allowID:     true,
-		},
-		{
-			description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed",
-			bidder:      openrtb_ext.BidderRubicon,
-			consent:     "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA",
-			allowGeo:    false,
-			allowID:     true,
-		},
-	}
-
-	for _, td := range testDefs {
-		allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", SignalYes, td.consent, td.weakVendorEnforcement)
-		assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description)
-		assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description)
-		assert.EqualValuesf(t, td.allowID, allowID, "AllowID failure on %s", td.description)
-	}
-}
-
-func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) {
-	vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34())
-	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){
-			tcf1SpecVersion: nil,
-			tcf2SpecVersion: 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
-	// Purpose one treatment will fail PI, but allow passing the IDs.
-	testDefs := []tcf2TestDef{
-		{
-			description: "Appnexus vendor test, insufficient purposes claimed",
-			bidder:      openrtb_ext.BidderAppnexus,
-			consent:     "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA",
-			allowGeo:    false,
-			allowID:     false,
-		},
-		{
-			description: "Pubmatic vendor test, flex purposes claimed",
-			bidder:      openrtb_ext.BidderPubmatic,
-			consent:     "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA",
-			allowGeo:    true,
-			allowID:     true,
-		},
-		{
-			description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed",
-			bidder:      openrtb_ext.BidderRubicon,
-			consent:     "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA",
-			allowGeo:    false,
-			allowID:     true,
-		},
-	}
-
-	for _, td := range testDefs {
-		allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", SignalYes, td.consent, td.weakVendorEnforcement)
-		assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description)
-		assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description)
-		assert.EqualValuesf(t, td.allowID, allowID, "AllowID failure on %s", td.description)
+		_, passGeo, passID, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidder, "", SignalYes, td.consent, td.weakVendorEnforcement)
+		assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description)
+		assert.EqualValuesf(t, td.passGeo, passGeo, "PassGeo failure on %s", td.description)
+		assert.EqualValuesf(t, td.passID, passID, "PassID failure on %s", td.description)
 	}
 }
 
@@ -811,3 +712,78 @@ func TestNormalizeGDPR(t *testing.T) {
 		assert.Equal(t, tt.wantSignal, normalizedSignal, tt.description)
 	}
 }
+
+func TestAllowActivitiesTCF2BidRequests(t *testing.T) {
+	purpose2AndVendorConsent := "CPF_61ePF_61eFxAAAENAiCAAEAAAAAAAAAAADAQAAAAAA"
+	purpose2ConsentWithoutVendorConsent := "CPF_61ePF_61eFxAAAENAiCAAEAAAAAAAAAAABIAAAAA"
+
+	testDefs := []struct {
+		description           string
+		purpose2Enabled       bool
+		bidder                openrtb_ext.BidderName
+		consent               string
+		allowBid              bool
+		passGeo               bool
+		passID                bool
+		weakVendorEnforcement bool
+	}{
+		{
+			description:     "Bid blocked - p2 enabled, user consents to p2 but not vendor, vendor consents to p2",
+			purpose2Enabled: true,
+			bidder:          openrtb_ext.BidderPubmatic,
+			consent:         purpose2ConsentWithoutVendorConsent,
+			allowBid:        false,
+			passGeo:         false,
+			passID:          false,
+		},
+		{
+			description:     "Bid allowed - p2 disabled, user consents to p2 but not vendor, vendor consents to p2",
+			purpose2Enabled: false,
+			bidder:          openrtb_ext.BidderPubmatic,
+			consent:         purpose2ConsentWithoutVendorConsent,
+			allowBid:        true,
+			passGeo:         false,
+			passID:          false,
+		},
+		{
+			description:     "Bid allowed - p2 enabled, user consents to p2 and vendor, vendor consents to p2",
+			purpose2Enabled: true,
+			bidder:          openrtb_ext.BidderPubmatic,
+			consent:         purpose2AndVendorConsent,
+			allowBid:        true,
+			passGeo:         false,
+			passID:          true,
+		},
+	}
+
+	for _, td := range testDefs {
+		vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34())
+		perms := permissionsImpl{
+			cfg: config.GDPR{
+				HostVendorID: 2,
+				TCF2: config.TCF2{
+					Enabled:         true,
+					Purpose1:        config.PurposeDetail{Enabled: true},
+					Purpose2:        config.PurposeDetail{Enabled: td.purpose2Enabled},
+					Purpose7:        config.PurposeDetail{Enabled: true},
+					SpecialPurpose1: config.PurposeDetail{Enabled: true},
+				},
+			},
+			vendorIDs: map[openrtb_ext.BidderName]uint16{
+				openrtb_ext.BidderPubmatic: 6,
+			},
+			fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
+				tcf1SpecVersion: nil,
+				tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
+					34: parseVendorListDataV2(t, vendorListData),
+				}),
+			},
+		}
+
+		allowBid, passGeo, passID, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidder, "", SignalYes, td.consent, td.weakVendorEnforcement)
+		assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description)
+		assert.EqualValuesf(t, td.allowBid, allowBid, "AllowBid failure on %s", td.description)
+		assert.EqualValuesf(t, td.passGeo, passGeo, "PassGeo failure on %s", td.description)
+		assert.EqualValuesf(t, td.passID, passID, "PassID failure on %s", td.description)
+	}
+}
diff --git a/metrics/config/metrics_test.go b/metrics/config/metrics_test.go
index 5b70b53b..7f964333 100644
--- a/metrics/config/metrics_test.go
+++ b/metrics/config/metrics_test.go
@@ -118,6 +118,8 @@ func TestMultiMetricsEngine(t *testing.T) {
 	metricsEngine.RecordStoredImpCacheResult(metrics.CacheHit, 5)
 	metricsEngine.RecordAccountCacheResult(metrics.CacheHit, 6)
 
+	metricsEngine.RecordAdapterGDPRRequestBlocked(openrtb_ext.BidderAppnexus)
+
 	metricsEngine.RecordRequestQueueTime(false, metrics.ReqTypeVideo, time.Duration(1))
 
 	//Make the metrics engine, instantiated here with goEngine, fill its RequestStatuses[RequestType][metrics.RequestStatusXX] with the new boolean values added to metrics.Labels
@@ -161,6 +163,8 @@ func TestMultiMetricsEngine(t *testing.T) {
 	VerifyMetrics(t, "StoredReqCache.Hit", goEngine.StoredReqCacheMeter[metrics.CacheHit].Count(), 4)
 	VerifyMetrics(t, "StoredImpCache.Hit", goEngine.StoredImpCacheMeter[metrics.CacheHit].Count(), 5)
 	VerifyMetrics(t, "AccountCache.Hit", goEngine.AccountCacheMeter[metrics.CacheHit].Count(), 6)
+
+	VerifyMetrics(t, "AdapterMetrics.AppNexus.GDPRRequestBlocked", goEngine.AdapterMetrics[openrtb_ext.BidderAppnexus].GDPRRequestBlocked.Count(), 1)
 }
 
 func VerifyMetrics(t *testing.T, name string, actual int64, expected int64) {
diff --git a/metrics/go_metrics_test.go b/metrics/go_metrics_test.go
index 2d0b9097..7c0a3f37 100644
--- a/metrics/go_metrics_test.go
+++ b/metrics/go_metrics_test.go
@@ -570,6 +570,45 @@ func TestRecordRequestPrivacy(t *testing.T) {
 	assert.Equal(t, m.PrivacyTCFRequestVersion[TCFVersionV2].Count(), int64(1), "TCF V2")
 }
 
+func TestRecordAdapterGDPRRequestBlocked(t *testing.T) {
+	var fakeBidder openrtb_ext.BidderName = "fooAdvertising"
+
+	tests := []struct {
+		description     string
+		metricsDisabled bool
+		adapterName     openrtb_ext.BidderName
+		expectedCount   int64
+	}{
+		{
+			description:     "",
+			metricsDisabled: false,
+			adapterName:     openrtb_ext.BidderAppnexus,
+			expectedCount:   1,
+		},
+		{
+			description:     "",
+			metricsDisabled: false,
+			adapterName:     fakeBidder,
+			expectedCount:   0,
+		},
+		{
+			description:     "",
+			metricsDisabled: true,
+			adapterName:     openrtb_ext.BidderAppnexus,
+			expectedCount:   0,
+		},
+	}
+
+	for _, tt := range tests {
+		registry := metrics.NewRegistry()
+		m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, config.DisabledMetrics{AdapterGDPRRequestBlocked: tt.metricsDisabled})
+
+		m.RecordAdapterGDPRRequestBlocked(tt.adapterName)
+
+		assert.Equal(t, tt.expectedCount, m.AdapterMetrics[openrtb_ext.BidderAppnexus].GDPRRequestBlocked.Count(), tt.description)
+	}
+}
+
 func ensureContainsBidTypeMetrics(t *testing.T, registry metrics.Registry, prefix string, mdm map[openrtb_ext.BidType]*MarkupDeliveryMetrics) {
 	ensureContains(t, registry, prefix+".banner.adm_bids_received", mdm[openrtb_ext.BidTypeBanner].AdmMeter)
 	ensureContains(t, registry, prefix+".banner.nurl_bids_received", mdm[openrtb_ext.BidTypeBanner].NurlMeter)
diff --git a/metrics/prometheus/prometheus_test.go b/metrics/prometheus/prometheus_test.go
index b2ffff34..72ddd105 100644
--- a/metrics/prometheus/prometheus_test.go
+++ b/metrics/prometheus/prometheus_test.go
@@ -1341,17 +1341,21 @@ func TestRecordAdapterConnections(t *testing.T) {
 	}
 }
 
-func TestDisableAdapterConnections(t *testing.T) {
+func TestDisabledMetrics(t *testing.T) {
 	prometheusMetrics := NewMetrics(config.PrometheusMetrics{
 		Port:      8080,
 		Namespace: "prebid",
 		Subsystem: "server",
-	}, config.DisabledMetrics{AdapterConnectionMetrics: true})
+	}, config.DisabledMetrics{
+		AdapterConnectionMetrics:  true,
+		AdapterGDPRRequestBlocked: true,
+	})
 
 	// Assert counter vector was not initialized
 	assert.Nil(t, prometheusMetrics.adapterReusedConnections, "Counter Vector adapterReusedConnections should be nil")
 	assert.Nil(t, prometheusMetrics.adapterCreatedConnections, "Counter Vector adapterCreatedConnections should be nil")
 	assert.Nil(t, prometheusMetrics.adapterConnectionWaitTime, "Counter Vector adapterConnectionWaitTime should be nil")
+	assert.Nil(t, prometheusMetrics.adapterGDPRBlockedRequests, "Counter Vector adapterGDPRBlockedRequests should be nil")
 }
 
 func TestRecordRequestPrivacy(t *testing.T) {
@@ -1510,3 +1514,18 @@ func assertHistogram(t *testing.T, name string, histogram dto.Histogram, expecte
 	assert.Equal(t, expectedCount, histogram.GetSampleCount(), name+":count")
 	assert.Equal(t, expectedSum, histogram.GetSampleSum(), name+":sum")
 }
+
+func TestRecordAdapterGDPRRequestBlocked(t *testing.T) {
+	m := createMetricsForTesting()
+
+	m.RecordAdapterGDPRRequestBlocked(openrtb_ext.BidderAppnexus)
+
+	assertCounterVecValue(t,
+		"Increment adapter GDPR request blocked counter",
+		"adapter_gdpr_requests_blocked",
+		m.adapterGDPRBlockedRequests,
+		1,
+		prometheus.Labels{
+			adapterLabel: string(openrtb_ext.BidderAppnexus),
+		})
+}