Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setuid: Cookie Family Fix #3675

Merged
merged 4 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
128 changes: 73 additions & 55 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,52 +113,57 @@ 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)
CTMBNara marked this conversation as resolved.
Show resolved Hide resolved
.filter(Predicate.not(StreamUtil.distinctBy(Function.identity())))
.distinct()
.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()
.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 @@ -173,6 +179,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 @@ -181,7 +192,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 @@ -211,11 +222,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 @@ -224,28 +235,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 @@ -256,16 +272,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 @@ -276,7 +294,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 @@ -285,7 +303,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 @@ -300,7 +318,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
Loading