Repository: prebid/prebid-server
Pull request #2903 (2023-07-05 20:49:46)

PWBID: Update Default Endpoint
This update changes the default bidder endpoint for PWBID to the current default.

onkarvhanumante: @GLStephen , JSON test framework still refers to old endpoint. For consistency recommended to also update test files to use new endpoint url
 
https://github.com/prebid/prebid-server/blob/6810d8424909030f24c5e41ef1db888545cf75ba/adapters/pwbid/pwbid_test.go#L13

Patch:
diff --git a/.semgrep/adapter/builder-struct-name.go b/.semgrep/adapter/builder-struct-name.go
new file mode 100644
index 00000000..37a938ca
--- /dev/null
+++ b/.semgrep/adapter/builder-struct-name.go
@@ -0,0 +1,124 @@
+/*
+	builder-struct-name tests
+	https://semgrep.dev/docs/writing-rules/testing-rules
+	"ruleid" prefix in comment indicates patterns that should be flagged by semgrep
+	"ok" prefix in comment indidcates  patterns that should not be flagged by the semgrep
+*/
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	foo1 := foo{}
+	// ruleid: builder-struct-name-check
+	return &fooadapter{foo: foo1}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	// ruleid: builder-struct-name-check
+	return &adapterbar{}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	// ruleid: builder-struct-name-check
+	return &fooadapterbar{}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	// ruleid: builder-struct-name-check
+	return &FooAdapter{}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	// ruleid: builder-struct-name-check
+	return &AdapterBar{}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	// ruleid: builder-struct-name-check
+	return &AdapterBar{}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	// ruleid: builder-struct-name-check
+	return &FooAdapterBar{}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	foo2 := foo{}
+	//ruleid: builder-struct-name-check
+	adpt1 := Adapter{foo: foo2}
+	return &adpt1, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	//ruleid: builder-struct-name-check
+	builder := &Adapter{foo{}}
+	return builder, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	foo3 := foo{}
+	if foo3.bar == "" {
+		foo3.bar = "bar"
+	}
+	//ruleid: builder-struct-name-check
+	adpt2 := Adapter{}
+	adpt2.foo = foo3
+	return &adpt2, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	//ruleid: builder-struct-name-check
+	return &foo{}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	//ruleid: builder-struct-name-check
+	var obj Adapter
+	obj.Foo = "foo"
+	if obj.Bar == "" {
+		obj.Bar = "bar"
+	}
+	return &obj, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	//ruleid: builder-struct-name-check
+	var obj *FooAdapterBar
+	obj.Foo = "foo"
+	if obj.Bar == "" {
+		obj.Bar = "bar"
+	}
+	return obj, nil
+}
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	// ok: builder-struct-name-check
+	return &adapter{endpoint: "www.foo.com"}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	builder := &adapter{}
+	builder.endpoint = "www.foo.com"
+	// ok: builder-struct-name-check
+	return builder, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	builder := adapter{}
+	builder.endpoint = "www.foo.com"
+	// ok: builder-struct-name-check
+	return &builder, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	var builder adapter
+	builder.endpoint = "www.foo.com"
+	// ok: builder-struct-name-check
+	return &builder, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	var builder *adapter
+	builder.endpoint = "www.foo.com"
+	// ok: builder-struct-name-check
+	return builder, nil
+}
\ No newline at end of file
diff --git a/.semgrep/adapter/builder-struct-name.yml b/.semgrep/adapter/builder-struct-name.yml
new file mode 100644
index 00000000..c80e8e12
--- /dev/null
+++ b/.semgrep/adapter/builder-struct-name.yml
@@ -0,0 +1,58 @@
+rules:
+  - id: builder-struct-name-check
+    languages:
+      - go
+    message: | 
+      You can call this simply "adapter", the `$BUILDER` identification is already supplied by the package name. As you have it, referencing your adapter from outside the package would be `$BUILDER.$BUILDER` which looks a little redundant. See example below:
+      
+      ```
+        package foo
+
+        type adapter struct {
+          endpoint string
+        }
+        
+        func  Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+          return &adapter{endpoint: "https://www.foo.com"}, nil
+        }
+      ```
+    severity: WARNING
+    patterns:
+      - pattern-either:
+          - pattern-inside: >
+              func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) {
+                ...
+                $BUILDER_OBJ := &$BUILDER{...}
+                ...
+                return $BUILDER_OBJ, nil
+              }
+          - pattern-inside: >
+              func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) {
+                ...
+                $BUILDER_OBJ := $BUILDER{...}
+                ...
+                return &$BUILDER_OBJ, nil
+              }
+          - pattern-inside: >
+              func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) {
+                ...
+                return &$BUILDER{...}, ...
+              }
+          - pattern-inside: >
+              func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) {
+                ...
+                var $BUILDER_OBJ $BUILDER
+                ...
+                return &$BUILDER_OBJ, ...
+              }            
+          - pattern-inside: >
+              func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) {
+                ...
+                var $BUILDER_OBJ *$BUILDER
+                ...
+                return $BUILDER_OBJ, ...
+              }        
+      - focus-metavariable: $BUILDER
+      - metavariable-regex:
+          metavariable: $BUILDER
+          regex: (?!adapter$)
diff --git a/.semgrep/adapter/type-bid-assignment.go b/.semgrep/adapter/type-bid-assignment.go
new file mode 100644
index 00000000..fb29a491
--- /dev/null
+++ b/.semgrep/adapter/type-bid-assignment.go
@@ -0,0 +1,181 @@
+/*
+	type-bid-assignment tests
+	https://semgrep.dev/docs/writing-rules/testing-rules
+	"ruleid" prefix in comment indicates patterns that should be flagged by semgrep
+	"ok" prefix in comment indidcates  patterns that should not be flagged by the semgrep
+*/
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	for _, seatBid := range bidResp.SeatBid {
+		for _, sb := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp)
+			if err != nil {
+				errs = append(errs, err)
+			} else {
+				b := &adapters.TypedBid{
+					// ruleid: type-bid-assignment-check
+					Bid:     &sb,
+					BidType: bidType,
+				}
+				bidResponse.Bids = append(bidResponse.Bids, b)
+			}
+		}
+	}
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	for _, seatBid := range bidResp.SeatBid {
+		for _, sb := range seatBid.Bid {
+			sbcopy := sb
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp)
+			if err != nil {
+				errs = append(errs, err)
+			} else {
+				b := &adapters.TypedBid{
+					// ok: type-bid-assignment-check
+					Bid:     &sbcopy,
+					BidType: bidType,
+				}
+				bidResponse.Bids = append(bidResponse.Bids, b)
+			}
+		}
+	}
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	for _, seatBid := range bidResp.SeatBid {
+		for _, sb := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp)
+			if err != nil {
+				return nil, err
+			}
+			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+				// ruleid: type-bid-assignment-check
+				Bid:     &sb,
+				BidType: bidType,
+			})
+
+		}
+	}
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	for _, seatBid := range bidResp.SeatBid {
+		for _, sb := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp)
+			if err != nil {
+				return nil, err
+			}
+			// ruleid: type-bid-assignment-check
+			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{Bid: &sb, BidType: bidType})
+		}
+	}
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	var errors []error
+	for _, seatBid := range bidResp.SeatBid {
+		for _, bid := range seatBid.Bid {
+			var t adapters.TypedBid
+			// ruleid: type-bid-assignment-check
+			t.Bid = &bid
+			bidResponse.Bids = append(bidResponse.Bids, &t)
+		}
+	}
+	return bidResponse, errors
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	var errors []error
+	for _, seatBid := range bidResp.SeatBid {
+		for _, bid := range seatBid.Bid {
+			var t adapters.TypedBid
+			t = adapters.TypedBid{
+				// ruleid: type-bid-assignment-check
+				Bid: &bid,
+			}
+
+			bidResponse.Bids = append(bidResponse.Bids, &t)
+		}
+	}
+	return bidResponse, errors
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	for _, seatBid := range bidResp.SeatBid {
+		for idx, _ := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp)
+			if err != nil {
+				errs = append(errs, err)
+			} else {
+				b := &adapters.TypedBid{
+					// ok: type-bid-assignment-check
+					Bid:     &seatBid.Bid[idx],
+					BidType: bidType,
+				}
+				bidResponse.Bids = append(bidResponse.Bids, b)
+			}
+		}
+	}
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	for _, seatBid := range bidResp.SeatBid {
+		for idx := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp)
+			if err != nil {
+				return nil, err
+			}
+			// ok: type-bid-assignment-check
+			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{Bid: &seatBid.Bid[idx], BidType: bidType})
+		}
+	}
+}
diff --git a/.semgrep/adapter/type-bid-assignment.yml b/.semgrep/adapter/type-bid-assignment.yml
new file mode 100644
index 00000000..95fb6811
--- /dev/null
+++ b/.semgrep/adapter/type-bid-assignment.yml
@@ -0,0 +1,64 @@
+rules:
+  - id: type-bid-assignment-check
+    languages:
+      - go
+    message: >      
+      Found incorrect assignment made to $KEY. $BID variable receives a new value in each iteration of range loop. Assigning the address of $BID `(&$BID)` to $KEY will result in a pointer that always points to the same memory address with the value of the last iteration. 
+      This can lead to unexpected behavior or incorrect results. Refer https://go.dev/play/p/9ZS1f-5h4qS
+
+      Consider using an index variable in the seatBids.Bid loop as shown below
+
+      ```
+        for _, seatBid := range response.SeatBid {
+          for i := range seatBids.Bid {
+            ...
+            responseBid := &adapters.TypedBid{
+              Bid: &seatBids.Bid[i],
+              ...
+            }
+            ...
+            ...
+          }
+        }
+      ```
+    severity: ERROR
+    patterns:
+      - pattern-either:
+          - pattern: >
+              for _, $BID := range ... {
+                ...
+                ... := &adapters.TypedBid{
+                  $KEY: &$BID,
+                  ...
+                }
+                ...
+              }
+          - pattern: >
+              for _, $BID := range ... {
+                ...
+                ... = adapters.TypedBid{
+                  $KEY: &$BID,
+                  ...
+                }
+                ...
+              } 
+          - pattern: >
+              for _, $BID := range ... {
+                ...
+                ... = append(...,  &adapters.TypedBid{
+                  $KEY: &$BID,
+                  ...
+                })
+                ...
+              }
+          - pattern: >
+              for _, $BID := range ... {
+                var $TYPEBID_OBJ adapters.TypedBid
+                ...
+                $TYPEBID_OBJ.$KEY = &$BID
+                ...
+              }                              
+      - focus-metavariable: $KEY
+      - metavariable-regex:
+          metavariable: $KEY
+          regex: Bid
diff --git a/adapters/ix/ix.go b/adapters/ix/ix.go
index 919f58da..796fefbb 100644
--- a/adapters/ix/ix.go
+++ b/adapters/ix/ix.go
@@ -7,7 +7,6 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/golang/glog"
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
@@ -123,7 +122,6 @@ func setSitePublisherId(requestCopy *openrtb2.BidRequest, uniqueSiteIDs map[stri
 			// Sorting siteIDs for predictable output as Go maps don't guarantee order
 			sort.Strings(siteIDs)
 			multipleSiteIDs := strings.Join(siteIDs, ", ")
-			glog.Warningf("Multiple SiteIDs found. %s", multipleSiteIDs)
 			ixDiag.MultipleSiteIds = multipleSiteIDs
 		}
 		requestCopy.Site = &site
diff --git a/config/account.go b/config/account.go
index 8beff9b6..02040211 100644
--- a/config/account.go
+++ b/config/account.go
@@ -40,7 +40,7 @@ type Account struct {
 	Validations             Validations                                 `mapstructure:"validations" json:"validations"`
 	DefaultBidLimit         int                                         `mapstructure:"default_bid_limit" json:"default_bid_limit"`
 	BidAdjustments          *openrtb_ext.ExtRequestPrebidBidAdjustments `mapstructure:"bidadjustments" json:"bidadjustments"`
-	Privacy                 AccountPrivacy                              `mapstructure:"privacy" json:"privacy"`
+	Privacy                 *AccountPrivacy                             `mapstructure:"privacy" json:"privacy"`
 }
 
 // CookieSync represents the account-level defaults for the cookie sync endpoint.
diff --git a/config/activity.go b/config/activity.go
index 987cbe84..5bddc7c6 100644
--- a/config/activity.go
+++ b/config/activity.go
@@ -8,16 +8,17 @@ type AllowActivities struct {
 	TransmitUserFPD          Activity `mapstructure:"transmitUfpd" json:"transmitUfpd"`
 	TransmitPreciseGeo       Activity `mapstructure:"transmitPreciseGeo" json:"transmitPreciseGeo"`
 	TransmitUniqueRequestIds Activity `mapstructure:"transmitUniqueRequestIds" json:"transmitUniqueRequestIds"`
+	TransmitTids             Activity `mapstructure:"transmitTid" json:"transmitTid"`
 }
 
 type Activity struct {
 	Default *bool          `mapstructure:"default" json:"default"`
 	Rules   []ActivityRule `mapstructure:"rules" json:"rules"`
-	Allow   bool           `mapstructure:"allow" json:"allow"`
 }
 
 type ActivityRule struct {
 	Condition ActivityCondition `mapstructure:"condition" json:"condition"`
+	Allow     bool              `mapstructure:"allow" json:"allow"`
 }
 
 type ActivityCondition struct {
diff --git a/config/config.go b/config/config.go
index ee18c746..543d1afd 100644
--- a/config/config.go
+++ b/config/config.go
@@ -155,6 +155,10 @@ func (cfg *Configuration) validate(v *viper.Viper) []error {
 		cfg.TmaxAdjustments.Enabled = false
 	}
 
+	if cfg.AccountDefaults.Privacy != nil {
+		glog.Warning("account_defaults.Privacy has no effect as the feature is under development.")
+	}
+
 	errs = cfg.Experiment.validate(errs)
 	errs = cfg.BidderInfos.validate(errs)
 	return errs
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 73f4842a..8d328b7c 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/prebid/prebid-server/privacy"
 	"net/http"
 	"net/url"
 	"strings"
@@ -222,6 +223,13 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 
 	tcf2Config := gdpr.NewTCF2Config(deps.cfg.GDPR.TCF2, account.GDPR)
 
+	activities, activitiesErr := privacy.NewActivityControl(account.Privacy)
+	if activitiesErr != nil {
+		errL = append(errL, activitiesErr)
+		writeError(errL, w, &labels)
+		return
+	}
+
 	secGPC := r.Header.Get("Sec-GPC")
 
 	auctionRequest := &exchange.AuctionRequest{
@@ -239,6 +247,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 		HookExecutor:               hookExecutor,
 		QueryParams:                r.URL.Query(),
 		TCF2Config:                 tcf2Config,
+		Activities:                 activities,
 	}
 
 	auctionResponse, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 216bff6e..3f01967a 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/prebid/prebid-server/privacy"
 	"io"
 	"io/ioutil"
 	"net/http"
@@ -191,6 +192,15 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 
 	tcf2Config := gdpr.NewTCF2Config(deps.cfg.GDPR.TCF2, account.GDPR)
 
+	activities, activitiesErr := privacy.NewActivityControl(account.Privacy)
+	if activitiesErr != nil {
+		errL = append(errL, activitiesErr)
+		if errortypes.ContainsFatalError(errL) {
+			writeError(errL, w, &labels)
+			return
+		}
+	}
+
 	ctx := context.Background()
 
 	timeout := deps.cfg.AuctionTimeouts.LimitAuctionTimeout(time.Duration(req.TMax) * time.Millisecond)
@@ -236,6 +246,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		PubID:                      labels.PubID,
 		HookExecutor:               hookExecutor,
 		TCF2Config:                 tcf2Config,
+		Activities:                 activities,
 	}
 	auctionResponse, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
 	ao.RequestWrapper = req
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 24ca193f..07dce45a 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/prebid/prebid-server/privacy"
 	"math/rand"
 	"net/url"
 	"runtime/debug"
@@ -195,6 +196,7 @@ type AuctionRequest struct {
 	GlobalPrivacyControlHeader string
 	ImpExtInfoMap              map[string]ImpExtInfo
 	TCF2Config                 gdpr.TCF2ConfigReader
+	Activities                 privacy.ActivityControl
 
 	// LegacyLabels is included here for temporary compatibility with cleanOpenRTBRequests
 	// in HoldAuction until we get to factoring it away. Do not use for anything new.
diff --git a/exchange/utils.go b/exchange/utils.go
index d43a4182..5b29fd62 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -150,6 +150,15 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 	for _, bidderRequest := range allBidderRequests {
 		bidRequestAllowed := true
 
+		// fetchBids activity
+		fetchBidsActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityFetchBids,
+			privacy.ScopedName{Scope: privacy.ScopeTypeBidder, Name: bidderRequest.BidderName.String()})
+		if fetchBidsActivityAllowed == privacy.ActivityDeny {
+			// skip the call to a bidder if fetchBids activity is not allowed
+			// do not add this bidder to allowedBidderRequests
+			continue
+		}
+
 		// CCPA
 		privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String())
 
diff --git a/privacy/activity.go b/privacy/activity.go
index 7cdef175..6ca48ae0 100644
--- a/privacy/activity.go
+++ b/privacy/activity.go
@@ -11,6 +11,7 @@ const (
 	ActivityTransmitUserFPD
 	ActivityTransmitPreciseGeo
 	ActivityTransmitUniqueRequestIds
+	ActivityTransmitTids
 )
 
 func (a Activity) String() string {
@@ -29,6 +30,8 @@ func (a Activity) String() string {
 		return "transmitPreciseGeo"
 	case ActivityTransmitUniqueRequestIds:
 		return "transmitUniqueRequestIds"
+	case ActivityTransmitTids:
+		return "transmitTid"
 	}
 
 	return ""
diff --git a/privacy/enforcer.go b/privacy/enforcer.go
index 0d5ecad5..d63cd8de 100644
--- a/privacy/enforcer.go
+++ b/privacy/enforcer.go
@@ -1,43 +1,236 @@
 package privacy
 
-// PolicyEnforcer determines if personally identifiable information (PII) should be removed or anonymized per the policy.
-type PolicyEnforcer interface {
-	// CanEnforce returns true when policy information is specifically provided by the publisher.
-	CanEnforce() bool
+import (
+	"fmt"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
+	"strings"
+)
 
-	// ShouldEnforce returns true when the OpenRTB request should have personally identifiable
-	// information (PII) removed or anonymized per the policy.
-	ShouldEnforce(bidder string) bool
+type ActivityResult int
+
+const (
+	ActivityAbstain ActivityResult = iota
+	ActivityAllow
+	ActivityDeny
+)
+
+const (
+	ScopeTypeBidder    = "bidder"
+	ScopeTypeAnalytics = "analytics"
+	ScopeTypeRTD       = "rtd" // real time data
+	ScopeTypeUserID    = "userid"
+	ScopeTypeGeneral   = "general"
+)
+
+type ActivityControl struct {
+	plans map[Activity]ActivityPlan
 }
 
-// NilPolicyEnforcer implements the PolicyEnforcer interface but will always return false.
-type NilPolicyEnforcer struct{}
+func NewActivityControl(privacyConf *config.AccountPrivacy) (ActivityControl, error) {
+	ac := ActivityControl{}
+	var err error
 
-// CanEnforce is hardcoded to always return false.
-func (NilPolicyEnforcer) CanEnforce() bool {
-	return false
+	if privacyConf == nil {
+		return ac, err
+	} else {
+		//temporarily disable Activities if they are specified at the account level
+		return ac, &errortypes.Warning{Message: "account.Privacy has no effect as the feature is under development."}
+	}
+
+	plans := make(map[Activity]ActivityPlan)
+
+	plans[ActivitySyncUser], err = buildEnforcementPlan(privacyConf.AllowActivities.SyncUser)
+	if err != nil {
+		return ac, err
+	}
+	plans[ActivityFetchBids], err = buildEnforcementPlan(privacyConf.AllowActivities.FetchBids)
+	if err != nil {
+		return ac, err
+	}
+	plans[ActivityEnrichUserFPD], err = buildEnforcementPlan(privacyConf.AllowActivities.EnrichUserFPD)
+	if err != nil {
+		return ac, err
+	}
+	plans[ActivityReportAnalytics], err = buildEnforcementPlan(privacyConf.AllowActivities.ReportAnalytics)
+	if err != nil {
+		return ac, err
+	}
+	plans[ActivityTransmitUserFPD], err = buildEnforcementPlan(privacyConf.AllowActivities.TransmitUserFPD)
+	if err != nil {
+		return ac, err
+	}
+	plans[ActivityTransmitPreciseGeo], err = buildEnforcementPlan(privacyConf.AllowActivities.TransmitPreciseGeo)
+	if err != nil {
+		return ac, err
+	}
+	plans[ActivityTransmitUniqueRequestIds], err = buildEnforcementPlan(privacyConf.AllowActivities.TransmitUniqueRequestIds)
+	if err != nil {
+		return ac, err
+	}
+	plans[ActivityTransmitTids], err = buildEnforcementPlan(privacyConf.AllowActivities.TransmitTids)
+	if err != nil {
+		return ac, err
+	}
+
+	ac.plans = plans
+
+	return ac, nil
+}
+
+func buildEnforcementPlan(activity config.Activity) (ActivityPlan, error) {
+	ef := ActivityPlan{}
+	rules, err := activityRulesToEnforcementRules(activity.Rules)
+	if err != nil {
+		return ef, err
+	}
+	ef.defaultResult = activityDefaultToDefaultResult(activity.Default)
+	ef.rules = rules
+	return ef, nil
 }
 
-// ShouldEnforce is hardcoded to always return false.
-func (NilPolicyEnforcer) ShouldEnforce(bidder string) bool {
-	return false
+func activityRulesToEnforcementRules(rules []config.ActivityRule) ([]ActivityRule, error) {
+	enfRules := make([]ActivityRule, 0)
+	for _, r := range rules {
+		cmpName, err := conditionToRuleComponentName(r.Condition.ComponentName)
+		if err != nil {
+			return nil, err
+		}
+		er := ComponentEnforcementRule{
+			allowed:       r.Allow,
+			componentName: cmpName,
+			componentType: r.Condition.ComponentType,
+		}
+		enfRules = append(enfRules, er)
+	}
+	return enfRules, nil
 }
 
-// EnabledPolicyEnforcer decorates a PolicyEnforcer with an enabled flag.
-type EnabledPolicyEnforcer struct {
-	Enabled        bool
-	PolicyEnforcer PolicyEnforcer
+func conditionToRuleComponentName(conditions []string) ([]ScopedName, error) {
+	sn := make([]ScopedName, 0)
+	for _, condition := range conditions {
+		scope, err := NewScopedName(condition)
+		if err != nil {
+			return sn, err
+		}
+		sn = append(sn, scope)
+	}
+	return sn, nil
 }
 
-// CanEnforce returns true when the PolicyEnforcer can enforce.
-func (p EnabledPolicyEnforcer) CanEnforce() bool {
-	return p.PolicyEnforcer.CanEnforce()
+func activityDefaultToDefaultResult(activityDefault *bool) ActivityResult {
+	if activityDefault == nil {
+		// if default is unspecified, the hardcoded default-default is true.
+		return ActivityAllow
+	} else if *activityDefault {
+		return ActivityAllow
+	}
+	return ActivityDeny
 }
 
-// ShouldEnforce returns true when the enforcer is enabled the PolicyEnforcer allows enforcement.
-func (p EnabledPolicyEnforcer) ShouldEnforce(bidder string) bool {
-	if p.Enabled {
-		return p.PolicyEnforcer.ShouldEnforce(bidder)
+func (e ActivityControl) Allow(activity Activity, target ScopedName) ActivityResult {
+	plan, planDefined := e.plans[activity]
+
+	if !planDefined {
+		return ActivityAbstain
 	}
-	return false
+
+	return plan.Allow(target)
+}
+
+type ActivityPlan struct {
+	defaultResult ActivityResult
+	rules         []ActivityRule
+}
+
+func (p ActivityPlan) Allow(target ScopedName) ActivityResult {
+	for _, rule := range p.rules {
+		result := rule.Allow(target)
+		if result == ActivityDeny || result == ActivityAllow {
+			return result
+		}
+	}
+	return p.defaultResult
+}
+
+type ActivityRule interface {
+	Allow(target ScopedName) ActivityResult
+}
+
+type ComponentEnforcementRule struct {
+	componentName []ScopedName
+	componentType []string
+	// include gppSectionId from 3.5
+	// include geo from 3.5
+	allowed bool
+}
+
+func (r ComponentEnforcementRule) Allow(target ScopedName) ActivityResult {
+	if len(r.componentName) == 0 && len(r.componentType) == 0 {
+		return ActivityAbstain
+	}
+
+	nameClauseExists := len(r.componentName) > 0
+	typeClauseExists := len(r.componentType) > 0
+
+	componentNameFound := false
+	for _, scope := range r.componentName {
+		if strings.EqualFold(scope.Scope, target.Scope) &&
+			(scope.Name == "*" || strings.EqualFold(scope.Name, target.Name)) {
+			componentNameFound = true
+			break
+		}
+	}
+
+	componentTypeFound := false
+	for _, componentType := range r.componentType {
+		if strings.EqualFold(componentType, target.Scope) {
+			componentTypeFound = true
+			break
+		}
+	}
+	// behavior if rule matches: can be either true=allow or false=deny. result is abstain if the rule doesn't match
+	matchFound := (componentNameFound || !nameClauseExists) && (componentTypeFound || !typeClauseExists)
+	if matchFound {
+		if r.allowed {
+			return ActivityAllow
+		} else {
+			return ActivityDeny
+		}
+	}
+	return ActivityAbstain
+}
+
+type ScopedName struct {
+	Scope string
+	Name  string
+}
+
+func NewScopedName(condition string) (ScopedName, error) {
+	if condition == "" {
+		return ScopedName{}, fmt.Errorf("unable to parse empty condition")
+	}
+	var scope, name string
+	split := strings.Split(condition, ".")
+	if len(split) == 2 {
+		s := strings.ToLower(split[0])
+		if s == ScopeTypeBidder || s == ScopeTypeAnalytics || s == ScopeTypeUserID {
+			scope = s
+		} else if strings.Contains(s, ScopeTypeRTD) {
+			scope = ScopeTypeRTD
+		} else {
+			scope = ScopeTypeGeneral
+		}
+		name = split[1]
+	} else if len(split) == 1 {
+		scope = ScopeTypeBidder
+		name = split[0]
+	} else {
+		return ScopedName{}, fmt.Errorf("unable to parse condition: %s", condition)
+	}
+
+	return ScopedName{
+		Scope: scope,
+		Name:  name,
+	}, nil
 }
diff --git a/privacy/policyenforcer.go b/privacy/policyenforcer.go
new file mode 100644
index 00000000..e70c0d3d
--- /dev/null
+++ b/privacy/policyenforcer.go
@@ -0,0 +1,45 @@
+package privacy
+
+// NOTE: Reanme this package. Will eventually replace in its entirety with Activites.
+
+// PolicyEnforcer determines if personally identifiable information (PII) should be removed or anonymized per the policy.
+type PolicyEnforcer interface {
+	// CanEnforce returns true when policy information is specifically provided by the publisher.
+	CanEnforce() bool
+
+	// ShouldEnforce returns true when the OpenRTB request should have personally identifiable
+	// information (PII) removed or anonymized per the policy.
+	ShouldEnforce(bidder string) bool
+}
+
+// NilPolicyEnforcer implements the PolicyEnforcer interface but will always return false.
+type NilPolicyEnforcer struct{}
+
+// CanEnforce is hardcoded to always return false.
+func (NilPolicyEnforcer) CanEnforce() bool {
+	return false
+}
+
+// ShouldEnforce is hardcoded to always return false.
+func (NilPolicyEnforcer) ShouldEnforce(bidder string) bool {
+	return false
+}
+
+// EnabledPolicyEnforcer decorates a PolicyEnforcer with an enabled flag.
+type EnabledPolicyEnforcer struct {
+	Enabled        bool
+	PolicyEnforcer PolicyEnforcer
+}
+
+// CanEnforce returns true when the PolicyEnforcer can enforce.
+func (p EnabledPolicyEnforcer) CanEnforce() bool {
+	return p.PolicyEnforcer.CanEnforce()
+}
+
+// ShouldEnforce returns true when the enforcer is enabled the PolicyEnforcer allows enforcement.
+func (p EnabledPolicyEnforcer) ShouldEnforce(bidder string) bool {
+	if p.Enabled {
+		return p.PolicyEnforcer.ShouldEnforce(bidder)
+	}
+	return false
+}
diff --git a/static/bidder-info/pwbid.yaml b/static/bidder-info/pwbid.yaml
index ac687273..a172ff39 100644
--- a/static/bidder-info/pwbid.yaml
+++ b/static/bidder-info/pwbid.yaml
@@ -1,4 +1,4 @@
-endpoint: "https://bid.pubwise.io/prebid"
+endpoint: "https://bidder.east2.pubwise.io/bid/pubwisedirect"
 maintainer:
   email: info@pubwise.io
 gvlVendorID: 842
@@ -17,4 +17,4 @@ userSync:
   # PubWise supports user syncing, but requires configuration by the host. contact this
   # bidder directly at the email address in this file to ask about enabling user sync.
   supports:
-    - redirect
\ No newline at end of file
+    - redirect


Test patch:
diff --git a/adapters/pwbid/pwbid_test.go b/adapters/pwbid/pwbid_test.go
index 21a41942..194e4bde 100644
--- a/adapters/pwbid/pwbid_test.go
+++ b/adapters/pwbid/pwbid_test.go
@@ -10,7 +10,7 @@ import (
 
 func TestJsonSamples(t *testing.T) {
 	bidder, buildErr := Builder(openrtb_ext.BidderPWBid, config.Adapter{
-		Endpoint: "https://bid.pubwise.io/prebid"},
+		Endpoint: "https://bidder.east2.pubwise.io/bid/pubwisedirect"},
 		config.Server{ExternalUrl: "http://hosturl.com", GvlID: 842, DataCenter: "2"})
 
 	if buildErr != nil {
diff --git a/adapters/pwbid/pwbidtest/exemplary/banner.json b/adapters/pwbid/pwbidtest/exemplary/banner.json
index ba618cb8..4cf93e1a 100644
--- a/adapters/pwbid/pwbidtest/exemplary/banner.json
+++ b/adapters/pwbid/pwbidtest/exemplary/banner.json
@@ -23,7 +23,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id-banner",
           "imp": [
@@ -90,4 +90,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/adapters/pwbid/pwbidtest/exemplary/native.json b/adapters/pwbid/pwbidtest/exemplary/native.json
index 907c16d4..ff57752c 100644
--- a/adapters/pwbid/pwbidtest/exemplary/native.json
+++ b/adapters/pwbid/pwbidtest/exemplary/native.json
@@ -18,7 +18,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id-native",
           "imp": [
@@ -78,4 +78,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/adapters/pwbid/pwbidtest/exemplary/optional-params.json b/adapters/pwbid/pwbidtest/exemplary/optional-params.json
index a080be90..5ababb24 100644
--- a/adapters/pwbid/pwbidtest/exemplary/optional-params.json
+++ b/adapters/pwbid/pwbidtest/exemplary/optional-params.json
@@ -25,7 +25,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id-banner",
           "imp": [
@@ -94,4 +94,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/adapters/pwbid/pwbidtest/exemplary/video.json b/adapters/pwbid/pwbidtest/exemplary/video.json
index b74c780d..6257de63 100644
--- a/adapters/pwbid/pwbidtest/exemplary/video.json
+++ b/adapters/pwbid/pwbidtest/exemplary/video.json
@@ -20,7 +20,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json b/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json
index 146ba93a..0d469893 100644
--- a/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json
+++ b/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json
@@ -19,7 +19,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/pwbid/pwbidtest/supplemental/response-204.json b/adapters/pwbid/pwbidtest/supplemental/response-204.json
index 5fff7ee3..4fc8961e 100644
--- a/adapters/pwbid/pwbidtest/supplemental/response-204.json
+++ b/adapters/pwbid/pwbidtest/supplemental/response-204.json
@@ -19,7 +19,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/pwbid/pwbidtest/supplemental/response-400.json b/adapters/pwbid/pwbidtest/supplemental/response-400.json
index d594e571..a1517883 100644
--- a/adapters/pwbid/pwbidtest/supplemental/response-400.json
+++ b/adapters/pwbid/pwbidtest/supplemental/response-400.json
@@ -19,7 +19,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/pwbid/pwbidtest/supplemental/response-500.json b/adapters/pwbid/pwbidtest/supplemental/response-500.json
index fa3d4d06..c2b56494 100644
--- a/adapters/pwbid/pwbidtest/supplemental/response-500.json
+++ b/adapters/pwbid/pwbidtest/supplemental/response-500.json
@@ -19,7 +19,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index d1b6622f..973edc53 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -17,6 +17,7 @@ import (
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/privacy"
 	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
@@ -4273,3 +4274,72 @@ func TestGetMediaTypeForBid(t *testing.T) {
 		})
 	}
 }
+
+func TemporarilyDisabledTestCleanOpenRTBRequestsActivitiesFetchBids(t *testing.T) {
+	testCases := []struct {
+		name              string
+		req               *openrtb2.BidRequest
+		componentName     string
+		allow             bool
+		expectedReqNumber int
+	}{
+		{
+			name:              "request_with_one_bidder_allowed",
+			req:               newBidRequest(t),
+			componentName:     "appnexus",
+			allow:             true,
+			expectedReqNumber: 1,
+		},
+		{
+			name:              "request_with_one_bidder_not_allowed",
+			req:               newBidRequest(t),
+			componentName:     "appnexus",
+			allow:             false,
+			expectedReqNumber: 0,
+		},
+	}
+
+	for _, test := range testCases {
+		privacyConfig := getDefaultActivityConfig(test.componentName, test.allow)
+		activities, err := privacy.NewActivityControl(privacyConfig)
+		assert.NoError(t, err, "")
+		auctionReq := AuctionRequest{
+			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: test.req},
+			UserSyncs:         &emptyUsersync{},
+			Activities:        activities,
+		}
+
+		bidderToSyncerKey := map[string]string{}
+		reqSplitter := &requestSplitter{
+			bidderToSyncerKey: bidderToSyncerKey,
+			me:                &metrics.MetricsEngineMock{},
+			hostSChainNode:    nil,
+			bidderInfo:        config.BidderInfos{},
+		}
+
+		t.Run(test.name, func(t *testing.T) {
+			bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo)
+			assert.Empty(t, errs)
+			assert.Len(t, bidderRequests, test.expectedReqNumber)
+		})
+	}
+}
+
+func getDefaultActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
+	return &config.AccountPrivacy{
+		AllowActivities: config.AllowActivities{
+			FetchBids: config.Activity{
+				Default: ptrutil.ToPtr(true),
+				Rules: []config.ActivityRule{
+					{
+						Allow: allow,
+						Condition: config.ActivityCondition{
+							ComponentName: []string{componentName},
+							ComponentType: []string{"bidder"},
+						},
+					},
+				},
+			},
+		},
+	}
+}
diff --git a/privacy/enforcer_test.go b/privacy/enforcer_test.go
index b0c4032c..e87a9eb2 100644
--- a/privacy/enforcer_test.go
+++ b/privacy/enforcer_test.go
@@ -1,18 +1,427 @@
 package privacy
 
 import (
-	"testing"
-
+	"errors"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
+	"testing"
 )
 
-func TestNilEnforcerCanEnforce(t *testing.T) {
-	nilEnforcer := &NilPolicyEnforcer{}
-	assert.False(t, nilEnforcer.CanEnforce())
+func TemporarilyDisabledTestNewActivityControl(t *testing.T) {
+
+	testCases := []struct {
+		name            string
+		privacyConf     *config.AccountPrivacy
+		activityControl ActivityControl
+		err             error
+	}{
+		{
+			name:            "privacy_config_is_nil",
+			privacyConf:     nil,
+			activityControl: ActivityControl{plans: nil},
+			err:             nil,
+		},
+		{
+			name: "privacy_config_is_specified_and_correct",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					SyncUser:                 getDefaultActivityConfig(),
+					FetchBids:                getDefaultActivityConfig(),
+					EnrichUserFPD:            getDefaultActivityConfig(),
+					ReportAnalytics:          getDefaultActivityConfig(),
+					TransmitUserFPD:          getDefaultActivityConfig(),
+					TransmitPreciseGeo:       getDefaultActivityConfig(),
+					TransmitUniqueRequestIds: getDefaultActivityConfig(),
+					TransmitTids:             getDefaultActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: map[Activity]ActivityPlan{
+				ActivitySyncUser:                 getDefaultActivityPlan(),
+				ActivityFetchBids:                getDefaultActivityPlan(),
+				ActivityEnrichUserFPD:            getDefaultActivityPlan(),
+				ActivityReportAnalytics:          getDefaultActivityPlan(),
+				ActivityTransmitUserFPD:          getDefaultActivityPlan(),
+				ActivityTransmitPreciseGeo:       getDefaultActivityPlan(),
+				ActivityTransmitUniqueRequestIds: getDefaultActivityPlan(),
+				ActivityTransmitTids:             getDefaultActivityPlan(),
+			}},
+			err: nil,
+		},
+		{
+			name: "privacy_config_is_specified_and_SyncUser_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					SyncUser: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name: "privacy_config_is_specified_and_FetchBids_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					FetchBids: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name: "privacy_config_is_specified_and_EnrichUserFPD_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					EnrichUserFPD: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name: "privacy_config_is_specified_and_ReportAnalytics_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					ReportAnalytics: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name: "privacy_config_is_specified_and_TransmitUserFPD_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					TransmitUserFPD: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name: "privacy_config_is_specified_and_TransmitPreciseGeo_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					TransmitPreciseGeo: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name: "privacy_config_is_specified_and_TransmitUniqueRequestIds_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					TransmitUniqueRequestIds: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name: "privacy_config_is_specified_and_TransmitTids_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					TransmitTids: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actualAC, actualErr := NewActivityControl(test.privacyConf)
+			if test.err == nil {
+				assert.Equal(t, test.activityControl, actualAC)
+				assert.NoError(t, actualErr)
+			} else {
+				assert.EqualError(t, actualErr, test.err.Error())
+			}
+		})
+	}
+}
+
+func TestActivityDefaultToDefaultResult(t *testing.T) {
+
+	testCases := []struct {
+		name            string
+		activityDefault *bool
+		expectedResult  ActivityResult
+	}{
+		{
+			name:            "activityDefault_is_nil",
+			activityDefault: nil,
+			expectedResult:  ActivityAllow,
+		},
+		{
+			name:            "activityDefault_is_true",
+			activityDefault: ptrutil.ToPtr(true),
+			expectedResult:  ActivityAllow,
+		},
+		{
+			name:            "activityDefault_is_false",
+			activityDefault: ptrutil.ToPtr(false),
+			expectedResult:  ActivityDeny,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actualResult := activityDefaultToDefaultResult(test.activityDefault)
+			assert.Equal(t, test.expectedResult, actualResult)
+		})
+	}
+}
+
+func TestAllowActivityControl(t *testing.T) {
+
+	testCases := []struct {
+		name            string
+		activityControl ActivityControl
+		activity        Activity
+		target          ScopedName
+		activityResult  ActivityResult
+	}{
+		{
+			name:            "plans_is_nil",
+			activityControl: ActivityControl{plans: nil},
+			activity:        ActivityFetchBids,
+			target:          ScopedName{Scope: "bidder", Name: "bidderA"},
+			activityResult:  ActivityAbstain,
+		},
+		{
+			name: "activity_not_defined",
+			activityControl: ActivityControl{plans: map[Activity]ActivityPlan{
+				ActivitySyncUser: getDefaultActivityPlan()}},
+			activity:       ActivityFetchBids,
+			target:         ScopedName{Scope: "bidder", Name: "bidderA"},
+			activityResult: ActivityAbstain,
+		},
+		{
+			name: "activity_defined_but_not_found_default_returned",
+			activityControl: ActivityControl{plans: map[Activity]ActivityPlan{
+				ActivityFetchBids: getDefaultActivityPlan()}},
+			activity:       ActivityFetchBids,
+			target:         ScopedName{Scope: "bidder", Name: "bidderB"},
+			activityResult: ActivityAllow,
+		},
+		{
+			name: "activity_defined_and_allowed",
+			activityControl: ActivityControl{plans: map[Activity]ActivityPlan{
+				ActivityFetchBids: getDefaultActivityPlan()}},
+			activity:       ActivityFetchBids,
+			target:         ScopedName{Scope: "bidder", Name: "bidderA"},
+			activityResult: ActivityAllow,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actualResult := test.activityControl.Allow(test.activity, test.target)
+			assert.Equal(t, test.activityResult, actualResult)
+
+		})
+	}
+}
+
+func TestAllowComponentEnforcementRule(t *testing.T) {
+
+	testCases := []struct {
+		name           string
+		componentRule  ComponentEnforcementRule
+		target         ScopedName
+		activityResult ActivityResult
+	}{
+		{
+			name: "activity_is_allowed",
+			componentRule: ComponentEnforcementRule{
+				allowed: true,
+				componentName: []ScopedName{
+					{Scope: "bidder", Name: "bidderA"},
+				},
+				componentType: []string{"bidder"},
+			},
+			target:         ScopedName{Scope: "bidder", Name: "bidderA"},
+			activityResult: ActivityAllow,
+		},
+		{
+			name: "activity_is_not_allowed",
+			componentRule: ComponentEnforcementRule{
+				allowed: false,
+				componentName: []ScopedName{
+					{Scope: "bidder", Name: "bidderA"},
+				},
+				componentType: []string{"bidder"},
+			},
+			target:         ScopedName{Scope: "bidder", Name: "bidderA"},
+			activityResult: ActivityDeny,
+		},
+		{
+			name: "abstain_both_clauses_do_not_match",
+			componentRule: ComponentEnforcementRule{
+				allowed: true,
+				componentName: []ScopedName{
+					{Scope: "bidder", Name: "bidderA"},
+				},
+				componentType: []string{"bidder"},
+			},
+			target:         ScopedName{Scope: "bidder", Name: "bidderB"},
+			activityResult: ActivityAbstain,
+		},
+		{
+			name: "activity_is_not_allowed_componentName_only",
+			componentRule: ComponentEnforcementRule{
+				allowed: true,
+				componentName: []ScopedName{
+					{Scope: "bidder", Name: "bidderA"},
+				},
+			},
+			target:         ScopedName{Scope: "bidder", Name: "bidderA"},
+			activityResult: ActivityAllow,
+		},
+		{
+			name: "activity_is_allowed_componentType_only",
+			componentRule: ComponentEnforcementRule{
+				allowed:       true,
+				componentType: []string{"bidder"},
+			},
+			target:         ScopedName{Scope: "bidder", Name: "bidderB"},
+			activityResult: ActivityAllow,
+		},
+		{
+			name: "abstain_activity_no_componentType_and_no_componentName",
+			componentRule: ComponentEnforcementRule{
+				allowed: true,
+			},
+			target:         ScopedName{Scope: "bidder", Name: "bidderB"},
+			activityResult: ActivityAbstain,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actualResult := test.componentRule.Allow(test.target)
+			assert.Equal(t, test.activityResult, actualResult)
+
+		})
+	}
+}
+
+func TestNewScopedName(t *testing.T) {
+
+	testCases := []struct {
+		name              string
+		condition         string
+		expectedScopeName ScopedName
+		err               error
+	}{
+		{
+			name:              "condition_is_empty",
+			condition:         "",
+			expectedScopeName: ScopedName{},
+			err:               errors.New("unable to parse empty condition"),
+		},
+		{
+			name:              "condition_is_incorrect",
+			condition:         "bidder.bidderA.bidderB",
+			expectedScopeName: ScopedName{},
+			err:               errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name:              "condition_is_scoped_to_bidder",
+			condition:         "bidder.bidderA",
+			expectedScopeName: ScopedName{Scope: "bidder", Name: "bidderA"},
+			err:               nil,
+		},
+		{
+			name:              "condition_is_scoped_to_analytics",
+			condition:         "analytics.bidderA",
+			expectedScopeName: ScopedName{Scope: "analytics", Name: "bidderA"},
+			err:               nil,
+		},
+		{
+			name:              "condition_is_scoped_to_userid",
+			condition:         "userid.bidderA",
+			expectedScopeName: ScopedName{Scope: "userid", Name: "bidderA"},
+			err:               nil,
+		},
+		{
+			name:              "condition_is_bidder_name",
+			condition:         "bidderA",
+			expectedScopeName: ScopedName{Scope: "bidder", Name: "bidderA"},
+			err:               nil,
+		},
+		{
+			name:              "condition_is_module_tag_rtd",
+			condition:         "rtd.test",
+			expectedScopeName: ScopedName{Scope: "rtd", Name: "test"},
+			err:               nil,
+		},
+		{
+			name:              "condition_scope_defaults_to_genera",
+			condition:         "test.test",
+			expectedScopeName: ScopedName{Scope: "general", Name: "test"},
+			err:               nil,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actualSN, actualErr := NewScopedName(test.condition)
+			if test.err == nil {
+				assert.Equal(t, test.expectedScopeName, actualSN)
+				assert.NoError(t, actualErr)
+			} else {
+				assert.EqualError(t, actualErr, test.err.Error())
+			}
+		})
+	}
+}
+
+// constants
+func getDefaultActivityConfig() config.Activity {
+	return config.Activity{
+		Default: ptrutil.ToPtr(true),
+		Rules: []config.ActivityRule{
+			{
+				Allow: true,
+				Condition: config.ActivityCondition{
+					ComponentName: []string{"bidderA"},
+					ComponentType: []string{"bidder"},
+				},
+			},
+		},
+	}
+}
+
+func getDefaultActivityPlan() ActivityPlan {
+	return ActivityPlan{
+		defaultResult: ActivityAllow,
+		rules: []ActivityRule{
+			ComponentEnforcementRule{
+				allowed: true,
+				componentName: []ScopedName{
+					{Scope: "bidder", Name: "bidderA"},
+				},
+				componentType: []string{"bidder"},
+			},
+		},
+	}
 }
 
-func TestNilEnforcerShouldEnforce(t *testing.T) {
-	nilEnforcer := &NilPolicyEnforcer{}
-	assert.False(t, nilEnforcer.ShouldEnforce(""))
-	assert.False(t, nilEnforcer.ShouldEnforce("anyBidder"))
+func getIncorrectActivityConfig() config.Activity {
+	return config.Activity{
+		Default: ptrutil.ToPtr(true),
+		Rules: []config.ActivityRule{
+			{
+				Allow: true,
+				Condition: config.ActivityCondition{
+					ComponentName: []string{"bidder.bidderA.bidderB"},
+					ComponentType: []string{"bidder"},
+				},
+			},
+		},
+	}
 }
diff --git a/privacy/policyenforcer_test.go b/privacy/policyenforcer_test.go
new file mode 100644
index 00000000..b0c4032c
--- /dev/null
+++ b/privacy/policyenforcer_test.go
@@ -0,0 +1,18 @@
+package privacy
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNilEnforcerCanEnforce(t *testing.T) {
+	nilEnforcer := &NilPolicyEnforcer{}
+	assert.False(t, nilEnforcer.CanEnforce())
+}
+
+func TestNilEnforcerShouldEnforce(t *testing.T) {
+	nilEnforcer := &NilPolicyEnforcer{}
+	assert.False(t, nilEnforcer.ShouldEnforce(""))
+	assert.False(t, nilEnforcer.ShouldEnforce("anyBidder"))
+}