diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/config/LiveIntentOmniChannelProperties.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/config/LiveIntentOmniChannelProperties.java index 01d37eabda0..383aacea3a9 100644 --- a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/config/LiveIntentOmniChannelProperties.java +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/config/LiveIntentOmniChannelProperties.java @@ -2,7 +2,7 @@ import lombok.Data; -import java.util.List; +import java.util.Set; @Data public final class LiveIntentOmniChannelProperties { @@ -15,5 +15,5 @@ public final class LiveIntentOmniChannelProperties { float treatmentRate; - List targetBidders; + Set targetBidders; } 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 32b608ce21c..605acc8506c 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 @@ -9,6 +9,7 @@ import io.vertx.core.MultiMap; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; +import org.apache.commons.collections4.SetUtils; import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; @@ -40,18 +41,15 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ListUtil; -import org.prebid.server.util.StreamUtil; import org.prebid.server.vertx.httpclient.HttpClient; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; -import java.util.stream.Stream; public class LiveIntentOmniChannelIdentityProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { @@ -65,7 +63,7 @@ public class LiveIntentOmniChannelIdentityProcessedAuctionRequestHook implements private final HttpClient httpClient; private final UserFpdActivityMask userFpdActivityMask; private final double logSamplingRate; - private final List targetBidders; + private final Set targetBidders; public LiveIntentOmniChannelIdentityProcessedAuctionRequestHook(LiveIntentOmniChannelProperties config, UserFpdActivityMask userFpdActivityMask, @@ -79,7 +77,7 @@ public LiveIntentOmniChannelIdentityProcessedAuctionRequestHook(LiveIntentOmniCh this.httpClient = Objects.requireNonNull(httpClient); this.logSamplingRate = logSamplingRate; this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); - this.targetBidders = ListUtils.emptyIfNull(config.getTargetBidders()); + this.targetBidders = SetUtils.emptyIfNull(config.getTargetBidders()); } @Override @@ -202,7 +200,7 @@ private AuctionRequestPayload updatedPayload(AuctionRequestPayload requestPayloa } private BidRequest updateAllowedBidders(BidRequest bidRequest, List resolvedEids) { - if (targetBidders.isEmpty()) { + if (CollectionUtils.isEmpty(targetBidders) || CollectionUtils.isEmpty(resolvedEids)) { return bidRequest; } @@ -210,6 +208,14 @@ private BidRequest updateAllowedBidders(BidRequest bidRequest, List resolve final ExtRequestPrebid extPrebid = ext != null ? ext.getPrebid() : null; final ExtRequestPrebidData extPrebidData = extPrebid != null ? extPrebid.getData() : null; + final List existingPerms = extPrebidData != null + ? extPrebidData.getEidPermissions() + : null; + + if (CollectionUtils.isEmpty(existingPerms)) { + return bidRequest; + } + final ExtRequestPrebid updatedExtPrebid = Optional.ofNullable(extPrebid) .map(ExtRequestPrebid::toBuilder) .orElseGet(ExtRequestPrebid::builder) @@ -225,35 +231,34 @@ private BidRequest updateAllowedBidders(BidRequest bidRequest, List resolve } private ExtRequestPrebidData updatePrebidData(ExtRequestPrebidData extPrebidData, List resolvedEids) { - final List prebidDataBidders = extPrebidData != null ? extPrebidData.getBidders() : null; - final List updatedPrebidDataBidders = prebidDataBidders != null - ? (List) CollectionUtils.union(targetBidders, prebidDataBidders) - : targetBidders; - - final Set resolvedSources = resolvedEids.stream().map(Eid::getSource).collect(Collectors.toSet()); - - final List initialPermissions = Optional.ofNullable(extPrebidData) - .map(ExtRequestPrebidData::getEidPermissions) - .orElse(Collections.emptyList()); - final List updatedPermissions = Stream.concat( - initialPermissions.stream() - .map(permission -> updateEidPermission(permission, resolvedSources)), - resolvedSources.stream() - .map(source -> ExtRequestPrebidDataEidPermissions.of(source, targetBidders))) - .filter(StreamUtil.distinctBy(ExtRequestPrebidDataEidPermissions::getSource)) + final List originalBidders = extPrebidData != null ? extPrebidData.getBidders() : null; + + final Set resolvedSources = resolvedEids.stream() + .map(Eid::getSource) + .collect(Collectors.toSet()); + + final List updatedPermissions = extPrebidData.getEidPermissions().stream() + .map(permission -> restrictEidPermission(permission, resolvedSources)) + .filter(Objects::nonNull) .toList(); - return ExtRequestPrebidData.of(updatedPrebidDataBidders, updatedPermissions); + return ExtRequestPrebidData.of(originalBidders, updatedPermissions); } - private ExtRequestPrebidDataEidPermissions updateEidPermission(ExtRequestPrebidDataEidPermissions permission, - Set resolvedSources) { + private ExtRequestPrebidDataEidPermissions restrictEidPermission(ExtRequestPrebidDataEidPermissions permission, + Set resolvedSources) { + + if (!resolvedSources.contains(permission.getSource())) { + return permission; + } + + final List finalBidders = ListUtils.emptyIfNull(permission.getBidders()).stream() + .filter(targetBidders::contains) + .toList(); - return resolvedSources.contains(permission.getSource()) - ? ExtRequestPrebidDataEidPermissions.of( - permission.getSource(), - (List) CollectionUtils.union(permission.getBidders(), targetBidders)) - : permission; + return CollectionUtils.isEmpty(finalBidders) + ? null + : ExtRequestPrebidDataEidPermissions.of(permission.getSource(), finalBidders); } @Override 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 7b05ec6072d..8de0d4914df 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 @@ -36,6 +36,7 @@ import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeoutException; import static java.util.Collections.singletonList; @@ -74,11 +75,11 @@ public class LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest { private LiveIntentOmniChannelIdentityProcessedAuctionRequestHook target; - private List configuredBidders; + private Set configuredBidders; @BeforeEach public void setUp() { - configuredBidders = List.of("bidder1", "bidder2"); + configuredBidders = Set.of("bidder1", "bidder2"); given(properties.getRequestTimeoutMs()).willReturn(5L); given(properties.getIdentityResolutionEndpoint()).willReturn("https://test.com/idres"); given(properties.getAuthToken()).willReturn("auth_token"); @@ -375,14 +376,33 @@ public void callShouldReturnFailureWhenRequestingEidsIsFailed() { } @Test - public void biddersConfiguredRestrictionShouldBeRespected() { + public void shouldRestrictExistingEidPermissionsByIntersectionAndKeepGlobalBiddersUnchanged() { + // 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 ExtRequestPrebidData expectedData = ExtRequestPrebidData.of(configuredBidders, List.of( - ExtRequestPrebidDataEidPermissions.of("liveintent.com", configuredBidders))); + // existing global bidders and eid permissions including liveintent.com + final ExtRequestPrebidData givenData = ExtRequestPrebidData.of( + List.of("bidderX"), + List.of( + ExtRequestPrebidDataEidPermissions.of("some.other-source.com", List.of("bidderY")), + ExtRequestPrebidDataEidPermissions.of("liveintent.com", List.of("bidder2", "bidder3")) + )); + + final BidRequest givenBidRequest = BidRequest.builder() + .id("request") + .user(givenUser) + .ext(ExtRequest.of(ExtRequestPrebid.builder().data(givenData).build())) + .build(); + + // expected: global bidders unchanged, liveintent.com bidders intersected with configuredBidders => [bidder2] + final ExtRequestPrebidData expectedData = ExtRequestPrebidData.of( + List.of("bidderX"), + List.of( + ExtRequestPrebidDataEidPermissions.of("some.other-source.com", List.of("bidderY")), + ExtRequestPrebidDataEidPermissions.of("liveintent.com", List.of("bidder2")) + )); final Eid expectedEid = Eid.builder().source("liveintent.com").build(); @@ -418,7 +438,7 @@ public void biddersConfiguredRestrictionShouldBeRespected() { } @Test - public void biddersConfiguredRestrictionShouldBeMergedWithProvided() { + public void shouldNotAddNewEidPermissionsOrModifyGlobalBiddersWhenSourceNotPresent() { // given final Uid givenUid = Uid.builder().id("id1").atype(2).build(); final Eid givenEid = Eid.builder().source("some.source.com").uids(singletonList(givenUid)).build(); @@ -429,12 +449,10 @@ public void biddersConfiguredRestrictionShouldBeMergedWithProvided() { ExtRequestPrebidDataEidPermissions.of("some.source.com", List.of("bidder3")))) ).build())).build(); - final List expectedBidders = List.of("bidder3", "bidder2", "bidder1"); - - final ExtRequestPrebidData expectedData = ExtRequestPrebidData.of(expectedBidders, List.of( + // expected: unchanged, because there is no existing permission for liveintent.com + final ExtRequestPrebidData expectedData = ExtRequestPrebidData.of(List.of("bidder3"), List.of( ExtRequestPrebidDataEidPermissions.of("some.other-source.com", List.of("bidder3")), - ExtRequestPrebidDataEidPermissions.of("some.source.com", List.of("bidder3")), - ExtRequestPrebidDataEidPermissions.of("liveintent.com", configuredBidders))); + ExtRequestPrebidDataEidPermissions.of("some.source.com", List.of("bidder3")))); final Eid expectedEid = Eid.builder().source("liveintent.com").build(); @@ -468,4 +486,64 @@ public void biddersConfiguredRestrictionShouldBeMergedWithProvided() { eq(MAPPER.encodeToString(givenBidRequest)), eq(5L)); } + + @Test + public void shouldRemovePermissionWhenIntersectionIsEmpty() { + // 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 ExtRequestPrebidData givenData = ExtRequestPrebidData.of( + List.of("bidderGlobal"), + List.of( + ExtRequestPrebidDataEidPermissions.of("liveintent.com", List.of("not-allowed")), + ExtRequestPrebidDataEidPermissions.of("keep.com", List.of("bidderGlobal")) + )); + + final BidRequest givenBidRequest = BidRequest.builder() + .id("request") + .user(givenUser) + .ext(ExtRequest.of(ExtRequestPrebid.builder().data(givenData).build())) + .build(); + + // Respond with liveintent.com so that restriction is applied and becomes empty -> remove entry + 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(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 + final ExtRequestPrebidData expectedData = ExtRequestPrebidData.of( + List.of("bidderGlobal"), + List.of( + ExtRequestPrebidDataEidPermissions.of("keep.com", List.of("bidderGlobal")) + )); + + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.payloadUpdate().apply(AuctionRequestPayloadImpl.of(givenBidRequest))) + .extracting(AuctionRequestPayload::bidRequest) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getData) + .isEqualTo(expectedData); + + verify(httpClient).post( + eq("https://test.com/idres"), + argThat(headers -> headers.contains("Authorization", "Bearer auth_token", true)), + eq(MAPPER.encodeToString(givenBidRequest)), + eq(5L)); + } }