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

Enhanced /setuid TCF support #3633

Merged
merged 3 commits into from
Jan 8, 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
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
Loading