diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/config/LiveIntentOmniChannelIdentityConfiguration.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/config/LiveIntentOmniChannelIdentityConfiguration.java index e3f286e0120..4a75fca6caa 100644 --- a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/config/LiveIntentOmniChannelIdentityConfiguration.java +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/config/LiveIntentOmniChannelIdentityConfiguration.java @@ -1,5 +1,6 @@ package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.config; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config.LiveIntentOmniChannelProperties; import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.LiveIntentOmniChannelIdentityModule; import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.hooks.LiveIntentOmniChannelIdentityProcessedAuctionRequestHook; @@ -30,12 +31,13 @@ LiveIntentOmniChannelProperties properties() { @Bean Module liveIntentOmniChannelIdentityModule(LiveIntentOmniChannelProperties properties, JacksonMapper mapper, + UserFpdActivityMask userFpdActivityMask, HttpClient httpClient, @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { final LiveIntentOmniChannelIdentityProcessedAuctionRequestHook hook = new LiveIntentOmniChannelIdentityProcessedAuctionRequestHook( - properties, mapper, httpClient, logSamplingRate); + properties, userFpdActivityMask, mapper, httpClient, logSamplingRate); return new LiveIntentOmniChannelIdentityModule(Collections.singleton(hook)); } diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java index 5c3e43f9952..e0025e70bbb 100644 --- a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java @@ -1,15 +1,26 @@ package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.hooks; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Source; import com.iab.openrtb.request.User; import io.vertx.core.Future; import io.vertx.core.MultiMap; import org.apache.commons.collections4.ListUtils; +import org.prebid.server.activity.Activity; +import org.prebid.server.activity.ComponentType; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; +import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl; +import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.IdResResponse; import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config.LiveIntentOmniChannelProperties; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.LiveIntentOmniChannelIdentityModule; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -39,9 +50,11 @@ public class LiveIntentOmniChannelIdentityProcessedAuctionRequestHook implements private final LiveIntentOmniChannelProperties config; private final JacksonMapper mapper; private final HttpClient httpClient; + private final UserFpdActivityMask userFpdActivityMask; private final double logSamplingRate; public LiveIntentOmniChannelIdentityProcessedAuctionRequestHook(LiveIntentOmniChannelProperties config, + UserFpdActivityMask userFpdActivityMask, JacksonMapper mapper, HttpClient httpClient, double logSamplingRate) { @@ -51,6 +64,7 @@ public LiveIntentOmniChannelIdentityProcessedAuctionRequestHook(LiveIntentOmniCh this.mapper = Objects.requireNonNull(mapper); this.httpClient = Objects.requireNonNull(httpClient); this.logSamplingRate = logSamplingRate; + this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); } @Override @@ -58,22 +72,76 @@ public Future> call(AuctionRequestPayloa AuctionInvocationContext invocationContext) { return config.getTreatmentRate() > ThreadLocalRandom.current().nextFloat() - ? requestIdentities(auctionRequestPayload.bidRequest()) + ? requestIdentities(auctionRequestPayload.bidRequest(), invocationContext.auctionContext()) .>map(this::update) .onFailure(throwable -> conditionalLogger.error( "Failed enrichment: %s".formatted(throwable.getMessage()), logSamplingRate)) : noAction(); } - private Future requestIdentities(BidRequest bidRequest) { + private Future requestIdentities(BidRequest bidRequest, AuctionContext auctionContext) { + final BidRequest restrictedBidRequest = applyActivityRestrictions(bidRequest, auctionContext); return httpClient.post( config.getIdentityResolutionEndpoint(), headers(), - mapper.encodeToString(bidRequest), + mapper.encodeToString(restrictedBidRequest), config.getRequestTimeoutMs()) .map(this::processResponse); } + private BidRequest applyActivityRestrictions(BidRequest bidRequest, AuctionContext auctionContext) { + final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of( + ActivityInvocationPayloadImpl.of( + ComponentType.GENERAL_MODULE, + LiveIntentOmniChannelIdentityModule.CODE), + bidRequest); + final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure(); + + final boolean disallowTransmitUfpd = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_UFPD, activityInvocationPayload); + final boolean disallowTransmitEids = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_EIDS, activityInvocationPayload); + final boolean disallowTransmitGeo = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_GEO, activityInvocationPayload); + final boolean disallowTransmitTid = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_TID, activityInvocationPayload); + + return maskUserPersonalInfo( + bidRequest, + disallowTransmitUfpd, + disallowTransmitEids, + disallowTransmitGeo, + disallowTransmitTid); + } + + private BidRequest maskUserPersonalInfo(BidRequest bidRequest, + boolean disallowTransmitUfpd, + boolean disallowTransmitEids, + boolean disallowTransmitGeo, + boolean disallowTransmitTid) { + + final User maskedUser = userFpdActivityMask.maskUser( + bidRequest.getUser(), disallowTransmitUfpd, disallowTransmitEids); + final Device maskedDevice = userFpdActivityMask.maskDevice( + bidRequest.getDevice(), disallowTransmitUfpd, disallowTransmitGeo); + + final Source maskedSource = maskSource(bidRequest.getSource(), disallowTransmitUfpd, disallowTransmitTid); + + return bidRequest.toBuilder() + .user(maskedUser) + .device(maskedDevice) + .source(maskedSource) + .build(); + } + + private Source maskSource(Source source, boolean mastUfpd, boolean maskTid) { + if (source == null || !(mastUfpd || maskTid)) { + return source; + } + + return source.toBuilder().tid(null).build(); + } + private MultiMap headers() { return MultiMap.caseInsensitiveMultiMap() .add(HttpUtil.AUTHORIZATION_HEADER, "Bearer " + config.getAuthToken()); diff --git a/extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java b/extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java index 9dc53916980..0558f9126f2 100644 --- a/extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java +++ b/extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java @@ -1,7 +1,10 @@ package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Source; import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; import io.vertx.core.Future; @@ -10,7 +13,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.prebid.server.hooks.execution.v1.auction.AuctionInvocationContextImpl; +import org.prebid.server.activity.Activity; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.IdResResponse; import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config.LiveIntentOmniChannelProperties; @@ -44,12 +50,24 @@ public class LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest { private static final JacksonMapper MAPPER = new JacksonMapper(ObjectMapperProvider.mapper()); + @Mock(strictness = LENIENT) + private UserFpdActivityMask userFpdActivityMask; + @Mock private HttpClient httpClient; @Mock(strictness = LENIENT) private LiveIntentOmniChannelProperties properties; + @Mock + private ActivityInfrastructure activityInfrastructure; + + @Mock + private AuctionInvocationContext auctionInvocationContext; + + @Mock + private AuctionContext auctionContext; + private LiveIntentOmniChannelIdentityProcessedAuctionRequestHook target; @BeforeEach @@ -60,7 +78,7 @@ public void setUp() { given(properties.getTreatmentRate()).willReturn(1.0f); target = new LiveIntentOmniChannelIdentityProcessedAuctionRequestHook( - properties, MAPPER, httpClient, 0.01d); + properties, userFpdActivityMask, MAPPER, httpClient, 0.01d); } @Test @@ -68,7 +86,146 @@ public void creationShouldFailOnInvalidIdentityUrl() { given(properties.getIdentityResolutionEndpoint()).willReturn("invalid_url"); assertThatIllegalArgumentException().isThrownBy(() -> new LiveIntentOmniChannelIdentityProcessedAuctionRequestHook( - properties, MAPPER, httpClient, 0.01d)); + properties, userFpdActivityMask, MAPPER, httpClient, 0.01d)); + } + + @Test + public void geoPassingRestrictionShouldBeRespected() { + // given + final Geo givenGeo = Geo.builder() + .lat(52.51671856406936f) + .lon(13.377639726342583f) + .city("Berlin") + .country("Germany") + .build(); + final Device givenDevice = Device.builder() + .geo(givenGeo) + .ip("192.168.127.12") + .ifa("foo") + .macsha1("bar") + .macmd5("baz") + .dpidsha1("boo") + .dpidmd5("far") + .didsha1("zoo") + .didmd5("goo") + .build(); + final BidRequest givenBidRequest = BidRequest.builder().id("request").device(givenDevice).build(); + + final Geo expectedGeo = Geo.builder() + .lat(52.52f) + .lon(13.38f) + .build(); + final Device expectedDevice = Device.builder() + .geo(expectedGeo) + .ip("192.168.127.0") + .build(); + final BidRequest expectedBidRequest = givenBidRequest.toBuilder().device(expectedDevice).build(); + + final Eid expectedEid = Eid.builder().source("liveintent.com").build(); + + final String responseBody = MAPPER.encodeToString(IdResResponse.of(List.of(expectedEid))); + given(httpClient.post(any(), any(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, responseBody))); + + given(auctionInvocationContext.auctionContext()).willReturn(auctionContext); + given(auctionContext.getActivityInfrastructure()).willReturn(activityInfrastructure); + given(activityInfrastructure.isAllowed(any(), any())).willReturn(true); + given(activityInfrastructure.isAllowed(eq(Activity.TRANSMIT_GEO), any())).willReturn(false); + given(activityInfrastructure.isAllowed(eq(Activity.TRANSMIT_UFPD), any())).willReturn(false); + given(userFpdActivityMask.maskUser(any(), eq(true), eq(false))) + .will(invocation -> invocation.getArgument(0)); + given(userFpdActivityMask.maskDevice(any(), eq(true), eq(true))) + .will(invocation -> expectedDevice); + + // when + final InvocationResult result = + target.call(AuctionRequestPayloadImpl.of(givenBidRequest), auctionInvocationContext).result(); + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + + verify(httpClient).post( + eq("https://test.com/idres"), + argThat(headers -> headers.contains("Authorization", "Bearer auth_token", true)), + eq(MAPPER.encodeToString(expectedBidRequest)), + eq(5L)); + } + + @Test + public void tidPassingRestrictionShouldBeRespected() { + // given + final Source givenSource = Source.builder().tid("tid1").build(); + final BidRequest givenBidRequest = BidRequest.builder().id("request").source(givenSource).build(); + + final Source expectedSource = givenSource.toBuilder().tid(null).build(); + final BidRequest expectedBidRequest = givenBidRequest.toBuilder().source(expectedSource).build(); + + final Eid expectedEid = Eid.builder().source("liveintent.com").build(); + + final String responseBody = MAPPER.encodeToString(IdResResponse.of(List.of(expectedEid))); + given(httpClient.post(any(), any(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, responseBody))); + + given(auctionInvocationContext.auctionContext()).willReturn(auctionContext); + given(auctionContext.getActivityInfrastructure()).willReturn(activityInfrastructure); + given(activityInfrastructure.isAllowed(any(), any())).willReturn(true); + + given(activityInfrastructure.isAllowed(eq(Activity.TRANSMIT_TID), any())).willReturn(false); + given(activityInfrastructure.isAllowed(eq(Activity.TRANSMIT_UFPD), any())).willReturn(false); + given(userFpdActivityMask.maskUser(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + given(userFpdActivityMask.maskDevice(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + + // when + final InvocationResult result = + target.call(AuctionRequestPayloadImpl.of(givenBidRequest), auctionInvocationContext).result(); + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + + verify(httpClient).post( + eq("https://test.com/idres"), + argThat(headers -> headers.contains("Authorization", "Bearer auth_token", true)), + eq(MAPPER.encodeToString(expectedBidRequest)), + eq(5L)); + } + + @Test + public void eidPassingRestrictionShouldBeRespected() { + // given + final Uid givenUid = Uid.builder().id("id1").atype(2).build(); + final Eid givenEid = Eid.builder().source("some.source.com").uids(singletonList(givenUid)).build(); + final User givenUser = User.builder().eids(singletonList(givenEid)).build(); + final BidRequest givenBidRequest = BidRequest.builder().id("request").user(givenUser).build(); + + final BidRequest expectedBidRequest = givenBidRequest.toBuilder().user(null).build(); + + final Eid expectedEid = Eid.builder().source("liveintent.com").build(); + + final String responseBody = MAPPER.encodeToString(IdResResponse.of(List.of(expectedEid))); + given(httpClient.post(any(), any(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, responseBody))); + + given(auctionInvocationContext.auctionContext()).willReturn(auctionContext); + given(auctionContext.getActivityInfrastructure()).willReturn(activityInfrastructure); + given(activityInfrastructure.isAllowed(any(), any())).willReturn(true); + given(activityInfrastructure.isAllowed(eq(Activity.TRANSMIT_EIDS), any())).willReturn(false); + given(activityInfrastructure.isAllowed(eq(Activity.TRANSMIT_UFPD), any())).willReturn(false); + given(userFpdActivityMask.maskUser(any(), eq(true), eq(true))) + .willReturn(null); + given(userFpdActivityMask.maskDevice(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + + // when + final InvocationResult result = + target.call(AuctionRequestPayloadImpl.of(givenBidRequest), auctionInvocationContext).result(); + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + + verify(httpClient).post( + eq("https://test.com/idres"), + argThat(headers -> headers.contains("Authorization", "Bearer auth_token", true)), + eq(MAPPER.encodeToString(expectedBidRequest)), + eq(5L)); } @Test @@ -88,8 +245,13 @@ public void callShouldEnrichUserEidsWithRequestedEids() { given(httpClient.post(any(), any(), any(), anyLong())) .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, responseBody))); - final AuctionInvocationContext auctionInvocationContext = AuctionInvocationContextImpl.of( - null, null, false, null, null); + given(auctionInvocationContext.auctionContext()).willReturn(auctionContext); + given(auctionContext.getActivityInfrastructure()).willReturn(activityInfrastructure); + given(activityInfrastructure.isAllowed(any(), any())).willReturn(true); + given(userFpdActivityMask.maskUser(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + given(userFpdActivityMask.maskDevice(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); // when final InvocationResult result = @@ -125,8 +287,13 @@ public void callShouldCreateUserAndUseRequestedEidsWhenUserIsAbsent() { given(httpClient.post(any(), any(), any(), anyLong())) .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, responseBody))); - final AuctionInvocationContext auctionInvocationContext = AuctionInvocationContextImpl.of( - null, null, false, null, null); + given(auctionInvocationContext.auctionContext()).willReturn(auctionContext); + given(auctionContext.getActivityInfrastructure()).willReturn(activityInfrastructure); + given(activityInfrastructure.isAllowed(any(), any())).willReturn(true); + given(userFpdActivityMask.maskUser(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + given(userFpdActivityMask.maskDevice(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); // when final InvocationResult result = @@ -151,13 +318,7 @@ public void callShouldCreateUserAndUseRequestedEidsWhenUserIsAbsent() { @Test public void callShouldReturnNoActionSuccessfullyWhenTreatmentRateIsLowerThanThreshold() { // given - final Uid givenUid = Uid.builder().id("id1").atype(2).build(); - final Eid givebEid = Eid.builder().source("some.source.com").uids(singletonList(givenUid)).build(); - final User givenUser = User.builder().eids(singletonList(givebEid)).build(); - final BidRequest givenBidRequest = BidRequest.builder().id("request").user(givenUser).build(); - - final AuctionInvocationContext auctionInvocationContext = AuctionInvocationContextImpl.of( - null, null, false, null, null); + final BidRequest givenBidRequest = BidRequest.builder().build(); given(properties.getTreatmentRate()).willReturn(0.0f); @@ -181,8 +342,13 @@ public void callShouldReturnFailureWhenRequestingEidsIsFailed() { final User givenUser = User.builder().eids(singletonList(givebEid)).build(); final BidRequest givenBidRequest = BidRequest.builder().id("request").user(givenUser).build(); - final AuctionInvocationContext auctionInvocationContext = AuctionInvocationContextImpl.of( - null, null, false, null, null); + given(auctionInvocationContext.auctionContext()).willReturn(auctionContext); + given(auctionContext.getActivityInfrastructure()).willReturn(activityInfrastructure); + given(activityInfrastructure.isAllowed(any(), any())).willReturn(true); + given(userFpdActivityMask.maskUser(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + given(userFpdActivityMask.maskDevice(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); given(httpClient.post(any(), any(), any(), anyLong())) .willReturn(Future.failedFuture(new TimeoutException("Timeout exceeded")));