Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TransmitUserFPD and TransmitPreciseGeo activity integration #2906

Merged
merged 11 commits into from
Jul 27, 2023
81 changes: 54 additions & 27 deletions exchange/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,13 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
lmtEnforcer := extractLMT(req.BidRequest, rs.privacyConfig)

// request level privacy policies
privacyEnforcement := privacy.Enforcement{
COPPA: req.BidRequest.Regs != nil && req.BidRequest.Regs.COPPA == 1,
LMT: lmtEnforcer.ShouldEnforce(unknownBidder),
}
coppa := req.BidRequest.Regs != nil && req.BidRequest.Regs.COPPA == 1
lmt := lmtEnforcer.ShouldEnforce(unknownBidder)

privacyLabels.CCPAProvided = ccpaEnforcer.CanEnforce()
privacyLabels.CCPAEnforced = ccpaEnforcer.ShouldEnforce(unknownBidder)
privacyLabels.COPPAEnforced = privacyEnforcement.COPPA
privacyLabels.LMTEnforced = lmtEnforcer.ShouldEnforce(unknownBidder)
privacyLabels.COPPAEnforced = coppa
privacyLabels.LMTEnforced = lmt

var gdprEnforced bool
var gdprPerms gdpr.Permissions = &gdpr.AlwaysAllow{}
Expand Down Expand Up @@ -148,46 +146,75 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,

// bidder level privacy policies
for _, bidderRequest := range allBidderRequests {
bidRequestAllowed := true
privacyEnforcement := privacy.Enforcement{
COPPA: coppa,
LMT: lmt,
}

// fetchBids activity
fetchBidsActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityFetchBids,
privacy.ScopedName{Scope: privacy.ScopeTypeBidder, Name: bidderRequest.BidderName.String()})
scopedName := privacy.ScopedName{Scope: privacy.ScopeTypeBidder, Name: bidderRequest.BidderName.String()}
fetchBidsActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityFetchBids, scopedName)
if fetchBidsActivityAllowed == privacy.ActivityDeny {
// skip the call to a bidder if fetchBids activity is not allowed
// do not add this bidder to allowedBidderRequests
continue
}

// CCPA
privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String())
var auctionPermissions gdpr.AuctionPermissions
var gdprErr error

// GDPR
if gdprEnforced {
auctionPermissions, err := gdprPerms.AuctionActivitiesAllowed(ctx, bidderRequest.BidderCoreName, bidderRequest.BidderName)
bidRequestAllowed = auctionPermissions.AllowBidRequest

if err == nil {
privacyEnforcement.GDPRGeo = !auctionPermissions.PassGeo
privacyEnforcement.GDPRID = !auctionPermissions.PassID
} else {
privacyEnforcement.GDPRGeo = true
privacyEnforcement.GDPRID = true
auctionPermissions, gdprErr = gdprPerms.AuctionActivitiesAllowed(ctx, bidderRequest.BidderCoreName, bidderRequest.BidderName)
if !auctionPermissions.AllowBidRequest {
// auction request is not permitted by GDPR
// do not add this bidder to allowedBidderRequests
rs.me.RecordAdapterGDPRRequestBlocked(bidderRequest.BidderCoreName)
continue
}
}

if !bidRequestAllowed {
rs.me.RecordAdapterGDPRRequestBlocked(bidderRequest.BidderCoreName)
passIDActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitUserFPD, scopedName)
if passIDActivityAllowed == privacy.ActivityDeny {
privacyEnforcement.UFPD = true
} else {
// run existing policies (GDPR, CCPA, COPPA, LMT)
// potentially block passing IDs based on GDPR
if gdprEnforced {
if gdprErr == nil {
privacyEnforcement.GDPRID = !auctionPermissions.PassID
} else {
privacyEnforcement.GDPRID = true
}
}
// potentially block passing IDs based on CCPA
privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String())
}

passGeoActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitPreciseGeo, scopedName)
if passGeoActivityAllowed == privacy.ActivityDeny {
privacyEnforcement.PreciseGeo = true
} else {
// run existing policies (GDPR, CCPA, COPPA, LMT)
// potentially block passing geo based on GDPR
if gdprEnforced {
if gdprErr == nil {
privacyEnforcement.GDPRGeo = !auctionPermissions.PassGeo
} else {
privacyEnforcement.GDPRGeo = true
}
}
// potentially block passing geo based on CCPA
privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String())

}

if auctionReq.FirstPartyData != nil && auctionReq.FirstPartyData[bidderRequest.BidderName] != nil {
applyFPD(auctionReq.FirstPartyData[bidderRequest.BidderName], bidderRequest.BidRequest)
}

if bidRequestAllowed {
privacyEnforcement.Apply(bidderRequest.BidRequest)
allowedBidderRequests = append(allowedBidderRequests, bidderRequest)
}
privacyEnforcement.Apply(bidderRequest.BidRequest)
allowedBidderRequests = append(allowedBidderRequests, bidderRequest)

// GPP downgrade: always downgrade unless we can confirm GPP is supported
if shouldSetLegacyPrivacy(rs.bidderInfo, string(bidderRequest.BidderCoreName)) {
setLegacyGDPRFromGPP(bidderRequest.BidRequest, gpp)
Expand Down
137 changes: 104 additions & 33 deletions exchange/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2296,6 +2296,7 @@ func TestCleanOpenRTBRequestsWithOpenRTBDowngrade(t *testing.T) {
bidReq.User.ID = ""
bidReq.User.BuyerUID = ""
bidReq.User.Yob = 0
bidReq.User.Geo = &openrtb2.Geo{Lat: 123.46}

downgradedRegs := *bidReq.Regs
downgradedUser := *bidReq.User
Expand Down Expand Up @@ -2610,6 +2611,7 @@ func newBidRequest(t *testing.T) *openrtb2.BidRequest {
BuyerUID: "their-id",
Yob: 1982,
Ext: json.RawMessage(`{}`),
Geo: &openrtb2.Geo{Lat: 123.456},
},
Imp: []openrtb2.Imp{{
ID: "some-imp-id",
Expand Down Expand Up @@ -4275,33 +4277,76 @@ func TestGetMediaTypeForBid(t *testing.T) {
}
}

func TemporarilyDisabledTestCleanOpenRTBRequestsActivitiesFetchBids(t *testing.T) {
func TestCleanOpenRTBRequestsActivitiesFetchBids(t *testing.T) {
testCases := []struct {
name string
req *openrtb2.BidRequest
componentName string
allow bool
expectedReqNumber int
name string
req *openrtb2.BidRequest
privacyConfig *config.AccountPrivacy
componentName string
allow bool
expectedReqNumber int
expectedUserYOB int64
expectedUserLat float64
expectedDeviceDIDMD5 string
}{
{
name: "request_with_one_bidder_allowed",
req: newBidRequest(t),
componentName: "appnexus",
allow: true,
expectedReqNumber: 1,
},
{
name: "request_with_one_bidder_not_allowed",
req: newBidRequest(t),
componentName: "appnexus",
allow: false,
expectedReqNumber: 0,
name: "fetch_bids_request_with_one_bidder_allowed",
req: newBidRequest(t),
privacyConfig: getFetchBidsActivityConfig("appnexus", true),
expectedReqNumber: 1,
expectedUserYOB: 1982,
expectedUserLat: 123.456,
expectedDeviceDIDMD5: "some device ID hash",
},
{
name: "fetch_bids_request_with_one_bidder_not_allowed",
req: newBidRequest(t),
privacyConfig: getFetchBidsActivityConfig("appnexus", false),
expectedReqNumber: 0,
expectedUserYOB: 1982,
expectedUserLat: 123.456,
expectedDeviceDIDMD5: "some device ID hash",
},
{
name: "transmit_ufpd_allowed",
req: newBidRequest(t),
privacyConfig: getTransmitUFPDActivityConfig("appnexus", true),
expectedReqNumber: 1,
expectedUserYOB: 1982,
expectedUserLat: 123.456,
expectedDeviceDIDMD5: "some device ID hash",
},
{
name: "transmit_ufpd_deny",
req: newBidRequest(t),
privacyConfig: getTransmitUFPDActivityConfig("appnexus", false),
expectedReqNumber: 1,
expectedUserYOB: 0,
expectedUserLat: 123.456,
expectedDeviceDIDMD5: "",
},
{
name: "transmit_precise_geo_allowed",
req: newBidRequest(t),
privacyConfig: getTransmitPreciseGeoActivityConfig("appnexus", true),
expectedReqNumber: 1,
expectedUserYOB: 1982,
expectedUserLat: 123.456,
expectedDeviceDIDMD5: "some device ID hash",
},
{
name: "transmit_precise_geo_deny",
req: newBidRequest(t),
privacyConfig: getTransmitPreciseGeoActivityConfig("appnexus", false),
expectedReqNumber: 1,
expectedUserYOB: 1982,
expectedUserLat: 123.46,
expectedDeviceDIDMD5: "some device ID hash",
},
}

for _, test := range testCases {
privacyConfig := getDefaultActivityConfig(test.componentName, test.allow)
activities, err := privacy.NewActivityControl(privacyConfig)
activities, err := privacy.NewActivityControl(test.privacyConfig)
assert.NoError(t, err, "")
auctionReq := AuctionRequest{
BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: test.req},
Expand All @@ -4321,25 +4366,51 @@ func TemporarilyDisabledTestCleanOpenRTBRequestsActivitiesFetchBids(t *testing.T
bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo)
assert.Empty(t, errs)
assert.Len(t, bidderRequests, test.expectedReqNumber)

if test.expectedReqNumber == 1 {
assert.Equal(t, test.expectedUserYOB, bidderRequests[0].BidRequest.User.Yob)
assert.Equal(t, test.expectedUserLat, bidderRequests[0].BidRequest.User.Geo.Lat)
assert.Equal(t, test.expectedDeviceDIDMD5, bidderRequests[0].BidRequest.Device.DIDMD5)
}
})
}
}

func getDefaultActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
return &config.AccountPrivacy{
AllowActivities: config.AllowActivities{
FetchBids: config.Activity{
Default: ptrutil.ToPtr(true),
Rules: []config.ActivityRule{
{
Allow: allow,
Condition: config.ActivityCondition{
ComponentName: []string{componentName},
ComponentType: []string{"bidder"},
},
},
func buildDefaultActivityConfig(componentName string, allow bool) config.Activity {
return config.Activity{
Default: ptrutil.ToPtr(true),
Rules: []config.ActivityRule{
{
Allow: allow,
Condition: config.ActivityCondition{
ComponentName: []string{componentName},
ComponentType: []string{"bidder"},
},
},
},
}
}

func getFetchBidsActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
return &config.AccountPrivacy{
AllowActivities: config.AllowActivities{
FetchBids: buildDefaultActivityConfig(componentName, allow),
},
}
}

func getTransmitUFPDActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
return &config.AccountPrivacy{
AllowActivities: config.AllowActivities{
TransmitUserFPD: buildDefaultActivityConfig(componentName, allow),
},
}
}

func getTransmitPreciseGeoActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
return &config.AccountPrivacy{
AllowActivities: config.AllowActivities{
TransmitPreciseGeo: buildDefaultActivityConfig(componentName, allow),
},
}
}
25 changes: 21 additions & 4 deletions privacy/enforcement.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,39 @@ type Enforcement struct {
GDPRGeo bool
GDPRID bool
LMT bool

// activities
UFPD bool
Eids bool
PreciseGeo bool
TID bool
}

// Any returns true if at least one privacy policy requires enforcement.
func (e Enforcement) Any() bool {
func (e Enforcement) AnyLegacy() bool {
return e.CCPA || e.COPPA || e.GDPRGeo || e.GDPRID || e.LMT
}

func (e Enforcement) AnyActivities() bool {
return e.UFPD || e.PreciseGeo || e.Eids || e.TID
}

// Apply cleans personally identifiable information from an OpenRTB bid request.
func (e Enforcement) Apply(bidRequest *openrtb2.BidRequest) {
e.apply(bidRequest, NewScrubber())
}

func (e Enforcement) apply(bidRequest *openrtb2.BidRequest, scrubber Scrubber) {
if bidRequest != nil && e.Any() {
bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getDeviceIDScrubStrategy(), e.getIPv4ScrubStrategy(), e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy())
bidRequest.User = scrubber.ScrubUser(bidRequest.User, e.getUserScrubStrategy(), e.getGeoScrubStrategy())
if bidRequest != nil {
if e.AnyActivities() {
bidRequest = scrubber.ScrubRequest(bidRequest, e)
}
if e.AnyLegacy() && !(e.UFPD && e.PreciseGeo && e.Eids) {
bidRequest.User = scrubber.ScrubUser(bidRequest.User, e.getUserScrubStrategy(), e.getGeoScrubStrategy())
}
if e.AnyLegacy() && !(e.UFPD && e.PreciseGeo) {
bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getDeviceIDScrubStrategy(), e.getIPv4ScrubStrategy(), e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy())
}
}
}

Expand Down
Loading