github.com/dtroyer-salad/og2/v2@v2.0.0-20240412154159-c47231610877/registry/remote/auth/cache.go (about) 1 /* 2 Copyright The ORAS Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package auth 17 18 import ( 19 "context" 20 "strings" 21 "sync" 22 23 "oras.land/oras-go/v2/errdef" 24 "oras.land/oras-go/v2/internal/syncutil" 25 ) 26 27 // DefaultCache is the sharable cache used by DefaultClient. 28 var DefaultCache Cache = NewCache() 29 30 // Cache caches the auth-scheme and auth-token for the "Authorization" header in 31 // accessing the remote registry. 32 // Precisely, the header is `Authorization: auth-scheme auth-token`. 33 // The `auth-token` is a generic term as `token68` in RFC 7235 section 2.1. 34 type Cache interface { 35 // GetScheme returns the auth-scheme part cached for the given registry. 36 // A single registry is assumed to have a consistent scheme. 37 // If a registry has different schemes per path, the auth client is still 38 // workable. However, the cache may not be effective as the cache cannot 39 // correctly guess the scheme. 40 GetScheme(ctx context.Context, registry string) (Scheme, error) 41 42 // GetToken returns the auth-token part cached for the given registry of a 43 // given scheme. 44 // The underlying implementation MAY cache the token for all schemes for the 45 // given registry. 46 GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) 47 48 // Set fetches the token using the given fetch function and caches the token 49 // for the given scheme with the given key for the given registry. 50 // The return values of the fetch function is returned by this function. 51 // The underlying implementation MAY combine the fetch operation if the Set 52 // function is invoked multiple times at the same time. 53 Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) 54 } 55 56 // cacheEntry is a cache entry for a single registry. 57 type cacheEntry struct { 58 scheme Scheme 59 tokens sync.Map // map[string]string 60 } 61 62 // concurrentCache is a cache suitable for concurrent invocation. 63 type concurrentCache struct { 64 status sync.Map // map[string]*syncutil.Once 65 cache sync.Map // map[string]*cacheEntry 66 } 67 68 // NewCache creates a new go-routine safe cache instance. 69 func NewCache() Cache { 70 return &concurrentCache{} 71 } 72 73 // GetScheme returns the auth-scheme part cached for the given registry. 74 func (cc *concurrentCache) GetScheme(ctx context.Context, registry string) (Scheme, error) { 75 entry, ok := cc.cache.Load(registry) 76 if !ok { 77 return SchemeUnknown, errdef.ErrNotFound 78 } 79 return entry.(*cacheEntry).scheme, nil 80 } 81 82 // GetToken returns the auth-token part cached for the given registry of a given 83 // scheme. 84 func (cc *concurrentCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) { 85 entryValue, ok := cc.cache.Load(registry) 86 if !ok { 87 return "", errdef.ErrNotFound 88 } 89 entry := entryValue.(*cacheEntry) 90 if entry.scheme != scheme { 91 return "", errdef.ErrNotFound 92 } 93 if token, ok := entry.tokens.Load(key); ok { 94 return token.(string), nil 95 } 96 return "", errdef.ErrNotFound 97 } 98 99 // Set fetches the token using the given fetch function and caches the token 100 // for the given scheme with the given key for the given registry. 101 // Set combines the fetch operation if the Set is invoked multiple times at the 102 // same time. 103 func (cc *concurrentCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) { 104 // fetch token 105 statusKey := strings.Join([]string{ 106 registry, 107 scheme.String(), 108 key, 109 }, " ") 110 statusValue, _ := cc.status.LoadOrStore(statusKey, syncutil.NewOnce()) 111 fetchOnce := statusValue.(*syncutil.Once) 112 fetchedFirst, result, err := fetchOnce.Do(ctx, func() (interface{}, error) { 113 return fetch(ctx) 114 }) 115 if fetchedFirst { 116 cc.status.Delete(statusKey) 117 } 118 if err != nil { 119 return "", err 120 } 121 token := result.(string) 122 if !fetchedFirst { 123 return token, nil 124 } 125 126 // cache token 127 newEntry := &cacheEntry{ 128 scheme: scheme, 129 } 130 entryValue, exists := cc.cache.LoadOrStore(registry, newEntry) 131 entry := entryValue.(*cacheEntry) 132 if exists && entry.scheme != scheme { 133 // there is a scheme change, which is not expected in most scenarios. 134 // force invalidating all previous cache. 135 entry = newEntry 136 cc.cache.Store(registry, entry) 137 } 138 entry.tokens.Store(key, token) 139 140 return token, nil 141 } 142 143 // noCache is a cache implementation that does not do cache at all. 144 type noCache struct{} 145 146 // GetScheme always returns not found error as it has no cache. 147 func (noCache) GetScheme(ctx context.Context, registry string) (Scheme, error) { 148 return SchemeUnknown, errdef.ErrNotFound 149 } 150 151 // GetToken always returns not found error as it has no cache. 152 func (noCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) { 153 return "", errdef.ErrNotFound 154 } 155 156 // Set calls fetch directly without caching. 157 func (noCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) { 158 return fetch(ctx) 159 } 160 161 // hostCache is an auth cache that ignores scopes. Uses only the registry's hostname to find a token. 162 type hostCache struct { 163 Cache 164 } 165 166 // GetToken implements Cache. 167 func (c *hostCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) { 168 return c.Cache.GetToken(ctx, registry, scheme, "") 169 } 170 171 // Set implements Cache. 172 func (c *hostCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) { 173 return c.Cache.Set(ctx, registry, scheme, "", fetch) 174 } 175 176 // fallbackCache tries the primary cache then falls back to the secondary cache. 177 type fallbackCache struct { 178 primary Cache 179 secondary Cache 180 } 181 182 // GetScheme implements Cache. 183 func (fc *fallbackCache) GetScheme(ctx context.Context, registry string) (Scheme, error) { 184 scheme, err := fc.primary.GetScheme(ctx, registry) 185 if err == nil { 186 return scheme, nil 187 } 188 189 // fallback 190 return fc.secondary.GetScheme(ctx, registry) 191 } 192 193 // GetToken implements Cache. 194 func (fc *fallbackCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) { 195 token, err := fc.primary.GetToken(ctx, registry, scheme, key) 196 if err == nil { 197 return token, nil 198 } 199 200 // fallback 201 return fc.secondary.GetToken(ctx, registry, scheme, key) 202 } 203 204 // Set implements Cache. 205 func (fc *fallbackCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) { 206 token, err := fc.primary.Set(ctx, registry, scheme, key, fetch) 207 if err != nil { 208 return "", err 209 } 210 211 return fc.secondary.Set(ctx, registry, scheme, key, func(ctx context.Context) (string, error) { 212 return token, nil 213 }) 214 } 215 216 // NewSingleContextCache creates a host-based cache for optimizing the auth flow for non-compliant registries. 217 // It is intended to be used in a single context, such as pulling from a single repository. 218 // This cache should not be shared. 219 // 220 // Note: [NewCache] should be used for compliant registries as it can be shared 221 // across context and will generally make less re-authentication requests. 222 func NewSingleContextCache() Cache { 223 cache := NewCache() 224 return &fallbackCache{ 225 primary: cache, 226 // We can re-use the came concurrentCache here because the key space is different 227 // (keys are always empty for the hostCache) so there is no collision. 228 // Even if there is a collision it is not an issue. 229 // Re-using saves a little memory. 230 secondary: &hostCache{cache}, 231 } 232 }