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

Fetching GDPR Vendor Lists #511

Merged
merged 7 commits into from
May 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
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
15 changes: 13 additions & 2 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@
name = "github.com/chasex/log"
branch = "analytics"

[[constraint]]
name = "github.com/prebid/go-gdpr"
version = "0.3.0"

[prune]
go-tests = true
unused-packages = true
31 changes: 31 additions & 0 deletions gdpr/gdpr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package gdpr

import (
"context"
"net/http"

"github.com/prebid/prebid-server/config"
"github.com/prebid/prebid-server/openrtb_ext"
)

type Permissions interface {
// Determines whether or not the host company is allowed to read/write cookies.
HostCookiesAllowed(ctx context.Context, consent string) (bool, error)

// Determines whether or not the given bidder is allowed to user personal info for ad targeting.
BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName, consent string) (bool, error)
}

// NewPermissions gets an instance of the Permissions for use elsewhere in the project.
func NewPermissions(ctx context.Context, cfg config.GDPR, vendorIDs map[openrtb_ext.BidderName]uint16, client *http.Client) Permissions {
// If the host doesn't buy into the IAB GDPR consent framework, then save some cycles and let all syncs happen.
if cfg.HostVendorID == 0 {
return alwaysAllow{}
}

return &permissionsImpl{
cfg: cfg,
vendorIDs: vendorIDs,
fetchVendorList: newVendorListFetcher(ctx, client, vendorListURLMaker),
}
}
104 changes: 104 additions & 0 deletions gdpr/impl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package gdpr

import (
"context"
"encoding/base64"

"github.com/prebid/go-gdpr/consentconstants"
"github.com/prebid/go-gdpr/vendorconsent"
"github.com/prebid/go-gdpr/vendorlist"
"github.com/prebid/prebid-server/config"
"github.com/prebid/prebid-server/openrtb_ext"
)

// This file implements GDPR permissions for the app.
// For more info, see https://github.com/prebid/prebid-server/issues/501
//
// Nothing in this file is exported. Public APIs can be found in gdpr.go

type permissionsImpl struct {
cfg config.GDPR
vendorIDs map[openrtb_ext.BidderName]uint16
fetchVendorList func(ctx context.Context, id uint16) (vendorlist.VendorList, error)
}

func (p *permissionsImpl) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) {
// If we're not given a consent string, respect the preferences in the app config.
if consent == "" {
return p.cfg.UsersyncIfAmbiguous, nil
}

data, err := base64.RawURLEncoding.DecodeString(consent)
if err != nil {
return false, err
}

parsedConsent, err := vendorconsent.Parse([]byte(data))
if err != nil {
return false, err
}

vendorList, err := p.fetchVendorList(ctx, parsedConsent.VendorListVersion())
if err != nil {
return false, err
}

// Config validation makes uint16 conversion safe here
return hasPermissions(parsedConsent, vendorList, uint16(p.cfg.HostVendorID), consentconstants.InfoStorageAccess), nil
}

func (p *permissionsImpl) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName, consent string) (bool, error) {
// If we're not given a consent string, respect the preferences in the app config.
if consent == "" {
return p.cfg.UsersyncIfAmbiguous, nil
}

id, ok := p.vendorIDs[bidder]
if !ok {
return false, nil
}

data, err := base64.RawURLEncoding.DecodeString(consent)
if err != nil {
return false, err
}

parsedConsent, err := vendorconsent.Parse([]byte(data))
if err != nil {
return false, err
}

vendorList, err := p.fetchVendorList(ctx, parsedConsent.VendorListVersion())
if err != nil {
return false, err
}

return hasPermissions(parsedConsent, vendorList, id, consentconstants.AdSelectionDeliveryReporting), nil
}

func hasPermissions(consent vendorconsent.VendorConsents, vendorList vendorlist.VendorList, vendorID uint16, purpose consentconstants.Purpose) bool {
vendor := vendorList.Vendor(vendorID)
if vendor == nil {
return false
}
if vendor.LegitimateInterest(purpose) {
return true
}

// If the host declared writing cookies to be a "normal" purpose, only do the sync if the user consented to it.
if vendor.Purpose(purpose) && consent.PurposeAllowed(purpose) && consent.VendorConsent(vendorID) {
return true
}

return false
}

type alwaysAllow struct{}

func (a alwaysAllow) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) {
return true, nil
}

func (a alwaysAllow) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName, consent string) (bool, error) {
return true, nil
}
185 changes: 185 additions & 0 deletions gdpr/impl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package gdpr

import (
"context"
"errors"
"fmt"
"testing"

"github.com/prebid/prebid-server/config"
"github.com/prebid/prebid-server/openrtb_ext"

"github.com/prebid/go-gdpr/vendorlist"
)

func TestNoConsentButAllowByDefault(t *testing.T) {
perms := permissionsImpl{
cfg: config.GDPR{
HostVendorID: 3,
UsersyncIfAmbiguous: true,
},
vendorIDs: nil,
fetchVendorList: failedListFetcher,
}
allowSync, err := perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderAppnexus, "")
assertBoolsEqual(t, true, allowSync)
assertNilErr(t, err)
allowSync, err = perms.HostCookiesAllowed(context.Background(), "")
assertBoolsEqual(t, true, allowSync)
assertNilErr(t, err)
}

func TestNoConsentAndRejectByDefault(t *testing.T) {
perms := permissionsImpl{
cfg: config.GDPR{
HostVendorID: 3,
UsersyncIfAmbiguous: false,
},
vendorIDs: nil,
fetchVendorList: failedListFetcher,
}
allowSync, err := perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderAppnexus, "")
assertBoolsEqual(t, false, allowSync)
assertNilErr(t, err)
allowSync, err = perms.HostCookiesAllowed(context.Background(), "")
assertBoolsEqual(t, false, allowSync)
assertNilErr(t, err)
}

func TestAllowedSyncs(t *testing.T) {
vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{
2: &purposes{
purposes: []uint8{1}, // cookie reads/writes
},
3: &purposes{
purposes: []uint8{3}, // ad personalization
},
})
perms := permissionsImpl{
cfg: config.GDPR{
HostVendorID: 2,
},
vendorIDs: map[openrtb_ext.BidderName]uint16{
openrtb_ext.BidderAppnexus: 2,
openrtb_ext.BidderPubmatic: 3,
},
fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{
1: parseVendorListData(t, vendorListData),
}),
}

allowSync, err := perms.HostCookiesAllowed(context.Background(), "BON3PCUON3PCUABABBAAABoAAAAAMw")
assertNilErr(t, err)
assertBoolsEqual(t, true, allowSync)

allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderPubmatic, "BON3PCUON3PCUABABBAAABoAAAAAMw")
assertNilErr(t, err)
assertBoolsEqual(t, true, allowSync)
}

func TestProhibitedPurposes(t *testing.T) {
vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{
2: &purposes{
purposes: []uint8{1}, // cookie reads/writes
},
3: &purposes{
purposes: []uint8{3}, // ad personalization
},
})
perms := permissionsImpl{
cfg: config.GDPR{
HostVendorID: 2,
},
vendorIDs: map[openrtb_ext.BidderName]uint16{
openrtb_ext.BidderAppnexus: 2,
openrtb_ext.BidderPubmatic: 3,
},
fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{
1: parseVendorListData(t, vendorListData),
}),
}

allowSync, err := perms.HostCookiesAllowed(context.Background(), "BON3PCUON3PCUABABBAAABAAAAAAMw")
assertNilErr(t, err)
assertBoolsEqual(t, false, allowSync)

allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderPubmatic, "BON3PCUON3PCUABABBAAABAAAAAAMw")
assertNilErr(t, err)
assertBoolsEqual(t, false, allowSync)
}

func TestProhibitedVendors(t *testing.T) {
vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{
2: &purposes{
purposes: []uint8{1}, // cookie reads/writes
},
3: &purposes{
purposes: []uint8{3}, // ad personalization
},
})
perms := permissionsImpl{
cfg: config.GDPR{
HostVendorID: 2,
},
vendorIDs: map[openrtb_ext.BidderName]uint16{
openrtb_ext.BidderAppnexus: 2,
openrtb_ext.BidderPubmatic: 3,
},
fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{
1: parseVendorListData(t, vendorListData),
}),
}

allowSync, err := perms.HostCookiesAllowed(context.Background(), "BON3PCUON3PCUABABBAAABoAAAAANA")
assertNilErr(t, err)
assertBoolsEqual(t, false, allowSync)

allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderPubmatic, "BON3PCUON3PCUABABBAAABoAAAAANA")
assertNilErr(t, err)
assertBoolsEqual(t, false, allowSync)
}

func parseVendorListData(t *testing.T, data string) vendorlist.VendorList {
t.Helper()
parsed, err := vendorlist.ParseEagerly([]byte(data))
if err != nil {
t.Fatalf("Failed to parse vendor list data. %v", err)
}
return parsed
}

func listFetcher(lists map[uint16]vendorlist.VendorList) func(context.Context, uint16) (vendorlist.VendorList, error) {
return func(ctx context.Context, id uint16) (vendorlist.VendorList, error) {
data, ok := lists[id]
if ok {
return data, nil
} else {
return nil, fmt.Errorf("vendorlist id=%d not found", id)
}
}
}

func failedListFetcher(ctx context.Context, id uint16) (vendorlist.VendorList, error) {
return nil, errors.New("vendor list can't be fetched")
}

func assertNilErr(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}

func assertErr(t *testing.T, err error) {
t.Helper()
if err == nil {
t.Errorf("Expected error did not occur.")
}
}

func assertBoolsEqual(t *testing.T, expected bool, actual bool) {
t.Helper()
if expected != actual {
t.Errorf("Expected %t, got %t", expected, actual)
}
}
Loading