diff --git a/analytics/build/build.go b/analytics/build/build.go index 7fc577daedf..cd0b870f793 100644 --- a/analytics/build/build.go +++ b/analytics/build/build.go @@ -8,6 +8,8 @@ import ( "github.com/prebid/prebid-server/v2/analytics/filesystem" "github.com/prebid/prebid-server/v2/analytics/pubstack" "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/ortb" "github.com/prebid/prebid-server/v2/privacy" ) @@ -46,8 +48,10 @@ type enabledAnalytics map[string]analytics.Module func (ea enabledAnalytics) LogAuctionObject(ao *analytics.AuctionObject, ac privacy.ActivityControl) { for name, module := range ea { - component := privacy.Component{Type: privacy.ComponentTypeAnalytics, Name: name} - if ac.Allow(privacy.ActivityReportAnalytics, component, privacy.ActivityRequest{}) { + if isAllowed, cloneBidderReq := evaluateActivities(ao.RequestWrapper, ac, name); isAllowed { + if cloneBidderReq != nil { + ao.RequestWrapper = cloneBidderReq + } module.LogAuctionObject(ao) } } @@ -55,10 +59,13 @@ func (ea enabledAnalytics) LogAuctionObject(ao *analytics.AuctionObject, ac priv func (ea enabledAnalytics) LogVideoObject(vo *analytics.VideoObject, ac privacy.ActivityControl) { for name, module := range ea { - component := privacy.Component{Type: privacy.ComponentTypeAnalytics, Name: name} - if ac.Allow(privacy.ActivityReportAnalytics, component, privacy.ActivityRequest{}) { + if isAllowed, cloneBidderReq := evaluateActivities(vo.RequestWrapper, ac, name); isAllowed { + if cloneBidderReq != nil { + vo.RequestWrapper = cloneBidderReq + } module.LogVideoObject(vo) } + } } @@ -76,8 +83,10 @@ func (ea enabledAnalytics) LogSetUIDObject(so *analytics.SetUIDObject) { func (ea enabledAnalytics) LogAmpObject(ao *analytics.AmpObject, ac privacy.ActivityControl) { for name, module := range ea { - component := privacy.Component{Type: privacy.ComponentTypeAnalytics, Name: name} - if ac.Allow(privacy.ActivityReportAnalytics, component, privacy.ActivityRequest{}) { + if isAllowed, cloneBidderReq := evaluateActivities(ao.RequestWrapper, ac, name); isAllowed { + if cloneBidderReq != nil { + ao.RequestWrapper = cloneBidderReq + } module.LogAmpObject(ao) } } @@ -91,3 +100,31 @@ func (ea enabledAnalytics) LogNotificationEventObject(ne *analytics.Notification } } } + +func evaluateActivities(rw *openrtb_ext.RequestWrapper, ac privacy.ActivityControl, componentName string) (bool, *openrtb_ext.RequestWrapper) { + // returned nil request wrapper means that request wrapper was not modified by activities and doesn't have to be changed in analytics object + // it is needed in order to use one function for all analytics objects with RequestWrapper + component := privacy.Component{Type: privacy.ComponentTypeAnalytics, Name: componentName} + if !ac.Allow(privacy.ActivityReportAnalytics, component, privacy.ActivityRequest{}) { + return false, nil + } + blockUserFPD := !ac.Allow(privacy.ActivityTransmitUserFPD, component, privacy.ActivityRequest{}) + blockPreciseGeo := !ac.Allow(privacy.ActivityTransmitPreciseGeo, component, privacy.ActivityRequest{}) + + if !blockUserFPD && !blockPreciseGeo { + return true, nil + } + + cloneReq := ortb.CloneBidderReq(rw.BidRequest) + + if blockUserFPD { + privacy.ScrubUserFPD(cloneReq) + } + if blockPreciseGeo { + ipConf := privacy.IPConf{IPV6: ac.IPv6Config, IPV4: ac.IPv4Config} + privacy.ScrubGeoAndDeviceIP(cloneReq, ipConf) + } + + cloneReq.RebuildRequest() + return true, cloneReq +} diff --git a/analytics/build/build_test.go b/analytics/build/build_test.go index efc0c862564..d6fea975eda 100644 --- a/analytics/build/build_test.go +++ b/analytics/build/build_test.go @@ -1,6 +1,9 @@ package build import ( + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/iputil" + "net/http" "os" "testing" @@ -143,7 +146,7 @@ func TestSampleModuleActivitiesAllowed(t *testing.T) { var count int am := initAnalytics(&count) - acAllowed := privacy.NewActivityControl(getDefaultActivityConfig("sampleModule", true)) + acAllowed := privacy.NewActivityControl(getActivityConfig("sampleModule", true, true, true)) ao := &analytics.AuctionObject{ Status: http.StatusOK, @@ -172,11 +175,46 @@ func TestSampleModuleActivitiesAllowed(t *testing.T) { } } +func TestSampleModuleActivitiesAllowedAndDenied(t *testing.T) { + var count int + am := initAnalytics(&count) + + acAllowed := privacy.NewActivityControl(getActivityConfig("sampleModule", true, false, true)) + + rw := &openrtb_ext.RequestWrapper{BidRequest: getDefaultBidRequest()} + ao := &analytics.AuctionObject{ + RequestWrapper: rw, + Status: http.StatusOK, + Errors: nil, + Response: &openrtb2.BidResponse{}, + } + + am.LogAuctionObject(ao, acAllowed) + if count != 1 { + t.Errorf("PBSAnalyticsModule failed at LogAuctionObject") + } + + am.LogAmpObject(&analytics.AmpObject{RequestWrapper: rw}, acAllowed) + if count != 2 { + t.Errorf("PBSAnalyticsModule failed at LogAmpObject") + } + + am.LogVideoObject(&analytics.VideoObject{RequestWrapper: rw}, acAllowed) + if count != 3 { + t.Errorf("PBSAnalyticsModule failed at LogVideoObject") + } + + am.LogNotificationEventObject(&analytics.NotificationEvent{}, acAllowed) + if count != 4 { + t.Errorf("PBSAnalyticsModule failed at LogNotificationEventObject") + } +} + func TestSampleModuleActivitiesDenied(t *testing.T) { var count int am := initAnalytics(&count) - acDenied := privacy.NewActivityControl(getDefaultActivityConfig("sampleModule", false)) + acDenied := privacy.NewActivityControl(getActivityConfig("sampleModule", false, true, true)) ao := &analytics.AuctionObject{ Status: http.StatusOK, @@ -205,14 +243,102 @@ func TestSampleModuleActivitiesDenied(t *testing.T) { } } -func getDefaultActivityConfig(componentName string, allow bool) *config.AccountPrivacy { +func TestEvaluateActivities(t *testing.T) { + testCases := []struct { + description string + givenActivityControl privacy.ActivityControl + expectedRequest *openrtb_ext.RequestWrapper + expectedAllowActivities bool + }{ + { + description: "all blocked", + givenActivityControl: privacy.NewActivityControl(getActivityConfig("sampleModule", false, false, false)), + expectedRequest: nil, + expectedAllowActivities: false, + }, + { + description: "all allowed", + givenActivityControl: privacy.NewActivityControl(getActivityConfig("sampleModule", true, true, true)), + expectedRequest: nil, + expectedAllowActivities: true, + }, + + { + description: "ActivityTransmitUserFPD and ActivityTransmitPreciseGeo disabled", + givenActivityControl: privacy.NewActivityControl(getActivityConfig("sampleModule", true, false, false)), + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ID: "test_request", User: &openrtb2.User{ID: ""}, Device: &openrtb2.Device{IFA: "", IP: "127.0.0.0"}}, + }, + expectedAllowActivities: true, + }, + { + description: "ActivityTransmitUserFPD enabled, ActivityTransmitPreciseGeo disabled", + givenActivityControl: privacy.NewActivityControl(getActivityConfig("sampleModule", true, true, false)), + expectedRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ID: "test_request", User: &openrtb2.User{ID: "user-id"}, Device: &openrtb2.Device{IFA: "device-ifa", IP: "127.0.0.0"}}, + }, + expectedAllowActivities: true, + }, + } + + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + rw := &openrtb_ext.RequestWrapper{BidRequest: getDefaultBidRequest()} + resActivityAllowed, resRequest := evaluateActivities(rw, test.givenActivityControl, "sampleModule") + assert.Equal(t, test.expectedAllowActivities, resActivityAllowed) + if test.expectedRequest != nil { + assert.Equal(t, test.expectedRequest.User.ID, resRequest.User.ID) + assert.Equal(t, test.expectedRequest.Device.IFA, resRequest.Device.IFA) + assert.Equal(t, test.expectedRequest.Device.IP, resRequest.Device.IP) + } else { + assert.Nil(t, resRequest) + } + + }) + } + +} + +func getDefaultBidRequest() *openrtb2.BidRequest { + return &openrtb2.BidRequest{ + ID: "test_request", + User: &openrtb2.User{ID: "user-id"}, + Device: &openrtb2.Device{IFA: "device-ifa", IP: "127.0.0.1"}} + +} + +func getActivityConfig(componentName string, allowReportAnalytics, allowTransmitUserFPD, allowTransmitPreciseGeo bool) *config.AccountPrivacy { return &config.AccountPrivacy{ AllowActivities: &config.AllowActivities{ ReportAnalytics: config.Activity{ Default: ptrutil.ToPtr(true), Rules: []config.ActivityRule{ { - Allow: allow, + Allow: allowReportAnalytics, + Condition: config.ActivityCondition{ + ComponentName: []string{componentName}, + ComponentType: []string{"analytics"}, + }, + }, + }, + }, + TransmitUserFPD: config.Activity{ + Default: ptrutil.ToPtr(true), + Rules: []config.ActivityRule{ + { + Allow: allowTransmitUserFPD, + Condition: config.ActivityCondition{ + ComponentName: []string{componentName}, + ComponentType: []string{"analytics"}, + }, + }, + }, + }, + TransmitPreciseGeo: config.Activity{ + Default: ptrutil.ToPtr(true), + Rules: []config.ActivityRule{ + { + Allow: allowTransmitPreciseGeo, Condition: config.ActivityCondition{ ComponentName: []string{componentName}, ComponentType: []string{"analytics"}, @@ -221,5 +347,11 @@ func getDefaultActivityConfig(componentName string, allow bool) *config.AccountP }, }, }, + IPv4Config: config.IPv4{ + AnonKeepBits: iputil.IPv4DefaultMaskingBitSize, + }, + IPv6Config: config.IPv6{ + AnonKeepBits: iputil.IPv6DefaultMaskingBitSize, + }, } } diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index 70a37aab5df..34606c1d051 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -1250,7 +1250,8 @@ func (m *mockAnalyticsModule) LogCookieSyncObject(cso *analytics.CookieSyncObjec func (m *mockAnalyticsModule) LogSetUIDObject(so *analytics.SetUIDObject) {} -func (m *mockAnalyticsModule) LogAmpObject(ao *analytics.AmpObject, _ privacy.ActivityControl) {} +func (m *mockAnalyticsModule) LogAmpObject(ao *analytics.AmpObject, _ privacy.ActivityControl) { +} func (m *mockAnalyticsModule) LogNotificationEventObject(ne *analytics.NotificationEvent, _ privacy.ActivityControl) { } diff --git a/privacy/activitycontrol.go b/privacy/activitycontrol.go index 1bb3fc6cdf6..7d1e16e99a5 100644 --- a/privacy/activitycontrol.go +++ b/privacy/activitycontrol.go @@ -37,7 +37,9 @@ func (r ActivityRequest) IsBidRequest() bool { } type ActivityControl struct { - plans map[Activity]ActivityPlan + plans map[Activity]ActivityPlan + IPv6Config config.IPv6 + IPv4Config config.IPv4 } func NewActivityControl(cfg *config.AccountPrivacy) ActivityControl { @@ -58,6 +60,9 @@ func NewActivityControl(cfg *config.AccountPrivacy) ActivityControl { plans[ActivityTransmitTIDs] = buildPlan(cfg.AllowActivities.TransmitTids) ac.plans = plans + ac.IPv4Config = cfg.IPv4Config + ac.IPv6Config = cfg.IPv6Config + return ac } diff --git a/privacy/activitycontrol_test.go b/privacy/activitycontrol_test.go index b8b06ee8886..5cd9f38b011 100644 --- a/privacy/activitycontrol_test.go +++ b/privacy/activitycontrol_test.go @@ -33,17 +33,23 @@ func TestNewActivityControl(t *testing.T) { TransmitUniqueRequestIds: getTestActivityConfig(true), TransmitTids: getTestActivityConfig(true), }, + IPv6Config: config.IPv6{AnonKeepBits: 32}, + IPv4Config: config.IPv4{AnonKeepBits: 16}, + }, + activityControl: ActivityControl{ + plans: map[Activity]ActivityPlan{ + ActivitySyncUser: getTestActivityPlan(ActivityAllow), + ActivityFetchBids: getTestActivityPlan(ActivityAllow), + ActivityEnrichUserFPD: getTestActivityPlan(ActivityAllow), + ActivityReportAnalytics: getTestActivityPlan(ActivityAllow), + ActivityTransmitUserFPD: getTestActivityPlan(ActivityAllow), + ActivityTransmitPreciseGeo: getTestActivityPlan(ActivityDeny), + ActivityTransmitUniqueRequestIDs: getTestActivityPlan(ActivityAllow), + ActivityTransmitTIDs: getTestActivityPlan(ActivityAllow), + }, + IPv6Config: config.IPv6{AnonKeepBits: 32}, + IPv4Config: config.IPv4{AnonKeepBits: 16}, }, - activityControl: ActivityControl{plans: map[Activity]ActivityPlan{ - ActivitySyncUser: getTestActivityPlan(ActivityAllow), - ActivityFetchBids: getTestActivityPlan(ActivityAllow), - ActivityEnrichUserFPD: getTestActivityPlan(ActivityAllow), - ActivityReportAnalytics: getTestActivityPlan(ActivityAllow), - ActivityTransmitUserFPD: getTestActivityPlan(ActivityAllow), - ActivityTransmitPreciseGeo: getTestActivityPlan(ActivityDeny), - ActivityTransmitUniqueRequestIDs: getTestActivityPlan(ActivityAllow), - ActivityTransmitTIDs: getTestActivityPlan(ActivityAllow), - }}, }, }