github.com/prebid/prebid-server/v2@v2.18.0/endpoints/cookie_sync.go (about)

     1  package endpoints
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/golang/glog"
    15  	"github.com/julienschmidt/httprouter"
    16  	gpplib "github.com/prebid/go-gpp"
    17  	gppConstants "github.com/prebid/go-gpp/constants"
    18  	accountService "github.com/prebid/prebid-server/v2/account"
    19  	"github.com/prebid/prebid-server/v2/analytics"
    20  	"github.com/prebid/prebid-server/v2/config"
    21  	"github.com/prebid/prebid-server/v2/errortypes"
    22  	"github.com/prebid/prebid-server/v2/gdpr"
    23  	"github.com/prebid/prebid-server/v2/macros"
    24  	"github.com/prebid/prebid-server/v2/metrics"
    25  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    26  	"github.com/prebid/prebid-server/v2/privacy"
    27  	"github.com/prebid/prebid-server/v2/privacy/ccpa"
    28  	gppPrivacy "github.com/prebid/prebid-server/v2/privacy/gpp"
    29  	"github.com/prebid/prebid-server/v2/stored_requests"
    30  	"github.com/prebid/prebid-server/v2/usersync"
    31  	"github.com/prebid/prebid-server/v2/util/jsonutil"
    32  	stringutil "github.com/prebid/prebid-server/v2/util/stringutil"
    33  	"github.com/prebid/prebid-server/v2/util/timeutil"
    34  )
    35  
    36  const receiveCookieDeprecation = "receive-cookie-deprecation"
    37  
    38  var (
    39  	errCookieSyncOptOut                            = errors.New("User has opted out")
    40  	errCookieSyncBody                              = errors.New("Failed to read request body")
    41  	errCookieSyncGDPRConsentMissing                = errors.New("gdpr_consent is required if gdpr=1")
    42  	errCookieSyncGDPRConsentMissingSignalAmbiguous = errors.New("gdpr_consent is required. gdpr is not specified and is assumed to be 1 by the server. set gdpr=0 to exempt this request")
    43  	errCookieSyncInvalidBiddersType                = errors.New("invalid bidders type. must either be a string '*' or a string array of bidders")
    44  	errCookieSyncAccountBlocked                    = errors.New("account is disabled, please reach out to the prebid server host")
    45  	errCookieSyncAccountConfigMalformed            = errors.New("account config is malformed and could not be read")
    46  	errCookieSyncAccountInvalid                    = errors.New("account must be valid if provided, please reach out to the prebid server host")
    47  	errSyncerIsNotPriority                         = errors.New("syncer key is not a priority, and there are only priority elements left")
    48  )
    49  
    50  var cookieSyncBidderFilterAllowAll = usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude)
    51  
    52  func NewCookieSyncEndpoint(
    53  	syncersByBidder map[string]usersync.Syncer,
    54  	config *config.Configuration,
    55  	gdprPermsBuilder gdpr.PermissionsBuilder,
    56  	tcf2CfgBuilder gdpr.TCF2ConfigBuilder,
    57  	metrics metrics.MetricsEngine,
    58  	analyticsRunner analytics.Runner,
    59  	accountsFetcher stored_requests.AccountFetcher,
    60  	bidders map[string]openrtb_ext.BidderName) HTTPRouterHandler {
    61  
    62  	bidderHashSet := make(map[string]struct{}, len(bidders))
    63  	for _, bidder := range bidders {
    64  		bidderHashSet[string(bidder)] = struct{}{}
    65  	}
    66  
    67  	return &cookieSyncEndpoint{
    68  		chooser: usersync.NewChooser(syncersByBidder, bidderHashSet, config.BidderInfos),
    69  		config:  config,
    70  		privacyConfig: usersyncPrivacyConfig{
    71  			gdprConfig:             config.GDPR,
    72  			gdprPermissionsBuilder: gdprPermsBuilder,
    73  			tcf2ConfigBuilder:      tcf2CfgBuilder,
    74  			ccpaEnforce:            config.CCPA.Enforce,
    75  			bidderHashSet:          bidderHashSet,
    76  		},
    77  		metrics:         metrics,
    78  		pbsAnalytics:    analyticsRunner,
    79  		accountsFetcher: accountsFetcher,
    80  		time:            &timeutil.RealTime{},
    81  	}
    82  }
    83  
    84  type cookieSyncEndpoint struct {
    85  	chooser         usersync.Chooser
    86  	config          *config.Configuration
    87  	privacyConfig   usersyncPrivacyConfig
    88  	metrics         metrics.MetricsEngine
    89  	pbsAnalytics    analytics.Runner
    90  	accountsFetcher stored_requests.AccountFetcher
    91  	time            timeutil.Time
    92  }
    93  
    94  func (c *cookieSyncEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    95  	request, privacyMacros, account, err := c.parseRequest(r)
    96  	c.setCookieDeprecationHeader(w, r, account)
    97  	if err != nil {
    98  		c.writeParseRequestErrorMetrics(err)
    99  		c.handleError(w, err, http.StatusBadRequest)
   100  		return
   101  	}
   102  	decoder := usersync.Base64Decoder{}
   103  
   104  	cookie := usersync.ReadCookie(r, decoder, &c.config.HostCookie)
   105  	usersync.SyncHostCookie(r, cookie, &c.config.HostCookie)
   106  
   107  	result := c.chooser.Choose(request, cookie)
   108  
   109  	switch result.Status {
   110  	case usersync.StatusBlockedByUserOptOut:
   111  		c.metrics.RecordCookieSync(metrics.CookieSyncOptOut)
   112  		c.handleError(w, errCookieSyncOptOut, http.StatusUnauthorized)
   113  	case usersync.StatusBlockedByPrivacy:
   114  		c.metrics.RecordCookieSync(metrics.CookieSyncGDPRHostCookieBlocked)
   115  		c.handleResponse(w, request.SyncTypeFilter, cookie, privacyMacros, nil, result.BiddersEvaluated, request.Debug)
   116  	case usersync.StatusOK:
   117  		c.metrics.RecordCookieSync(metrics.CookieSyncOK)
   118  		c.writeSyncerMetrics(result.BiddersEvaluated)
   119  		c.handleResponse(w, request.SyncTypeFilter, cookie, privacyMacros, result.SyncersChosen, result.BiddersEvaluated, request.Debug)
   120  	}
   121  }
   122  
   123  func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, macros.UserSyncPrivacy, *config.Account, error) {
   124  	defer r.Body.Close()
   125  	body, err := io.ReadAll(r.Body)
   126  	if err != nil {
   127  		return usersync.Request{}, macros.UserSyncPrivacy{}, nil, errCookieSyncBody
   128  	}
   129  
   130  	request := cookieSyncRequest{}
   131  	if err := jsonutil.UnmarshalValid(body, &request); err != nil {
   132  		return usersync.Request{}, macros.UserSyncPrivacy{}, nil, fmt.Errorf("JSON parsing failed: %s", err.Error())
   133  	}
   134  
   135  	if request.Account == "" {
   136  		request.Account = metrics.PublisherUnknown
   137  	}
   138  	account, fetchErrs := accountService.GetAccount(context.Background(), c.config, c.accountsFetcher, request.Account, c.metrics)
   139  	if len(fetchErrs) > 0 {
   140  		return usersync.Request{}, macros.UserSyncPrivacy{}, nil, combineErrors(fetchErrs)
   141  	}
   142  
   143  	request = c.setLimit(request, account.CookieSync)
   144  	request = c.setCooperativeSync(request, account.CookieSync)
   145  
   146  	privacyMacros, gdprSignal, privacyPolicies, err := extractPrivacyPolicies(request, c.privacyConfig.gdprConfig.DefaultValue)
   147  	if err != nil {
   148  		return usersync.Request{}, macros.UserSyncPrivacy{}, account, err
   149  	}
   150  
   151  	ccpaParsedPolicy := ccpa.ParsedPolicy{}
   152  	if request.USPrivacy != "" {
   153  		parsedPolicy, err := ccpa.Policy{Consent: request.USPrivacy}.Parse(c.privacyConfig.bidderHashSet)
   154  		if err != nil {
   155  			privacyMacros.USPrivacy = ""
   156  		}
   157  		if c.privacyConfig.ccpaEnforce {
   158  			ccpaParsedPolicy = parsedPolicy
   159  		}
   160  	}
   161  
   162  	activityControl := privacy.NewActivityControl(&account.Privacy)
   163  
   164  	syncTypeFilter, err := parseTypeFilter(request.FilterSettings)
   165  	if err != nil {
   166  		return usersync.Request{}, macros.UserSyncPrivacy{}, account, err
   167  	}
   168  
   169  	gdprRequestInfo := gdpr.RequestInfo{
   170  		Consent:    privacyMacros.GDPRConsent,
   171  		GDPRSignal: gdprSignal,
   172  	}
   173  
   174  	tcf2Cfg := c.privacyConfig.tcf2ConfigBuilder(c.privacyConfig.gdprConfig.TCF2, account.GDPR)
   175  	gdprPerms := c.privacyConfig.gdprPermissionsBuilder(tcf2Cfg, gdprRequestInfo)
   176  
   177  	rx := usersync.Request{
   178  		Bidders: request.Bidders,
   179  		Cooperative: usersync.Cooperative{
   180  			Enabled:        (request.CooperativeSync != nil && *request.CooperativeSync) || (request.CooperativeSync == nil && c.config.UserSync.Cooperative.EnabledByDefault),
   181  			PriorityGroups: c.config.UserSync.PriorityGroups,
   182  		},
   183  		Debug: request.Debug,
   184  		Limit: request.Limit,
   185  		Privacy: usersyncPrivacy{
   186  			gdprPermissions:  gdprPerms,
   187  			ccpaParsedPolicy: ccpaParsedPolicy,
   188  			activityControl:  activityControl,
   189  			activityRequest:  privacy.NewRequestFromPolicies(privacyPolicies),
   190  			gdprSignal:       gdprSignal,
   191  		},
   192  		SyncTypeFilter: syncTypeFilter,
   193  		GPPSID:         request.GPPSID,
   194  	}
   195  	return rx, privacyMacros, account, nil
   196  }
   197  
   198  func extractPrivacyPolicies(request cookieSyncRequest, usersyncDefaultGDPRValue string) (macros.UserSyncPrivacy, gdpr.Signal, privacy.Policies, error) {
   199  	// GDPR
   200  	gppSID, err := stringutil.StrToInt8Slice(request.GPPSID)
   201  	if err != nil {
   202  		return macros.UserSyncPrivacy{}, gdpr.SignalNo, privacy.Policies{}, err
   203  	}
   204  
   205  	gdprSignal, gdprString, err := extractGDPRSignal(request.GDPR, gppSID)
   206  	if err != nil {
   207  		return macros.UserSyncPrivacy{}, gdpr.SignalNo, privacy.Policies{}, err
   208  	}
   209  
   210  	var gpp gpplib.GppContainer
   211  	if len(request.GPP) > 0 {
   212  		var errs []error
   213  		gpp, errs = gpplib.Parse(request.GPP)
   214  		if len(errs) > 0 {
   215  			return macros.UserSyncPrivacy{}, gdpr.SignalNo, privacy.Policies{}, errs[0]
   216  		}
   217  	}
   218  
   219  	gdprConsent := request.GDPRConsent
   220  	if i := gppPrivacy.IndexOfSID(gpp, gppConstants.SectionTCFEU2); i >= 0 {
   221  		gdprConsent = gpp.Sections[i].GetValue()
   222  	}
   223  
   224  	if gdprConsent == "" {
   225  		if gdprSignal == gdpr.SignalYes {
   226  			return macros.UserSyncPrivacy{}, gdpr.SignalNo, privacy.Policies{}, errCookieSyncGDPRConsentMissing
   227  		}
   228  
   229  		if gdprSignal == gdpr.SignalAmbiguous && gdpr.SignalNormalize(gdprSignal, usersyncDefaultGDPRValue) == gdpr.SignalYes {
   230  			return macros.UserSyncPrivacy{}, gdpr.SignalNo, privacy.Policies{}, errCookieSyncGDPRConsentMissingSignalAmbiguous
   231  		}
   232  	}
   233  
   234  	// CCPA
   235  	ccpaString, err := ccpa.SelectCCPAConsent(request.USPrivacy, gpp, gppSID)
   236  	if err != nil {
   237  		return macros.UserSyncPrivacy{}, gdpr.SignalNo, privacy.Policies{}, err
   238  	}
   239  
   240  	privacyMacros := macros.UserSyncPrivacy{
   241  		GDPR:        gdprString,
   242  		GDPRConsent: gdprConsent,
   243  		USPrivacy:   ccpaString,
   244  		GPP:         request.GPP,
   245  		GPPSID:      request.GPPSID,
   246  	}
   247  
   248  	privacyPolicies := privacy.Policies{
   249  		GPPSID: gppSID,
   250  	}
   251  
   252  	return privacyMacros, gdprSignal, privacyPolicies, nil
   253  }
   254  
   255  func extractGDPRSignal(requestGDPR *int, gppSID []int8) (gdpr.Signal, string, error) {
   256  	if len(gppSID) > 0 {
   257  		if gppPrivacy.IsSIDInList(gppSID, gppConstants.SectionTCFEU2) {
   258  			return gdpr.SignalYes, strconv.Itoa(int(gdpr.SignalYes)), nil
   259  		}
   260  		return gdpr.SignalNo, strconv.Itoa(int(gdpr.SignalNo)), nil
   261  	}
   262  
   263  	if requestGDPR == nil {
   264  		return gdpr.SignalAmbiguous, "", nil
   265  	}
   266  
   267  	gdprSignal, err := gdpr.IntSignalParse(*requestGDPR)
   268  	if err != nil {
   269  		return gdpr.SignalAmbiguous, strconv.Itoa(*requestGDPR), err
   270  	}
   271  	return gdprSignal, strconv.Itoa(*requestGDPR), nil
   272  }
   273  
   274  func (c *cookieSyncEndpoint) writeParseRequestErrorMetrics(err error) {
   275  	switch err {
   276  	case errCookieSyncAccountBlocked:
   277  		c.metrics.RecordCookieSync(metrics.CookieSyncAccountBlocked)
   278  	case errCookieSyncAccountConfigMalformed:
   279  		c.metrics.RecordCookieSync(metrics.CookieSyncAccountConfigMalformed)
   280  	case errCookieSyncAccountInvalid:
   281  		c.metrics.RecordCookieSync(metrics.CookieSyncAccountInvalid)
   282  	default:
   283  		c.metrics.RecordCookieSync(metrics.CookieSyncBadRequest)
   284  	}
   285  }
   286  
   287  func (c *cookieSyncEndpoint) setLimit(request cookieSyncRequest, cookieSyncConfig config.CookieSync) cookieSyncRequest {
   288  	if request.Limit <= 0 && cookieSyncConfig.DefaultLimit != nil {
   289  		request.Limit = *cookieSyncConfig.DefaultLimit
   290  	}
   291  	if cookieSyncConfig.MaxLimit != nil && (request.Limit <= 0 || request.Limit > *cookieSyncConfig.MaxLimit) {
   292  		request.Limit = *cookieSyncConfig.MaxLimit
   293  	}
   294  	if request.Limit < 0 {
   295  		request.Limit = 0
   296  	}
   297  
   298  	return request
   299  }
   300  
   301  func (c *cookieSyncEndpoint) setCooperativeSync(request cookieSyncRequest, cookieSyncConfig config.CookieSync) cookieSyncRequest {
   302  	if request.CooperativeSync == nil && cookieSyncConfig.DefaultCoopSync != nil {
   303  		request.CooperativeSync = cookieSyncConfig.DefaultCoopSync
   304  	}
   305  
   306  	return request
   307  }
   308  
   309  func parseTypeFilter(request *cookieSyncRequestFilterSettings) (usersync.SyncTypeFilter, error) {
   310  	syncTypeFilter := usersync.SyncTypeFilter{
   311  		IFrame:   cookieSyncBidderFilterAllowAll,
   312  		Redirect: cookieSyncBidderFilterAllowAll,
   313  	}
   314  
   315  	if request != nil {
   316  		if filter, err := parseBidderFilter(request.IFrame); err == nil {
   317  			syncTypeFilter.IFrame = filter
   318  		} else {
   319  			return usersync.SyncTypeFilter{}, fmt.Errorf("error parsing filtersettings.iframe: %v", err)
   320  		}
   321  
   322  		if filter, err := parseBidderFilter(request.Redirect); err == nil {
   323  			syncTypeFilter.Redirect = filter
   324  		} else {
   325  			return usersync.SyncTypeFilter{}, fmt.Errorf("error parsing filtersettings.image: %v", err)
   326  		}
   327  	}
   328  
   329  	return syncTypeFilter, nil
   330  }
   331  
   332  func parseBidderFilter(filter *cookieSyncRequestFilter) (usersync.BidderFilter, error) {
   333  	if filter == nil {
   334  		return cookieSyncBidderFilterAllowAll, nil
   335  	}
   336  
   337  	var mode usersync.BidderFilterMode
   338  	switch filter.Mode {
   339  	case "include":
   340  		mode = usersync.BidderFilterModeInclude
   341  	case "exclude":
   342  		mode = usersync.BidderFilterModeExclude
   343  	default:
   344  		return nil, fmt.Errorf("invalid filter value '%s'. must be either 'include' or 'exclude'", filter.Mode)
   345  	}
   346  
   347  	switch v := filter.Bidders.(type) {
   348  	case string:
   349  		if v == "*" {
   350  			return usersync.NewUniformBidderFilter(mode), nil
   351  		}
   352  		return nil, fmt.Errorf("invalid bidders value `%s`. must either be '*' or a string array", v)
   353  	case []interface{}:
   354  		bidders := make([]string, len(v))
   355  		for i, x := range v {
   356  			if bidder, ok := x.(string); ok {
   357  				bidders[i] = bidder
   358  			} else {
   359  				return nil, errCookieSyncInvalidBiddersType
   360  			}
   361  		}
   362  		return usersync.NewSpecificBidderFilter(bidders, mode), nil
   363  	default:
   364  		return nil, errCookieSyncInvalidBiddersType
   365  	}
   366  }
   367  
   368  func (c *cookieSyncEndpoint) handleError(w http.ResponseWriter, err error, httpStatus int) {
   369  	http.Error(w, err.Error(), httpStatus)
   370  	c.pbsAnalytics.LogCookieSyncObject(&analytics.CookieSyncObject{
   371  		Status:       httpStatus,
   372  		Errors:       []error{err},
   373  		BidderStatus: []*analytics.CookieSyncBidder{},
   374  	})
   375  }
   376  
   377  func combineErrors(errs []error) error {
   378  	var errorStrings []string
   379  	for _, err := range errs {
   380  		// preserve knowledge of special account errors
   381  		switch errortypes.ReadCode(err) {
   382  		case errortypes.AccountDisabledErrorCode:
   383  			return errCookieSyncAccountBlocked
   384  		case errortypes.AcctRequiredErrorCode:
   385  			return errCookieSyncAccountInvalid
   386  		case errortypes.MalformedAcctErrorCode:
   387  			return errCookieSyncAccountConfigMalformed
   388  		}
   389  
   390  		errorStrings = append(errorStrings, err.Error())
   391  	}
   392  	combinedErrors := strings.Join(errorStrings, " ")
   393  	return errors.New(combinedErrors)
   394  }
   395  
   396  func (c *cookieSyncEndpoint) writeSyncerMetrics(biddersEvaluated []usersync.BidderEvaluation) {
   397  	for _, bidder := range biddersEvaluated {
   398  		switch bidder.Status {
   399  		case usersync.StatusOK:
   400  			c.metrics.RecordSyncerRequest(bidder.SyncerKey, metrics.SyncerCookieSyncOK)
   401  		case usersync.StatusBlockedByPrivacy:
   402  			c.metrics.RecordSyncerRequest(bidder.SyncerKey, metrics.SyncerCookieSyncPrivacyBlocked)
   403  		case usersync.StatusAlreadySynced:
   404  			c.metrics.RecordSyncerRequest(bidder.SyncerKey, metrics.SyncerCookieSyncAlreadySynced)
   405  		case usersync.StatusTypeNotSupported:
   406  			c.metrics.RecordSyncerRequest(bidder.SyncerKey, metrics.SyncerCookieSyncTypeNotSupported)
   407  		}
   408  	}
   409  }
   410  
   411  func (c *cookieSyncEndpoint) handleResponse(w http.ResponseWriter, tf usersync.SyncTypeFilter, co *usersync.Cookie, m macros.UserSyncPrivacy, s []usersync.SyncerChoice, biddersEvaluated []usersync.BidderEvaluation, debug bool) {
   412  	status := "no_cookie"
   413  	if co.HasAnyLiveSyncs() {
   414  		status = "ok"
   415  	}
   416  
   417  	response := cookieSyncResponse{
   418  		Status:       status,
   419  		BidderStatus: make([]cookieSyncResponseBidder, 0, len(s)),
   420  	}
   421  
   422  	for _, syncerChoice := range s {
   423  		syncTypes := tf.ForBidder(syncerChoice.Bidder)
   424  		sync, err := syncerChoice.Syncer.GetSync(syncTypes, m)
   425  		if err != nil {
   426  			glog.Errorf("Failed to get usersync info for %s: %v", syncerChoice.Bidder, err)
   427  			continue
   428  		}
   429  
   430  		response.BidderStatus = append(response.BidderStatus, cookieSyncResponseBidder{
   431  			BidderCode: syncerChoice.Bidder,
   432  			NoCookie:   true,
   433  			UsersyncInfo: cookieSyncResponseSync{
   434  				URL:         sync.URL,
   435  				Type:        string(sync.Type),
   436  				SupportCORS: sync.SupportCORS,
   437  			},
   438  		})
   439  	}
   440  
   441  	if debug {
   442  		biddersSeen := make(map[string]struct{})
   443  		var debugInfo []cookieSyncResponseDebug
   444  		for _, bidderEval := range biddersEvaluated {
   445  			var debugResponse cookieSyncResponseDebug
   446  			debugResponse.Bidder = bidderEval.Bidder
   447  			if bidderEval.Status == usersync.StatusDuplicate && biddersSeen[bidderEval.Bidder] == struct{}{} {
   448  				debugResponse.Error = getDebugMessage(bidderEval.Status) + " synced as " + bidderEval.SyncerKey
   449  				debugInfo = append(debugInfo, debugResponse)
   450  			} else if bidderEval.Status != usersync.StatusOK {
   451  				debugResponse.Error = getDebugMessage(bidderEval.Status)
   452  				debugInfo = append(debugInfo, debugResponse)
   453  			}
   454  			biddersSeen[bidderEval.Bidder] = struct{}{}
   455  		}
   456  		response.Debug = debugInfo
   457  	}
   458  
   459  	c.pbsAnalytics.LogCookieSyncObject(&analytics.CookieSyncObject{
   460  		Status:       http.StatusOK,
   461  		BidderStatus: mapBidderStatusToAnalytics(response.BidderStatus),
   462  	})
   463  
   464  	w.Header().Set("Content-Type", "application/json; charset=utf-8")
   465  
   466  	enc := json.NewEncoder(w)
   467  	enc.SetEscapeHTML(false)
   468  	enc.Encode(response)
   469  }
   470  
   471  func (c *cookieSyncEndpoint) setCookieDeprecationHeader(w http.ResponseWriter, r *http.Request, account *config.Account) {
   472  	if rcd, err := r.Cookie(receiveCookieDeprecation); err == nil && rcd != nil {
   473  		return
   474  	}
   475  	if account == nil || !account.Privacy.PrivacySandbox.CookieDeprecation.Enabled {
   476  		return
   477  	}
   478  	cookie := &http.Cookie{
   479  		Name:     receiveCookieDeprecation,
   480  		Value:    "1",
   481  		Secure:   true,
   482  		HttpOnly: true,
   483  		Path:     "/",
   484  		SameSite: http.SameSiteNoneMode,
   485  		Expires:  c.time.Now().Add(time.Second * time.Duration(account.Privacy.PrivacySandbox.CookieDeprecation.TTLSec)),
   486  	}
   487  	setCookiePartitioned(w, cookie)
   488  }
   489  
   490  // setCookiePartitioned temporary substitute for http.SetCookie(w, cookie) until it supports Partitioned cookie type. Refer https://github.com/golang/go/issues/62490
   491  func setCookiePartitioned(w http.ResponseWriter, cookie *http.Cookie) {
   492  	if v := cookie.String(); v != "" {
   493  		w.Header().Add("Set-Cookie", v+"; Partitioned;")
   494  	}
   495  }
   496  
   497  func mapBidderStatusToAnalytics(from []cookieSyncResponseBidder) []*analytics.CookieSyncBidder {
   498  	to := make([]*analytics.CookieSyncBidder, len(from))
   499  	for i, b := range from {
   500  		to[i] = &analytics.CookieSyncBidder{
   501  			BidderCode: b.BidderCode,
   502  			NoCookie:   b.NoCookie,
   503  			UsersyncInfo: &analytics.UsersyncInfo{
   504  				URL:         b.UsersyncInfo.URL,
   505  				Type:        b.UsersyncInfo.Type,
   506  				SupportCORS: b.UsersyncInfo.SupportCORS,
   507  			},
   508  		}
   509  	}
   510  	return to
   511  }
   512  
   513  func getDebugMessage(status usersync.Status) string {
   514  	switch status {
   515  	case usersync.StatusAlreadySynced:
   516  		return "Already in sync"
   517  	case usersync.StatusBlockedByPrivacy:
   518  		return "Rejected by privacy"
   519  	case usersync.StatusBlockedByUserOptOut:
   520  		return "Status blocked by user opt out"
   521  	case usersync.StatusDuplicate:
   522  		return "Duplicate bidder"
   523  	case usersync.StatusUnknownBidder:
   524  		return "Unsupported bidder"
   525  	case usersync.StatusUnconfiguredBidder:
   526  		return "No sync config"
   527  	case usersync.StatusTypeNotSupported:
   528  		return "Type not supported"
   529  	case usersync.StatusBlockedByDisabledUsersync:
   530  		return "Sync disabled by config"
   531  	}
   532  	return ""
   533  }
   534  
   535  type cookieSyncRequest struct {
   536  	Bidders         []string                         `json:"bidders"`
   537  	GDPR            *int                             `json:"gdpr"`
   538  	GDPRConsent     string                           `json:"gdpr_consent"`
   539  	USPrivacy       string                           `json:"us_privacy"`
   540  	Limit           int                              `json:"limit"`
   541  	GPP             string                           `json:"gpp"`
   542  	GPPSID          string                           `json:"gpp_sid"`
   543  	CooperativeSync *bool                            `json:"coopSync"`
   544  	FilterSettings  *cookieSyncRequestFilterSettings `json:"filterSettings"`
   545  	Account         string                           `json:"account"`
   546  	Debug           bool                             `json:"debug"`
   547  }
   548  
   549  type cookieSyncRequestFilterSettings struct {
   550  	IFrame   *cookieSyncRequestFilter `json:"iframe"`
   551  	Redirect *cookieSyncRequestFilter `json:"image"`
   552  }
   553  
   554  type cookieSyncRequestFilter struct {
   555  	Bidders interface{} `json:"bidders"`
   556  	Mode    string      `json:"filter"`
   557  }
   558  
   559  type cookieSyncResponse struct {
   560  	Status       string                     `json:"status"`
   561  	BidderStatus []cookieSyncResponseBidder `json:"bidder_status"`
   562  	Debug        []cookieSyncResponseDebug  `json:"debug,omitempty"`
   563  }
   564  
   565  type cookieSyncResponseBidder struct {
   566  	BidderCode   string                 `json:"bidder"`
   567  	NoCookie     bool                   `json:"no_cookie,omitempty"`
   568  	UsersyncInfo cookieSyncResponseSync `json:"usersync,omitempty"`
   569  }
   570  
   571  type cookieSyncResponseSync struct {
   572  	URL         string `json:"url,omitempty"`
   573  	Type        string `json:"type,omitempty"`
   574  	SupportCORS bool   `json:"supportCORS,omitempty"`
   575  }
   576  
   577  type cookieSyncResponseDebug struct {
   578  	Bidder string `json:"bidder"`
   579  	Error  string `json:"error,omitempty"`
   580  }
   581  
   582  type usersyncPrivacyConfig struct {
   583  	gdprConfig             config.GDPR
   584  	gdprPermissionsBuilder gdpr.PermissionsBuilder
   585  	tcf2ConfigBuilder      gdpr.TCF2ConfigBuilder
   586  	ccpaEnforce            bool
   587  	bidderHashSet          map[string]struct{}
   588  }
   589  
   590  type usersyncPrivacy struct {
   591  	gdprPermissions  gdpr.Permissions
   592  	ccpaParsedPolicy ccpa.ParsedPolicy
   593  	activityControl  privacy.ActivityControl
   594  	activityRequest  privacy.ActivityRequest
   595  	gdprSignal       gdpr.Signal
   596  }
   597  
   598  func (p usersyncPrivacy) GDPRAllowsHostCookie() bool {
   599  	allowCookie, err := p.gdprPermissions.HostCookiesAllowed(context.Background())
   600  	return err == nil && allowCookie
   601  }
   602  
   603  func (p usersyncPrivacy) GDPRAllowsBidderSync(bidder string) bool {
   604  	allowSync, err := p.gdprPermissions.BidderSyncAllowed(context.Background(), openrtb_ext.BidderName(bidder))
   605  	return err == nil && allowSync
   606  }
   607  
   608  func (p usersyncPrivacy) CCPAAllowsBidderSync(bidder string) bool {
   609  	enforce := p.ccpaParsedPolicy.CanEnforce() && p.ccpaParsedPolicy.ShouldEnforce(bidder)
   610  	return !enforce
   611  }
   612  
   613  func (p usersyncPrivacy) ActivityAllowsUserSync(bidder string) bool {
   614  	return p.activityControl.Allow(
   615  		privacy.ActivitySyncUser,
   616  		privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidder},
   617  		p.activityRequest)
   618  }
   619  
   620  func (p usersyncPrivacy) GDPRInScope() bool {
   621  	return p.gdprSignal == gdpr.SignalYes
   622  }