github.com/prebid/prebid-server/v2@v2.18.0/privacy/ccpa/parsedpolicy.go (about)

     1  package ccpa
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  
     7  	"github.com/prebid/prebid-server/v2/errortypes"
     8  )
     9  
    10  const (
    11  	ccpaVersion1      = '1'
    12  	ccpaYes           = 'Y'
    13  	ccpaNo            = 'N'
    14  	ccpaNotApplicable = '-'
    15  )
    16  
    17  const (
    18  	indexVersion                = 0
    19  	indexExplicitNotice         = 1
    20  	indexOptOutSale             = 2
    21  	indexLSPACoveredTransaction = 3
    22  )
    23  
    24  const allBiddersMarker = "*"
    25  
    26  // ValidateConsent returns true if the consent string is empty or valid per the IAB CCPA spec.
    27  func ValidateConsent(consent string) bool {
    28  	_, err := parseConsent(consent)
    29  	return err == nil
    30  }
    31  
    32  // ParsedPolicy represents parsed and validated CCPA regulatory information. Use this struct
    33  // to make enforcement decisions.
    34  type ParsedPolicy struct {
    35  	consentSpecified      bool
    36  	consentOptOutSale     bool
    37  	noSaleForAllBidders   bool
    38  	noSaleSpecificBidders map[string]struct{}
    39  }
    40  
    41  // Parse returns a parsed and validated ParsedPolicy intended for use in enforcement decisions.
    42  func (p Policy) Parse(validBidders map[string]struct{}) (ParsedPolicy, error) {
    43  	consentOptOut, err := parseConsent(p.Consent)
    44  	if err != nil {
    45  		msg := fmt.Sprintf("request.regs.ext.us_privacy %s", err.Error())
    46  		return ParsedPolicy{}, &errortypes.Warning{
    47  			Message:     msg,
    48  			WarningCode: errortypes.InvalidPrivacyConsentWarningCode,
    49  		}
    50  	}
    51  
    52  	noSaleForAllBidders, noSaleSpecificBidders, err := parseNoSaleBidders(p.NoSaleBidders, validBidders)
    53  	if err != nil {
    54  		return ParsedPolicy{}, fmt.Errorf("request.ext.prebid.nosale is invalid: %s", err.Error())
    55  	}
    56  
    57  	return ParsedPolicy{
    58  		consentSpecified:      p.Consent != "",
    59  		consentOptOutSale:     consentOptOut,
    60  		noSaleForAllBidders:   noSaleForAllBidders,
    61  		noSaleSpecificBidders: noSaleSpecificBidders,
    62  	}, nil
    63  }
    64  
    65  func parseConsent(consent string) (consentOptOutSale bool, err error) {
    66  	if consent == "" {
    67  		return false, nil
    68  	}
    69  
    70  	if len(consent) != 4 {
    71  		return false, errors.New("must contain 4 characters")
    72  	}
    73  
    74  	if consent[indexVersion] != ccpaVersion1 {
    75  		return false, errors.New("must specify version 1")
    76  	}
    77  
    78  	var c byte
    79  
    80  	c = consent[indexExplicitNotice]
    81  	if c != ccpaNo && c != ccpaYes && c != ccpaNotApplicable {
    82  		return false, errors.New("must specify 'N', 'Y', or '-' for the explicit notice")
    83  	}
    84  
    85  	c = consent[indexOptOutSale]
    86  	if c != ccpaNo && c != ccpaYes && c != ccpaNotApplicable {
    87  		return false, errors.New("must specify 'N', 'Y', or '-' for the opt-out sale")
    88  	}
    89  
    90  	c = consent[indexLSPACoveredTransaction]
    91  	if c != ccpaNo && c != ccpaYes && c != ccpaNotApplicable {
    92  		return false, errors.New("must specify 'N', 'Y', or '-' for the limited service provider agreement")
    93  	}
    94  
    95  	return consent[indexOptOutSale] == ccpaYes, nil
    96  }
    97  
    98  func parseNoSaleBidders(noSaleBidders []string, validBidders map[string]struct{}) (noSaleForAllBidders bool, noSaleSpecificBidders map[string]struct{}, err error) {
    99  	noSaleSpecificBidders = make(map[string]struct{})
   100  
   101  	if len(noSaleBidders) == 1 && noSaleBidders[0] == allBiddersMarker {
   102  		noSaleForAllBidders = true
   103  		return
   104  	}
   105  
   106  	for _, bidder := range noSaleBidders {
   107  		if bidder == allBiddersMarker {
   108  			err = errors.New("can only specify all bidders if no other bidders are provided")
   109  			return
   110  		}
   111  
   112  		if _, exists := validBidders[bidder]; exists {
   113  			noSaleSpecificBidders[bidder] = struct{}{}
   114  		} else {
   115  			err = fmt.Errorf("unrecognized bidder '%s'", bidder)
   116  			return
   117  		}
   118  	}
   119  
   120  	return
   121  }
   122  
   123  // CanEnforce returns true when consent is specifically provided by the publisher, as opposed to an empty string.
   124  func (p ParsedPolicy) CanEnforce() bool {
   125  	return p.consentSpecified
   126  }
   127  
   128  func (p ParsedPolicy) isNoSaleForBidder(bidder string) bool {
   129  	if p.noSaleForAllBidders {
   130  		return true
   131  	}
   132  
   133  	_, exists := p.noSaleSpecificBidders[bidder]
   134  	return exists
   135  }
   136  
   137  // ShouldEnforce returns true when the opt-out signal is explicitly detected.
   138  func (p ParsedPolicy) ShouldEnforce(bidder string) bool {
   139  	return !p.isNoSaleForBidder(bidder) && p.consentOptOutSale
   140  }