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 }