github.com/prebid/prebid-server/v2@v2.18.0/firstpartydata/first_party_data.go (about)

     1  package firstpartydata
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/prebid/openrtb/v20/openrtb2"
    10  	jsonpatch "gopkg.in/evanphx/json-patch.v4"
    11  
    12  	"github.com/prebid/prebid-server/v2/errortypes"
    13  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    14  	"github.com/prebid/prebid-server/v2/util/jsonutil"
    15  	"github.com/prebid/prebid-server/v2/util/ptrutil"
    16  )
    17  
    18  var (
    19  	ErrBadRequest = errors.New("invalid request ext")
    20  	ErrBadFPD     = errors.New("invalid first party data ext")
    21  )
    22  
    23  const (
    24  	siteKey = "site"
    25  	appKey  = "app"
    26  	userKey = "user"
    27  	dataKey = "data"
    28  
    29  	userDataKey        = "userData"
    30  	appContentDataKey  = "appContentData"
    31  	siteContentDataKey = "siteContentData"
    32  )
    33  
    34  type ResolvedFirstPartyData struct {
    35  	Site *openrtb2.Site
    36  	App  *openrtb2.App
    37  	User *openrtb2.User
    38  }
    39  
    40  // ExtractGlobalFPD extracts request level FPD from the request and removes req.{site,app,user}.ext.data if exists
    41  func ExtractGlobalFPD(req *openrtb_ext.RequestWrapper) (map[string][]byte, error) {
    42  	fpdReqData := make(map[string][]byte, 3)
    43  
    44  	siteExt, err := req.GetSiteExt()
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	refreshExt := false
    49  
    50  	if len(siteExt.GetExt()[dataKey]) > 0 {
    51  		newSiteExt := siteExt.GetExt()
    52  		fpdReqData[siteKey] = newSiteExt[dataKey]
    53  		delete(newSiteExt, dataKey)
    54  		siteExt.SetExt(newSiteExt)
    55  		refreshExt = true
    56  	}
    57  
    58  	appExt, err := req.GetAppExt()
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  	if len(appExt.GetExt()[dataKey]) > 0 {
    63  		newAppExt := appExt.GetExt()
    64  		fpdReqData[appKey] = newAppExt[dataKey]
    65  		delete(newAppExt, dataKey)
    66  		appExt.SetExt(newAppExt)
    67  		refreshExt = true
    68  	}
    69  
    70  	userExt, err := req.GetUserExt()
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	if len(userExt.GetExt()[dataKey]) > 0 {
    75  		newUserExt := userExt.GetExt()
    76  		fpdReqData[userKey] = newUserExt[dataKey]
    77  		delete(newUserExt, dataKey)
    78  		userExt.SetExt(newUserExt)
    79  		refreshExt = true
    80  	}
    81  	if refreshExt {
    82  		// need to keep site/app/user ext clean in case bidder is not in global fpd bidder list
    83  		// rebuild/resync the request in the request wrapper.
    84  		if err := req.RebuildRequest(); err != nil {
    85  			return nil, err
    86  		}
    87  	}
    88  
    89  	return fpdReqData, nil
    90  }
    91  
    92  // ExtractOpenRtbGlobalFPD extracts and deletes user.data and {app/site}.content.data from request
    93  func ExtractOpenRtbGlobalFPD(bidRequest *openrtb2.BidRequest) map[string][]openrtb2.Data {
    94  	openRtbGlobalFPD := make(map[string][]openrtb2.Data, 3)
    95  	if bidRequest.User != nil && len(bidRequest.User.Data) > 0 {
    96  		openRtbGlobalFPD[userDataKey] = bidRequest.User.Data
    97  		bidRequest.User.Data = nil
    98  	}
    99  
   100  	if bidRequest.Site != nil && bidRequest.Site.Content != nil && len(bidRequest.Site.Content.Data) > 0 {
   101  		openRtbGlobalFPD[siteContentDataKey] = bidRequest.Site.Content.Data
   102  		bidRequest.Site.Content.Data = nil
   103  	}
   104  
   105  	if bidRequest.App != nil && bidRequest.App.Content != nil && len(bidRequest.App.Content.Data) > 0 {
   106  		openRtbGlobalFPD[appContentDataKey] = bidRequest.App.Content.Data
   107  		bidRequest.App.Content.Data = nil
   108  	}
   109  
   110  	return openRtbGlobalFPD
   111  }
   112  
   113  // ResolveFPD consolidates First Party Data from different sources and returns valid FPD that will be applied to bidders later or returns errors
   114  func ResolveFPD(bidRequest *openrtb2.BidRequest, fpdBidderConfigData map[openrtb_ext.BidderName]*openrtb_ext.ORTB2, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, biddersWithGlobalFPD []string) (map[openrtb_ext.BidderName]*ResolvedFirstPartyData, []error) {
   115  	var errL []error
   116  
   117  	resolvedFpd := make(map[openrtb_ext.BidderName]*ResolvedFirstPartyData)
   118  
   119  	allBiddersTable := make(map[string]struct{})
   120  
   121  	if biddersWithGlobalFPD == nil {
   122  		// add all bidders in bidder configs to receive global data and bidder specific data
   123  		for bidderName := range fpdBidderConfigData {
   124  			if _, present := allBiddersTable[string(bidderName)]; !present {
   125  				allBiddersTable[string(bidderName)] = struct{}{}
   126  			}
   127  		}
   128  	} else {
   129  		// only bidders in global bidder list will receive global data and bidder specific data
   130  		for _, bidder := range biddersWithGlobalFPD {
   131  			bidderName := openrtb_ext.NormalizeBidderNameOrUnchanged(bidder)
   132  
   133  			if _, present := allBiddersTable[string(bidderName)]; !present {
   134  				allBiddersTable[string(bidderName)] = struct{}{}
   135  			}
   136  		}
   137  	}
   138  
   139  	for bidderName := range allBiddersTable {
   140  		fpdConfig := fpdBidderConfigData[openrtb_ext.BidderName(bidderName)]
   141  
   142  		resolvedFpdConfig := &ResolvedFirstPartyData{}
   143  
   144  		newUser, err := resolveUser(fpdConfig, bidRequest.User, globalFPD, openRtbGlobalFPD, bidderName)
   145  		if err != nil {
   146  			errL = append(errL, err)
   147  		}
   148  		resolvedFpdConfig.User = newUser
   149  
   150  		newApp, err := resolveApp(fpdConfig, bidRequest.App, globalFPD, openRtbGlobalFPD, bidderName)
   151  		if err != nil {
   152  			errL = append(errL, err)
   153  		}
   154  		resolvedFpdConfig.App = newApp
   155  
   156  		newSite, err := resolveSite(fpdConfig, bidRequest.Site, globalFPD, openRtbGlobalFPD, bidderName)
   157  		if err != nil {
   158  			errL = append(errL, err)
   159  		}
   160  		resolvedFpdConfig.Site = newSite
   161  
   162  		if len(errL) == 0 {
   163  			resolvedFpd[openrtb_ext.BidderName(bidderName)] = resolvedFpdConfig
   164  		}
   165  	}
   166  	return resolvedFpd, errL
   167  }
   168  
   169  func resolveUser(fpdConfig *openrtb_ext.ORTB2, bidRequestUser *openrtb2.User, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.User, error) {
   170  	var fpdConfigUser json.RawMessage
   171  
   172  	if fpdConfig != nil && fpdConfig.User != nil {
   173  		fpdConfigUser = fpdConfig.User
   174  	}
   175  
   176  	if bidRequestUser == nil && fpdConfigUser == nil {
   177  		return nil, nil
   178  	}
   179  
   180  	var newUser *openrtb2.User
   181  	if bidRequestUser != nil {
   182  		newUser = ptrutil.Clone(bidRequestUser)
   183  	} else {
   184  		newUser = &openrtb2.User{}
   185  	}
   186  
   187  	//apply global fpd
   188  	if len(globalFPD[userKey]) > 0 {
   189  		extData := buildExtData(globalFPD[userKey])
   190  		if len(newUser.Ext) > 0 {
   191  			var err error
   192  			newUser.Ext, err = jsonpatch.MergePatch(newUser.Ext, extData)
   193  			if err != nil {
   194  				return nil, formatMergePatchError(err)
   195  			}
   196  		} else {
   197  			newUser.Ext = extData
   198  		}
   199  	}
   200  	if openRtbGlobalFPD != nil && len(openRtbGlobalFPD[userDataKey]) > 0 {
   201  		newUser.Data = openRtbGlobalFPD[userDataKey]
   202  	}
   203  	if fpdConfigUser != nil {
   204  		if err := jsonutil.MergeClone(newUser, fpdConfigUser); err != nil {
   205  			return nil, formatMergeCloneError(err)
   206  		}
   207  	}
   208  
   209  	return newUser, nil
   210  }
   211  
   212  func resolveSite(fpdConfig *openrtb_ext.ORTB2, bidRequestSite *openrtb2.Site, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.Site, error) {
   213  	var fpdConfigSite json.RawMessage
   214  
   215  	if fpdConfig != nil && fpdConfig.Site != nil {
   216  		fpdConfigSite = fpdConfig.Site
   217  	}
   218  
   219  	if bidRequestSite == nil && fpdConfigSite == nil {
   220  		return nil, nil
   221  	}
   222  	if bidRequestSite == nil && fpdConfigSite != nil {
   223  		return nil, &errortypes.BadInput{
   224  			Message: fmt.Sprintf("incorrect First Party Data for bidder %s: Site object is not defined in request, but defined in FPD config", bidderName),
   225  		}
   226  	}
   227  
   228  	var newSite *openrtb2.Site
   229  	if bidRequestSite != nil {
   230  		newSite = ptrutil.Clone(bidRequestSite)
   231  	} else {
   232  		newSite = &openrtb2.Site{}
   233  	}
   234  
   235  	//apply global fpd
   236  	if len(globalFPD[siteKey]) > 0 {
   237  		extData := buildExtData(globalFPD[siteKey])
   238  		if len(newSite.Ext) > 0 {
   239  			var err error
   240  			newSite.Ext, err = jsonpatch.MergePatch(newSite.Ext, extData)
   241  			if err != nil {
   242  				return nil, formatMergePatchError(err)
   243  			}
   244  		} else {
   245  			newSite.Ext = extData
   246  		}
   247  	}
   248  	// apply global openRTB fpd if exists
   249  	if len(openRtbGlobalFPD) > 0 && len(openRtbGlobalFPD[siteContentDataKey]) > 0 {
   250  		if newSite.Content == nil {
   251  			newSite.Content = &openrtb2.Content{}
   252  		} else {
   253  			contentCopy := *newSite.Content
   254  			newSite.Content = &contentCopy
   255  		}
   256  		newSite.Content.Data = openRtbGlobalFPD[siteContentDataKey]
   257  	}
   258  	if fpdConfigSite != nil {
   259  		if err := jsonutil.MergeClone(newSite, fpdConfigSite); err != nil {
   260  			return nil, formatMergeCloneError(err)
   261  		}
   262  
   263  		// Re-Validate Site
   264  		if newSite.ID == "" && newSite.Page == "" {
   265  			return nil, &errortypes.BadInput{
   266  				Message: fmt.Sprintf("incorrect First Party Data for bidder %s: Site object cannot set empty page if req.site.id is empty", bidderName),
   267  			}
   268  		}
   269  	}
   270  	return newSite, nil
   271  }
   272  
   273  func formatMergePatchError(err error) error {
   274  	if errors.Is(err, jsonpatch.ErrBadJSONDoc) {
   275  		return ErrBadRequest
   276  	}
   277  
   278  	if errors.Is(err, jsonpatch.ErrBadJSONPatch) {
   279  		return ErrBadFPD
   280  	}
   281  
   282  	return err
   283  }
   284  
   285  func formatMergeCloneError(err error) error {
   286  	if strings.Contains(err.Error(), "invalid json on existing object") {
   287  		return ErrBadRequest
   288  	}
   289  	return ErrBadFPD
   290  }
   291  
   292  func resolveApp(fpdConfig *openrtb_ext.ORTB2, bidRequestApp *openrtb2.App, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.App, error) {
   293  	var fpdConfigApp json.RawMessage
   294  
   295  	if fpdConfig != nil {
   296  		fpdConfigApp = fpdConfig.App
   297  	}
   298  
   299  	if bidRequestApp == nil && fpdConfigApp == nil {
   300  		return nil, nil
   301  	}
   302  
   303  	if bidRequestApp == nil && fpdConfigApp != nil {
   304  		return nil, &errortypes.BadInput{
   305  			Message: fmt.Sprintf("incorrect First Party Data for bidder %s: App object is not defined in request, but defined in FPD config", bidderName),
   306  		}
   307  	}
   308  
   309  	var newApp *openrtb2.App
   310  	if bidRequestApp != nil {
   311  		newApp = ptrutil.Clone(bidRequestApp)
   312  	} else {
   313  		newApp = &openrtb2.App{}
   314  	}
   315  
   316  	//apply global fpd if exists
   317  	if len(globalFPD[appKey]) > 0 {
   318  		extData := buildExtData(globalFPD[appKey])
   319  		if len(newApp.Ext) > 0 {
   320  			var err error
   321  			newApp.Ext, err = jsonpatch.MergePatch(newApp.Ext, extData)
   322  			if err != nil {
   323  				return nil, formatMergePatchError(err)
   324  			}
   325  		} else {
   326  			newApp.Ext = extData
   327  		}
   328  	}
   329  
   330  	// apply global openRTB fpd if exists
   331  	if len(openRtbGlobalFPD) > 0 && len(openRtbGlobalFPD[appContentDataKey]) > 0 {
   332  		if newApp.Content == nil {
   333  			newApp.Content = &openrtb2.Content{}
   334  		} else {
   335  			contentCopy := *newApp.Content
   336  			newApp.Content = &contentCopy
   337  		}
   338  		newApp.Content.Data = openRtbGlobalFPD[appContentDataKey]
   339  	}
   340  
   341  	if fpdConfigApp != nil {
   342  		if err := jsonutil.MergeClone(newApp, fpdConfigApp); err != nil {
   343  			return nil, formatMergeCloneError(err)
   344  		}
   345  	}
   346  
   347  	return newApp, nil
   348  }
   349  
   350  func buildExtData(data []byte) []byte {
   351  	res := make([]byte, 0, len(data)+len(`"{"data":}"`))
   352  	res = append(res, []byte(`{"data":`)...)
   353  	res = append(res, data...)
   354  	res = append(res, []byte(`}`)...)
   355  	return res
   356  }
   357  
   358  // ExtractBidderConfigFPD extracts bidder specific configs from req.ext.prebid.bidderconfig
   359  func ExtractBidderConfigFPD(reqExt *openrtb_ext.RequestExt) (map[openrtb_ext.BidderName]*openrtb_ext.ORTB2, error) {
   360  	fpd := make(map[openrtb_ext.BidderName]*openrtb_ext.ORTB2)
   361  
   362  	reqExtPrebid := reqExt.GetPrebid()
   363  	if reqExtPrebid != nil {
   364  		for _, bidderConfig := range reqExtPrebid.BidderConfigs {
   365  			for _, bidder := range bidderConfig.Bidders {
   366  				bidderName := openrtb_ext.NormalizeBidderNameOrUnchanged(bidder)
   367  
   368  				if _, duplicate := fpd[bidderName]; duplicate {
   369  					return nil, &errortypes.BadInput{
   370  						Message: fmt.Sprintf("multiple First Party Data bidder configs provided for bidder: %s", bidder),
   371  					}
   372  				}
   373  
   374  				fpdBidderData := &openrtb_ext.ORTB2{}
   375  
   376  				if bidderConfig.Config != nil && bidderConfig.Config.ORTB2 != nil {
   377  					fpdBidderData.Site = bidderConfig.Config.ORTB2.Site
   378  					fpdBidderData.App = bidderConfig.Config.ORTB2.App
   379  					fpdBidderData.User = bidderConfig.Config.ORTB2.User
   380  				}
   381  
   382  				fpd[bidderName] = fpdBidderData
   383  			}
   384  		}
   385  		reqExtPrebid.BidderConfigs = nil
   386  		reqExt.SetPrebid(reqExtPrebid)
   387  	}
   388  	return fpd, nil
   389  }
   390  
   391  // ExtractFPDForBidders extracts FPD data from request if specified
   392  func ExtractFPDForBidders(req *openrtb_ext.RequestWrapper) (map[openrtb_ext.BidderName]*ResolvedFirstPartyData, []error) {
   393  	reqExt, err := req.GetRequestExt()
   394  	if err != nil {
   395  		return nil, []error{err}
   396  	}
   397  	if reqExt == nil || reqExt.GetPrebid() == nil {
   398  		return nil, nil
   399  	}
   400  	var biddersWithGlobalFPD []string
   401  
   402  	extPrebid := reqExt.GetPrebid()
   403  	if extPrebid.Data != nil {
   404  		biddersWithGlobalFPD = extPrebid.Data.Bidders
   405  		extPrebid.Data.Bidders = nil
   406  		reqExt.SetPrebid(extPrebid)
   407  	}
   408  
   409  	fbdBidderConfigData, err := ExtractBidderConfigFPD(reqExt)
   410  	if err != nil {
   411  		return nil, []error{err}
   412  	}
   413  
   414  	var globalFpd map[string][]byte
   415  	var openRtbGlobalFPD map[string][]openrtb2.Data
   416  
   417  	if biddersWithGlobalFPD != nil {
   418  		//global fpd data should not be extracted and removed from request if global bidder list is nil.
   419  		//Bidders that don't have any fpd config should receive request data as is
   420  		globalFpd, err = ExtractGlobalFPD(req)
   421  		if err != nil {
   422  			return nil, []error{err}
   423  		}
   424  		openRtbGlobalFPD = ExtractOpenRtbGlobalFPD(req.BidRequest)
   425  	}
   426  
   427  	return ResolveFPD(req.BidRequest, fbdBidderConfigData, globalFpd, openRtbGlobalFPD, biddersWithGlobalFPD)
   428  }