github.com/yimialmonte/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 }