github.com/defanghe/fabric@v2.1.1+incompatible/discovery/authcache_test.go (about)

     1  /*
     2  Copyright IBM Corp. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package discovery
     8  
     9  import (
    10  	"encoding/asn1"
    11  	"errors"
    12  	"sync"
    13  	"testing"
    14  
    15  	"github.com/hyperledger/fabric/protoutil"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/mock"
    18  )
    19  
    20  func TestSignedDataToKey(t *testing.T) {
    21  	key1, err1 := signedDataToKey(protoutil.SignedData{
    22  		Data:      []byte{1, 2, 3, 4},
    23  		Identity:  []byte{5, 6, 7},
    24  		Signature: []byte{8, 9},
    25  	})
    26  	key2, err2 := signedDataToKey(protoutil.SignedData{
    27  		Data:      []byte{1, 2, 3},
    28  		Identity:  []byte{4, 5, 6},
    29  		Signature: []byte{7, 8, 9},
    30  	})
    31  	assert.NoError(t, err1)
    32  	assert.NoError(t, err2)
    33  	assert.NotEqual(t, key1, key2)
    34  }
    35  
    36  type mockAcSupport struct {
    37  	mock.Mock
    38  }
    39  
    40  func (as *mockAcSupport) EligibleForService(channel string, data protoutil.SignedData) error {
    41  	return as.Called(channel, data).Error(0)
    42  }
    43  
    44  func (as *mockAcSupport) ConfigSequence(channel string) uint64 {
    45  	return as.Called(channel).Get(0).(uint64)
    46  }
    47  
    48  func TestCacheDisabled(t *testing.T) {
    49  	sd := protoutil.SignedData{
    50  		Data:      []byte{1, 2, 3},
    51  		Identity:  []byte("authorizedIdentity"),
    52  		Signature: []byte{1, 2, 3},
    53  	}
    54  
    55  	as := &mockAcSupport{}
    56  	as.On("ConfigSequence", "foo").Return(uint64(0))
    57  	as.On("EligibleForService", "foo", sd).Return(nil)
    58  	cache := newAuthCache(as, authCacheConfig{maxCacheSize: 100, purgeRetentionRatio: 0.5})
    59  
    60  	// Call the cache twice with the same argument and ensure the call isn't cached
    61  	cache.EligibleForService("foo", sd)
    62  	cache.EligibleForService("foo", sd)
    63  	as.AssertNumberOfCalls(t, "EligibleForService", 2)
    64  }
    65  
    66  func TestCacheUsage(t *testing.T) {
    67  	as := &mockAcSupport{}
    68  	as.On("ConfigSequence", "foo").Return(uint64(0))
    69  	as.On("ConfigSequence", "bar").Return(uint64(0))
    70  	cache := newAuthCache(as, defaultConfig())
    71  
    72  	sd1 := protoutil.SignedData{
    73  		Data:      []byte{1, 2, 3},
    74  		Identity:  []byte("authorizedIdentity"),
    75  		Signature: []byte{1, 2, 3},
    76  	}
    77  
    78  	sd2 := protoutil.SignedData{
    79  		Data:      []byte{1, 2, 3},
    80  		Identity:  []byte("authorizedIdentity"),
    81  		Signature: []byte{1, 2, 3},
    82  	}
    83  
    84  	sd3 := protoutil.SignedData{
    85  		Data:      []byte{1, 2, 3, 3},
    86  		Identity:  []byte("unAuthorizedIdentity"),
    87  		Signature: []byte{1, 2, 3},
    88  	}
    89  
    90  	testCases := []struct {
    91  		channel     string
    92  		expectedErr error
    93  		sd          protoutil.SignedData
    94  	}{
    95  		{
    96  			sd:      sd1,
    97  			channel: "foo",
    98  		},
    99  		{
   100  			sd:      sd2,
   101  			channel: "bar",
   102  		},
   103  		{
   104  			channel:     "bar",
   105  			sd:          sd3,
   106  			expectedErr: errors.New("user revoked"),
   107  		},
   108  	}
   109  
   110  	for _, tst := range testCases {
   111  		// Scenario I: Invocation is not cached
   112  		invoked := false
   113  		as.On("EligibleForService", tst.channel, tst.sd).Return(tst.expectedErr).Once().Run(func(_ mock.Arguments) {
   114  			invoked = true
   115  		})
   116  		t.Run("Not cached test", func(t *testing.T) {
   117  			err := cache.EligibleForService(tst.channel, tst.sd)
   118  			if tst.expectedErr == nil {
   119  				assert.NoError(t, err)
   120  			} else {
   121  				assert.Equal(t, tst.expectedErr.Error(), err.Error())
   122  			}
   123  			assert.True(t, invoked)
   124  			// Reset invoked to false for next test
   125  			invoked = false
   126  		})
   127  
   128  		// Scenario II: Invocation is cached.
   129  		// We don't define the mock invocation because it should be the same as last time.
   130  		// If the cache isn't used, the test would fail because the mock wasn't defined
   131  		t.Run("Cached test", func(t *testing.T) {
   132  			err := cache.EligibleForService(tst.channel, tst.sd)
   133  			if tst.expectedErr == nil {
   134  				assert.NoError(t, err)
   135  			} else {
   136  				assert.Equal(t, tst.expectedErr.Error(), err.Error())
   137  			}
   138  			assert.False(t, invoked)
   139  		})
   140  	}
   141  }
   142  
   143  func TestCacheMarshalFailure(t *testing.T) {
   144  	as := &mockAcSupport{}
   145  	cache := newAuthCache(as, defaultConfig())
   146  	asBytes = func(_ interface{}) ([]byte, error) {
   147  		return nil, errors.New("failed marshaling ASN1")
   148  	}
   149  	defer func() {
   150  		asBytes = asn1.Marshal
   151  	}()
   152  	err := cache.EligibleForService("mychannel", protoutil.SignedData{})
   153  	assert.Contains(t, err.Error(), "failed marshaling ASN1")
   154  }
   155  
   156  func TestCacheConfigChange(t *testing.T) {
   157  	as := &mockAcSupport{}
   158  	sd := protoutil.SignedData{
   159  		Data:      []byte{1, 2, 3},
   160  		Identity:  []byte("identity"),
   161  		Signature: []byte{1, 2, 3},
   162  	}
   163  
   164  	cache := newAuthCache(as, defaultConfig())
   165  
   166  	// Scenario I: At first, the identity is authorized
   167  	as.On("EligibleForService", "mychannel", sd).Return(nil).Once()
   168  	as.On("ConfigSequence", "mychannel").Return(uint64(0)).Times(2)
   169  	err := cache.EligibleForService("mychannel", sd)
   170  	assert.NoError(t, err)
   171  
   172  	// Scenario II: The identity is still authorized, and config hasn't changed yet.
   173  	// Result should be cached
   174  	as.On("ConfigSequence", "mychannel").Return(uint64(0)).Once()
   175  	err = cache.EligibleForService("mychannel", sd)
   176  	assert.NoError(t, err)
   177  
   178  	// Scenario III: A config change occurred, cache should be disregarded
   179  	as.On("ConfigSequence", "mychannel").Return(uint64(1)).Times(2)
   180  	as.On("EligibleForService", "mychannel", sd).Return(errors.New("unauthorized")).Once()
   181  	err = cache.EligibleForService("mychannel", sd)
   182  	assert.Contains(t, err.Error(), "unauthorized")
   183  }
   184  
   185  func TestCachePurgeCache(t *testing.T) {
   186  	as := &mockAcSupport{}
   187  	cache := newAuthCache(as, authCacheConfig{maxCacheSize: 4, purgeRetentionRatio: 0.75, enabled: true})
   188  	as.On("ConfigSequence", "mychannel").Return(uint64(0))
   189  
   190  	// Warm up the cache - attempt to place 4 identities to fill it up
   191  	for _, id := range []string{"identity1", "identity2", "identity3", "identity4"} {
   192  		sd := protoutil.SignedData{
   193  			Data:      []byte{1, 2, 3},
   194  			Identity:  []byte(id),
   195  			Signature: []byte{1, 2, 3},
   196  		}
   197  		// At first, all identities are eligible of the service
   198  		as.On("EligibleForService", "mychannel", sd).Return(nil).Once()
   199  		err := cache.EligibleForService("mychannel", sd)
   200  		assert.NoError(t, err)
   201  	}
   202  
   203  	// Now, ensure that at least 1 of the identities was evicted from the cache, but not all
   204  	var evicted int
   205  	for _, id := range []string{"identity5", "identity1", "identity2"} {
   206  		sd := protoutil.SignedData{
   207  			Data:      []byte{1, 2, 3},
   208  			Identity:  []byte(id),
   209  			Signature: []byte{1, 2, 3},
   210  		}
   211  		as.On("EligibleForService", "mychannel", sd).Return(errors.New("unauthorized")).Once()
   212  		err := cache.EligibleForService("mychannel", sd)
   213  		if err != nil {
   214  			evicted++
   215  		}
   216  	}
   217  	assert.True(t, evicted > 0 && evicted < 4, "evicted: %d, but expected between 1 and 3 evictions", evicted)
   218  }
   219  
   220  func TestCacheConcurrentConfigUpdate(t *testing.T) {
   221  	// Scenario: 2 requests for the same identity are made concurrently.
   222  	// Both are not cached, and thus their computation results might both enter the cache.
   223  	// The first request enters when the config sequence is 0, and a config update
   224  	// that revokes the identity takes place in the same time the access control check of the first request is evaluated.
   225  	// The first request's computation is stalled because of scheduling, and completes after the second,
   226  	// which happens after the config update takes place.
   227  	// The second request's computation result should not be overridden by the computation result
   228  	// of the first request although the first request's computation completes after the second request.
   229  
   230  	as := &mockAcSupport{}
   231  	sd := protoutil.SignedData{
   232  		Data:      []byte{1, 2, 3},
   233  		Identity:  []byte{1, 2, 3},
   234  		Signature: []byte{1, 2, 3},
   235  	}
   236  	var firstRequestInvoked sync.WaitGroup
   237  	firstRequestInvoked.Add(1)
   238  	var firstRequestFinished sync.WaitGroup
   239  	firstRequestFinished.Add(1)
   240  	var secondRequestFinished sync.WaitGroup
   241  	secondRequestFinished.Add(1)
   242  	cache := newAuthCache(as, defaultConfig())
   243  	// At first, the identity is eligible.
   244  	as.On("EligibleForService", "mychannel", mock.Anything).Return(nil).Once().Run(func(_ mock.Arguments) {
   245  		firstRequestInvoked.Done()
   246  		secondRequestFinished.Wait()
   247  	})
   248  	// But after the config change, it is not
   249  	as.On("EligibleForService", "mychannel", mock.Anything).Return(errors.New("unauthorized")).Once()
   250  	// The config sequence the first request sees
   251  	as.On("ConfigSequence", "mychannel").Return(uint64(0)).Once()
   252  	as.On("ConfigSequence", "mychannel").Return(uint64(1)).Once()
   253  	// The config sequence the second request sees
   254  	as.On("ConfigSequence", "mychannel").Return(uint64(1)).Times(2)
   255  
   256  	// First request returns OK
   257  	go func() {
   258  		defer firstRequestFinished.Done()
   259  		firstResult := cache.EligibleForService("mychannel", sd)
   260  		assert.NoError(t, firstResult)
   261  	}()
   262  	firstRequestInvoked.Wait()
   263  	// Second request returns that the identity isn't authorized
   264  	secondResult := cache.EligibleForService("mychannel", sd)
   265  	// Mark second request as finished to signal first request to finish its computation
   266  	secondRequestFinished.Done()
   267  	// Wait for first request to return
   268  	firstRequestFinished.Wait()
   269  	assert.Contains(t, secondResult.Error(), "unauthorized")
   270  
   271  	// Now make another request and ensure that the second request's result (an-authorized) was cached,
   272  	// even though it finished before the first request.
   273  	as.On("ConfigSequence", "mychannel").Return(uint64(1)).Once()
   274  	cachedResult := cache.EligibleForService("mychannel", sd)
   275  	assert.Contains(t, cachedResult.Error(), "unauthorized")
   276  }
   277  
   278  func defaultConfig() authCacheConfig {
   279  	return authCacheConfig{maxCacheSize: defaultMaxCacheSize, purgeRetentionRatio: defaultRetentionRatio, enabled: true}
   280  }