diff --git a/src/main/java/org/prebid/server/auction/AllowedAlternateBidderCodes.java b/src/main/java/org/prebid/server/auction/AllowedAlternateBidderCodes.java new file mode 100644 index 00000000000..14abc3761a1 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/AllowedAlternateBidderCodes.java @@ -0,0 +1,64 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestAlternateBidderCodes; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestAlternateBidderCodesBidder; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.util.ObjectUtil; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Helper class for applying the allow-alternate-bidder-codes configuration + */ +public class AllowedAlternateBidderCodes { + + private static final String WILDCARD = "*"; + + private AllowedAlternateBidderCodes() { } + + public static Set allowedCodesForBidder(String bidder, BidRequest bidRequest) { + final ExtRequestAlternateBidderCodes alternateBidderCodes = ObjectUtil.getIfNotNull( + bidRequest.getExt().getPrebid(), ExtRequestPrebid::getAlternatebiddercodes); + + if (alternateBidderCodes == null) { + return null; + } + + if (!alternateBidderCodes.getEnabled()) { + return null; + } + + final Map alternateBidderCodesBidderMap = + alternateBidderCodes.getBidders(); + + if (alternateBidderCodesBidderMap == null) { + return null; + } + + final ExtRequestAlternateBidderCodesBidder alternateBidderCodesBidder = + alternateBidderCodesBidderMap.get(bidder); + + if (alternateBidderCodesBidder == null || !alternateBidderCodesBidder.getEnabled()) { + return null; + } + + final List allowedAlternates = alternateBidderCodesBidder.getAllowedBidderCodes(); + return allowedAlternates != null ? new HashSet<>(allowedAlternates) : null; + } + + public static String applySeatForBid(Set allowedAlternateBidderCodes, String bidder, String wantedSeat) { + if (allowedAlternateBidderCodes == null || wantedSeat == null) { + return bidder; + } + + if (allowedAlternateBidderCodes.contains(wantedSeat) || allowedAlternateBidderCodes.contains(WILDCARD)) { + return wantedSeat; + } + + return bidder; + } +} diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index 4242281a5dd..475d0ecb767 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -274,6 +274,8 @@ private Future> updateBids(List bidderRespo for (final BidderResponse bidderResponse : bidderResponses) { final String bidder = bidderResponse.getBidder(); + final Set allowedAlternateBidderCodes = AllowedAlternateBidderCodes.allowedCodesForBidder( + bidder, auctionContext.getBidRequest()); final List modifiedBidderBids = new ArrayList<>(); final BidderSeatBid seatBid = bidderResponse.getSeatBid(); @@ -283,10 +285,15 @@ private Future> updateBids(List bidderRespo final Bid updatedBid = updateBid( receivedBid, bidType, bidder, videoStoredDataResult, auctionContext, eventsContext); - modifiedBidderBids.add(bidderBid.toBuilder().bid(updatedBid).build()); + + modifiedBidderBids.add(bidderBid.toBuilder().bid(updatedBid) + .seat(AllowedAlternateBidderCodes.applySeatForBid( + allowedAlternateBidderCodes, bidder, bidderBid.getSeat())) + .build()); } final BidderSeatBid modifiedSeatBid = seatBid.with(modifiedBidderBids); + result.add(bidderResponse.with(modifiedSeatBid)); } @@ -425,15 +432,14 @@ private List toBidderResponseInfos(CategoryMappingResult cat final List bidderResponses = categoryMappingResult.getBidderResponses(); for (final BidderResponse bidderResponse : bidderResponses) { - final String bidder = bidderResponse.getBidder(); - final List bidInfos = new ArrayList<>(); final BidderSeatBid seatBid = bidderResponse.getSeatBid(); for (final BidderBid bidderBid : seatBid.getBids()) { final Bid bid = bidderBid.getBid(); final BidType type = bidderBid.getType(); - final BidInfo bidInfo = toBidInfo(bid, type, imps, bidder, categoryMappingResult, cacheInfo, account); + final BidInfo bidInfo = toBidInfo( + bid, type, imps, bidderBid.getSeat(), categoryMappingResult, cacheInfo, account); bidInfos.add(bidInfo); } @@ -445,6 +451,7 @@ private List toBidderResponseInfos(CategoryMappingResult cat seatBid.getFledgeAuctionConfigs(), seatBid.getIgi()); + final String bidder = bidderResponse.getBidder(); result.add(BidderResponseInfo.of(bidder, bidderSeatBidInfo, bidderResponse.getResponseTime())); } @@ -1339,7 +1346,7 @@ private BidResponse toBidResponse(List bidderResponseInfos, .map(BidderResponseInfo::getSeatBid) .map(BidderSeatBidInfo::getBidsInfos) .filter(CollectionUtils::isNotEmpty) - .map(bidInfos -> toSeatBid( + .flatMap(bidInfos -> toSeatBids( bidInfos, targeting, bidRequest, @@ -1417,45 +1424,59 @@ private boolean checkEchoVideoAttrs(Imp imp) { } /** - * Creates an OpenRTB {@link SeatBid} for a bidder. It will contain all the bids supplied by a bidder and a "bidder" - * extension field populated. + * Creates a OpenRTB {@link SeatBid}s for a bidder. It will contain all the bids supplied by a bidder and a "bidder" + * extension field populated. Will return a list of a single SeatBid unless the bids have overriden the "bidder". */ - private SeatBid toSeatBid(List bidInfos, - ExtRequestTargeting targeting, - BidRequest bidRequest, - BidRequestCacheInfo requestCacheInfo, - Map bidToCacheInfo, - Account account, - Map> bidErrors, - Map> bidWarnings) { - - final String bidder = bidInfos.stream() - .map(BidInfo::getBidder) - .findFirst() - // Should never occur - .orElseThrow(() -> new IllegalArgumentException("Bidder was not defined for bidInfo")); + private Stream toSeatBids(List bidInfos, + ExtRequestTargeting targeting, + BidRequest bidRequest, + BidRequestCacheInfo requestCacheInfo, + Map bidToCacheInfo, + Account account, + Map> bidErrors, + Map> bidWarnings) { + + final Map> bidsByBidder = new HashMap<>(1); + + for (BidInfo bidInfo : bidInfos) { + bidInfo = injectAdmWithCacheInfo( + bidInfo, + requestCacheInfo, + bidToCacheInfo, + bidErrors); + + if (bidInfo == null) { + continue; + } - final List bids = bidInfos.stream() - .map(bidInfo -> injectAdmWithCacheInfo( - bidInfo, - requestCacheInfo, - bidToCacheInfo, - bidErrors)) - .filter(Objects::nonNull) - .map(bidInfo -> toBid( - bidInfo, - targeting, - bidRequest, - account, - bidWarnings)) - .filter(Objects::nonNull) - .toList(); + final String bidder = bidInfo.getBidder(); + if (bidder == null) { + logger.warn("BidInfo missing bidder, skipping bid", bidInfo); + continue; + } - return SeatBid.builder() - .seat(bidder) - .bid(bids) - .group(0) // prebid cannot support roadblocking - .build(); + final Bid bid = toBid( + bidInfo, + targeting, + bidRequest, + account, + bidWarnings); + + if (bid == null) { + continue; + } + + bidsByBidder.putIfAbsent(bidder, new ArrayList<>(1)); + bidsByBidder.get(bidder).add(bid); + } + + return bidsByBidder.entrySet().stream() + .map(kvp -> + SeatBid.builder() + .seat(kvp.getKey()) + .bid(kvp.getValue()) + .group(0) // prebid cannot support roadblocking + .build()); } private BidInfo injectAdmWithCacheInfo(BidInfo bidInfo, diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 56cd8192501..d9adc3c8283 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -1201,6 +1201,7 @@ private Future requestBids(BidderRequest bidderRequest, final CaseInsensitiveMultiMap requestHeaders = auctionContext.getHttpRequest().getHeaders(); final String bidderName = bidderRequest.getBidder(); + final String resolvedBidderName = aliases.resolveBidder(bidderName); final Bidder bidder = bidderCatalog.bidderByName(resolvedBidderName); final long bidderTmaxDeductionMs = bidderCatalog.bidderInfoByName(resolvedBidderName).getTmaxDeductionMs(); diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java index 5bcabe413db..d5ed7a1065d 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java @@ -49,6 +49,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestAlternateBidderCodes; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; @@ -726,6 +727,8 @@ private ExtRequest populateRequestExt(ExtRequest ext, Account account) { final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(ext, ExtRequest::getPrebid); + final ExtRequestAlternateBidderCodes accountAlternateBidderCodes = ObjectUtil.getIfNotNull( + ObjectUtil.getIfNotNull(account, Account::getAuction), AccountAuctionConfig::getAlternateBidderCodes); final ExtRequestTargeting updatedTargeting = targetingOrNull(prebid, imps, account); final ExtRequestPrebidCache updatedCache = cacheOrNull(prebid); @@ -742,6 +745,10 @@ private ExtRequest populateRequestExt(ExtRequest ext, ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getCache))) .channel(ObjectUtils.defaultIfNull(updatedChannel, ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getChannel))) + .alternatebiddercodes(ObjectUtils.defaultIfNull( + ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getAlternatebiddercodes), + accountAlternateBidderCodes + )) .server(serverInfo.with(endpoint)) .build()); diff --git a/src/main/java/org/prebid/server/bidder/amx/AmxBidder.java b/src/main/java/org/prebid/server/bidder/amx/AmxBidder.java index bed5622ec06..4c25cbc0086 100644 --- a/src/main/java/org/prebid/server/bidder/amx/AmxBidder.java +++ b/src/main/java/org/prebid/server/bidder/amx/AmxBidder.java @@ -173,8 +173,10 @@ private BidderBid createBidderBid(Bid bid, String cur, List errors) errors.add(BidderError.badInput(e.getMessage())); return null; } - // TODO: After adding support to change seat data, add bid.ext bidderCode processing - return BidderBid.of(resolveBid(bid, amxBidExt.getDemandSource()), getBidType(amxBidExt), cur); + + return BidderBid.of( + resolveBid(bid, amxBidExt.getDemandSource()), + getBidType(amxBidExt), cur, amxBidExt.getBidderCode()); } private AmxBidExt parseBidderExt(ObjectNode ext) { diff --git a/src/main/java/org/prebid/server/bidder/model/BidderBid.java b/src/main/java/org/prebid/server/bidder/model/BidderBid.java index ef4b18eaa2d..ee9ecd48776 100644 --- a/src/main/java/org/prebid/server/bidder/model/BidderBid.java +++ b/src/main/java/org/prebid/server/bidder/model/BidderBid.java @@ -45,11 +45,22 @@ public class BidderBid { */ PriceFloorInfo priceFloorInfo; - public static BidderBid of(Bid bid, BidType bidType, String bidCurrency) { + /** + * The seat, which will override the default seat (e.g. the bidder name) + * if alternate-bidder-codes (ext.prebid.alternatebiddercodes) are allowed + */ + String seat; + + public static BidderBid of(Bid bid, BidType bidType, String bidCurrency, String seat) { return BidderBid.builder() .bid(bid) .type(bidType) .bidCurrency(bidCurrency) + .seat(seat) .build(); } + + public static BidderBid of(Bid bid, BidType bidType, String bidCurrency) { + return of(bid, bidType, bidCurrency, null); + } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestAlternateBidderCodes.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestAlternateBidderCodes.java new file mode 100644 index 00000000000..00ba6aa1f52 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestAlternateBidderCodes.java @@ -0,0 +1,22 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import lombok.Builder; +import lombok.Value; + +import java.util.Map; + + +/** + * Defines the contract for bidrequest.ext.prebid.alternatebiddercodes + */ +@Builder(toBuilder = true) +@Value +public class ExtRequestAlternateBidderCodes { + + /** + * Is this feature enabled + */ + Boolean enabled; + + Map bidders; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestAlternateBidderCodesBidder.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestAlternateBidderCodesBidder.java new file mode 100644 index 00000000000..4d32ab69527 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestAlternateBidderCodesBidder.java @@ -0,0 +1,15 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder(toBuilder = true) +@Value +public class ExtRequestAlternateBidderCodesBidder { + + Boolean enabled; + + List allowedBidderCodes; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java index 33b326b2638..9460cb18951 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java @@ -41,6 +41,11 @@ public class ExtRequestPrebid { */ Map aliases; + /** + * Defines the contract for bidrequest.ext.prebid.alternatebiddercodes + */ + ExtRequestAlternateBidderCodes alternatebiddercodes; + /** * Defines the contract for bidrequest.ext.prebid.aliasgvlids */ diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java index ac7da04dd31..73ba0a3f126 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java @@ -6,6 +6,7 @@ import lombok.Builder; import lombok.Value; import org.prebid.server.auction.model.PaaFormat; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestAlternateBidderCodes; import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.Map; @@ -32,6 +33,9 @@ public class AccountAuctionConfig { @JsonAlias("debug-allow") Boolean debugAllow; + @JsonAlias("alternate-bidder-codes") + ExtRequestAlternateBidderCodes alternateBidderCodes; + @JsonAlias("bid-validations") AccountBidValidationConfig bidValidations; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 64ce4e516a4..602fa78f83b 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -102,6 +102,7 @@ adapter-defaults: allow: true auction: ad-server-currency: USD + alternate-bidder-codes: blocklisted-accounts: blocklisted-apps: biddertmax: diff --git a/src/test/java/org/prebid/server/bidder/amx/AmxBidderTest.java b/src/test/java/org/prebid/server/bidder/amx/AmxBidderTest.java index bf5d788808f..776b04ccc83 100644 --- a/src/test/java/org/prebid/server/bidder/amx/AmxBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/amx/AmxBidderTest.java @@ -316,6 +316,8 @@ public void makeBidsShouldSetDemandSourceFromBidExtDsField() throws JsonProcessi // given final ObjectNode givenBidExt = mapper.createObjectNode(); givenBidExt.set("ds", new TextNode("someDs")); + givenBidExt.set("bc", new TextNode("someBc")); + final BidderCall httpCall = givenHttpCall( BidRequest.builder() .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build())) @@ -328,6 +330,10 @@ public void makeBidsShouldSetDemandSourceFromBidExtDsField() throws JsonProcessi // then assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getSeat) + .containsExactly("someBc"); + assertThat(result.getValue()) .extracting(BidderBid::getBid) .extracting(Bid::getExt)