diff --git a/config/config.go b/config/config.go index 56b6a1ba88d..0f470c6a611 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/config/config_test.go b/config/config_test.go index ee8e68e7025..2b291fe978d 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/exchange/exchange.go b/exchange/exchange.go index 660beb641ef..84ae35d644c 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/exchange_test.go b/exchange/exchange_test.go index e9b2127e18b..4f329962a53 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 00000000000..9a15c87953e --- /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 00000000000..440f8c76472 --- /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.go b/exchange/utils.go index f602d1e8fba..54122d13c09 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/exchange/utils_test.go b/exchange/utils_test.go index acbf25ff691..4dad3f54648 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.go b/privacy/enforcement.go index d302192ec3f..8a5d201fc95 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/enforcement_test.go b/privacy/enforcement_test.go index 0e82648d4b9..968c6354710 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.go b/privacy/lmt/policy.go new file mode 100644 index 00000000000..79425bf59f7 --- /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 +} diff --git a/privacy/lmt/policy_test.go b/privacy/lmt/policy_test.go new file mode 100644 index 00000000000..45de219a9bf --- /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) + } +}