Skip to content

Commit

Permalink
Merge branch 'master' into feature/paa
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/main/java/org/prebid/server/handler/SetuidHandler.java
  • Loading branch information
And1sS committed Jan 14, 2025
2 parents 032f15f + 579de03 commit 5163003
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 95 deletions.
9 changes: 2 additions & 7 deletions src/main/java/org/prebid/server/cookie/CookieSyncService.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.prebid.server.spring.config.bidder.model.usersync.CookieFamilySource;
import org.prebid.server.util.HttpUtil;
import org.prebid.server.util.ObjectUtil;
import org.prebid.server.util.StreamUtil;

import java.util.ArrayList;
import java.util.Collection;
Expand All @@ -54,7 +55,6 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -385,16 +385,11 @@ private static Set<String> allowedBiddersByPriority(CookieSyncContext cookieSync

private List<BidderUsersyncStatus> validStatuses(Set<String> biddersToSync, CookieSyncContext cookieSyncContext) {
return biddersToSync.stream()
.filter(distinctBy(bidder -> bidderCatalog.cookieFamilyName(bidder).orElseThrow()))
.filter(StreamUtil.distinctBy(bidder -> bidderCatalog.cookieFamilyName(bidder).orElseThrow()))
.map(bidder -> validStatus(bidder, cookieSyncContext))
.toList();
}

private static <T> Predicate<T> distinctBy(Function<? super T, ?> keyExtractor) {
final Set<Object> seen = new HashSet<>();
return value -> seen.add(keyExtractor.apply(value));
}

private BidderUsersyncStatus validStatus(String bidder, CookieSyncContext cookieSyncContext) {
final BiddersContext biddersContext = cookieSyncContext.getBiddersContext();
final RoutingContext routingContext = cookieSyncContext.getRoutingContext();
Expand Down
130 changes: 74 additions & 56 deletions src/main/java/org/prebid/server/handler/SetuidHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.ext.web.RoutingContext;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.prebid.server.activity.Activity;
import org.prebid.server.activity.ComponentType;
import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
Expand All @@ -27,7 +28,6 @@
import org.prebid.server.auction.privacy.contextfactory.SetuidPrivacyContextFactory;
import org.prebid.server.bidder.BidderCatalog;
import org.prebid.server.bidder.UsersyncFormat;
import org.prebid.server.bidder.UsersyncMethod;
import org.prebid.server.bidder.UsersyncMethodType;
import org.prebid.server.bidder.UsersyncUtil;
import org.prebid.server.bidder.Usersyncer;
Expand All @@ -54,19 +54,20 @@
import org.prebid.server.settings.model.AccountGdprConfig;
import org.prebid.server.settings.model.AccountPrivacyConfig;
import org.prebid.server.util.HttpUtil;
import org.prebid.server.util.StreamUtil;
import org.prebid.server.vertx.verticles.server.HttpEndpoint;
import org.prebid.server.vertx.verticles.server.application.ApplicationResource;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class SetuidHandler implements ApplicationResource {

Expand All @@ -88,7 +89,7 @@ public class SetuidHandler implements ApplicationResource {
private final AnalyticsReporterDelegator analyticsDelegator;
private final Metrics metrics;
private final TimeoutFactory timeoutFactory;
private final Map<String, UsersyncMethodType> cookieNameToSyncType;
private final Map<String, Pair<String, UsersyncMethodType>> cookieNameToBidderAndSyncType;

public SetuidHandler(long defaultTimeout,
UidsCookieService uidsCookieService,
Expand All @@ -112,53 +113,58 @@ public SetuidHandler(long defaultTimeout,
this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator);
this.metrics = Objects.requireNonNull(metrics);
this.timeoutFactory = Objects.requireNonNull(timeoutFactory);
this.cookieNameToSyncType = collectMap(bidderCatalog);
this.cookieNameToBidderAndSyncType = collectUsersyncers(bidderCatalog);
}

private static Map<String, UsersyncMethodType> collectMap(BidderCatalog bidderCatalog) {
private static Map<String, Pair<String, UsersyncMethodType>> collectUsersyncers(BidderCatalog bidderCatalog) {
validateUsersyncersDuplicates(bidderCatalog);

return bidderCatalog.usersyncReadyBidders().stream()
.sorted(Comparator.comparing(bidderName -> BooleanUtils.toInteger(bidderCatalog.isAlias(bidderName))))
.filter(StreamUtil.distinctBy(bidderCatalog::cookieFamilyName))
.map(bidderName -> bidderCatalog.usersyncerByName(bidderName)
.map(usersyncer -> Pair.of(bidderName, usersyncer)))
.flatMap(Optional::stream)
.collect(Collectors.toMap(
pair -> pair.getRight().getCookieFamilyName(),
pair -> Pair.of(pair.getLeft(), preferredUserSyncType(pair.getRight()))));
}

final Supplier<Stream<Usersyncer>> usersyncers = () -> bidderCatalog.names()
.stream()
.filter(bidderCatalog::isActive)
private static void validateUsersyncersDuplicates(BidderCatalog bidderCatalog) {
final List<String> duplicatedCookieFamilyNames = bidderCatalog.usersyncReadyBidders().stream()
.filter(bidderName -> !isAliasWithRootCookieFamilyName(bidderCatalog, bidderName))
.map(bidderCatalog::usersyncerByName)
.filter(Optional::isPresent)
.map(Optional::get)
.distinct();

validateUsersyncers(usersyncers.get());
.flatMap(Optional::stream)
.map(Usersyncer::getCookieFamilyName)
.filter(Predicate.not(StreamUtil.distinctBy(Function.identity())))
.distinct()
.sorted()
.toList();

return usersyncers.get()
.collect(Collectors.toMap(Usersyncer::getCookieFamilyName, SetuidHandler::preferredUserSyncType));
if (!duplicatedCookieFamilyNames.isEmpty()) {
throw new IllegalArgumentException(
"Duplicated \"cookie-family-name\" found, values: "
+ String.join(", ", duplicatedCookieFamilyNames));
}
}

@Override
public List<HttpEndpoint> endpoints() {
return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.setuid.value()));
private static boolean isAliasWithRootCookieFamilyName(BidderCatalog bidderCatalog, String bidder) {
final String bidderCookieFamilyName = bidderCatalog.cookieFamilyName(bidder).orElse(StringUtils.EMPTY);
final String parentCookieFamilyName =
bidderCatalog.cookieFamilyName(bidderCatalog.resolveBaseBidder(bidder)).orElse(null);

return bidderCatalog.isAlias(bidder)
&& parentCookieFamilyName != null
&& parentCookieFamilyName.equals(bidderCookieFamilyName);
}

private static UsersyncMethodType preferredUserSyncType(Usersyncer usersyncer) {
return Stream.of(usersyncer.getIframe(), usersyncer.getRedirect())
.filter(Objects::nonNull)
.findFirst()
.map(UsersyncMethod::getType)
.get(); // when usersyncer is present, it will contain at least one method
return ObjectUtils.firstNonNull(usersyncer.getIframe(), usersyncer.getRedirect()).getType();
}

private static void validateUsersyncers(Stream<Usersyncer> usersyncers) {
final List<String> cookieFamilyNameDuplicates = usersyncers.map(Usersyncer::getCookieFamilyName)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet()
.stream()
.filter(name -> name.getValue() > 1)
.map(Map.Entry::getKey)
.distinct()
.sorted()
.toList();
if (CollectionUtils.isNotEmpty(cookieFamilyNameDuplicates)) {
throw new IllegalArgumentException(
"Duplicated \"cookie-family-name\" found, values: "
+ String.join(", ", cookieFamilyNameDuplicates));
}
@Override
public List<HttpEndpoint> endpoints() {
return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.setuid.value()));
}

@Override
Expand All @@ -174,6 +180,11 @@ private Future<SetuidContext> toSetuidContext(RoutingContext routingContext) {
final String requestAccount = httpRequest.getParam(ACCOUNT_PARAM);
final Timeout timeout = timeoutFactory.create(defaultTimeout);

final UsersyncMethodType syncType = Optional.ofNullable(cookieName)
.map(cookieNameToBidderAndSyncType::get)
.map(Pair::getRight)
.orElse(null);

return accountById(requestAccount, timeout)
.compose(account -> setuidPrivacyContextFactory.contextFrom(httpRequest, account, timeout)
.map(privacyContext -> SetuidContext.builder()
Expand All @@ -182,7 +193,7 @@ private Future<SetuidContext> toSetuidContext(RoutingContext routingContext) {
.timeout(timeout)
.account(account)
.cookieName(cookieName)
.syncType(cookieNameToSyncType.get(cookieName))
.syncType(syncType)
.privacyContext(privacyContext)
.build()))

Expand Down Expand Up @@ -212,11 +223,11 @@ private void handleSetuidContextResult(AsyncResult<SetuidContext> setuidContextR

if (setuidContextResult.succeeded()) {
final SetuidContext setuidContext = setuidContextResult.result();
final String bidderCookieName = setuidContext.getCookieName();
final String bidderCookieFamily = setuidContext.getCookieName();
final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext();

try {
validateSetuidContext(setuidContext, bidderCookieName);
validateSetuidContext(setuidContext, bidderCookieFamily);
} catch (InvalidRequestException | UnauthorizedUidsException | UnavailableForLegalReasonsException e) {
handleErrors(e, routingContext, tcfContext);
return;
Expand All @@ -225,28 +236,33 @@ private void handleSetuidContextResult(AsyncResult<SetuidContext> setuidContextR
final AccountPrivacyConfig privacyConfig = setuidContext.getAccount().getPrivacy();
final AccountGdprConfig accountGdprConfig = privacyConfig != null ? privacyConfig.getGdpr() : null;

final String bidderName = cookieNameToBidderAndSyncType.get(bidderCookieFamily).getLeft();

Future.all(
tcfDefinerService.isAllowedForHostVendorId(tcfContext),
tcfDefinerService.resultForBidderNames(
Collections.singleton(bidderCookieName), tcfContext, accountGdprConfig))
.onComplete(hostTcfResponseResult -> respondByTcfResponse(hostTcfResponseResult, setuidContext));
tcfDefinerService.isAllowedForHostVendorId(tcfContext),
tcfDefinerService.resultForBidderNames(
Collections.singleton(bidderName), tcfContext, accountGdprConfig))
.onComplete(hostTcfResponseResult -> respondByTcfResponse(
hostTcfResponseResult,
bidderName,
setuidContext));
} else {
final Throwable error = setuidContextResult.cause();
handleErrors(error, routingContext, null);
}
}

private void validateSetuidContext(SetuidContext setuidContext, String bidder) {
private void validateSetuidContext(SetuidContext setuidContext, String bidderCookieFamily) {
final String cookieName = setuidContext.getCookieName();
final boolean isCookieNameBlank = StringUtils.isBlank(cookieName);
if (isCookieNameBlank || !cookieNameToSyncType.containsKey(cookieName)) {
if (isCookieNameBlank || !cookieNameToBidderAndSyncType.containsKey(cookieName)) {
final String cookieNameError = isCookieNameBlank ? "required" : "invalid";
throw new InvalidRequestException("\"bidder\" query param is " + cookieNameError);
}

final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext();
if (tcfContext.isInGdprScope() && !tcfContext.isConsentValid()) {
metrics.updateUserSyncTcfInvalidMetric(bidder);
metrics.updateUserSyncTcfInvalidMetric(bidderCookieFamily);
throw new InvalidRequestException("Consent string is invalid");
}

Expand All @@ -257,16 +273,18 @@ private void validateSetuidContext(SetuidContext setuidContext, String bidder) {

final ActivityInfrastructure activityInfrastructure = setuidContext.getActivityInfrastructure();
final ActivityInvocationPayload activityInvocationPayload = TcfContextActivityInvocationPayload.of(
ActivityInvocationPayloadImpl.of(ComponentType.BIDDER, bidder),
ActivityInvocationPayloadImpl.of(ComponentType.BIDDER, bidderCookieFamily),
tcfContext);

if (!activityInfrastructure.isAllowed(Activity.SYNC_USER, activityInvocationPayload)) {
throw new UnavailableForLegalReasonsException();
}
}

private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseResult, SetuidContext setuidContext) {
final String bidderCookieName = setuidContext.getCookieName();
private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseResult,
String bidderName,
SetuidContext setuidContext) {

final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext();
final RoutingContext routingContext = setuidContext.getRoutingContext();

Expand All @@ -277,7 +295,7 @@ private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseRe

final Map<String, PrivacyEnforcementAction> vendorIdToAction = bidderTcfResponse.getActions();
final PrivacyEnforcementAction action = vendorIdToAction != null
? vendorIdToAction.get(bidderCookieName)
? vendorIdToAction.get(bidderName)
: null;

final boolean notInGdprScope = BooleanUtils.isFalse(bidderTcfResponse.getUserInGdprScope());
Expand All @@ -286,7 +304,7 @@ private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseRe
if (hostVendorTcfResponse.isVendorAllowed() && isBidderVendorAllowed) {
respondWithCookie(setuidContext);
} else {
metrics.updateUserSyncTcfBlockedMetric(bidderCookieName);
metrics.updateUserSyncTcfBlockedMetric(setuidContext.getCookieName());

final HttpResponseStatus status = new HttpResponseStatus(UNAVAILABLE_FOR_LEGAL_REASONS,
"Unavailable for legal reasons");
Expand All @@ -301,7 +319,7 @@ private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseRe
}
} else {
final Throwable error = hostTcfResponseResult.cause();
metrics.updateUserSyncTcfBlockedMetric(bidderCookieName);
metrics.updateUserSyncTcfBlockedMetric(setuidContext.getCookieName());
handleErrors(error, routingContext, tcfContext);
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/org/prebid/server/util/StreamUtil.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package org.prebid.server.util;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.Spliterator;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

Expand All @@ -17,4 +21,9 @@ public static <T> Stream<T> asStream(Spliterator<T> spliterator) {
public static <T> Stream<T> asStream(Iterator<T> iterator) {
return StreamSupport.stream(IterableUtil.iterable(iterator).spliterator(), false);
}

public static <T> Predicate<T> distinctBy(Function<? super T, ?> keyExtractor) {
final Set<Object> seen = new HashSet<>();
return value -> seen.add(keyExtractor.apply(value));
}
}
Loading

0 comments on commit 5163003

Please sign in to comment.