Skip to content

Commit

Permalink
Core: Add DSA Validations (#3133)
Browse files Browse the repository at this point in the history
  • Loading branch information
AntoxaAntoxic authored May 29, 2024
1 parent 0b89797 commit 4c7b77b
Show file tree
Hide file tree
Showing 20 changed files with 746 additions and 95 deletions.
87 changes: 78 additions & 9 deletions src/main/java/org/prebid/server/auction/DsaEnforcer.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.prebid.server.auction;

import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.JsonNode;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Regs;
import com.iab.openrtb.response.Bid;
Expand All @@ -12,19 +12,35 @@
import org.prebid.server.bidder.model.BidderBid;
import org.prebid.server.bidder.model.BidderError;
import org.prebid.server.bidder.model.BidderSeatBid;
import org.prebid.server.exception.PreBidException;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.proto.openrtb.ext.request.DsaPublisherRender;
import org.prebid.server.proto.openrtb.ext.request.DsaRequired;
import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa;
import org.prebid.server.proto.openrtb.ext.response.DsaAdvertiserRender;
import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa;
import org.prebid.server.util.ObjectUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

public class DsaEnforcer {

private static final String DSA_EXT = "dsa";
private static final Set<Integer> DSA_REQUIRED = Set.of(2, 3);
private static final Set<Integer> DSA_REQUIRED = Set.of(
DsaRequired.REQUIRED.getValue(),
DsaRequired.REQUIRED_ONLINE_PLATFORM.getValue());
private static final int MAX_DSA_FIELD_LENGTH = 100;

private final JacksonMapper mapper;

public DsaEnforcer(JacksonMapper mapper) {
this.mapper = Objects.requireNonNull(mapper);
}

public AuctionParticipation enforce(BidRequest bidRequest,
AuctionParticipation auctionParticipation,
Expand All @@ -34,7 +50,7 @@ public AuctionParticipation enforce(BidRequest bidRequest,
final BidderSeatBid seatBid = ObjectUtil.getIfNotNull(bidderResponse, BidderResponse::getSeatBid);
final List<BidderBid> bidderBids = ObjectUtil.getIfNotNull(seatBid, BidderSeatBid::getBids);

if (CollectionUtils.isEmpty(bidderBids) || !isDsaValidationRequired(bidRequest)) {
if (CollectionUtils.isEmpty(bidderBids)) {
return auctionParticipation;
}

Expand All @@ -44,9 +60,19 @@ public AuctionParticipation enforce(BidRequest bidRequest,
for (BidderBid bidderBid : bidderBids) {
final Bid bid = bidderBid.getBid();

if (!isValid(bid)) {
warnings.add(BidderError.invalidBid("Bid \"%s\" missing DSA".formatted(bid.getId())));
rejectionTracker.reject(bid.getImpid(), BidRejectionReason.GENERAL);
final ExtBidDsa dsaResponse = Optional.ofNullable(bid.getExt())
.map(ext -> ext.get(DSA_EXT))
.map(this::getDsaResponse)
.orElse(null);

try {
validateFieldLength(dsaResponse);
if (isDsaValidationRequired(bidRequest)) {
validateDsa(bidRequest, dsaResponse);
}
} catch (PreBidException e) {
warnings.add(BidderError.invalidBid("Bid \"%s\": %s".formatted(bid.getId(), e.getMessage())));
rejectionTracker.reject(bid.getImpid(), BidRejectionReason.REJECTED_BY_DSA_PRIVACY);
updatedBidderBids.remove(bidderBid);
}
}
Expand All @@ -71,9 +97,52 @@ private static boolean isDsaValidationRequired(BidRequest bidRequest) {
.orElse(false);
}

private boolean isValid(Bid bid) {
final ObjectNode bidExt = bid.getExt();
return bidExt != null && bidExt.hasNonNull(DSA_EXT) && !bidExt.get(DSA_EXT).isEmpty();
private static void validateDsa(BidRequest bidRequest, ExtBidDsa dsaResponse) {
if (dsaResponse == null) {
throw new PreBidException("DSA object missing when required");
}

final Integer adRender = dsaResponse.getAdRender();
final Integer pubRender = bidRequest.getRegs().getExt().getDsa().getPubRender();

if (pubRender == null) {
return;
}

if (pubRender == DsaPublisherRender.WILL_RENDER.getValue()
&& adRender != null && adRender == DsaAdvertiserRender.WILL_RENDER.getValue()) {
throw new PreBidException("DSA publisher and buyer both signal will render");
}

if (pubRender == DsaPublisherRender.NOT_RENDER.getValue()
&& (adRender == null || adRender == DsaAdvertiserRender.NOT_RENDER.getValue())) {
throw new PreBidException("DSA publisher and buyer both signal will not render");
}
}

private static void validateFieldLength(ExtBidDsa dsaResponse) {
if (dsaResponse == null) {
return;
}

if (!hasValidLength(dsaResponse.getBehalf())) {
throw new PreBidException("DSA behalf exceeds limit of 100 chars");
}
if (!hasValidLength(dsaResponse.getPaid())) {
throw new PreBidException("DSA paid exceeds limit of 100 chars");
}
}

private static boolean hasValidLength(String value) {
return value == null || value.length() <= MAX_DSA_FIELD_LENGTH;
}

private ExtBidDsa getDsaResponse(JsonNode dsaExt) {
try {
return mapper.mapper().convertValue(dsaExt, ExtBidDsa.class);
} catch (IllegalArgumentException e) {
return null;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum BidRejectionReason {
REJECTED_BY_MEDIA_TYPE(204),
GENERAL(300),
REJECTED_DUE_TO_PRICE_FLOOR(301),
REJECTED_BY_DSA_PRIVACY(305),
FAILED_TO_REQUEST_BIDS(100),
OTHER_ERROR(100);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid;
import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa;
import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsaTransparency;
import org.prebid.server.proto.openrtb.ext.request.DsaTransparency;
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting;
Expand Down Expand Up @@ -289,9 +289,9 @@ private static Regs enrichRegs(Regs regs, PrivacyContext privacyContext, Account
}

private static ExtRegs mapRegsExtDsa(DefaultDsa defaultDsa, ExtRegs regsExt) {
final List<ExtRegsDsaTransparency> enrichedDsaTransparencies = defaultDsa.getTransparency()
final List<DsaTransparency> enrichedDsaTransparencies = defaultDsa.getTransparency()
.stream()
.map(dsaTransparency -> ExtRegsDsaTransparency.of(
.map(dsaTransparency -> DsaTransparency.of(
dsaTransparency.getDomain(), dsaTransparency.getDsaParams()))
.toList();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa;
import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsaTransparency;
import org.prebid.server.proto.openrtb.ext.request.DsaTransparency;
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
import org.prebid.server.proto.openrtb.ext.request.ExtUser;
Expand Down Expand Up @@ -271,7 +271,7 @@ private static Map<String, String> extractDsaRequestParamsFromDsaRegsExtension(f
dsaRequestParams.put("dsadatatopub", dsa.getDataToPub().toString());
}

final List<ExtRegsDsaTransparency> dsaTransparency = dsa.getTransparency();
final List<DsaTransparency> dsaTransparency = dsa.getTransparency();
if (CollectionUtils.isNotEmpty(dsaTransparency)) {
final String encodedTransparencies = encodeTransparenciesAsString(dsaTransparency);
if (StringUtils.isNotBlank(encodedTransparencies)) {
Expand All @@ -282,20 +282,20 @@ private static Map<String, String> extractDsaRequestParamsFromDsaRegsExtension(f
return dsaRequestParams;
}

private static String encodeTransparenciesAsString(List<ExtRegsDsaTransparency> transparencies) {
private static String encodeTransparenciesAsString(List<DsaTransparency> transparencies) {
return transparencies.stream()
.filter(YieldlabBidder::isTransparencyValid)
.map(YieldlabBidder::encodeTransparency)
.collect(Collectors.joining(TRANSPARENCY_TEMPLATE_DELIMITER));
}

private static boolean isTransparencyValid(ExtRegsDsaTransparency transparency) {
private static boolean isTransparencyValid(DsaTransparency transparency) {
return StringUtils.isNotBlank(transparency.getDomain())
&& transparency.getDsaParams() != null
&& CollectionUtils.isNotEmpty(transparency.getDsaParams());
}

private static String encodeTransparency(ExtRegsDsaTransparency transparency) {
private static String encodeTransparency(DsaTransparency transparency) {
return TRANSPARENCY_TEMPLATE.formatted(transparency.getDomain(),
encodeTransparencyParams(transparency.getDsaParams()));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.prebid.server.proto.openrtb.ext.request;

public enum DsaPublisherRender {

NOT_RENDER(0),
COULD_RENDER(1),
WILL_RENDER(2);

private final int value;

DsaPublisherRender(final int value) {
this.value = value;
}

public int getValue() {
return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.prebid.server.proto.openrtb.ext.request;

public enum DsaRequired {

NOT_REQUIRED(0),
SUPPORTED(1),
REQUIRED(2),
REQUIRED_ONLINE_PLATFORM(3);

private final int value;

DsaRequired(final int value) {
this.value = value;
}

public int getValue() {
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@

/**
* Defines the contract for bidrequest.regs.ext.dsa.transparency[i]
* and bidresponse.seatbid[i].bid[i].ext.dsa.transparency[i]
*/
@Value(staticConstructor = "of")
public class ExtRegsDsaTransparency {
public class DsaTransparency {

/**
* Defines the contract for bidrequest.regs.ext.dsa.transparency[i].domain
* and bidresponse.seatbid[i].bid[i].ext.dsa.transparency[i].domain
*/
String domain;

/**
* Defines the contract for bidrequest.regs.ext.dsa.transparency[i].dsaparams[]
* and bidresponse.seatbid[i].bid[i].ext.dsa.transparency[i].dsaparams[]
*/
@JsonProperty("dsaparams")
List<Integer> dsaParams;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ public class ExtRegsDsa {
/**
* Defines the contract for bidrequest.regs.ext.dsa.transparency[]
*/
List<ExtRegsDsaTransparency> transparency;
List<DsaTransparency> transparency;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.prebid.server.proto.openrtb.ext.response;

public enum DsaAdvertiserRender {

NOT_RENDER(0),
WILL_RENDER(1);

private final int value;

DsaAdvertiserRender(final int value) {
this.value = value;
}

public int getValue() {
return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.prebid.server.proto.openrtb.ext.response;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;
import org.prebid.server.proto.openrtb.ext.request.DsaTransparency;

import java.util.List;

/**
* Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa
*/
@Value(staticConstructor = "of")
public class ExtBidDsa {

/**
* Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa.behalf
*/
String behalf;

/**
* Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa.paid
*/
String paid;

/**
* Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa.transparency[]
*/
List<DsaTransparency> transparency;

/**
* Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa.adrender
*/
@JsonProperty("adrender")
Integer adRender;

}
Original file line number Diff line number Diff line change
Expand Up @@ -1103,8 +1103,8 @@ LoggerControlKnob loggerControlKnob(Vertx vertx) {
}

@Bean
DsaEnforcer dsaEnforcer() {
return new DsaEnforcer();
DsaEnforcer dsaEnforcer(JacksonMapper mapper) {
return new DsaEnforcer(mapper);
}

private static List<String> splitToList(String listAsString) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import org.prebid.server.functional.util.PBSUtils

import static org.prebid.server.functional.model.request.auction.DsaPubRender.PUB_MIGHT_RENDER

@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy)
@EqualsAndHashCode
@ToString(includeNames = true, ignoreNulls = true)
Expand All @@ -18,7 +20,7 @@ class Dsa {

static Dsa getDefaultDsa(DsaRequired dsaRequired = PBSUtils.getRandomEnum(DsaRequired)) {
new Dsa(dsaRequired: dsaRequired,
pubRender: PBSUtils.getRandomEnum(DsaPubRender),
pubRender: PUB_MIGHT_RENDER,
dataToPub: PBSUtils.getRandomEnum(DsaDataToPub),
transparency: [DsaTransparency.defaultDsaTransparency])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ class BidExt {
Prebid prebid
BigDecimal origbidcpm
Currency origbidcur
Dsa dsa
DsaResponse dsa
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum BidRejectionReason {
REJECTED_BY_MEDIA_TYPE(204),
GENERAL(300),
REJECTED_DUE_TO_PRICE_FLOOR(301),
REJECTED_DUE_TO_DSA(305),
OTHER_ERROR(100)

@JsonValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import org.prebid.server.functional.util.PBSUtils
@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy)
@EqualsAndHashCode
@ToString(includeNames = true, ignoreNulls = true)
class Dsa {
class DsaResponse {

String behalf
String paid
List<DsaTransparency> transparency
DsaAdRender adRender

static Dsa getDefaultDsa() {
new Dsa(
static DsaResponse getDefaultDsa() {
new DsaResponse(
behalf: PBSUtils.randomString,
paid: PBSUtils.randomString,
adRender: PBSUtils.getRandomEnum(DsaAdRender),
Expand Down
Loading

0 comments on commit 4c7b77b

Please sign in to comment.