diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index d00c8211993..7424d65005e 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -2216,9 +2216,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) gdpr.AuctionPermissions { +func (m *MockGDPRPerms) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) { args := m.Called(ctx, bidderCoreName, bidder) - return args.Get(0).(gdpr.AuctionPermissions) + return args.Get(0).(gdpr.AuctionPermissions), args.Error(1) } type FakeAccountsFetcher struct { @@ -2248,10 +2248,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) gdpr.AuctionPermissions { +func (p *fakePermissions) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) { return gdpr.AuctionPermissions{ AllowBidRequest: true, - } + }, nil } func getDefaultActivityConfig(componentName string, allow bool) *config.AccountPrivacy { diff --git a/endpoints/openrtb2/test_utils.go b/endpoints/openrtb2/test_utils.go index 331c0d57dfb..b2f2826739f 100644 --- a/endpoints/openrtb2/test_utils.go +++ b/endpoints/openrtb2/test_utils.go @@ -1429,10 +1429,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) gdpr.AuctionPermissions { +func (p *fakePermissions) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) { return gdpr.AuctionPermissions{ AllowBidRequest: true, - } + }, nil } type mockPlanBuilder struct { diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go index 87208a09114..d576b8a0093 100644 --- a/endpoints/setuid_test.go +++ b/endpoints/setuid_test.go @@ -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) gdpr.AuctionPermissions { +func (g *fakePermsSetUID) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions gdpr.AuctionPermissions, err error) { return gdpr.AuctionPermissions{ AllowBidRequest: g.personalInfoAllowed, PassGeo: g.personalInfoAllowed, PassID: g.personalInfoAllowed, - } + }, nil } type fakeSyncer struct { diff --git a/exchange/utils.go b/exchange/utils.go index f56a88151d9..9b4ffd21f3e 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -71,6 +71,8 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, return } + allowedBidderRequests = make([]BidderRequest, 0) + bidderImpWithBidResp := stored_responses.InitStoredBidResponses(req.BidRequest, auctionReq.StoredBidResponses) hasStoredAuctionResponses := len(auctionReq.StoredAuctionResponses) > 0 @@ -146,110 +148,102 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, gdprPerms = rs.gdprPermsBuilder(auctionReq.TCF2Config, gdprRequestInfo) } - allowedBidderRequests = make([]BidderRequest, 0, len(allBidderRequests)) - + // bidder level privacy policies for _, bidderRequest := range allBidderRequests { - auctionPermissions := gdprPerms.AuctionActivitiesAllowed(ctx, bidderRequest.BidderCoreName, bidderRequest.BidderName) - - // privacy blocking - if rs.isBidderBlockedByPrivacy(req, auctionReq.Activities, auctionPermissions, bidderRequest.BidderCoreName, bidderRequest.BidderName) { + // 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 continue } - // fpd - applyFPD(auctionReq.FirstPartyData, bidderRequest) - - // privacy scrubbing - if err := rs.applyPrivacy(&bidderRequest, auctionReq, auctionPermissions, ccpaEnforcer, lmt, coppa); err != nil { - errs = append(errs, err) - continue - } + var auctionPermissions gdpr.AuctionPermissions + var gdprErr error - // 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 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 + } } - allowedBidderRequests = append(allowedBidderRequests, bidderRequest) - } + ipConf := privacy.IPConf{IPV6: auctionReq.Account.Privacy.IPv6Config, IPV4: auctionReq.Account.Privacy.IPv4Config} - 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 - } - - // gdpr - if !auctionPermissions.AllowBidRequest { - rs.me.RecordAdapterGDPRRequestBlocked(coreBidder) - return true - } - - 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), - } + // FPD should be applied before policies, otherwise it overrides policies and activities restricted data + applyFPD(auctionReq.FirstPartyData, bidderRequest) - 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 + reqWrapper := &openrtb_ext.RequestWrapper{ + BidRequest: ortb.CloneBidRequestPartial(bidderRequest.BidRequest), } - if ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String()) { - privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", false) + passIDActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitUserFPD, scopedName, privacy.NewRequestFromBidRequest(*req)) + buyerUIDSet := reqWrapper.User != nil && reqWrapper.User.BuyerUID != "" + buyerUIDRemoved := false + if !passIDActivityAllowed { + privacy.ScrubUserFPD(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) } - } - if buyerUIDSet && buyerUIDRemoved { - rs.me.RecordAdapterBuyerUIDScrubbed(bidderRequest.BidderCoreName) - } - passGeoActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitPreciseGeo, scope, privacy.NewRequestFromBidRequest(*reqWrapper)) - if !passGeoActivityAllowed { - privacy.ScrubGeoAndDeviceIP(reqWrapper, ipConf) - } else { - if !auctionPermissions.PassGeo { + 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) + + if lmt || coppa { + privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", coppa) } - } - if lmt || coppa { - privacy.ScrubDeviceIDsIPsUserDemoExt(reqWrapper, ipConf, "eids", coppa) - } + passTIDAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitTIDs, scopedName, privacy.NewRequestFromBidRequest(*req)) + if !passTIDAllowed { + privacy.ScrubTID(reqWrapper) + } - passTIDAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitTIDs, scope, privacy.NewRequestFromBidRequest(*reqWrapper)) - if !passTIDAllowed { - privacy.ScrubTID(reqWrapper) - } + err := reqWrapper.RebuildRequest() + if err != nil { + errs = append(errs, err) + } + bidderRequest.BidRequest = reqWrapper.BidRequest - if err := reqWrapper.RebuildRequest(); err != nil { - return err + allowedBidderRequests = append(allowedBidderRequests, bidderRequest) + + // 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) + } } - bidderRequest.BidRequest = reqWrapper.BidRequest - return nil + return } func shouldSetLegacyPrivacy(bidderInfo config.BidderInfos, bidder string) bool { @@ -462,6 +456,7 @@ 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 } diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 5d2a83d5b49..69cfdf70abf 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -46,7 +46,7 @@ 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 { +func (p *permissionsMock) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (gdpr.AuctionPermissions, error) { permissions := gdpr.AuctionPermissions{ PassGeo: p.passGeo, PassID: p.passID, @@ -54,7 +54,7 @@ func (p *permissionsMock) AuctionActivitiesAllowed(ctx context.Context, bidderCo if p.allowAllBidders { permissions.AllowBidRequest = true - return permissions + return permissions, p.activitiesError } for _, allowedBidder := range p.allowedBidders { @@ -63,7 +63,7 @@ func (p *permissionsMock) AuctionActivitiesAllowed(ctx context.Context, bidderCo } } - return permissions + return permissions, p.activitiesError } type fakePermissionsBuilder struct { diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go index 4f7df2f3ab7..1b4f6cb4680 100644 --- a/gdpr/gdpr.go +++ b/gdpr/gdpr.go @@ -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 no permissions are granted. - AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) AuctionPermissions + // 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) } type PermissionsBuilder func(TCF2ConfigReader, RequestInfo) Permissions diff --git a/gdpr/impl.go b/gdpr/impl.go index 27c10cf8fd5..614c06d9a6a 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -56,36 +56,33 @@ 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) AuctionPermissions { +func (p *permissionsImpl) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions AuctionPermissions, err error) { if _, ok := p.nonStandardPublishers[p.publisherID]; ok { - return AllowAll + return AllowAll, nil } - if p.gdprSignal != SignalYes { - return AllowAll + return AllowAll, nil } - if p.consent == "" { - return p.defaultPermissions() + return p.defaultPermissions(), nil } - pc, err := parseConsent(p.consent) if err != nil { - return p.defaultPermissions() + return p.defaultPermissions(), err } - vendorID, _ := p.resolveVendorID(bidderCoreName, bidder) vendor, err := p.getVendor(ctx, vendorID, *pc) if err != nil { - return p.defaultPermissions() + return p.defaultPermissions(), err } - 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), - } + + 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 } // defaultPermissions returns a permissions object that denies passing user IDs while @@ -225,6 +222,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) AuctionPermissions { - return AllowAll +func (a AlwaysAllow) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions AuctionPermissions, err error) { + return AllowAll, nil } diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index 2a5826ad4cb..64fa4434d4d 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -335,8 +335,9 @@ func TestAllowActivities(t *testing.T) { perms.gdprSignal = tt.gdpr perms.publisherID = tt.publisherID - permissions := perms.AuctionActivitiesAllowed(context.Background(), tt.bidderCoreName, tt.bidderName) + permissions, err := perms.AuctionActivitiesAllowed(context.Background(), tt.bidderCoreName, tt.bidderName) + assert.Nil(t, err, tt.description) assert.Equal(t, tt.passID, permissions.PassID, tt.description) } } @@ -436,7 +437,8 @@ func TestAllowActivitiesBidderWithoutGVLID(t *testing.T) { purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig), } - permissions := perms.AuctionActivitiesAllowed(context.Background(), bidderWithoutGVLID, bidderWithoutGVLID) + permissions, err := perms.AuctionActivitiesAllowed(context.Background(), bidderWithoutGVLID, bidderWithoutGVLID) + assert.NoError(t, err) assert.Equal(t, tt.allowBidRequest, permissions.AllowBidRequest) assert.Equal(t, tt.passID, permissions.PassID) }) @@ -656,7 +658,8 @@ func TestAllowActivitiesGeoAndID(t *testing.T) { perms.consent = td.consent perms.purposeEnforcerBuilder = NewPurposeEnforcerBuilder(&tcf2AggConfig) - permissions := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) + permissions, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) + assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description) assert.EqualValuesf(t, td.allowBidRequest, permissions.AllowBidRequest, "AllowBid failure on %s", td.description) assert.EqualValuesf(t, td.passGeo, permissions.PassGeo, "PassGeo failure on %s", td.description) assert.EqualValuesf(t, td.passID, permissions.PassID, "PassID failure on %s", td.description) @@ -692,7 +695,8 @@ func TestAllowActivitiesWhitelist(t *testing.T) { } // Assert that an item that otherwise would not be allowed PI access, gets approved because it is found in the GDPR.NonStandardPublishers array - permissions := perms.AuctionActivitiesAllowed(context.Background(), openrtb_ext.BidderAppnexus, openrtb_ext.BidderAppnexus) + permissions, err := perms.AuctionActivitiesAllowed(context.Background(), openrtb_ext.BidderAppnexus, openrtb_ext.BidderAppnexus) + assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed") assert.EqualValuesf(t, true, permissions.PassGeo, "PassGeo failure") assert.EqualValuesf(t, true, permissions.PassID, "PassID failure") } @@ -763,7 +767,8 @@ func TestAllowActivitiesPubRestrict(t *testing.T) { perms.aliasGVLIDs = td.aliasGVLIDs perms.consent = td.consent - permissions := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) + permissions, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) + assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description) assert.EqualValuesf(t, td.passGeo, permissions.PassGeo, "PassGeo failure on %s", td.description) assert.EqualValuesf(t, td.passID, permissions.PassID, "PassID failure on %s", td.description) } @@ -1096,7 +1101,8 @@ func TestAllowActivitiesBidRequests(t *testing.T) { perms.cfg = &tcf2AggConfig perms.purposeEnforcerBuilder = NewPurposeEnforcerBuilder(&tcf2AggConfig) - permissions := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) + permissions, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) + assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description) assert.EqualValuesf(t, td.allowBidRequest, permissions.AllowBidRequest, "AllowBid failure on %s", td.description) assert.EqualValuesf(t, td.passGeo, permissions.PassGeo, "PassGeo failure on %s", td.description) assert.EqualValuesf(t, td.passID, permissions.PassID, "PassID failure on %s", td.description) @@ -1190,7 +1196,8 @@ func TestAllowActivitiesVendorException(t *testing.T) { perms.cfg = &tcf2AggConfig perms.purposeEnforcerBuilder = NewPurposeEnforcerBuilder(&tcf2AggConfig) - permissions := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) + permissions, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder) + assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description) assert.EqualValuesf(t, td.allowBidRequest, permissions.AllowBidRequest, "AllowBid failure on %s", td.description) assert.EqualValuesf(t, td.passGeo, permissions.PassGeo, "PassGeo failure on %s", td.description) assert.EqualValuesf(t, td.passID, permissions.PassID, "PassID failure on %s", td.description)