go_study/fabric-main/discovery/authcache_test.go

281 lines
9.0 KiB
Go

/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package discovery
import (
"encoding/asn1"
"errors"
"sync"
"testing"
"github.com/hyperledger/fabric/protoutil"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestSignedDataToKey(t *testing.T) {
key1, err1 := signedDataToKey(protoutil.SignedData{
Data: []byte{1, 2, 3, 4},
Identity: []byte{5, 6, 7},
Signature: []byte{8, 9},
})
key2, err2 := signedDataToKey(protoutil.SignedData{
Data: []byte{1, 2, 3},
Identity: []byte{4, 5, 6},
Signature: []byte{7, 8, 9},
})
require.NoError(t, err1)
require.NoError(t, err2)
require.NotEqual(t, key1, key2)
}
type mockAcSupport struct {
mock.Mock
}
func (as *mockAcSupport) EligibleForService(channel string, data protoutil.SignedData) error {
return as.Called(channel, data).Error(0)
}
func (as *mockAcSupport) ConfigSequence(channel string) uint64 {
return as.Called(channel).Get(0).(uint64)
}
func TestCacheDisabled(t *testing.T) {
sd := protoutil.SignedData{
Data: []byte{1, 2, 3},
Identity: []byte("authorizedIdentity"),
Signature: []byte{1, 2, 3},
}
as := &mockAcSupport{}
as.On("ConfigSequence", "foo").Return(uint64(0))
as.On("EligibleForService", "foo", sd).Return(nil)
cache := newAuthCache(as, authCacheConfig{maxCacheSize: 100, purgeRetentionRatio: 0.5})
// Call the cache twice with the same argument and ensure the call isn't cached
cache.EligibleForService("foo", sd)
cache.EligibleForService("foo", sd)
as.AssertNumberOfCalls(t, "EligibleForService", 2)
}
func TestCacheUsage(t *testing.T) {
as := &mockAcSupport{}
as.On("ConfigSequence", "foo").Return(uint64(0))
as.On("ConfigSequence", "bar").Return(uint64(0))
cache := newAuthCache(as, defaultConfig())
sd1 := protoutil.SignedData{
Data: []byte{1, 2, 3},
Identity: []byte("authorizedIdentity"),
Signature: []byte{1, 2, 3},
}
sd2 := protoutil.SignedData{
Data: []byte{1, 2, 3},
Identity: []byte("authorizedIdentity"),
Signature: []byte{1, 2, 3},
}
sd3 := protoutil.SignedData{
Data: []byte{1, 2, 3, 3},
Identity: []byte("unAuthorizedIdentity"),
Signature: []byte{1, 2, 3},
}
testCases := []struct {
channel string
expectedErr error
sd protoutil.SignedData
}{
{
sd: sd1,
channel: "foo",
},
{
sd: sd2,
channel: "bar",
},
{
channel: "bar",
sd: sd3,
expectedErr: errors.New("user revoked"),
},
}
for _, tst := range testCases {
// Scenario I: Invocation is not cached
invoked := false
as.On("EligibleForService", tst.channel, tst.sd).Return(tst.expectedErr).Once().Run(func(_ mock.Arguments) {
invoked = true
})
t.Run("Not cached test", func(t *testing.T) {
err := cache.EligibleForService(tst.channel, tst.sd)
if tst.expectedErr == nil {
require.NoError(t, err)
} else {
require.Equal(t, tst.expectedErr.Error(), err.Error())
}
require.True(t, invoked)
// Reset invoked to false for next test
invoked = false
})
// Scenario II: Invocation is cached.
// We don't define the mock invocation because it should be the same as last time.
// If the cache isn't used, the test would fail because the mock wasn't defined
t.Run("Cached test", func(t *testing.T) {
err := cache.EligibleForService(tst.channel, tst.sd)
if tst.expectedErr == nil {
require.NoError(t, err)
} else {
require.Equal(t, tst.expectedErr.Error(), err.Error())
}
require.False(t, invoked)
})
}
}
func TestCacheMarshalFailure(t *testing.T) {
as := &mockAcSupport{}
cache := newAuthCache(as, defaultConfig())
asBytes = func(_ interface{}) ([]byte, error) {
return nil, errors.New("failed marshaling ASN1")
}
defer func() {
asBytes = asn1.Marshal
}()
err := cache.EligibleForService("mychannel", protoutil.SignedData{})
require.Contains(t, err.Error(), "failed marshaling ASN1")
}
func TestCacheConfigChange(t *testing.T) {
as := &mockAcSupport{}
sd := protoutil.SignedData{
Data: []byte{1, 2, 3},
Identity: []byte("identity"),
Signature: []byte{1, 2, 3},
}
cache := newAuthCache(as, defaultConfig())
// Scenario I: At first, the identity is authorized
as.On("EligibleForService", "mychannel", sd).Return(nil).Once()
as.On("ConfigSequence", "mychannel").Return(uint64(0)).Times(2)
err := cache.EligibleForService("mychannel", sd)
require.NoError(t, err)
// Scenario II: The identity is still authorized, and config hasn't changed yet.
// Result should be cached
as.On("ConfigSequence", "mychannel").Return(uint64(0)).Once()
err = cache.EligibleForService("mychannel", sd)
require.NoError(t, err)
// Scenario III: A config change occurred, cache should be disregarded
as.On("ConfigSequence", "mychannel").Return(uint64(1)).Times(2)
as.On("EligibleForService", "mychannel", sd).Return(errors.New("unauthorized")).Once()
err = cache.EligibleForService("mychannel", sd)
require.Contains(t, err.Error(), "unauthorized")
}
func TestCachePurgeCache(t *testing.T) {
as := &mockAcSupport{}
cache := newAuthCache(as, authCacheConfig{maxCacheSize: 4, purgeRetentionRatio: 0.75, enabled: true})
as.On("ConfigSequence", "mychannel").Return(uint64(0))
// Warm up the cache - attempt to place 4 identities to fill it up
for _, id := range []string{"identity1", "identity2", "identity3", "identity4"} {
sd := protoutil.SignedData{
Data: []byte{1, 2, 3},
Identity: []byte(id),
Signature: []byte{1, 2, 3},
}
// At first, all identities are eligible of the service
as.On("EligibleForService", "mychannel", sd).Return(nil).Once()
err := cache.EligibleForService("mychannel", sd)
require.NoError(t, err)
}
// Now, ensure that at least 1 of the identities was evicted from the cache, but not all
var evicted int
for _, id := range []string{"identity5", "identity1", "identity2"} {
sd := protoutil.SignedData{
Data: []byte{1, 2, 3},
Identity: []byte(id),
Signature: []byte{1, 2, 3},
}
as.On("EligibleForService", "mychannel", sd).Return(errors.New("unauthorized")).Once()
err := cache.EligibleForService("mychannel", sd)
if err != nil {
evicted++
}
}
require.True(t, evicted > 0 && evicted < 4, "evicted: %d, but expected between 1 and 3 evictions", evicted)
}
func TestCacheConcurrentConfigUpdate(t *testing.T) {
// Scenario: 2 requests for the same identity are made concurrently.
// Both are not cached, and thus their computation results might both enter the cache.
// The first request enters when the config sequence is 0, and a config update
// that revokes the identity takes place in the same time the access control check of the first request is evaluated.
// The first request's computation is stalled because of scheduling, and completes after the second,
// which happens after the config update takes place.
// The second request's computation result should not be overridden by the computation result
// of the first request although the first request's computation completes after the second request.
as := &mockAcSupport{}
sd := protoutil.SignedData{
Data: []byte{1, 2, 3},
Identity: []byte{1, 2, 3},
Signature: []byte{1, 2, 3},
}
var firstRequestInvoked sync.WaitGroup
firstRequestInvoked.Add(1)
var firstRequestFinished sync.WaitGroup
firstRequestFinished.Add(1)
var secondRequestFinished sync.WaitGroup
secondRequestFinished.Add(1)
cache := newAuthCache(as, defaultConfig())
// At first, the identity is eligible.
as.On("EligibleForService", "mychannel", mock.Anything).Return(nil).Once().Run(func(_ mock.Arguments) {
firstRequestInvoked.Done()
secondRequestFinished.Wait()
})
// But after the config change, it is not
as.On("EligibleForService", "mychannel", mock.Anything).Return(errors.New("unauthorized")).Once()
// The config sequence the first request sees
as.On("ConfigSequence", "mychannel").Return(uint64(0)).Once()
as.On("ConfigSequence", "mychannel").Return(uint64(1)).Once()
// The config sequence the second request sees
as.On("ConfigSequence", "mychannel").Return(uint64(1)).Times(2)
// First request returns OK
go func() {
defer firstRequestFinished.Done()
firstResult := cache.EligibleForService("mychannel", sd)
require.NoError(t, firstResult)
}()
firstRequestInvoked.Wait()
// Second request returns that the identity isn't authorized
secondResult := cache.EligibleForService("mychannel", sd)
// Mark second request as finished to signal first request to finish its computation
secondRequestFinished.Done()
// Wait for first request to return
firstRequestFinished.Wait()
require.Contains(t, secondResult.Error(), "unauthorized")
// Now make another request and ensure that the second request's result (an-authorized) was cached,
// even though it finished before the first request.
as.On("ConfigSequence", "mychannel").Return(uint64(1)).Once()
cachedResult := cache.EligibleForService("mychannel", sd)
require.Contains(t, cachedResult.Error(), "unauthorized")
}
func defaultConfig() authCacheConfig {
return authCacheConfig{maxCacheSize: defaultMaxCacheSize, purgeRetentionRatio: defaultRetentionRatio, enabled: true}
}