Repository: prebid/prebid-server
Pull request #1350 (2020-06-11 20:44:30)

Build With Go 1.14
All PRs are now building with Go 1.14, so it's time for the final phase and build our release binaries with 1.14.


Patch:
diff --git a/.travis.yml b/.travis.yml
index ea2c46c4..692141f7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,8 @@
 language: go
 
 go:
-  - '1.12'
   - '1.13'
-  - '1.14'
+  - '1.14.2'
 
 go_import_path: github.com/prebid/prebid-server
 
diff --git a/Dockerfile b/Dockerfile
index a8fea9c3..2c60b9e3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,8 +3,8 @@ RUN apt-get update && \
     apt-get -y upgrade && \
     apt-get install -y wget
 RUN cd /tmp && \
-    wget https://dl.google.com/go/go1.12.7.linux-amd64.tar.gz && \
-    tar -xf go1.12.7.linux-amd64.tar.gz && \
+    wget https://dl.google.com/go/go1.14.2.linux-amd64.tar.gz && \
+    tar -xf go1.14.2.linux-amd64.tar.gz && \
     mv go /usr/local
 RUN mkdir -p /app/prebid-server/
 WORKDIR /app/prebid-server/
diff --git a/README.md b/README.md
index a59bf5f6..b69e7e76 100644
--- a/README.md
+++ b/README.md
@@ -18,9 +18,10 @@ For more information, see:
 
 ## Installation
 
-First install [Go 1.12](https://golang.org/doc/install) latest version.
+First install [Go](https://golang.org/doc/install) version 1.13 or newer.
+
 Note that prebid-server is using [Go modules](https://blog.golang.org/using-go-modules).
-If using Go version <1.13 and are inside GOPATH `GO111MODULE` needs to be set to `GO111MODULE=on`.
+We officially support the most recent two major versions of the Go runtime. However, if you'd like to use a version <1.13 and are inside GOPATH `GO111MODULE` needs to be set to `GO111MODULE=on`.
 
 Download and prepare Prebid Server:
 
diff --git a/adapters/consumable/consumable.go b/adapters/consumable/consumable.go
index 1fa23377..243f1b80 100644
--- a/adapters/consumable/consumable.go
+++ b/adapters/consumable/consumable.go
@@ -268,8 +268,11 @@ func (a *ConsumableAdapter) MakeBids(
 			//bid.referrer = utils.getTopWindowUrl();
 
 			bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{
-				Bid:     &bid,
-				BidType: getMediaTypeForImp(getImp(bid.ImpID, internalRequest.Imp)),
+				Bid: &bid,
+				// Consumable units are always HTML, never VAST.
+				// From Prebid's point of view, this means that Consumable units
+				// are always "banners".
+				BidType: openrtb_ext.BidTypeBanner,
 			})
 		}
 	}
@@ -303,16 +306,6 @@ func extractExtensions(impression openrtb.Imp) (*adapters.ExtImpBidder, *openrtb
 	return &bidderExt, &consumableExt, nil
 }
 
-func getMediaTypeForImp(imp *openrtb.Imp) openrtb_ext.BidType {
-	// TODO: Whatever logic we need here possibly as follows - may always be Video when we bid
-	if imp.Banner != nil {
-		return openrtb_ext.BidTypeBanner
-	} else if imp.Video != nil {
-		return openrtb_ext.BidTypeVideo
-	}
-	return openrtb_ext.BidTypeVideo
-}
-
 func testConsumableBidder(testClock instant, endpoint string) *ConsumableAdapter {
 	return &ConsumableAdapter{testClock, endpoint}
 }
diff --git a/config/config.go b/config/config.go
index 56b6a1ba..0f470c6a 100755
--- a/config/config.go
+++ b/config/config.go
@@ -49,6 +49,7 @@ type Configuration struct {
 	AMPTimeoutAdjustment int64              `mapstructure:"amp_timeout_adjustment_ms"`
 	GDPR                 GDPR               `mapstructure:"gdpr"`
 	CCPA                 CCPA               `mapstructure:"ccpa"`
+	LMT                  LMT                `mapstructure:"lmt"`
 	CurrencyConverter    CurrencyConverter  `mapstructure:"currency_converter"`
 	DefReqConfig         DefReqConfig       `mapstructure:"default_request"`
 
@@ -139,6 +140,13 @@ func (cfg *AuctionTimeouts) LimitAuctionTimeout(requested time.Duration) time.Du
 	return requested
 }
 
+// Privacy is a grouping of privacy related configs to assist in dependency injection.
+type Privacy struct {
+	CCPA CCPA
+	GDPR GDPR
+	LMT  LMT
+}
+
 type GDPR struct {
 	HostVendorID            int          `mapstructure:"host_vendor_id"`
 	UsersyncIfAmbiguous     bool         `mapstructure:"usersync_if_ambiguous"`
@@ -193,6 +201,10 @@ type CCPA struct {
 	Enforce bool `mapstructure:"enforce"`
 }
 
+type LMT struct {
+	Enforce bool `mapstructure:"enforce"`
+}
+
 type Analytics struct {
 	File FileLogs `mapstructure:"file"`
 }
@@ -836,6 +848,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("gdpr.tcf2.purpose_one_treatement.access_allowed", true)
 	v.SetDefault("gdpr.amp_exception", false)
 	v.SetDefault("ccpa.enforce", false)
+	v.SetDefault("lmt.enforce", true)
 	v.SetDefault("currency_converter.fetch_url", "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json")
 	v.SetDefault("currency_converter.fetch_interval_seconds", 1800) // fetch currency rates every 30 minutes
 	v.SetDefault("default_request.type", "")
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 586481dd..2dcd572c 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -407,31 +407,34 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope
 }
 
 func makeFormatReplacement(overrideWidth uint64, overrideHeight uint64, width uint64, height uint64, multisize string) []openrtb.Format {
+	var formats []openrtb.Format
 	if overrideWidth != 0 && overrideHeight != 0 {
-		return []openrtb.Format{{
+		formats = []openrtb.Format{{
 			W: overrideWidth,
 			H: overrideHeight,
 		}}
 	} else if overrideWidth != 0 && height != 0 {
-		return []openrtb.Format{{
+		formats = []openrtb.Format{{
 			W: overrideWidth,
 			H: height,
 		}}
 	} else if width != 0 && overrideHeight != 0 {
-		return []openrtb.Format{{
+		formats = []openrtb.Format{{
 			W: width,
 			H: overrideHeight,
 		}}
-	} else if parsedSizes := parseMultisize(multisize); len(parsedSizes) != 0 {
-		return parsedSizes
 	} else if width != 0 && height != 0 {
-		return []openrtb.Format{{
+		formats = []openrtb.Format{{
 			W: width,
 			H: height,
 		}}
 	}
 
-	return nil
+	if parsedSizes := parseMultisize(multisize); len(parsedSizes) != 0 {
+		formats = append(formats, parsedSizes...)
+	}
+
+	return formats
 }
 
 func setWidths(formats []openrtb.Format, width uint64) {
diff --git a/exchange/bidder.go b/exchange/bidder.go
index f9b4a522..df9f0a3b 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -389,7 +389,20 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou
 			}
 		} else {
 			bidder.me.RecordTimeoutNotice(false)
+			if bidder.DebugConfig.TimeoutNotification.Log {
+				msg := fmt.Sprintf("TimeoutNotification: Failed to make timeout request: method(%s), uri(%s), error(%s)", toReq.Method, toReq.Uri, err.Error())
+				util.LogRandomSample(msg, glog.Warningf, bidder.DebugConfig.TimeoutNotification.SamplingRate)
+			}
+		}
+	} else if bidder.DebugConfig.TimeoutNotification.Log {
+		reqJSON, err := json.Marshal(req)
+		var msg string
+		if err == nil {
+			msg = fmt.Sprintf("TimeoutNotification: Failed to generate timeout request: error(%s), bidder request(%s)", errL[0].Error(), string(reqJSON))
+		} else {
+			msg = fmt.Sprintf("TimeoutNotification: Failed to generate timeout request: error(%s), bidder request marshal failed(%s)", errL[0].Error(), err.Error())
 		}
+		util.LogRandomSample(msg, glog.Warningf, bidder.DebugConfig.TimeoutNotification.SamplingRate)
 	}
 
 }
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 660beb64..84ae35d6 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -48,7 +48,7 @@ type exchange struct {
 	currencyConverter   *currencies.RateConverter
 	UsersyncIfAmbiguous bool
 	defaultTTLs         config.DefaultTTLs
-	enforceCCPA         bool
+	privacyConfig       config.Privacy
 }
 
 // Container to pass out response ext data from the GetAllBids goroutines back into the main thread
@@ -77,7 +77,11 @@ func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *con
 	e.currencyConverter = currencyConverter
 	e.UsersyncIfAmbiguous = cfg.GDPR.UsersyncIfAmbiguous
 	e.defaultTTLs = cfg.CacheURL.DefaultTTLs
-	e.enforceCCPA = cfg.CCPA.Enforce
+	e.privacyConfig = config.Privacy{
+		CCPA: cfg.CCPA,
+		GDPR: cfg.GDPR,
+		LMT:  cfg.LMT,
+	}
 	return e
 }
 
@@ -100,7 +104,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 
 	// Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder
 	blabels := make(map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels)
-	cleanRequests, aliases, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.enforceCCPA)
+	cleanRequests, aliases, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.privacyConfig)
 
 	// List of bidders we have requests for.
 	liveAdapters := listBiddersWithRequests(cleanRequests)
diff --git a/exchange/utils.go b/exchange/utils.go
index f602d1e8..54122d13 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -8,11 +8,13 @@ import (
 
 	"github.com/buger/jsonparser"
 	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbsmetrics"
 	"github.com/prebid/prebid-server/privacy"
 	"github.com/prebid/prebid-server/privacy/ccpa"
+	"github.com/prebid/prebid-server/privacy/lmt"
 )
 
 // cleanOpenRTBRequests splits the input request into requests which are sanitized for each bidder. Intended behavior is:
@@ -26,8 +28,8 @@ func cleanOpenRTBRequests(ctx context.Context,
 	blables map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels,
 	labels pbsmetrics.Labels,
 	gDPR gdpr.Permissions,
-	usersyncIfAmbiguous,
-	enforceCCPA bool) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, errs []error) {
+	usersyncIfAmbiguous bool,
+	privacyConfig config.Privacy) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, errs []error) {
 
 	impsByBidder, errs := splitImps(orig.Imp)
 	if len(errs) > 0 {
@@ -45,15 +47,24 @@ func cleanOpenRTBRequests(ctx context.Context,
 	consent := extractConsent(orig)
 	ampGDPRException := (labels.RType == pbsmetrics.ReqTypeAMP) && gDPR.AMPException()
 
-	privacyEnforcement := privacy.Enforcement{
-		COPPA: orig.Regs != nil && orig.Regs.COPPA == 1,
+	var ccpaPolicy ccpa.Policy
+	if privacyConfig.CCPA.Enforce {
+		ccpaPolicy, _ = ccpa.ReadPolicy(orig)
+	}
+
+	var lmtPolicy lmt.Policy
+	if privacyConfig.LMT.Enforce {
+		lmtPolicy = lmt.ReadPolicy(orig)
 	}
 
-	if enforceCCPA {
-		ccpaPolicy, _ := ccpa.ReadPolicy(orig)
-		privacyEnforcement.CCPA = ccpaPolicy.ShouldEnforce()
+	// request level privacy policies
+	privacyEnforcement := privacy.Enforcement{
+		CCPA:  ccpaPolicy.ShouldEnforce(),
+		COPPA: orig.Regs != nil && orig.Regs.COPPA == 1,
+		LMT:   lmtPolicy.ShouldEnforce(),
 	}
 
+	// bidder level privacy policies
 	for bidder, bidReq := range requestsByBidder {
 
 		if gdpr == 1 {
diff --git a/go.mod b/go.mod
index 0224057e..72bb9b74 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/prebid/prebid-server
 
-go 1.12
+go 1.13
 
 require (
 	github.com/BurntSushi/toml v0.3.1 // indirect
diff --git a/privacy/enforcement.go b/privacy/enforcement.go
index d302192e..8a5d201f 100644
--- a/privacy/enforcement.go
+++ b/privacy/enforcement.go
@@ -10,11 +10,12 @@ type Enforcement struct {
 	COPPA   bool
 	GDPR    bool
 	GDPRGeo bool
+	LMT     bool
 }
 
 // Any returns true if at least one privacy policy requires enforcement.
 func (e Enforcement) Any() bool {
-	return e.CCPA || e.COPPA || e.GDPR || e.GDPRGeo
+	return e.CCPA || e.COPPA || e.GDPR || e.GDPRGeo || e.LMT
 }
 
 // Apply cleans personally identifiable information from an OpenRTB bid request.
@@ -34,7 +35,7 @@ func (e Enforcement) getIPv6ScrubStrategy() ScrubStrategyIPV6 {
 		return ScrubStrategyIPV6Lowest32
 	}
 
-	if e.GDPR || e.CCPA {
+	if e.GDPR || e.CCPA || e.LMT {
 		return ScrubStrategyIPV6Lowest16
 	}
 
@@ -46,7 +47,7 @@ func (e Enforcement) getGeoScrubStrategy() ScrubStrategyGeo {
 		return ScrubStrategyGeoFull
 	}
 
-	if e.GDPRGeo || e.CCPA {
+	if e.GDPRGeo || e.CCPA || e.LMT {
 		return ScrubStrategyGeoReducedPrecision
 	}
 
@@ -63,7 +64,7 @@ func (e Enforcement) getUserScrubStrategy(ampGDPRException bool) ScrubStrategyUs
 	}
 
 	// If no user scrubbing is needed, then return none, else scrub ID (COPPA checked above)
-	if e.CCPA || e.GDPR {
+	if e.CCPA || e.GDPR || e.LMT {
 		return ScrubStrategyUserID
 	}
 
diff --git a/privacy/lmt/policy.go b/privacy/lmt/policy.go
new file mode 100644
index 00000000..79425bf5
--- /dev/null
+++ b/privacy/lmt/policy.go
@@ -0,0 +1,33 @@
+package lmt
+
+import (
+	"github.com/mxmCherry/openrtb"
+)
+
+const (
+	trackingUnrestricted = 0
+	trackingRestricted   = 1
+)
+
+// Policy represents the LMT (Limit Ad Tracking) policy for an OpenRTB bid request.
+type Policy struct {
+	Signal         int
+	SignalProvided bool
+}
+
+// ReadPolicy extracts the LMT (Limit Ad Tracking) policy from an OpenRTB bid request.
+func ReadPolicy(req *openrtb.BidRequest) Policy {
+	policy := Policy{}
+
+	if req != nil && req.Device != nil && req.Device.Lmt != nil {
+		policy.Signal = int(*req.Device.Lmt)
+		policy.SignalProvided = true
+	}
+
+	return policy
+}
+
+// ShouldEnforce returns true when the LMT (Limit Ad Tracking) policy is in effect.
+func (p Policy) ShouldEnforce() bool {
+	return p.SignalProvided && p.Signal == trackingRestricted
+}


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index ee8e68e7..2b291fe9 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -43,6 +43,8 @@ gdpr:
   non_standard_publishers: ["siteID","fake-site-id","appID","agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA"]
 ccpa:
   enforce: true
+lmt:
+  enforce: true
 host_cookie:
   cookie_name: userid
   family: prebid
@@ -240,6 +242,7 @@ func TestFullConfig(t *testing.T) {
 	cmpBools(t, "cfg.GDPR.NonStandardPublisherMap", found, false)
 
 	cmpBools(t, "ccpa.enforce", cfg.CCPA.Enforce, true)
+	cmpBools(t, "lmt.enforce", cfg.LMT.Enforce, true)
 
 	//Assert the NonStandardPublishers was correctly unmarshalled
 	cmpStrings(t, "blacklisted_apps", cfg.BlacklistedApps[0], "spamAppID")
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 289db3f4..731fd55e 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -832,6 +832,24 @@ func TestMultisize(t *testing.T) {
 	}.execute(t)
 }
 
+func TestSizeWithMultisize(t *testing.T) {
+	formatOverrideSpec{
+		width:     20,
+		height:    40,
+		multisize: "200x50,100x60",
+		expect: []openrtb.Format{{
+			W: 20,
+			H: 40,
+		}, {
+			W: 200,
+			H: 50,
+		}, {
+			W: 100,
+			H: 60,
+		}},
+	}.execute(t)
+}
+
 func TestHeightOnly(t *testing.T) {
 	formatOverrideSpec{
 		height: 200,
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index fa04e6a4..fff397f0 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -1229,6 +1229,64 @@ func TestSetAssetTypes(t *testing.T) {
 	}
 }
 
+func TestTimeoutNotificationOff(t *testing.T) {
+	respBody := "{\"bid\":false}"
+	respStatus := 200
+	server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody))
+	defer server.Close()
+
+	bidderImpl := &notifingBidder{
+		notiRequest: adapters.RequestData{
+			Method:  "GET",
+			Uri:     server.URL + "/notify/me",
+			Body:    nil,
+			Headers: http.Header{},
+		},
+	}
+	bidder := &bidderAdapter{
+		Bidder:      bidderImpl,
+		Client:      server.Client(),
+		DebugConfig: config.Debug{},
+		me:          &metricsConfig.DummyMetricsEngine{},
+	}
+	if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); !ok {
+		t.Error("Failed to cast bidder to a TimeoutBidder")
+	} else {
+		bidder.doTimeoutNotification(tb, &adapters.RequestData{})
+	}
+}
+
+func TestTimeoutNotificationOn(t *testing.T) {
+	respBody := "{\"bid\":false}"
+	respStatus := 200
+	server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody))
+	defer server.Close()
+
+	bidderImpl := &notifingBidder{
+		notiRequest: adapters.RequestData{
+			Method:  "GET",
+			Uri:     server.URL + "/notify/me",
+			Body:    nil,
+			Headers: http.Header{},
+		},
+	}
+	bidder := &bidderAdapter{
+		Bidder: bidderImpl,
+		Client: server.Client(),
+		DebugConfig: config.Debug{
+			TimeoutNotification: config.TimeoutNotification{
+				Log: true,
+			},
+		},
+		me: &metricsConfig.DummyMetricsEngine{},
+	}
+	if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); !ok {
+		t.Error("Failed to cast bidder to a TimeoutBidder")
+	} else {
+		bidder.doTimeoutNotification(tb, &adapters.RequestData{})
+	}
+}
+
 type goodSingleBidder struct {
 	bidRequest   *openrtb.BidRequest
 	httpRequest  *adapters.RequestData
@@ -1302,3 +1360,19 @@ func (bidder *bidRejector) MakeBids(internalRequest *openrtb.BidRequest, externa
 	bidder.httpResponse = response
 	return nil, []error{errors.New("Can't make a response.")}
 }
+
+type notifingBidder struct {
+	notiRequest adapters.RequestData
+}
+
+func (bidder *notifingBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	return nil, nil
+}
+
+func (bidder *notifingBidder) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	return nil, nil
+}
+
+func (bidder *notifingBidder) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error) {
+	return &bidder.notiRequest, nil
+}
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index e9b2127e..4f329962 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -731,7 +731,17 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 	if len(errs) != 0 {
 		t.Fatalf("%s: Failed to parse aliases", filename)
 	}
-	ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, spec.EnforceCCPA)
+
+	privacyConfig := config.Privacy{
+		CCPA: config.CCPA{
+			Enforce: spec.EnforceCCPA,
+		},
+		LMT: config.LMT{
+			Enforce: spec.EnforceLMT,
+		},
+	}
+
+	ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, privacyConfig)
 	biddersInAuction := findBiddersInAuction(t, filename, &spec.IncomingRequest.OrtbRequest)
 	categoriesFetcher, error := newCategoryFetcher("./test/category-mapping")
 	if error != nil {
@@ -816,7 +826,7 @@ func extractResponseTimes(t *testing.T, context string, bid *openrtb.BidResponse
 	}
 }
 
-func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, enforceCCPA bool) Exchange {
+func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, privacyConfig config.Privacy) Exchange {
 	adapters := make(map[openrtb_ext.BidderName]adaptedBidder)
 	for _, bidderName := range openrtb_ext.BidderMap {
 		if spec, ok := expectations[string(bidderName)]; ok {
@@ -854,7 +864,7 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
 		gDPR:                gdpr.AlwaysAllow{},
 		currencyConverter:   currencies.NewRateConverterDefault(),
 		UsersyncIfAmbiguous: false,
-		enforceCCPA:         enforceCCPA,
+		privacyConfig:       privacyConfig,
 	}
 }
 
@@ -1620,6 +1630,7 @@ type exchangeSpec struct {
 	OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"`
 	Response         exchangeResponse       `json:"response,omitempty"`
 	EnforceCCPA      bool                   `json:"enforceCcpa"`
+	EnforceLMT       bool                   `json:"enforceLmt"`
 	DebugLog         *DebugLog              `json:"debuglog,omitempty"`
 }
 
diff --git a/exchange/exchangetest/lmt-featureflag-off.json b/exchange/exchangetest/lmt-featureflag-off.json
new file mode 100644
index 00000000..9a15c879
--- /dev/null
+++ b/exchange/exchangetest/lmt-featureflag-off.json
@@ -0,0 +1,63 @@
+{
+    "enforceLmt": false,
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [{
+                "id": "my-imp-id",
+                "video": {
+                    "mimes": ["video/mp4"]
+                },
+                "ext": {
+                    "appnexus": {
+                        "placementId": 1
+                    }
+                }
+            }],
+            "device": {
+                "lmt": 1
+            },
+            "user": {
+                "id": "some-id",
+                "buyeruid": "some-buyer-id"
+            }
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "my-imp-id",
+                        "video": {
+                            "mimes": ["video/mp4"]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": 1
+                            }
+                        }
+                    }],
+                    "device": {
+                        "lmt": 1
+                    },
+                    "user": {
+                        "id": "some-id",
+                        "buyeruid": "some-buyer-id"
+                    }
+                },
+                "bidAdjustment": 1.0
+            },
+            "mockResponse": {
+                "errors": ["appnexus-error"]
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/lmt-featureflag-on.json b/exchange/exchangetest/lmt-featureflag-on.json
new file mode 100644
index 00000000..440f8c76
--- /dev/null
+++ b/exchange/exchangetest/lmt-featureflag-on.json
@@ -0,0 +1,61 @@
+{
+    "enforceLmt": true,
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [{
+                "id": "my-imp-id",
+                "video": {
+                    "mimes": ["video/mp4"]
+                },
+                "ext": {
+                    "appnexus": {
+                        "placementId": 1
+                    }
+                }
+            }],
+            "device": {
+                "lmt": 1
+            },
+            "user": {
+                "id": "some-id",
+                "buyeruid": "some-buyer-id"
+            }
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "my-imp-id",
+                        "video": {
+                            "mimes": ["video/mp4"]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": 1
+                            }
+                        }
+                    }],
+                    "device": {
+                        "lmt": 1
+                    },
+                    "user": {
+                    }
+                },
+                "bidAdjustment": 1.0
+            },
+            "mockResponse": {
+                "errors": ["appnexus-error"]
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index acbf25ff..4dad3f54 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -6,6 +6,7 @@ import (
 	"testing"
 
 	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbsmetrics"
 	"github.com/stretchr/testify/assert"
@@ -69,8 +70,17 @@ func TestCleanOpenRTBRequests(t *testing.T) {
 			applyCOPPA: false, consentedVendors: map[string]bool{"appnexus": true, "brightroll": true}},
 	}
 
+	privacyConfig := config.Privacy{
+		CCPA: config.CCPA{
+			Enforce: true,
+		},
+		LMT: config.LMT{
+			Enforce: true,
+		},
+	}
+
 	for _, test := range testCases {
-		reqByBidders, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, true)
+		reqByBidders, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
 		if test.hasError {
 			assert.NotNil(t, err, "Error shouldn't be nil")
 		} else {
@@ -99,9 +109,80 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		req := newCCPABidRequest(t)
+		req := newBidRequest(t)
+		req.Regs = &openrtb.Regs{
+			Ext: json.RawMessage(`{"us_privacy":"1-Y-"}`),
+		}
 
-		results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, test.enforceCCPA)
+		privacyConfig := config.Privacy{
+			CCPA: config.CCPA{
+				Enforce: test.enforceCCPA,
+			},
+		}
+
+		results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
+		result := results["appnexus"]
+
+		assert.Nil(t, errs)
+
+		if test.expectDataScrub {
+			assert.Equal(t, result.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.Equal(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5")
+		} else {
+			assert.NotEqual(t, result.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.NotEqual(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5")
+		}
+	}
+}
+
+func TestCleanOpenRTBRequestsLMT(t *testing.T) {
+	var (
+		enabled  int8 = 1
+		disabled int8 = 0
+	)
+	testCases := []struct {
+		description     string
+		lmt             *int8
+		enforceLMT      bool
+		expectDataScrub bool
+	}{
+		{
+			description:     "Feature Flag Enabled - OpenTRB Enabled",
+			lmt:             &enabled,
+			enforceLMT:      true,
+			expectDataScrub: true,
+		},
+		{
+			description:     "Feature Flag Disabled - OpenTRB Enabled",
+			lmt:             &enabled,
+			enforceLMT:      false,
+			expectDataScrub: false,
+		},
+		{
+			description:     "Feature Flag Enabled - OpenTRB Disabled",
+			lmt:             &disabled,
+			enforceLMT:      true,
+			expectDataScrub: false,
+		},
+		{
+			description:     "Feature Flag Disabled - OpenTRB Disabled",
+			lmt:             &disabled,
+			enforceLMT:      false,
+			expectDataScrub: false,
+		},
+	}
+
+	for _, test := range testCases {
+		req := newBidRequest(t)
+		req.Device.Lmt = test.lmt
+
+		privacyConfig := config.Privacy{
+			LMT: config.LMT{
+				Enforce: test.enforceLMT,
+			},
+		}
+
+		results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
 		result := results["appnexus"]
 
 		assert.Nil(t, errs)
@@ -163,8 +244,7 @@ func newAdapterAliasBidRequest(t *testing.T) *openrtb.BidRequest {
 	}
 }
 
-func newCCPABidRequest(t *testing.T) *openrtb.BidRequest {
-	dnt := int8(1)
+func newBidRequest(t *testing.T) *openrtb.BidRequest {
 	return &openrtb.BidRequest{
 		Site: &openrtb.Site{
 			Page:   "www.some.domain.com",
@@ -178,7 +258,6 @@ func newCCPABidRequest(t *testing.T) *openrtb.BidRequest {
 			UA:       "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
 			IFA:      "ifa",
 			IP:       "132.173.230.74",
-			DNT:      &dnt,
 			Language: "EN",
 		},
 		Source: &openrtb.Source{
@@ -189,9 +268,6 @@ func newCCPABidRequest(t *testing.T) *openrtb.BidRequest {
 			BuyerUID: "their-id",
 			Ext:      json.RawMessage(`{"digitrust":{"id":"digi-id","keyv":1,"pref":1}}`),
 		},
-		Regs: &openrtb.Regs{
-			Ext: json.RawMessage(`{"us_privacy":"1-Y-"}`),
-		},
 		Imp: []openrtb.Imp{{
 			ID: "some-imp-id",
 			Banner: &openrtb.Banner{
diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go
index 0e82648d..968c6354 100644
--- a/privacy/enforcement_test.go
+++ b/privacy/enforcement_test.go
@@ -21,6 +21,7 @@ func TestAny(t *testing.T) {
 				COPPA:   false,
 				GDPR:    false,
 				GDPRGeo: false,
+				LMT:     false,
 			},
 			expected: false,
 		},
@@ -31,6 +32,7 @@ func TestAny(t *testing.T) {
 				COPPA:   true,
 				GDPR:    true,
 				GDPRGeo: true,
+				LMT:     true,
 			},
 			expected: true,
 		},
@@ -41,16 +43,7 @@ func TestAny(t *testing.T) {
 				COPPA:   true,
 				GDPR:    false,
 				GDPRGeo: false,
-			},
-			expected: true,
-		},
-		{
-			description: "GDPRGeo only",
-			enforcement: Enforcement{
-				CCPA:    false,
-				COPPA:   false,
-				GDPR:    false,
-				GDPRGeo: true,
+				LMT:     true,
 			},
 			expected: true,
 		},
@@ -79,6 +72,7 @@ func TestApply(t *testing.T) {
 				COPPA:   true,
 				GDPR:    true,
 				GDPRGeo: true,
+				LMT:     true,
 			},
 			ampGDPRException:   false,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest32,
@@ -93,6 +87,7 @@ func TestApply(t *testing.T) {
 				COPPA:   false,
 				GDPR:    false,
 				GDPRGeo: false,
+				LMT:     false,
 			},
 			ampGDPRException:   false,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest16,
@@ -107,6 +102,7 @@ func TestApply(t *testing.T) {
 				COPPA:   true,
 				GDPR:    false,
 				GDPRGeo: false,
+				LMT:     false,
 			},
 			ampGDPRException:   false,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest32,
@@ -121,6 +117,7 @@ func TestApply(t *testing.T) {
 				COPPA:   false,
 				GDPR:    true,
 				GDPRGeo: true,
+				LMT:     false,
 			},
 			ampGDPRException:   false,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest16,
@@ -135,6 +132,7 @@ func TestApply(t *testing.T) {
 				COPPA:   false,
 				GDPR:    true,
 				GDPRGeo: true,
+				LMT:     false,
 			},
 			ampGDPRException:   true,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest16,
@@ -149,6 +147,7 @@ func TestApply(t *testing.T) {
 				COPPA:   false,
 				GDPR:    false,
 				GDPRGeo: false,
+				LMT:     false,
 			},
 			ampGDPRException:   true,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest16,
@@ -163,6 +162,7 @@ func TestApply(t *testing.T) {
 				COPPA:   true,
 				GDPR:    true,
 				GDPRGeo: true,
+				LMT:     false,
 			},
 			ampGDPRException:   true,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest32,
@@ -177,6 +177,7 @@ func TestApply(t *testing.T) {
 				COPPA:   false,
 				GDPR:    true,
 				GDPRGeo: false,
+				LMT:     false,
 			},
 			ampGDPRException:   false,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest16,
@@ -191,6 +192,7 @@ func TestApply(t *testing.T) {
 				COPPA:   false,
 				GDPR:    false,
 				GDPRGeo: true,
+				LMT:     false,
 			},
 			ampGDPRException:   false,
 			expectedDeviceIPv6: ScrubStrategyIPV6None,
@@ -198,6 +200,36 @@ func TestApply(t *testing.T) {
 			expectedUser:       ScrubStrategyUserNone,
 			expectedUserGeo:    ScrubStrategyGeoReducedPrecision,
 		},
+		{
+			description: "LMT Only",
+			enforcement: Enforcement{
+				CCPA:    false,
+				COPPA:   false,
+				GDPR:    false,
+				GDPRGeo: false,
+				LMT:     true,
+			},
+			ampGDPRException:   false,
+			expectedDeviceIPv6: ScrubStrategyIPV6Lowest16,
+			expectedDeviceGeo:  ScrubStrategyGeoReducedPrecision,
+			expectedUser:       ScrubStrategyUserID,
+			expectedUserGeo:    ScrubStrategyGeoReducedPrecision,
+		},
+		{
+			description: "LMT Only, ampGDPRException",
+			enforcement: Enforcement{
+				CCPA:    false,
+				COPPA:   false,
+				GDPR:    false,
+				GDPRGeo: false,
+				LMT:     true,
+			},
+			ampGDPRException:   true,
+			expectedDeviceIPv6: ScrubStrategyIPV6Lowest16,
+			expectedDeviceGeo:  ScrubStrategyGeoReducedPrecision,
+			expectedUser:       ScrubStrategyUserID,
+			expectedUserGeo:    ScrubStrategyGeoReducedPrecision,
+		},
 	}
 
 	for _, test := range testCases {
@@ -229,6 +261,7 @@ func TestApplyNoneApplicable(t *testing.T) {
 		CCPA:  false,
 		COPPA: false,
 		GDPR:  false,
+		LMT:   false,
 	}
 	enforcement.apply(req, false, m)
 
diff --git a/privacy/lmt/policy_test.go b/privacy/lmt/policy_test.go
new file mode 100644
index 00000000..45de219a
--- /dev/null
+++ b/privacy/lmt/policy_test.go
@@ -0,0 +1,128 @@
+package lmt
+
+import (
+	"testing"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRead(t *testing.T) {
+	var one int8 = 1
+
+	testCases := []struct {
+		description    string
+		request        *openrtb.BidRequest
+		expectedPolicy Policy
+	}{
+		{
+			description: "Nil Request",
+			request:     nil,
+			expectedPolicy: Policy{
+				Signal:         0,
+				SignalProvided: false,
+			},
+		},
+		{
+			description: "Nil Device",
+			request: &openrtb.BidRequest{
+				Device: nil,
+			},
+			expectedPolicy: Policy{
+				Signal:         0,
+				SignalProvided: false,
+			},
+		},
+		{
+			description: "Nil Device.Lmt",
+			request: &openrtb.BidRequest{
+				Device: &openrtb.Device{
+					Lmt: nil,
+				},
+			},
+			expectedPolicy: Policy{
+				Signal:         0,
+				SignalProvided: false,
+			},
+		},
+		{
+			description: "Enabled",
+			request: &openrtb.BidRequest{
+				Device: &openrtb.Device{
+					Lmt: &one,
+				},
+			},
+			expectedPolicy: Policy{
+				Signal:         1,
+				SignalProvided: true,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		p := ReadPolicy(test.request)
+		assert.Equal(t, test.expectedPolicy, p, test.description)
+	}
+}
+
+func TestShouldEnforce(t *testing.T) {
+	testCases := []struct {
+		description string
+		policy      Policy
+		expected    bool
+	}{
+		{
+			description: "Signal Not Provided - Zero",
+			policy: Policy{
+				Signal:         0,
+				SignalProvided: false,
+			},
+			expected: false,
+		},
+		{
+			description: "Signal Not Provided - One",
+			policy: Policy{
+				Signal:         1,
+				SignalProvided: false,
+			},
+			expected: false,
+		},
+		{
+			description: "Signal Not Provided - Other",
+			policy: Policy{
+				Signal:         42,
+				SignalProvided: false,
+			},
+			expected: false,
+		},
+		{
+			description: "Signal Provided - Zero",
+			policy: Policy{
+				Signal:         0,
+				SignalProvided: true,
+			},
+			expected: false,
+		},
+		{
+			description: "Signal Provided - One",
+			policy: Policy{
+				Signal:         1,
+				SignalProvided: true,
+			},
+			expected: true,
+		},
+		{
+			description: "Signal Provided - Other",
+			policy: Policy{
+				Signal:         42,
+				SignalProvided: true,
+			},
+			expected: false,
+		},
+	}
+
+	for _, test := range testCases {
+		result := test.policy.ShouldEnforce()
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}