Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Bid Splitter Privacy Functions #3645

Merged
merged 5 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions endpoints/cookie_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2212,9 +2212,9 @@ func (m *MockGDPRPerms) BidderSyncAllowed(ctx context.Context, bidder openrtb_ex
return args.Bool(0), args.Error(1)
}

func (m *MockGDPRPerms) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) {
func (m *MockGDPRPerms) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) gdpr.AuctionPermissions {
args := m.Called(ctx, bidderCoreName, bidder)
return args.Get(0).(gdpr.AuctionPermissions), args.Error(1)
return args.Get(0).(gdpr.AuctionPermissions)
}

type FakeAccountsFetcher struct {
Expand Down Expand Up @@ -2244,10 +2244,10 @@ func (p *fakePermissions) BidderSyncAllowed(ctx context.Context, bidder openrtb_
return true, nil
}

func (p *fakePermissions) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) {
func (p *fakePermissions) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) gdpr.AuctionPermissions {
return gdpr.AuctionPermissions{
AllowBidRequest: true,
}, nil
}
}

func getDefaultActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
Expand Down
4 changes: 2 additions & 2 deletions endpoints/openrtb2/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -1400,10 +1400,10 @@ func (p *fakePermissions) BidderSyncAllowed(ctx context.Context, bidder openrtb_
return true, nil
}

func (p *fakePermissions) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) {
func (p *fakePermissions) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) gdpr.AuctionPermissions {
return gdpr.AuctionPermissions{
AllowBidRequest: true,
}, nil
}
}

type mockPlanBuilder struct {
Expand Down
4 changes: 2 additions & 2 deletions endpoints/setuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1701,12 +1701,12 @@ func (g *fakePermsSetUID) BidderSyncAllowed(ctx context.Context, bidder openrtb_
return false, nil
}

func (g *fakePermsSetUID) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) {
func (g *fakePermsSetUID) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) gdpr.AuctionPermissions {
return gdpr.AuctionPermissions{
AllowBidRequest: g.personalInfoAllowed,
PassGeo: g.personalInfoAllowed,
PassID: g.personalInfoAllowed,
}, nil
}
}

type fakeSyncer struct {
Expand Down
159 changes: 82 additions & 77 deletions exchange/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
return
}

allowedBidderRequests = make([]BidderRequest, 0)

bidderImpWithBidResp := stored_responses.InitStoredBidResponses(req.BidRequest, auctionReq.StoredBidResponses)

impsByBidder, err := splitImps(req.BidRequest.Imp)
Expand Down Expand Up @@ -149,102 +147,110 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
gdprPerms = rs.gdprPermsBuilder(auctionReq.TCF2Config, gdprRequestInfo)
}

// bidder level privacy policies
allowedBidderRequests = make([]BidderRequest, 0, len(allBidderRequests))

for _, bidderRequest := range allBidderRequests {
// fetchBids activity
scopedName := privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidderRequest.BidderName.String()}
fetchBidsActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityFetchBids, scopedName, privacy.NewRequestFromBidRequest(*req))
if !fetchBidsActivityAllowed {
// skip the call to a bidder if fetchBids activity is not allowed
// do not add this bidder to allowedBidderRequests
auctionPermissions := gdprPerms.AuctionActivitiesAllowed(ctx, bidderRequest.BidderCoreName, bidderRequest.BidderName)

// privacy blocking
if rs.isBidderBlockedByPrivacy(req, auctionReq.Activities, auctionPermissions, bidderRequest.BidderCoreName, bidderRequest.BidderName) {
continue
}

var auctionPermissions gdpr.AuctionPermissions
var gdprErr error
// fpd
applyFPD(auctionReq.FirstPartyData, bidderRequest)

if gdprEnforced {
auctionPermissions, gdprErr = gdprPerms.AuctionActivitiesAllowed(ctx, bidderRequest.BidderCoreName, bidderRequest.BidderName)
if !auctionPermissions.AllowBidRequest {
// auction request is not permitted by GDPR
// do not add this bidder to allowedBidderRequests
rs.me.RecordAdapterGDPRRequestBlocked(bidderRequest.BidderCoreName)
continue
}
// privacy scrubbing
AlexBVolcy marked this conversation as resolved.
Show resolved Hide resolved
if err := rs.applyPrivacy(&bidderRequest, auctionReq, auctionPermissions, ccpaEnforcer, lmt, coppa); err != nil {
errs = append(errs, err)
continue
}

ipConf := privacy.IPConf{IPV6: auctionReq.Account.Privacy.IPv6Config, IPV4: auctionReq.Account.Privacy.IPv4Config}
// GPP downgrade: always downgrade unless we can confirm GPP is supported
if shouldSetLegacyPrivacy(rs.bidderInfo, string(bidderRequest.BidderCoreName)) {
setLegacyGDPRFromGPP(bidderRequest.BidRequest, gpp)
setLegacyUSPFromGPP(bidderRequest.BidRequest, gpp)
}

// FPD should be applied before policies, otherwise it overrides policies and activities restricted data
applyFPD(auctionReq.FirstPartyData, bidderRequest)
allowedBidderRequests = append(allowedBidderRequests, bidderRequest)
}

reqWrapper := &openrtb_ext.RequestWrapper{
BidRequest: ortb.CloneBidRequestPartial(bidderRequest.BidRequest),
}
return
}

func (rs *requestSplitter) isBidderBlockedByPrivacy(r *openrtb_ext.RequestWrapper, activities privacy.ActivityControl, auctionPermissions gdpr.AuctionPermissions, coreBidder, bidderName openrtb_ext.BidderName) bool {
// activities control
scope := privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidderName.String()}
fetchBidsActivityAllowed := activities.Allow(privacy.ActivityFetchBids, scope, privacy.NewRequestFromBidRequest(*r))
if !fetchBidsActivityAllowed {
return true
Comment on lines +184 to +186
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking you could just have the function call in the if statement like

if !activities.Allow(privacy.ActivityFetchBids, scope, privacy.NewRequestFromBidRequest(*r)) {
return true
}

As opposed to using the fetchBidsActivityAllowed variable. Or is that out of the scope of this PR?

}

// gdpr
if !auctionPermissions.AllowBidRequest {
rs.me.RecordAdapterGDPRRequestBlocked(coreBidder)
return true
}

passIDActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitUserFPD, scopedName, privacy.NewRequestFromBidRequest(*req))
buyerUIDSet := reqWrapper.User != nil && reqWrapper.User.BuyerUID != ""
buyerUIDRemoved := false
if !passIDActivityAllowed {
privacy.ScrubUserFPD(reqWrapper)
return false
}

func (rs *requestSplitter) applyPrivacy(bidderRequest *BidderRequest, auctionReq AuctionRequest, auctionPermissions gdpr.AuctionPermissions, ccpaEnforcer privacy.PolicyEnforcer, lmt bool, coppa bool) error {
scope := privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidderRequest.BidderName.String()}
ipConf := privacy.IPConf{IPV6: auctionReq.Account.Privacy.IPv6Config, IPV4: auctionReq.Account.Privacy.IPv4Config}

reqWrapper := &openrtb_ext.RequestWrapper{
BidRequest: ortb.CloneBidRequestPartial(bidderRequest.BidRequest),
}

passIDActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitUserFPD, scope, privacy.NewRequestFromBidRequest(*reqWrapper))
buyerUIDSet := reqWrapper.User != nil && reqWrapper.User.BuyerUID != ""
buyerUIDRemoved := false
if !passIDActivityAllowed {
privacy.ScrubUserFPD(reqWrapper)
buyerUIDRemoved = true
} else {
if !auctionPermissions.PassID {
privacy.ScrubGdprID(reqWrapper)
buyerUIDRemoved = true
} else {
// run existing policies (GDPR, CCPA, COPPA, LMT)
// potentially block passing IDs based on GDPR
if gdprEnforced && (gdprErr != nil || !auctionPermissions.PassID) {
privacy.ScrubGdprID(reqWrapper)
buyerUIDRemoved = true
}
// potentially block passing IDs based on CCPA
if ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String()) {
privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", false)
buyerUIDRemoved = true
}
}
if buyerUIDSet && buyerUIDRemoved {
rs.me.RecordAdapterBuyerUIDScrubbed(bidderRequest.BidderCoreName)
}

passGeoActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitPreciseGeo, scopedName, privacy.NewRequestFromBidRequest(*req))
if !passGeoActivityAllowed {
privacy.ScrubGeoAndDeviceIP(reqWrapper, ipConf)
} else {
// run existing policies (GDPR, CCPA, COPPA, LMT)
// potentially block passing geo based on GDPR
if gdprEnforced && (gdprErr != nil || !auctionPermissions.PassGeo) {
privacy.ScrubGeoAndDeviceIP(reqWrapper, ipConf)
}
// potentially block passing geo based on CCPA
if ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String()) {
privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", false)
}
if ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String()) {
privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", false)
buyerUIDRemoved = true
}
}
if buyerUIDSet && buyerUIDRemoved {
rs.me.RecordAdapterBuyerUIDScrubbed(bidderRequest.BidderCoreName)
}

if lmt || coppa {
privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", coppa)
passGeoActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitPreciseGeo, scope, privacy.NewRequestFromBidRequest(*reqWrapper))
if !passGeoActivityAllowed {
privacy.ScrubGeoAndDeviceIP(reqWrapper, ipConf)
} else {
if !auctionPermissions.PassGeo {
privacy.ScrubGeoAndDeviceIP(reqWrapper, ipConf)
}

passTIDAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitTIDs, scopedName, privacy.NewRequestFromBidRequest(*req))
if !passTIDAllowed {
privacy.ScrubTID(reqWrapper)
if ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String()) {
privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", false)
AlexBVolcy marked this conversation as resolved.
Show resolved Hide resolved
}
}

err := reqWrapper.RebuildRequest()
if err != nil {
errs = append(errs, err)
}
bidderRequest.BidRequest = reqWrapper.BidRequest
if lmt || coppa {
privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", coppa)
}

allowedBidderRequests = append(allowedBidderRequests, bidderRequest)
passTIDAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitTIDs, scope, privacy.NewRequestFromBidRequest(*reqWrapper))
if !passTIDAllowed {
privacy.ScrubTID(reqWrapper)
}

// GPP downgrade: always downgrade unless we can confirm GPP is supported
if shouldSetLegacyPrivacy(rs.bidderInfo, string(bidderRequest.BidderCoreName)) {
setLegacyGDPRFromGPP(bidderRequest.BidRequest, gpp)
setLegacyUSPFromGPP(bidderRequest.BidRequest, gpp)
}
if err := reqWrapper.RebuildRequest(); err != nil {
return err
}

return
bidderRequest.BidRequest = reqWrapper.BidRequest
return nil
}

func shouldSetLegacyPrivacy(bidderInfo config.BidderInfos, bidder string) bool {
Expand Down Expand Up @@ -457,7 +463,6 @@ func buildRequestExtForBidder(bidder string, requestExt json.RawMessage, request
}

func buildRequestExtAlternateBidderCodes(bidder string, accABC *openrtb_ext.ExtAlternateBidderCodes, reqABC *openrtb_ext.ExtAlternateBidderCodes) *openrtb_ext.ExtAlternateBidderCodes {

if altBidderCodes := copyExtAlternateBidderCodes(bidder, reqABC); altBidderCodes != nil {
return altBidderCodes
}
Expand Down
6 changes: 3 additions & 3 deletions exchange/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ func (p *permissionsMock) BidderSyncAllowed(ctx context.Context, bidder openrtb_
return true, nil
}

func (p *permissionsMock) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (gdpr.AuctionPermissions, error) {
func (p *permissionsMock) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) gdpr.AuctionPermissions {
permissions := gdpr.AuctionPermissions{
PassGeo: p.passGeo,
PassID: p.passID,
}

if p.allowAllBidders {
permissions.AllowBidRequest = true
return permissions, p.activitiesError
return permissions // might need a tweak
SyntaxNode marked this conversation as resolved.
Show resolved Hide resolved
}

for _, allowedBidder := range p.allowedBidders {
Expand All @@ -62,7 +62,7 @@ func (p *permissionsMock) AuctionActivitiesAllowed(ctx context.Context, bidderCo
}
}

return permissions, p.activitiesError
return permissions
}

type fakePermissionsBuilder struct {
Expand Down
4 changes: 2 additions & 2 deletions gdpr/gdpr.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ type Permissions interface {

// Determines whether or not to send PI information to a bidder, or mask it out.
//
// If the consent string was nonsensical, the returned error will be an ErrorMalformedConsent.
AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions AuctionPermissions, err error)
// If the consent string was nonsensical, the no permissions are granted.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo, should be: the_n_ no permissions are granted

AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) AuctionPermissions
}

type PermissionsBuilder func(TCF2ConfigReader, RequestInfo) Permissions
Expand Down
33 changes: 18 additions & 15 deletions gdpr/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,33 +56,36 @@ func (p *permissionsImpl) BidderSyncAllowed(ctx context.Context, bidder openrtb_
}

// AuctionActivitiesAllowed determines whether auction activities are permitted for a given bidder
func (p *permissionsImpl) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions AuctionPermissions, err error) {
func (p *permissionsImpl) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) AuctionPermissions {
if _, ok := p.nonStandardPublishers[p.publisherID]; ok {
return AllowAll, nil
return AllowAll
}

if p.gdprSignal != SignalYes {
return AllowAll, nil
return AllowAll
}

if p.consent == "" {
return p.defaultPermissions(), nil
return p.defaultPermissions()
}

pc, err := parseConsent(p.consent)
if err != nil {
return p.defaultPermissions(), err
return DenyAll
Copy link
Contributor Author

@SyntaxNode SyntaxNode Apr 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error was always only interpreted this way, so codifying that behavior here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an err always is treated as DenyAll, then what about line 80 below where an error becomes p.defaultPermissions()?

}

vendorID, _ := p.resolveVendorID(bidderCoreName, bidder)
vendor, err := p.getVendor(ctx, vendorID, *pc)
if err != nil {
return p.defaultPermissions(), err
return p.defaultPermissions()
}
vendorInfo := VendorInfo{vendorID: vendorID, vendor: vendor}

permissions = AuctionPermissions{}
permissions.AllowBidRequest = p.allowBidRequest(bidderCoreName, pc.consentMeta, vendorInfo)
permissions.PassGeo = p.allowGeo(bidderCoreName, pc.consentMeta, vendor)
permissions.PassID = p.allowID(bidderCoreName, pc.consentMeta, vendorInfo)

return permissions, nil
vendorInfo := VendorInfo{vendorID: vendorID, vendor: vendor}
return AuctionPermissions{
AllowBidRequest: p.allowBidRequest(bidderCoreName, pc.consentMeta, vendorInfo),
PassGeo: p.allowGeo(bidderCoreName, pc.consentMeta, vendor),
PassID: p.allowID(bidderCoreName, pc.consentMeta, vendorInfo),
}
}

// defaultPermissions returns a permissions object that denies passing user IDs while
Expand Down Expand Up @@ -222,6 +225,6 @@ func (a AlwaysAllow) HostCookiesAllowed(ctx context.Context) (bool, error) {
func (a AlwaysAllow) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName) (bool, error) {
return true, nil
}
func (a AlwaysAllow) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions AuctionPermissions, err error) {
return AllowAll, nil
func (a AlwaysAllow) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) AuctionPermissions {
return AllowAll
}
Loading
Loading