From a3153a7c897127a6eef36b62ef53bee2463199eb Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Sun, 11 Jan 2026 23:16:04 -0500 Subject: [PATCH 1/4] EID Permissions extension --- .../server/auction/ExchangeService.java | 56 ++++----- .../auction/model/EidPermissionIndex.java | 118 ++++++++++++++++++ .../ExtRequestPrebidDataEidPermissions.java | 27 +++- .../server/validation/RequestValidator.java | 20 ++- .../server/auction/ExchangeServiceTest.java | 55 +++++--- .../AuctionRequestFactoryTest.java | 12 +- .../validation/RequestValidatorTest.java | 41 ++++-- 7 files changed, 262 insertions(+), 67 deletions(-) create mode 100644 src/main/java/org/prebid/server/auction/model/EidPermissionIndex.java diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 8bea0bc315e..290670d2ae4 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -43,6 +43,7 @@ import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.auction.model.EidPermissionIndex; import org.prebid.server.auction.model.MultiBidConfig; import org.prebid.server.auction.model.StoredResponseResult; import org.prebid.server.auction.model.TimeoutContext; @@ -88,7 +89,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidMultiBid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; @@ -131,7 +131,6 @@ public class ExchangeService { private static final String ALL_BIDDERS_CONFIG = "*"; private static final Integer DEFAULT_MULTIBID_LIMIT_MIN = 1; private static final Integer DEFAULT_MULTIBID_LIMIT_MAX = 9; - private static final String EID_ALLOWED_FOR_ALL_BIDDERS = "*"; private static final BigDecimal THOUSAND = BigDecimal.valueOf(1000); private static final Set BIDDER_FIELDS_EXCEPTION_LIST = Set.of( "adunitcode", "storedrequest", "options", "is_rewarded_inventory"); @@ -435,7 +434,7 @@ private void removeInvalidBidRejectionTrackers(Map BidderAliases aliases) { final Set bidderNames = new HashSet<>(bidRejectionTrackers.keySet()); - for (String bidder: bidderNames) { + for (String bidder : bidderNames) { if (!isValidBidder(bidder, aliases)) { bidRejectionTrackers.remove(bidder); logger.warn("Pre-rejected impressions of the bidder {} have been removed. " @@ -541,9 +540,9 @@ private Future> makeAuctionParticipation( final ExtRequest requestExt = bidRequest.getExt(); final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); final Map biddersToConfigs = getBiddersToConfigs(prebid); - final Map> eidPermissions = getEidPermissions(prebid); + final EidPermissionIndex eidPermissionIndex = getEidPermissions(prebid); final Map> bidderToUserAndDevice = - prepareUsersAndDevices(bidders, context, aliases, biddersToConfigs, eidPermissions); + prepareUsersAndDevices(bidders, context, aliases, biddersToConfigs, eidPermissionIndex); return privacyEnforcementService.mask(context, bidderToUserAndDevice, aliases) .map(bidderToPrivacyResult -> getAuctionParticipation( @@ -581,14 +580,12 @@ private Map getBiddersToConfigs(ExtRequestPrebid pr return bidderToConfig; } - private Map> getEidPermissions(ExtRequestPrebid prebid) { - final ExtRequestPrebidData prebidData = prebid != null ? prebid.getData() : null; - final List eidPermissions = prebidData != null - ? prebidData.getEidPermissions() - : null; - return CollectionUtils.emptyIfNull(eidPermissions).stream() - .collect(Collectors.toMap(ExtRequestPrebidDataEidPermissions::getSource, - ExtRequestPrebidDataEidPermissions::getBidders)); + private EidPermissionIndex getEidPermissions(ExtRequestPrebid prebid) { + return Optional.ofNullable(prebid) + .map(ExtRequestPrebid::getData) + .map(ExtRequestPrebidData::getEidPermissions) + .map(EidPermissionIndex::build) + .orElse(null); } private static List firstPartyDataBidders(ExtRequest requestExt) { @@ -597,11 +594,12 @@ private static List firstPartyDataBidders(ExtRequest requestExt) { return data == null ? null : data.getBidders(); } - private Map> prepareUsersAndDevices(List bidders, - AuctionContext context, - BidderAliases aliases, - Map biddersToConfigs, - Map> eidPermissions) { + private Map> prepareUsersAndDevices( + List bidders, + AuctionContext context, + BidderAliases aliases, + Map biddersToConfigs, + EidPermissionIndex eidPermissionIndex) { final BidRequest bidRequest = context.getBidRequest(); final List firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt()); @@ -613,7 +611,7 @@ private Map> prepareUsersAndDevices(List bidd final boolean useFirstPartyData = firstPartyDataBidders == null || firstPartyDataBidders.stream() .anyMatch(fpdBidder -> StringUtils.equalsIgnoreCase(fpdBidder, bidder)); final User preparedUser = prepareUser( - bidder, context, aliases, useFirstPartyData, fpdConfig, eidPermissions); + bidder, context, aliases, useFirstPartyData, fpdConfig, eidPermissionIndex); final Device preparedDevice = prepareDevice( bidRequest.getDevice(), fpdConfig, useFirstPartyData); bidderToUserAndDevice.put(bidder, Pair.of(preparedUser, preparedDevice)); @@ -626,13 +624,13 @@ private User prepareUser(String bidder, BidderAliases aliases, boolean useFirstPartyData, ExtBidderConfigOrtb fpdConfig, - Map> eidPermissions) { + EidPermissionIndex eidPermissionIndex) { final User user = context.getBidRequest().getUser(); final ExtUser extUser = user != null ? user.getExt() : null; final UpdateResult buyerUidUpdateResult = uidUpdater.updateUid(bidder, context, aliases); final List userEids = extractUserEids(user); - final List allowedUserEids = resolveAllowedEids(userEids, bidder, eidPermissions); + final List allowedUserEids = resolveAllowedEids(userEids, bidder, eidPermissionIndex); final boolean shouldUpdateUserEids = allowedUserEids.size() != CollectionUtils.emptyIfNull(userEids).size(); final boolean shouldCleanExtPrebid = extUser != null && extUser.getPrebid() != null; final boolean shouldCleanExtData = extUser != null && extUser.getData() != null && !useFirstPartyData; @@ -672,18 +670,20 @@ private List extractUserEids(User user) { return user != null ? user.getEids() : null; } - private List resolveAllowedEids(List userEids, String bidder, Map> eidPermissions) { + private List resolveAllowedEids(List userEids, String bidder, EidPermissionIndex eidPermissionIndex) { return CollectionUtils.emptyIfNull(userEids) .stream() - .filter(userEid -> isUserEidAllowed(userEid.getSource(), eidPermissions, bidder)) + .filter(userEid -> isUserEidAllowed(userEid, eidPermissionIndex, bidder)) .toList(); } - private boolean isUserEidAllowed(String source, Map> eidPermissions, String bidder) { - final List allowedBidders = eidPermissions.get(source); - return CollectionUtils.isEmpty(allowedBidders) || allowedBidders.stream() - .anyMatch(allowedBidder -> StringUtils.equalsIgnoreCase(allowedBidder, bidder) - || EID_ALLOWED_FOR_ALL_BIDDERS.equals(allowedBidder)); + private boolean isUserEidAllowed(Eid eid, + EidPermissionIndex eidPermissionIndex, + String bidder) { + if (eidPermissionIndex == null) { + return true; + } + return eidPermissionIndex.isAllowed(eid, bidder); } private List getAuctionParticipation( diff --git a/src/main/java/org/prebid/server/auction/model/EidPermissionIndex.java b/src/main/java/org/prebid/server/auction/model/EidPermissionIndex.java new file mode 100644 index 00000000000..8c121b02c98 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/EidPermissionIndex.java @@ -0,0 +1,118 @@ +package org.prebid.server.auction.model; + +import com.iab.openrtb.request.Eid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public final class EidPermissionIndex { + + // bitmask for which fields are present in a permission + private static final int INSERTER = 1; // 0001 + private static final int SOURCE = 2; // 0010 + private static final int MATCHER = 4; // 0100 + private static final int MM = 8; // 1000 + private static final String WILDCARD_BIDDER = "*"; + + private final Map>> ruleIndexByMask; + + private record Key(String inserter, String source, String matcher, Integer mm) { + } + + private EidPermissionIndex(Map>> ruleIndexByMask) { + this.ruleIndexByMask = ruleIndexByMask; + } + + public static EidPermissionIndex build(List permissions) { + if (ObjectUtils.isEmpty(permissions)) { + return null; + } + + final Map>> idx = new HashMap<>(); + + for (ExtRequestPrebidDataEidPermissions permission : permissions) { + final List bidders = CollectionUtils.emptyIfNull(permission.getBidders()) + .stream() + .filter(StringUtils::isNotBlank) + .map(String::toLowerCase) + .toList(); + + if (bidders.isEmpty()) { + continue; + } + + final int ruleMask = maskOf(permission.getInserter(), + permission.getSource(), + permission.getMatcher(), + permission.getMm()); + final Key ruleKey = new Key(permission.getInserter(), + permission.getSource(), + permission.getMatcher(), + permission.getMm()); + + idx.computeIfAbsent(ruleMask, ignored -> new HashMap<>()) + .computeIfAbsent(ruleKey, ignored -> new HashSet<>()) + .addAll(bidders); + } + + return new EidPermissionIndex(idx); + } + + private static int maskOf(String inserter, String source, String matcher, Integer mm) { + int mask = 0; + + if (inserter != null) { + mask |= INSERTER; + } + if (source != null) { + mask |= SOURCE; + } + if (matcher != null) { + mask |= MATCHER; + } + if (mm != null) { + mask |= MM; + } + + return mask; + } + + public boolean isAllowed(Eid eid, String bidder) { + final int eidMask = maskOf(eid.getInserter(), eid.getSource(), eid.getMatcher(), eid.getMm()); + + boolean ruleMatched = false; + + // Check every permission bucket whose criteria fields are a subset of the Eid’s populated fields + for (Map.Entry>> ruleBucket : ruleIndexByMask.entrySet()) { + final int ruleMask = ruleBucket.getKey(); + + // rule can only match if all its required fields exist on the Eid + if ((ruleMask & eidMask) != ruleMask) { + continue; + } + + final Key normalizedEidKey = new Key((ruleMask & INSERTER) != 0 ? eid.getInserter() : null, + (ruleMask & SOURCE) != 0 ? eid.getSource() : null, + (ruleMask & MATCHER) != 0 ? eid.getMatcher() : null, + (ruleMask & MM) != 0 ? eid.getMm() : null); + + final Set allowedBidders = ruleBucket.getValue().get(normalizedEidKey); + if (allowedBidders != null) { + ruleMatched = true; + if (allowedBidders.contains(WILDCARD_BIDDER) || allowedBidders.contains(bidder.toLowerCase())) { + return true; + } + } + } + + // allow-by-default: if no rule matched at all, allow + return !ruleMatched; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java index 82b2e6007a1..2373c40e886 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java @@ -1,24 +1,43 @@ package org.prebid.server.proto.openrtb.ext.request; import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Builder; import lombok.Value; import java.util.List; -/** - * Defines the contract for bidrequest.ext.prebid.data.eidPermissions - */ -@Value(staticConstructor = "of") +@Value +@Builder public class ExtRequestPrebidDataEidPermissions { + /** + * Defines the contract for bidrequest.ext.prebid.data.eidPermissions.inserter + */ + String inserter; + /** * Defines the contract for bidrequest.ext.prebid.data.eidPermissions.source */ String source; + /** + * Defines the contract for bidrequest.ext.prebid.data.eidPermissions.matcher + */ + String matcher; + + /** + * Defines the contract for bidrequest.ext.prebid.data.eidPermissions.mm + */ + Integer mm; + /** * Defines the contract for bidrequest.ext.prebid.data.eidPermissions.bidders */ @JsonFormat(without = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) List bidders; + + @Deprecated + public static ExtRequestPrebidDataEidPermissions of(String source, List bidders) { + return new ExtRequestPrebidDataEidPermissions(null, source, null, null, bidders); + } } diff --git a/src/main/java/org/prebid/server/validation/RequestValidator.java b/src/main/java/org/prebid/server/validation/RequestValidator.java index d4b9fb06726..a0d1f749cc7 100644 --- a/src/main/java/org/prebid/server/validation/RequestValidator.java +++ b/src/main/java/org/prebid/server/validation/RequestValidator.java @@ -376,7 +376,7 @@ private void validateEidPermissions(List eid boolean isDebugEnabled, List warnings) throws ValidationException { - if (eidPermissions == null) { + if (ObjectUtils.isEmpty(eidPermissions)) { return; } @@ -385,14 +385,24 @@ private void validateEidPermissions(List eid throw new ValidationException("request.ext.prebid.data.eidpermissions[i] can't be null"); } - validateEidPermissionSource(eidPermission.getSource()); + validateEidPermissionCriteria(eidPermission.getInserter(), + eidPermission.getSource(), + eidPermission.getMatcher(), + eidPermission.getMm()); validateEidPermissionBidders(eidPermission.getBidders(), aliases, isDebugEnabled, warnings); } } - private void validateEidPermissionSource(String source) throws ValidationException { - if (StringUtils.isEmpty(source)) { - throw new ValidationException("Missing required value request.ext.prebid.data.eidPermissions[].source"); + private void validateEidPermissionCriteria(String inserter, + String source, + String matcher, + Integer mm) throws ValidationException { + if (StringUtils.isEmpty(inserter) + && StringUtils.isEmpty(source) + && StringUtils.isEmpty(matcher) + && mm == null) { + throw new ValidationException("Missing required parameter(s) in request.ext.prebid.data.eidPermissions[]. " + + "Either one or a combination of inserter, source, matcher, or mm should be defined."); } } diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 20561c5b685..f7286170273 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -53,8 +53,8 @@ import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.auction.model.BidderResponse; -import org.prebid.server.auction.model.MultiBidConfig; import org.prebid.server.auction.model.ImpRejection; +import org.prebid.server.auction.model.MultiBidConfig; import org.prebid.server.auction.model.StoredResponseResult; import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.auction.model.debug.DebugContext; @@ -165,7 +165,6 @@ import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.List; @@ -1239,7 +1238,7 @@ public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderW builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .auctiontimestamp(1000L) .build())))) - .originalPriceFloors(Collections.emptyMap()) + .originalPriceFloors(emptyMap()) .build()), any(), any(), @@ -1262,7 +1261,7 @@ public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderW builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .auctiontimestamp(1000L) .build())))) - .originalPriceFloors(Collections.emptyMap()) + .originalPriceFloors(emptyMap()) .build()), any(), any(), @@ -1502,7 +1501,7 @@ public void shouldCallBidResponseCreatorWithExpectedParamsAndUpdateDebugErrors() final ExtRequestPrebidMultiBid multiBid5 = ExtRequestPrebidMultiBid.of("bidder6", Arrays.asList("bidder4", "bidder5"), 0, "bi6"); final ExtRequestPrebidMultiBid multiBid6 = ExtRequestPrebidMultiBid.of(null, - Collections.emptyList(), 0, "bi7"); + emptyList(), 0, "bi7"); final ExtRequestTargeting targeting = givenTargeting(true); final ObjectNode events = mapper.createObjectNode(); @@ -2132,7 +2131,7 @@ public void shouldAddMultibidInfoOnlyAboutRequestedBidder() { // given final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)), builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .multibid(Collections.singletonList( + .multibid(singletonList( ExtRequestPrebidMultiBid.of(null, asList("someBidder", "anotherBidder"), 3, null))) .build()))); @@ -2231,7 +2230,10 @@ public void shouldFilterUserExtEidsWhenBidderIsNotAllowedForSourceIgnoringCase() testUserEidsPermissionFiltering( // given asList(Eid.builder().source("source1").build(), Eid.builder().source("source2").build()), - singletonList(ExtRequestPrebidDataEidPermissions.of("source1", singletonList("OtHeRbIdDeR"))), + singletonList(ExtRequestPrebidDataEidPermissions.builder() + .source("source1") + .bidders(singletonList("OtHeRbIdDeR")) + .build()), emptyMap(), // expected singletonList(Eid.builder().source("source2").build())); @@ -2242,7 +2244,10 @@ public void shouldNotFilterUserExtEidsWhenEidsPermissionDoesNotContainSourceIgno testUserEidsPermissionFiltering( // given singletonList(Eid.builder().source("source1").build()), - singletonList(ExtRequestPrebidDataEidPermissions.of("source2", singletonList("OtHeRbIdDeR"))), + singletonList(ExtRequestPrebidDataEidPermissions.builder() + .source("source2") + .bidders(singletonList("OtHeRbIdDeR")) + .build()), emptyMap(), // expected singletonList(Eid.builder().source("source1").build())); @@ -2253,7 +2258,10 @@ public void shouldNotFilterUserExtEidsWhenSourceAllowedForAllBiddersIgnoringCase testUserEidsPermissionFiltering( // given singletonList(Eid.builder().source("source1").build()), - singletonList(ExtRequestPrebidDataEidPermissions.of("source1", singletonList("*"))), + singletonList(ExtRequestPrebidDataEidPermissions.builder() + .source("source1") + .bidders(singletonList("*")) + .build()), emptyMap(), // expected singletonList(Eid.builder().source("source1").build())); @@ -2264,7 +2272,10 @@ public void shouldNotFilterUserExtEidsWhenSourceAllowedForBidderIgnoringCase() { testUserEidsPermissionFiltering( // given singletonList(Eid.builder().source("source1").build()), - singletonList(ExtRequestPrebidDataEidPermissions.of("source1", singletonList("SoMeBiDdEr"))), + singletonList(ExtRequestPrebidDataEidPermissions.builder() + .source("source1") + .bidders(singletonList("SoMeBiDdEr")) + .build()), emptyMap(), // expected singletonList(Eid.builder().source("source1").build())); @@ -2281,8 +2292,10 @@ public void shouldFilterUserExtEidsWhenBidderIsNotAllowedForSourceAndSetNullIfNo builder -> builder .ext(ExtRequest.of(ExtRequestPrebid.builder() .data(ExtRequestPrebidData.of(null, singletonList( - ExtRequestPrebidDataEidPermissions.of("source1", - singletonList("otherBidder"))))) + ExtRequestPrebidDataEidPermissions.builder() + .source("source1") + .bidders(singletonList("otherBidder")) + .build()))) .build())) .user(User.builder() .eids(singletonList(Eid.builder().source("source1").build())) @@ -2317,8 +2330,10 @@ public void shouldFilterUserExtEidsWhenBidderPermissionsGivenToBidderAliasOnly() .ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(singletonMap("someBidder", "someBidderAlias")) .data(ExtRequestPrebidData.of(null, singletonList( - ExtRequestPrebidDataEidPermissions.of("source1", - singletonList("someBidderAlias"))))) + ExtRequestPrebidDataEidPermissions.builder() + .source("source1") + .bidders(singletonList("someBidderAlias")) + .build()))) .build())) .user(User.builder() .eids(singletonList(Eid.builder().source("source1").build())) @@ -2353,8 +2368,10 @@ public void shouldFilterUserExtEidsWhenPermissionsGivenToBidderButNotForAlias() .ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(singletonMap("someBidder", "someBidderAlias")) .data(ExtRequestPrebidData.of(null, singletonList( - ExtRequestPrebidDataEidPermissions.of("source1", - singletonList("someBidder"))))) + ExtRequestPrebidDataEidPermissions.builder() + .source("source1") + .bidders(singletonList("someBidder")) + .build()))) .build())) .user(User.builder() .eids(singletonList(Eid.builder().source("source1").build())) @@ -3983,7 +4000,7 @@ public void shouldResponseWithEmptySeatBidIfBidderNotSupportProvidedMediaTypes() final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder().build(); given(mediaTypeProcessor.process(any(), anyString(), any(), any())) - .willReturn(MediaTypeProcessingResult.rejected(Collections.singletonList( + .willReturn(MediaTypeProcessingResult.rejected(singletonList( BidderError.badInput("MediaTypeProcessor error.")))); given(bidResponseCreator.create( argThat(argument -> argument.getAuctionParticipations().getFirst() @@ -3991,7 +4008,7 @@ public void shouldResponseWithEmptySeatBidIfBidderNotSupportProvidedMediaTypes() .equals(BidderResponse.of( "bidder1", BidderSeatBid.builder() - .warnings(Collections.singletonList( + .warnings(singletonList( BidderError.badInput("MediaTypeProcessor error."))) .build(), 0))), @@ -4041,7 +4058,7 @@ public void shouldResponseWithEmptySeatBidIfBidderNotSupportRequestCurrency() { .equals(BidderResponse.of( "bidder1", BidderSeatBid.builder() - .warnings(Collections.singletonList( + .warnings(singletonList( BidderError.generic( "No match between the configured currencies and bidRequest.cur" ))) diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java index 2e30e8d1bc4..59615bdbaab 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java @@ -487,7 +487,11 @@ public void shouldReturnFailedFutureIfEidsPermissionsContainsWrongDataType() { final ObjectNode requestNode = mapper.convertValue(bidRequest, ObjectNode.class); final JsonNode eidPermissionNode = mapper.convertValue( - ExtRequestPrebidDataEidPermissions.of("source", emptyList()), JsonNode.class); + ExtRequestPrebidDataEidPermissions.builder() + .source("source") + .bidders(emptyList()) + .build(), + JsonNode.class); requestNode .putObject("ext") @@ -520,7 +524,11 @@ public void shouldReturnFailedFutureIfEidsPermissionsBiddersContainsWrongDataTyp final ObjectNode requestNode = mapper.convertValue(bidRequest, ObjectNode.class); final ObjectNode eidPermissionNode = mapper.convertValue( - ExtRequestPrebidDataEidPermissions.of("source", emptyList()), ObjectNode.class); + ExtRequestPrebidDataEidPermissions.builder() + .source("source") + .bidders(emptyList()) + .build(), + ObjectNode.class); eidPermissionNode.put("bidders", "notArrayValue"); diff --git a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java index e4efd99feb9..653b7bf6a1d 100644 --- a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java @@ -635,7 +635,9 @@ public void validateShouldReturnValidationMessageWhenEidsPermissionsBiddersIsNul final BidRequest bidRequest = validBidRequestBuilder() .ext(ExtRequest.of(ExtRequestPrebid.builder() .data(ExtRequestPrebidData.of(null, - singletonList(ExtRequestPrebidDataEidPermissions.of("source", null)))) + singletonList(ExtRequestPrebidDataEidPermissions.builder() + .source("source") + .build()))) .build())) .build(); @@ -654,7 +656,10 @@ public void validateShouldReturnValidationMessageWhenEidsPermissionsBiddersIsEmp final BidRequest bidRequest = validBidRequestBuilder() .ext(ExtRequest.of(ExtRequestPrebid.builder() .data(ExtRequestPrebidData.of(null, - singletonList(ExtRequestPrebidDataEidPermissions.of("source", emptyList())))) + singletonList(ExtRequestPrebidDataEidPermissions.builder() + .source("source") + .bidders(Collections.emptyList()) + .build()))) .build())) .build(); @@ -675,7 +680,10 @@ public void validateShouldReturnWarningsMessageWhenEidsPermissionsBidderIsNotRec .ext(ExtRequest.of(ExtRequestPrebid.builder() .data(ExtRequestPrebidData.of(null, singletonList( - ExtRequestPrebidDataEidPermissions.of("source", singletonList("bidder1"))))) + ExtRequestPrebidDataEidPermissions.builder() + .source("source") + .bidders(Collections.singletonList("bidder1")) + .build()))) .build())) .build(); @@ -698,7 +706,10 @@ public void validateShouldNotReturnWarningsMessageWhenEidsPermissionsBidderIsNot .ext(ExtRequest.of(ExtRequestPrebid.builder() .data(ExtRequestPrebidData.of(null, singletonList( - ExtRequestPrebidDataEidPermissions.of("source", singletonList("bidder1"))))) + ExtRequestPrebidDataEidPermissions.builder() + .source("source") + .bidders(Collections.singletonList("bidder1")) + .build()))) .build())) .build(); @@ -718,7 +729,10 @@ public void validateShouldReturnWarningMessageWhenEidsPermissionsBidderHasBlankV .ext(ExtRequest.of(ExtRequestPrebid.builder() .data(ExtRequestPrebidData.of(null, singletonList( - ExtRequestPrebidDataEidPermissions.of("source", singletonList(" "))))) + ExtRequestPrebidDataEidPermissions.builder() + .source("source") + .bidders(Collections.singletonList(" ")) + .build()))) .build())) .build(); @@ -744,7 +758,10 @@ public void validateShouldNotReturnValidationErrorWhenBidderIsAlias() { .aliases(singletonMap("bidder1Alias", "bidder1")) .data(ExtRequestPrebidData.of(null, singletonList( - ExtRequestPrebidDataEidPermissions.of("source", singletonList("bidder1"))))) + ExtRequestPrebidDataEidPermissions.builder() + .source("source") + .bidders(Collections.singletonList("bidder1")) + .build()))) .build())) .build(); @@ -762,7 +779,10 @@ public void validateShouldNotReturnValidationErrorWhenBidderIsAsterisk() { .ext(ExtRequest.of(ExtRequestPrebid.builder() .data(ExtRequestPrebidData.of(null, singletonList( - ExtRequestPrebidDataEidPermissions.of("source", singletonList("*"))))) + ExtRequestPrebidDataEidPermissions.builder() + .source("source") + .bidders(Collections.singletonList("*")) + .build()))) .build())) .build(); @@ -780,7 +800,9 @@ public void validateShouldReturnValidationMessageWhenEidsPermissionsHasMissingSo .ext(ExtRequest.of(ExtRequestPrebid.builder() .data(ExtRequestPrebidData.of(null, singletonList( - ExtRequestPrebidDataEidPermissions.of(null, singletonList("bidder1"))))) + ExtRequestPrebidDataEidPermissions.builder() + .bidders(Collections.singletonList("bidder1")) + .build()))) .build())) .build(); @@ -789,7 +811,8 @@ public void validateShouldReturnValidationMessageWhenEidsPermissionsHasMissingSo // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Missing required value request.ext.prebid.data.eidPermissions[].source"); + .containsOnly("Missing required parameter(s) in request.ext.prebid.data.eidPermissions[]. " + + "Either one or a combination of inserter, source, matcher, or mm should be defined."); } @Test From 9a711eb9c1938009bd17499ca439ba888bd53348 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:39:08 -0500 Subject: [PATCH 2/4] Minor refactoring --- .../server/auction/ExchangeService.java | 6 +-- .../auction/model/EidPermissionIndex.java | 52 +++++++++---------- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 290670d2ae4..2ba5ea54731 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -584,6 +584,7 @@ private EidPermissionIndex getEidPermissions(ExtRequestPrebid prebid) { return Optional.ofNullable(prebid) .map(ExtRequestPrebid::getData) .map(ExtRequestPrebidData::getEidPermissions) + .map(CollectionUtils::emptyIfNull) .map(EidPermissionIndex::build) .orElse(null); } @@ -680,10 +681,7 @@ private List resolveAllowedEids(List userEids, String bidder, EidPermi private boolean isUserEidAllowed(Eid eid, EidPermissionIndex eidPermissionIndex, String bidder) { - if (eidPermissionIndex == null) { - return true; - } - return eidPermissionIndex.isAllowed(eid, bidder); + return eidPermissionIndex == null || eidPermissionIndex.isAllowed(eid, bidder); } private List getAuctionParticipation( diff --git a/src/main/java/org/prebid/server/auction/model/EidPermissionIndex.java b/src/main/java/org/prebid/server/auction/model/EidPermissionIndex.java index 8c121b02c98..a09bf72cc48 100644 --- a/src/main/java/org/prebid/server/auction/model/EidPermissionIndex.java +++ b/src/main/java/org/prebid/server/auction/model/EidPermissionIndex.java @@ -6,6 +6,7 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -14,11 +15,11 @@ public final class EidPermissionIndex { - // bitmask for which fields are present in a permission - private static final int INSERTER = 1; // 0001 - private static final int SOURCE = 2; // 0010 - private static final int MATCHER = 4; // 0100 - private static final int MM = 8; // 1000 + // Bitmask for which fields are present in the EID permission + private static final int INSERTER = 1 << 0; + private static final int SOURCE = 1 << 1; + private static final int MATCHER = 1 << 2; + private static final int MM = 1 << 3; private static final String WILDCARD_BIDDER = "*"; private final Map>> ruleIndexByMask; @@ -30,13 +31,13 @@ private EidPermissionIndex(Map>> ruleIndexByMask) this.ruleIndexByMask = ruleIndexByMask; } - public static EidPermissionIndex build(List permissions) { + public static EidPermissionIndex build(Collection permissions) { + final Map>> idx = new HashMap<>(); + if (ObjectUtils.isEmpty(permissions)) { - return null; + return new EidPermissionIndex(idx); } - final Map>> idx = new HashMap<>(); - for (ExtRequestPrebidDataEidPermissions permission : permissions) { final List bidders = CollectionUtils.emptyIfNull(permission.getBidders()) .stream() @@ -66,25 +67,17 @@ public static EidPermissionIndex build(List } private static int maskOf(String inserter, String source, String matcher, Integer mm) { - int mask = 0; - - if (inserter != null) { - mask |= INSERTER; - } - if (source != null) { - mask |= SOURCE; - } - if (matcher != null) { - mask |= MATCHER; - } - if (mm != null) { - mask |= MM; - } - - return mask; + return (inserter != null ? INSERTER : 0) + | (source != null ? SOURCE : 0) + | (matcher != null ? MATCHER : 0) + | (mm != null ? MM : 0); } public boolean isAllowed(Eid eid, String bidder) { + if (ObjectUtils.isEmpty(ruleIndexByMask)) { + return true; + } + final int eidMask = maskOf(eid.getInserter(), eid.getSource(), eid.getMatcher(), eid.getMm()); boolean ruleMatched = false; @@ -93,8 +86,7 @@ public boolean isAllowed(Eid eid, String bidder) { for (Map.Entry>> ruleBucket : ruleIndexByMask.entrySet()) { final int ruleMask = ruleBucket.getKey(); - // rule can only match if all its required fields exist on the Eid - if ((ruleMask & eidMask) != ruleMask) { + if (!isMaskMatched(ruleMask, eidMask)) { continue; } @@ -112,7 +104,11 @@ public boolean isAllowed(Eid eid, String bidder) { } } - // allow-by-default: if no rule matched at all, allow + // Allow by default if no rule matched at all return !ruleMatched; } + + private boolean isMaskMatched(int ruleMaks, int eidMask) { + return (ruleMaks & eidMask) == ruleMaks; + } } From e49d0ffb12216850f9b91cd8ae8c7139848bcfd0 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:10:42 -0500 Subject: [PATCH 3/4] Simplification refactoring --- .../server/auction/ExchangeService.java | 27 ++-- .../auction/model/EidPermissionHolder.java | 77 +++++++++ .../auction/model/EidPermissionIndex.java | 114 -------------- .../server/auction/ExchangeServiceTest.java | 146 ++++++++++++++++++ 4 files changed, 236 insertions(+), 128 deletions(-) create mode 100644 src/main/java/org/prebid/server/auction/model/EidPermissionHolder.java delete mode 100644 src/main/java/org/prebid/server/auction/model/EidPermissionIndex.java diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 2ba5ea54731..7f2ecc4f88e 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -43,7 +43,7 @@ import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.auction.model.BidderResponse; -import org.prebid.server.auction.model.EidPermissionIndex; +import org.prebid.server.auction.model.EidPermissionHolder; import org.prebid.server.auction.model.MultiBidConfig; import org.prebid.server.auction.model.StoredResponseResult; import org.prebid.server.auction.model.TimeoutContext; @@ -540,9 +540,9 @@ private Future> makeAuctionParticipation( final ExtRequest requestExt = bidRequest.getExt(); final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); final Map biddersToConfigs = getBiddersToConfigs(prebid); - final EidPermissionIndex eidPermissionIndex = getEidPermissions(prebid); + final EidPermissionHolder eidPermissionHolder = getEidPermissions(prebid); final Map> bidderToUserAndDevice = - prepareUsersAndDevices(bidders, context, aliases, biddersToConfigs, eidPermissionIndex); + prepareUsersAndDevices(bidders, context, aliases, biddersToConfigs, eidPermissionHolder); return privacyEnforcementService.mask(context, bidderToUserAndDevice, aliases) .map(bidderToPrivacyResult -> getAuctionParticipation( @@ -580,12 +580,11 @@ private Map getBiddersToConfigs(ExtRequestPrebid pr return bidderToConfig; } - private EidPermissionIndex getEidPermissions(ExtRequestPrebid prebid) { + private EidPermissionHolder getEidPermissions(ExtRequestPrebid prebid) { return Optional.ofNullable(prebid) .map(ExtRequestPrebid::getData) .map(ExtRequestPrebidData::getEidPermissions) - .map(CollectionUtils::emptyIfNull) - .map(EidPermissionIndex::build) + .map(EidPermissionHolder::new) .orElse(null); } @@ -600,7 +599,7 @@ private Map> prepareUsersAndDevices( AuctionContext context, BidderAliases aliases, Map biddersToConfigs, - EidPermissionIndex eidPermissionIndex) { + EidPermissionHolder eidPermissionHolder) { final BidRequest bidRequest = context.getBidRequest(); final List firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt()); @@ -612,7 +611,7 @@ private Map> prepareUsersAndDevices( final boolean useFirstPartyData = firstPartyDataBidders == null || firstPartyDataBidders.stream() .anyMatch(fpdBidder -> StringUtils.equalsIgnoreCase(fpdBidder, bidder)); final User preparedUser = prepareUser( - bidder, context, aliases, useFirstPartyData, fpdConfig, eidPermissionIndex); + bidder, context, aliases, useFirstPartyData, fpdConfig, eidPermissionHolder); final Device preparedDevice = prepareDevice( bidRequest.getDevice(), fpdConfig, useFirstPartyData); bidderToUserAndDevice.put(bidder, Pair.of(preparedUser, preparedDevice)); @@ -625,13 +624,13 @@ private User prepareUser(String bidder, BidderAliases aliases, boolean useFirstPartyData, ExtBidderConfigOrtb fpdConfig, - EidPermissionIndex eidPermissionIndex) { + EidPermissionHolder eidPermissionHolder) { final User user = context.getBidRequest().getUser(); final ExtUser extUser = user != null ? user.getExt() : null; final UpdateResult buyerUidUpdateResult = uidUpdater.updateUid(bidder, context, aliases); final List userEids = extractUserEids(user); - final List allowedUserEids = resolveAllowedEids(userEids, bidder, eidPermissionIndex); + final List allowedUserEids = resolveAllowedEids(userEids, bidder, eidPermissionHolder); final boolean shouldUpdateUserEids = allowedUserEids.size() != CollectionUtils.emptyIfNull(userEids).size(); final boolean shouldCleanExtPrebid = extUser != null && extUser.getPrebid() != null; final boolean shouldCleanExtData = extUser != null && extUser.getData() != null && !useFirstPartyData; @@ -671,17 +670,17 @@ private List extractUserEids(User user) { return user != null ? user.getEids() : null; } - private List resolveAllowedEids(List userEids, String bidder, EidPermissionIndex eidPermissionIndex) { + private List resolveAllowedEids(List userEids, String bidder, EidPermissionHolder eidPermissionHolder) { return CollectionUtils.emptyIfNull(userEids) .stream() - .filter(userEid -> isUserEidAllowed(userEid, eidPermissionIndex, bidder)) + .filter(userEid -> isUserEidAllowed(userEid, eidPermissionHolder, bidder)) .toList(); } private boolean isUserEidAllowed(Eid eid, - EidPermissionIndex eidPermissionIndex, + EidPermissionHolder eidPermissionHolder, String bidder) { - return eidPermissionIndex == null || eidPermissionIndex.isAllowed(eid, bidder); + return eidPermissionHolder == null || eidPermissionHolder.isAllowed(eid, bidder); } private List getAuctionParticipation( diff --git a/src/main/java/org/prebid/server/auction/model/EidPermissionHolder.java b/src/main/java/org/prebid/server/auction/model/EidPermissionHolder.java new file mode 100644 index 00000000000..a35a154c145 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/EidPermissionHolder.java @@ -0,0 +1,77 @@ +package org.prebid.server.auction.model; + +import com.iab.openrtb.request.Eid; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; + +import java.util.List; + +public final class EidPermissionHolder { + + private static final String WILDCARD_BIDDER = "*"; + + private final List eidPermissions; + + public EidPermissionHolder(List eidPermissions) { + this.eidPermissions = eidPermissions; + } + + public boolean isAllowed(Eid eid, String bidder) { + if (ObjectUtils.isEmpty(eidPermissions)) { + return true; + } + + boolean isBestMatch = false; + int bestSpecificity = -1; + + for (ExtRequestPrebidDataEidPermissions eidPermission : eidPermissions) { + if (!isRuleMatched(eid, eidPermission)) { + continue; + } + + final int ruleSpecificity = getRuleSpecificity(eidPermission); + + final boolean isBidderAllowed = isBidderAllowed(bidder, eidPermission.getBidders()); + + if (ruleSpecificity > bestSpecificity) { + bestSpecificity = ruleSpecificity; + isBestMatch = isBidderAllowed; + } else if (ruleSpecificity == bestSpecificity) { + isBestMatch |= isBidderAllowed; + } + } + + return bestSpecificity == -1 || isBestMatch; + } + + private int getRuleSpecificity(ExtRequestPrebidDataEidPermissions eidPermission) { + int specificity = 0; + if (eidPermission.getInserter() != null) { + specificity++; + } + if (eidPermission.getSource() != null) { + specificity++; + } + if (eidPermission.getMatcher() != null) { + specificity++; + } + if (eidPermission.getMm() != null) { + specificity++; + } + return specificity; + } + + private boolean isRuleMatched(Eid eid, ExtRequestPrebidDataEidPermissions eidPermission) { + return (eidPermission.getInserter() == null || eidPermission.getInserter().equals(eid.getInserter())) + && (eidPermission.getSource() == null || eidPermission.getSource().equals(eid.getSource())) + && (eidPermission.getMatcher() == null || eidPermission.getMatcher().equals(eid.getMatcher())) + && (eidPermission.getMm() == null || eidPermission.getMm().equals(eid.getMm())); + } + + private boolean isBidderAllowed(String bidder, List ruleBidders) { + return ruleBidders == null || ruleBidders.stream() + .anyMatch(allowedBidder -> StringUtils.equalsIgnoreCase(allowedBidder, bidder) + || WILDCARD_BIDDER.equals(allowedBidder)); + } +} diff --git a/src/main/java/org/prebid/server/auction/model/EidPermissionIndex.java b/src/main/java/org/prebid/server/auction/model/EidPermissionIndex.java deleted file mode 100644 index a09bf72cc48..00000000000 --- a/src/main/java/org/prebid/server/auction/model/EidPermissionIndex.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.prebid.server.auction.model; - -import com.iab.openrtb.request.Eid; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; - -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public final class EidPermissionIndex { - - // Bitmask for which fields are present in the EID permission - private static final int INSERTER = 1 << 0; - private static final int SOURCE = 1 << 1; - private static final int MATCHER = 1 << 2; - private static final int MM = 1 << 3; - private static final String WILDCARD_BIDDER = "*"; - - private final Map>> ruleIndexByMask; - - private record Key(String inserter, String source, String matcher, Integer mm) { - } - - private EidPermissionIndex(Map>> ruleIndexByMask) { - this.ruleIndexByMask = ruleIndexByMask; - } - - public static EidPermissionIndex build(Collection permissions) { - final Map>> idx = new HashMap<>(); - - if (ObjectUtils.isEmpty(permissions)) { - return new EidPermissionIndex(idx); - } - - for (ExtRequestPrebidDataEidPermissions permission : permissions) { - final List bidders = CollectionUtils.emptyIfNull(permission.getBidders()) - .stream() - .filter(StringUtils::isNotBlank) - .map(String::toLowerCase) - .toList(); - - if (bidders.isEmpty()) { - continue; - } - - final int ruleMask = maskOf(permission.getInserter(), - permission.getSource(), - permission.getMatcher(), - permission.getMm()); - final Key ruleKey = new Key(permission.getInserter(), - permission.getSource(), - permission.getMatcher(), - permission.getMm()); - - idx.computeIfAbsent(ruleMask, ignored -> new HashMap<>()) - .computeIfAbsent(ruleKey, ignored -> new HashSet<>()) - .addAll(bidders); - } - - return new EidPermissionIndex(idx); - } - - private static int maskOf(String inserter, String source, String matcher, Integer mm) { - return (inserter != null ? INSERTER : 0) - | (source != null ? SOURCE : 0) - | (matcher != null ? MATCHER : 0) - | (mm != null ? MM : 0); - } - - public boolean isAllowed(Eid eid, String bidder) { - if (ObjectUtils.isEmpty(ruleIndexByMask)) { - return true; - } - - final int eidMask = maskOf(eid.getInserter(), eid.getSource(), eid.getMatcher(), eid.getMm()); - - boolean ruleMatched = false; - - // Check every permission bucket whose criteria fields are a subset of the Eid’s populated fields - for (Map.Entry>> ruleBucket : ruleIndexByMask.entrySet()) { - final int ruleMask = ruleBucket.getKey(); - - if (!isMaskMatched(ruleMask, eidMask)) { - continue; - } - - final Key normalizedEidKey = new Key((ruleMask & INSERTER) != 0 ? eid.getInserter() : null, - (ruleMask & SOURCE) != 0 ? eid.getSource() : null, - (ruleMask & MATCHER) != 0 ? eid.getMatcher() : null, - (ruleMask & MM) != 0 ? eid.getMm() : null); - - final Set allowedBidders = ruleBucket.getValue().get(normalizedEidKey); - if (allowedBidders != null) { - ruleMatched = true; - if (allowedBidders.contains(WILDCARD_BIDDER) || allowedBidders.contains(bidder.toLowerCase())) { - return true; - } - } - } - - // Allow by default if no rule matched at all - return !ruleMatched; - } - - private boolean isMaskMatched(int ruleMaks, int eidMask) { - return (ruleMaks & eidMask) == ruleMaks; - } -} diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index f7286170273..dc8dcc6431a 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -2239,6 +2239,152 @@ public void shouldFilterUserExtEidsWhenBidderIsNotAllowedForSourceIgnoringCase() singletonList(Eid.builder().source("source2").build())); } + @Test + public void shouldFilterUserExtEidsWhenBidderIsNotAllowedForInserterIgnoringCase() { + testUserEidsPermissionFiltering( + // given + asList(Eid.builder().inserter("inserter1").build(), Eid.builder().inserter("inserter2").build()), + singletonList(ExtRequestPrebidDataEidPermissions.builder() + .inserter("inserter1") + .bidders(singletonList("OtHeRbIdDeR")) + .build()), + emptyMap(), + // expected + singletonList(Eid.builder().inserter("inserter2").build())); + } + + @Test + public void shouldFilterUserExtEidsWhenBidderIsNotAllowedForMatcherIgnoringCase() { + testUserEidsPermissionFiltering( + // given + asList(Eid.builder().matcher("matcher1").build(), Eid.builder().matcher("matcher2").build()), + singletonList(ExtRequestPrebidDataEidPermissions.builder() + .matcher("matcher1") + .bidders(singletonList("OtHeRbIdDeR")) + .build()), + emptyMap(), + // expected + singletonList(Eid.builder().matcher("matcher2").build())); + } + + @Test + public void shouldFilterUserExtEidsWhenBidderIsNotAllowedForMm() { + testUserEidsPermissionFiltering( + // given + asList(Eid.builder().mm(1).build(), Eid.builder().mm(2).build()), + singletonList(ExtRequestPrebidDataEidPermissions.builder() + .mm(1) + .bidders(singletonList("OtHeRbIdDeR")) + .build()), + emptyMap(), + // expected + singletonList(Eid.builder().mm(2).build())); + } + + @Test + public void shouldFilterUserExtEidsWhenBidderIsNotAllowedUsingMultipleCriteria() { + testUserEidsPermissionFiltering( + // given + asList(Eid.builder().inserter("inserter1").source("source1").matcher("matcher1").mm(1).build(), + Eid.builder().inserter("inserter2").source("source2").matcher("matcher2").mm(2).build()), + singletonList(ExtRequestPrebidDataEidPermissions.builder() + .inserter("inserter1") + .source("source1") + .matcher("matcher1") + .mm(1) + .bidders(singletonList("OtHeRbIdDeR")) + .build()), + emptyMap(), + // expected + singletonList(Eid.builder().inserter("inserter2").source("source2").matcher("matcher2").mm(2).build())); + } + + @Test + public void shouldFilterUserExtEidsWhenEveryCriteriaMatches() { + testUserEidsPermissionFiltering( + // given + asList(Eid.builder().inserter("inserter1").source("source1").matcher("matcher1").mm(1).build(), + Eid.builder().inserter("inserter2").source("source2").matcher("matcher2").mm(2).build()), + singletonList(ExtRequestPrebidDataEidPermissions.builder() + .inserter("inserter1") + .source("source2") + .matcher("matcher3") + .mm(4) + .bidders(singletonList("OtHeRbIdDeR")) + .build()), + emptyMap(), + // expected + asList(Eid.builder().inserter("inserter1").source("source1").matcher("matcher1").mm(1).build(), + Eid.builder().inserter("inserter2").source("source2").matcher("matcher2").mm(2).build())); + } + + @Test + public void shouldFilterUserExtEidsWhenBidderIsNotAllowedUsingTheMostSpecificRule() { + testUserEidsPermissionFiltering( + // given + asList(Eid.builder().inserter("inserter1").source("source1").matcher("matcher1").mm(1).build(), + Eid.builder().inserter("inserter2").source("source2").matcher("matcher2").mm(2).build()), + asList(ExtRequestPrebidDataEidPermissions.builder() + .inserter("inserter1") + .bidders(singletonList("someBidder")) + .build(), + ExtRequestPrebidDataEidPermissions.builder() + .inserter("inserter1") + .source("source1") + .matcher("matcher1") + .mm(1) + .bidders(singletonList("OtHeRbIdDeR")) + .build()), + emptyMap(), + // expected + singletonList(Eid.builder().inserter("inserter2").source("source2").matcher("matcher2").mm(2).build())); + } + + @Test + public void shouldNotFilterUserExtEidsWhenBidderIsAllowedUsingTheMostSpecificRule() { + testUserEidsPermissionFiltering( + // given + asList(Eid.builder().inserter("inserter1").source("source1").matcher("matcher1").mm(1).build(), + Eid.builder().inserter("inserter2").source("source2").matcher("matcher2").mm(2).build()), + asList(ExtRequestPrebidDataEidPermissions.builder() + .inserter("inserter1") + .bidders(singletonList("OtHeRbIdDeR")) + .build(), + ExtRequestPrebidDataEidPermissions.builder() + .inserter("inserter1") + .source("source1") + .matcher("matcher1") + .mm(1) + .bidders(singletonList("someBidder")) + .build()), + emptyMap(), + // expected + asList(Eid.builder().inserter("inserter1").source("source1").matcher("matcher1").mm(1).build(), + Eid.builder().inserter("inserter2").source("source2").matcher("matcher2").mm(2).build())); + } + + @Test + public void shouldNotFilterUserExtEidsWhenBidderIsAllowedUsingMultipleSameSpecificityRules() { + testUserEidsPermissionFiltering( + // given + asList(Eid.builder().inserter("inserter1").source("source1").matcher("matcher1").mm(1).build(), + Eid.builder().inserter("inserter2").source("source2").matcher("matcher2").mm(2).build()), + asList(ExtRequestPrebidDataEidPermissions.builder() + .inserter("inserter1") + .source("source1") + .bidders(singletonList("OtHeRbIdDeR")) + .build(), + ExtRequestPrebidDataEidPermissions.builder() + .matcher("matcher1") + .mm(1) + .bidders(singletonList("someBidder")) + .build()), + emptyMap(), + // expected + asList(Eid.builder().inserter("inserter1").source("source1").matcher("matcher1").mm(1).build(), + Eid.builder().inserter("inserter2").source("source2").matcher("matcher2").mm(2).build())); + } + @Test public void shouldNotFilterUserExtEidsWhenEidsPermissionDoesNotContainSourceIgnoringCase() { testUserEidsPermissionFiltering( From 86492b59bda4566e5fbbeca343ab4fa3490bd4b5 Mon Sep 17 00:00:00 2001 From: Viktor Kryshtal Date: Thu, 15 Jan 2026 13:40:24 +0200 Subject: [PATCH 4/4] Refactor --- .../server/auction/ExchangeService.java | 12 +-- .../auction/model/EidPermissionHolder.java | 77 +++++++++---------- 2 files changed, 39 insertions(+), 50 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 8d85bf57d25..0affdbf8741 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -581,8 +581,8 @@ private EidPermissionHolder getEidPermissions(ExtRequestPrebid prebid) { return Optional.ofNullable(prebid) .map(ExtRequestPrebid::getData) .map(ExtRequestPrebidData::getEidPermissions) - .map(EidPermissionHolder::new) - .orElse(null); + .map(EidPermissionHolder::of) + .orElse(EidPermissionHolder.empty()); } private static List firstPartyDataBidders(ExtRequest requestExt) { @@ -670,16 +670,10 @@ private List extractUserEids(User user) { private List resolveAllowedEids(List userEids, String bidder, EidPermissionHolder eidPermissionHolder) { return CollectionUtils.emptyIfNull(userEids) .stream() - .filter(userEid -> isUserEidAllowed(userEid, eidPermissionHolder, bidder)) + .filter(userEid -> eidPermissionHolder.isAllowed(userEid, bidder)) .toList(); } - private boolean isUserEidAllowed(Eid eid, - EidPermissionHolder eidPermissionHolder, - String bidder) { - return eidPermissionHolder == null || eidPermissionHolder.isAllowed(eid, bidder); - } - private List getAuctionParticipation( List bidderPrivacyResults, BidRequest bidRequest, diff --git a/src/main/java/org/prebid/server/auction/model/EidPermissionHolder.java b/src/main/java/org/prebid/server/auction/model/EidPermissionHolder.java index a35a154c145..9f03d017d37 100644 --- a/src/main/java/org/prebid/server/auction/model/EidPermissionHolder.java +++ b/src/main/java/org/prebid/server/auction/model/EidPermissionHolder.java @@ -1,65 +1,60 @@ package org.prebid.server.auction.model; import com.iab.openrtb.request.Eid; -import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; public final class EidPermissionHolder { private static final String WILDCARD_BIDDER = "*"; - private final List eidPermissions; - - public EidPermissionHolder(List eidPermissions) { - this.eidPermissions = eidPermissions; - } - - public boolean isAllowed(Eid eid, String bidder) { - if (ObjectUtils.isEmpty(eidPermissions)) { - return true; - } + private static final ExtRequestPrebidDataEidPermissions DEFAULT_RULE = ExtRequestPrebidDataEidPermissions.builder() + .bidders(Collections.singletonList(WILDCARD_BIDDER)) + .build(); - boolean isBestMatch = false; - int bestSpecificity = -1; + private static final EidPermissionHolder EMPTY = new EidPermissionHolder(Collections.emptyList()); - for (ExtRequestPrebidDataEidPermissions eidPermission : eidPermissions) { - if (!isRuleMatched(eid, eidPermission)) { - continue; - } + private final List eidPermissions; - final int ruleSpecificity = getRuleSpecificity(eidPermission); + private EidPermissionHolder(List eidPermissions) { + this.eidPermissions = new ArrayList<>(eidPermissions); + this.eidPermissions.add(DEFAULT_RULE); + } - final boolean isBidderAllowed = isBidderAllowed(bidder, eidPermission.getBidders()); + public static EidPermissionHolder of(List eidPermissions) { + return new EidPermissionHolder(eidPermissions); + } - if (ruleSpecificity > bestSpecificity) { - bestSpecificity = ruleSpecificity; - isBestMatch = isBidderAllowed; - } else if (ruleSpecificity == bestSpecificity) { - isBestMatch |= isBidderAllowed; - } - } + public static EidPermissionHolder empty() { + return EMPTY; + } - return bestSpecificity == -1 || isBestMatch; + public boolean isAllowed(Eid eid, String bidder) { + final Map> matchingRulesBySpecificity = eidPermissions + .stream() + .filter(rule -> isRuleMatched(eid, rule)) + .collect(Collectors.groupingBy(this::getRuleSpecificity)); + + final int highestSpecificityMatchingRules = Collections.max(matchingRulesBySpecificity.keySet()); + return matchingRulesBySpecificity.get(highestSpecificityMatchingRules).stream() + .anyMatch(eidPermission -> isBidderAllowed(bidder, eidPermission.getBidders())); } private int getRuleSpecificity(ExtRequestPrebidDataEidPermissions eidPermission) { - int specificity = 0; - if (eidPermission.getInserter() != null) { - specificity++; - } - if (eidPermission.getSource() != null) { - specificity++; - } - if (eidPermission.getMatcher() != null) { - specificity++; - } - if (eidPermission.getMm() != null) { - specificity++; - } - return specificity; + return (int) Stream.of(eidPermission.getInserter(), + eidPermission.getSource(), + eidPermission.getMatcher(), + eidPermission.getMm()) + .filter(Objects::nonNull) + .count(); } private boolean isRuleMatched(Eid eid, ExtRequestPrebidDataEidPermissions eidPermission) {