Skip to content

Commit

Permalink
Enhanced /setuid TCF support (#3633)
Browse files Browse the repository at this point in the history
  • Loading branch information
AntoxaAntoxic authored Jan 8, 2025
1 parent 6bdb547 commit 74fbd3e
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 49 deletions.
63 changes: 26 additions & 37 deletions src/main/java/org/prebid/server/handler/SetuidHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.AsyncResult;
import io.vertx.core.CompositeFuture;
import io.vertx.core.Future;
import io.vertx.core.http.Cookie;
import io.vertx.core.http.HttpHeaders;
Expand Down Expand Up @@ -50,6 +51,8 @@
import org.prebid.server.privacy.gdpr.model.TcfResponse;
import org.prebid.server.settings.ApplicationSettings;
import org.prebid.server.settings.model.Account;
import org.prebid.server.settings.model.AccountGdprConfig;
import org.prebid.server.settings.model.AccountPrivacyConfig;
import org.prebid.server.util.HttpUtil;
import org.prebid.server.vertx.verticles.server.HttpEndpoint;
import org.prebid.server.vertx.verticles.server.application.ApplicationResource;
Expand Down Expand Up @@ -208,17 +211,23 @@ private void handleSetuidContextResult(AsyncResult<SetuidContext> setuidContextR

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

try {
validateSetuidContext(setuidContext, bidder);
validateSetuidContext(setuidContext, bidderCookieName);
} catch (InvalidRequestException | UnauthorizedUidsException | UnavailableForLegalReasonsException e) {
handleErrors(e, routingContext, tcfContext);
return;
}

isAllowedForHostVendorId(tcfContext)
final AccountPrivacyConfig privacyConfig = setuidContext.getAccount().getPrivacy();
final AccountGdprConfig accountGdprConfig = privacyConfig != null ? privacyConfig.getGdpr() : null;

Future.all(
tcfDefinerService.isAllowedForHostVendorId(tcfContext),
tcfDefinerService.resultForBidderNames(
Collections.singleton(bidderCookieName), tcfContext, accountGdprConfig))
.onComplete(hostTcfResponseResult -> respondByTcfResponse(hostTcfResponseResult, setuidContext));
} else {
final Throwable error = setuidContextResult.cause();
Expand Down Expand Up @@ -255,44 +264,25 @@ private void validateSetuidContext(SetuidContext setuidContext, String bidder) {
}
}

/**
* If host vendor id is null, host allowed to setuid.
*/
private Future<HostVendorTcfResponse> isAllowedForHostVendorId(TcfContext tcfContext) {
final Integer gdprHostVendorId = tcfDefinerService.getGdprHostVendorId();
return gdprHostVendorId == null
? Future.succeededFuture(HostVendorTcfResponse.allowedVendor())
: tcfDefinerService.resultForVendorIds(Collections.singleton(gdprHostVendorId), tcfContext)
.map(this::toHostVendorTcfResponse);
}

private HostVendorTcfResponse toHostVendorTcfResponse(TcfResponse<Integer> tcfResponse) {
return HostVendorTcfResponse.of(tcfResponse.getUserInGdprScope(), tcfResponse.getCountry(),
isSetuidAllowed(tcfResponse));
}

private boolean isSetuidAllowed(TcfResponse<Integer> hostTcfResponseToSetuidContext) {
// allow cookie only if user is not in GDPR scope or vendor passed GDPR check
final boolean notInGdprScope = BooleanUtils.isFalse(hostTcfResponseToSetuidContext.getUserInGdprScope());

final Map<Integer, PrivacyEnforcementAction> vendorIdToAction = hostTcfResponseToSetuidContext.getActions();
final PrivacyEnforcementAction hostPrivacyAction = vendorIdToAction != null
? vendorIdToAction.get(tcfDefinerService.getGdprHostVendorId())
: null;
final boolean blockPixelSync = hostPrivacyAction == null || hostPrivacyAction.isBlockPixelSync();

return notInGdprScope || !blockPixelSync;
}

private void respondByTcfResponse(AsyncResult<HostVendorTcfResponse> hostTcfResponseResult,
SetuidContext setuidContext) {
private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseResult, SetuidContext setuidContext) {
final String bidderCookieName = setuidContext.getCookieName();
final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext();
final RoutingContext routingContext = setuidContext.getRoutingContext();

if (hostTcfResponseResult.succeeded()) {
final HostVendorTcfResponse hostTcfResponse = hostTcfResponseResult.result();
if (hostTcfResponse.isVendorAllowed()) {
final CompositeFuture compositeFuture = hostTcfResponseResult.result();
final HostVendorTcfResponse hostVendorTcfResponse = compositeFuture.resultAt(0);
final TcfResponse<String> bidderTcfResponse = compositeFuture.resultAt(1);

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

final boolean notInGdprScope = BooleanUtils.isFalse(bidderTcfResponse.getUserInGdprScope());
final boolean isBidderVendorAllowed = notInGdprScope || action == null || !action.isBlockPixelSync();

if (hostVendorTcfResponse.isVendorAllowed() && isBidderVendorAllowed) {
respondWithCookie(setuidContext);
} else {
metrics.updateUserSyncTcfBlockedMetric(bidderCookieName);
Expand All @@ -308,7 +298,6 @@ private void respondByTcfResponse(AsyncResult<HostVendorTcfResponse> hostTcfResp

analyticsDelegator.processEvent(SetuidEvent.error(status.code()), tcfContext);
}

} else {
final Throwable error = hostTcfResponseResult.cause();
metrics.updateUserSyncTcfBlockedMetric(bidderCookieName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ private HostVendorTcfResponse toHostVendorTcfResponse(TcfResponse<Integer> tcfRe
return HostVendorTcfResponse.of(
tcfResponse.getUserInGdprScope(),
tcfResponse.getCountry(),
isCookieSyncAllowed(tcfResponse));
isVendorAllowed(tcfResponse));
}

private boolean isCookieSyncAllowed(TcfResponse<Integer> hostTcfResponse) {
private boolean isVendorAllowed(TcfResponse<Integer> hostTcfResponse) {
return Optional.ofNullable(hostTcfResponse.getActions())
.map(vendorIdToAction -> vendorIdToAction.get(gdprHostVendorId))
.map(hostActions -> !hostActions.isBlockPixelSync())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class SetUidSpec extends BaseSpec {
"adapters.${APPNEXUS.value}.usersync.cookie-family-name" : APPNEXUS.value,
"adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : USER_SYNC_URL,
"adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()]
private static final String TCF_ERROR_MESSAGE = "The gdpr_consent param prevents cookies from being saved"
private static final int UNAVAILABLE_FOR_LEGAL_REASONS_CODE = 451

@Shared
PrebidServerService prebidServerService = pbsServiceFactory.getService(PBS_CONFIG)
Expand Down Expand Up @@ -199,8 +201,8 @@ class SetUidSpec extends BaseSpec {

then: "Request should fail with error"
def exception = thrown(PrebidServerException)
assert exception.statusCode == 451
assert exception.responseBody == "The gdpr_consent param prevents cookies from being saved"
assert exception.statusCode == UNAVAILABLE_FOR_LEGAL_REASONS_CODE
assert exception.responseBody == TCF_ERROR_MESSAGE

and: "usersync.FAMILY.tcf.blocked metric should be updated"
def metric = prebidServerService.sendCollectedMetricsRequest()
Expand Down
Loading

0 comments on commit 74fbd3e

Please sign in to comment.