github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/api/controller/crossmodelrelations/macarooncache.go (about) 1 // Copyright 2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package crossmodelrelations 5 6 import ( 7 "runtime" 8 "sync" 9 "time" 10 11 "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 12 "github.com/juju/clock" 13 "gopkg.in/macaroon.v2" 14 15 coremacaroon "github.com/juju/juju/core/macaroon" 16 ) 17 18 // MacaroonCache contains macaroons which are removed at a specified interval. 19 type MacaroonCache struct { 20 // We use a separate struct for the actual cache and a wrapper to export to the 21 // user so that the expiry worker which references the internal cache doesn't 22 // prevent the exported cache from being garbage collected. 23 *cacheInternal 24 } 25 26 // NewMacaroonCache returns a cache containing macaroons which are removed 27 // after the macaroons' expiry time. 28 func NewMacaroonCache(clock clock.Clock) *MacaroonCache { 29 c := &cacheInternal{clock: clock, macaroons: make(map[string]*macaroonEntry)} 30 cache := &MacaroonCache{c} 31 // The interval to run the expiry worker is somewhat arbitrary. 32 // Expired macaroons will be re-issued as needed; we just want to ensure 33 // that those which fall out of use are eventually cleaned up. 34 c.runExpiryWorker(10 * time.Minute) 35 runtime.SetFinalizer(cache, stopMacaroonCacheExpiryWorker) 36 return cache 37 } 38 39 type expiryWorker struct { 40 clock clock.Clock 41 interval time.Duration 42 stop chan bool 43 } 44 45 func (w *expiryWorker) loop(c *cacheInternal) { 46 for { 47 select { 48 case <-w.clock.After(w.interval): 49 c.deleteExpired() 50 case <-w.stop: 51 return 52 } 53 } 54 } 55 56 type macaroonEntry struct { 57 ms macaroon.Slice 58 expiryTime *time.Time 59 } 60 61 func (i *macaroonEntry) expired(clock clock.Clock) bool { 62 return i.expiryTime != nil && i.expiryTime.Before(clock.Now()) 63 } 64 65 type cacheInternal struct { 66 sync.Mutex 67 clock clock.Clock 68 69 macaroons map[string]*macaroonEntry 70 expiryWorker *expiryWorker 71 } 72 73 // Upsert inserts or updates a macaroon slice in the cache. 74 func (c *cacheInternal) Upsert(token string, ms macaroon.Slice) { 75 c.Lock() 76 defer c.Unlock() 77 78 var et *time.Time 79 if expiryTime, ok := checkers.MacaroonsExpiryTime(coremacaroon.MacaroonNamespace, ms); ok { 80 et = &expiryTime 81 } 82 c.macaroons[token] = &macaroonEntry{ 83 ms: ms, 84 expiryTime: et, 85 } 86 } 87 88 // Get returns a macaroon slice from the cache, and a bool indicating 89 // if the slice for the key was found. 90 func (c *cacheInternal) Get(k string) (macaroon.Slice, bool) { 91 c.Lock() 92 defer c.Unlock() 93 94 entry, found := c.macaroons[k] 95 if !found { 96 return nil, false 97 } 98 if entry.expired(c.clock) { 99 delete(c.macaroons, k) 100 return nil, false 101 } 102 return entry.ms, true 103 } 104 105 func (c *cacheInternal) deleteExpired() { 106 c.Lock() 107 defer c.Unlock() 108 109 for k, v := range c.macaroons { 110 if v.expired(c.clock) { 111 delete(c.macaroons, k) 112 } 113 } 114 } 115 116 func stopMacaroonCacheExpiryWorker(mc *MacaroonCache) { 117 mc.expiryWorker.stop <- true 118 } 119 120 func (c *cacheInternal) runExpiryWorker(interval time.Duration) { 121 w := &expiryWorker{ 122 interval: interval, 123 clock: c.clock, 124 stop: make(chan bool), 125 } 126 c.expiryWorker = w 127 go w.loop(c) 128 }