github.com/ewagmig/fabric@v2.1.1+incompatible/discovery/authcache.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  	"encoding/hex"
    12  	"sync"
    13  
    14  	"github.com/hyperledger/fabric/common/util"
    15  	"github.com/hyperledger/fabric/protoutil"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  const (
    20  	defaultMaxCacheSize   = 1000
    21  	defaultRetentionRatio = 0.75
    22  )
    23  
    24  var (
    25  	// asBytes is the function that is used to marshal protoutil.SignedData to bytes
    26  	asBytes = asn1.Marshal
    27  )
    28  
    29  type acSupport interface {
    30  	// Eligible returns whether the given peer is eligible for receiving
    31  	// service from the discovery service for a given channel
    32  	EligibleForService(channel string, data protoutil.SignedData) error
    33  
    34  	// ConfigSequence returns the configuration sequence of the given channel
    35  	ConfigSequence(channel string) uint64
    36  }
    37  
    38  type authCacheConfig struct {
    39  	enabled bool
    40  	// maxCacheSize is the maximum size of the cache, after which
    41  	// a purge takes place
    42  	maxCacheSize int
    43  	// purgeRetentionRatio is the % of entries that remain in the cache
    44  	// after the cache is purged due to overpopulation
    45  	purgeRetentionRatio float64
    46  }
    47  
    48  // authCache defines an interface that authenticates a request in a channel context,
    49  // and does memoization of invocations
    50  type authCache struct {
    51  	credentialCache map[string]*accessCache
    52  	acSupport
    53  	sync.RWMutex
    54  	conf authCacheConfig
    55  }
    56  
    57  func newAuthCache(s acSupport, conf authCacheConfig) *authCache {
    58  	return &authCache{
    59  		acSupport:       s,
    60  		credentialCache: make(map[string]*accessCache),
    61  		conf:            conf,
    62  	}
    63  }
    64  
    65  // Eligible returns whether the given peer is eligible for receiving
    66  // service from the discovery service for a given channel
    67  func (ac *authCache) EligibleForService(channel string, data protoutil.SignedData) error {
    68  	if !ac.conf.enabled {
    69  		return ac.acSupport.EligibleForService(channel, data)
    70  	}
    71  	// Check whether we already have a cache for this channel
    72  	ac.RLock()
    73  	cache := ac.credentialCache[channel]
    74  	ac.RUnlock()
    75  	if cache == nil {
    76  		// Cache for given channel wasn't found, so create a new one
    77  		ac.Lock()
    78  		cache = ac.newAccessCache(channel)
    79  		// And store the cache instance.
    80  		ac.credentialCache[channel] = cache
    81  		ac.Unlock()
    82  	}
    83  	return cache.EligibleForService(data)
    84  }
    85  
    86  type accessCache struct {
    87  	sync.RWMutex
    88  	channel      string
    89  	ac           *authCache
    90  	lastSequence uint64
    91  	entries      map[string]error
    92  }
    93  
    94  func (ac *authCache) newAccessCache(channel string) *accessCache {
    95  	return &accessCache{
    96  		channel: channel,
    97  		ac:      ac,
    98  		entries: make(map[string]error),
    99  	}
   100  }
   101  
   102  func (cache *accessCache) EligibleForService(data protoutil.SignedData) error {
   103  	key, err := signedDataToKey(data)
   104  	if err != nil {
   105  		logger.Warningf("Failed computing key of signed data: +%v", err)
   106  		return errors.Wrap(err, "failed computing key of signed data")
   107  	}
   108  	currSeq := cache.ac.acSupport.ConfigSequence(cache.channel)
   109  	if cache.isValid(currSeq) {
   110  		foundInCache, isEligibleErr := cache.lookup(key)
   111  		if foundInCache {
   112  			return isEligibleErr
   113  		}
   114  	} else {
   115  		cache.configChange(currSeq)
   116  	}
   117  
   118  	// Make sure the cache doesn't overpopulate.
   119  	// It might happen that it overgrows the maximum size due to concurrent
   120  	// goroutines waiting on the lock above, but that's acceptable.
   121  	cache.purgeEntriesIfNeeded()
   122  
   123  	// Compute the eligibility of the client for the service
   124  	err = cache.ac.acSupport.EligibleForService(cache.channel, data)
   125  	cache.Lock()
   126  	defer cache.Unlock()
   127  	// Check if the sequence hasn't changed since last time
   128  	if currSeq != cache.ac.acSupport.ConfigSequence(cache.channel) {
   129  		// The sequence at which we computed the eligibility might have changed,
   130  		// so we can't put it into the cache because a more fresh computation result
   131  		// might already be present in the cache by now, and we don't want to override it
   132  		// with a stale computation result, so just return the result.
   133  		return err
   134  	}
   135  	// Else, the eligibility of the client has been computed under the latest sequence,
   136  	// so store the computation result in the cache
   137  	cache.entries[key] = err
   138  	return err
   139  }
   140  
   141  func (cache *accessCache) isPurgeNeeded() bool {
   142  	cache.RLock()
   143  	defer cache.RUnlock()
   144  	return len(cache.entries)+1 > cache.ac.conf.maxCacheSize
   145  }
   146  
   147  func (cache *accessCache) purgeEntriesIfNeeded() {
   148  	if !cache.isPurgeNeeded() {
   149  		return
   150  	}
   151  
   152  	cache.Lock()
   153  	defer cache.Unlock()
   154  
   155  	maxCacheSize := cache.ac.conf.maxCacheSize
   156  	purgeRatio := cache.ac.conf.purgeRetentionRatio
   157  	entries2evict := maxCacheSize - int(purgeRatio*float64(maxCacheSize))
   158  
   159  	for key := range cache.entries {
   160  		if entries2evict == 0 {
   161  			return
   162  		}
   163  		entries2evict--
   164  		delete(cache.entries, key)
   165  	}
   166  }
   167  
   168  func (cache *accessCache) isValid(currSeq uint64) bool {
   169  	cache.RLock()
   170  	defer cache.RUnlock()
   171  	return currSeq == cache.lastSequence
   172  }
   173  
   174  func (cache *accessCache) configChange(currSeq uint64) {
   175  	cache.Lock()
   176  	defer cache.Unlock()
   177  	cache.lastSequence = currSeq
   178  	// Invalidate entries
   179  	cache.entries = make(map[string]error)
   180  }
   181  
   182  func (cache *accessCache) lookup(key string) (cacheHit bool, lookupResult error) {
   183  	cache.RLock()
   184  	defer cache.RUnlock()
   185  
   186  	lookupResult, cacheHit = cache.entries[key]
   187  	return
   188  }
   189  
   190  func signedDataToKey(data protoutil.SignedData) (string, error) {
   191  	b, err := asBytes(data)
   192  	if err != nil {
   193  		return "", errors.Wrap(err, "failed marshaling signed data")
   194  	}
   195  	return hex.EncodeToString(util.ComputeSHA256(b)), nil
   196  }