This repository has been archived by the owner on Dec 5, 2022. It is now read-only.
forked from kubernetes-sigs/aws-iam-authenticator
/
filecache.go
314 lines (277 loc) · 10.4 KB
/
filecache.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
package token
import (
"context"
"errors"
"fmt"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/gofrs/flock"
"gopkg.in/yaml.v2"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"time"
)
// env variable name for custom credential cache file location
const cacheFileNameEnv = "AWS_IAM_AUTHENTICATOR_CACHE_FILE"
// A mockable filesystem interface
var f filesystem = osFS{}
type filesystem interface {
Stat(filename string) (os.FileInfo, error)
ReadFile(filename string) ([]byte, error)
WriteFile(filename string, data []byte, perm os.FileMode) error
MkdirAll(path string, perm os.FileMode) error
}
// default os based implementation
type osFS struct{}
func (osFS) Stat(filename string) (os.FileInfo, error) {
return os.Stat(filename)
}
func (osFS) ReadFile(filename string) ([]byte, error) {
return ioutil.ReadFile(filename)
}
func (osFS) WriteFile(filename string, data []byte, perm os.FileMode) error {
return ioutil.WriteFile(filename, data, perm)
}
func (osFS) MkdirAll(path string, perm os.FileMode) error {
return os.MkdirAll(path, perm)
}
// A mockable environment interface
var e environment = osEnv{}
type environment interface {
Getenv(key string) string
LookupEnv(key string) (string, bool)
}
// default os based implementation
type osEnv struct{}
func (osEnv) Getenv(key string) string {
return os.Getenv(key)
}
func (osEnv) LookupEnv(key string) (string, bool) {
return os.LookupEnv(key)
}
// A mockable flock interface
type filelock interface {
Unlock() error
TryLockContext(ctx context.Context, retryDelay time.Duration) (bool, error)
TryRLockContext(ctx context.Context, retryDelay time.Duration) (bool, error)
}
var newFlock = func(filename string) filelock {
return flock.New(filename)
}
// cacheFile is a map of clusterID/roleARNs to cached credentials
type cacheFile struct {
// a map of clusterIDs/profiles/roleARNs to cachedCredentials
ClusterMap map[string]map[string]map[string]cachedCredential `yaml:"clusters"`
}
// a utility type for dealing with compound cache keys
type cacheKey struct {
clusterID string
profile string
roleARN string
}
func (c *cacheFile) Put(key cacheKey, credential cachedCredential) {
if _, ok := c.ClusterMap[key.clusterID]; !ok {
// first use of this cluster id
c.ClusterMap[key.clusterID] = map[string]map[string]cachedCredential{}
}
if _, ok := c.ClusterMap[key.clusterID][key.profile]; !ok {
// first use of this profile
c.ClusterMap[key.clusterID][key.profile] = map[string]cachedCredential{}
}
c.ClusterMap[key.clusterID][key.profile][key.roleARN] = credential
}
func (c *cacheFile) Get(key cacheKey) (credential cachedCredential) {
if _, ok := c.ClusterMap[key.clusterID]; ok {
if _, ok := c.ClusterMap[key.clusterID][key.profile]; ok {
// we at least have this cluster and profile combo in the map, if no matching roleARN, map will
// return the zero-value for cachedCredential, which expired a long time ago.
credential = c.ClusterMap[key.clusterID][key.profile][key.roleARN]
}
}
return
}
// cachedCredential is a single cached credential entry, along with expiration time
type cachedCredential struct {
Credential credentials.Value
Expiration time.Time
// If set will be used by IsExpired to determine the current time.
// Defaults to time.Now if CurrentTime is not set. Available for testing
// to be able to mock out the current time.
currentTime func() time.Time
}
// IsExpired determines if the cached credential has expired
func (c *cachedCredential) IsExpired() bool {
curTime := c.currentTime
if curTime == nil {
curTime = time.Now
}
return c.Expiration.Before(curTime())
}
// readCacheWhileLocked reads the contents of the credential cache and returns the
// parsed yaml as a cacheFile object. This method must be called while a shared
// lock is held on the filename.
func readCacheWhileLocked(filename string) (cache cacheFile, err error) {
cache = cacheFile{
map[string]map[string]map[string]cachedCredential{},
}
data, err := f.ReadFile(filename)
if err != nil {
err = fmt.Errorf("unable to open file %s: %v", filename, err)
return
}
err = yaml.Unmarshal(data, &cache)
if err != nil {
err = fmt.Errorf("unable to parse file %s: %v", filename, err)
}
return
}
// writeCacheWhileLocked writes the contents of the credential cache using the
// yaml marshaled form of the passed cacheFile object. This method must be
// called while an exclusive lock is held on the filename.
func writeCacheWhileLocked(filename string, cache cacheFile) error {
data, err := yaml.Marshal(cache)
if err == nil {
// write privately owned by the user
err = f.WriteFile(filename, data, 0600)
}
return err
}
// FileCacheProvider is a Provider implementation that wraps an underlying Provider
// (contained in Credentials) and provides caching support for credentials for the
// specified clusterID, profile, and roleARN (contained in cacheKey)
type FileCacheProvider struct {
credentials *credentials.Credentials // the underlying implementation that has the *real* Provider
cacheKey cacheKey // cache key parameters used to create Provider
cachedCredential cachedCredential // the cached credential, if it exists
}
// NewFileCacheProvider creates a new Provider implementation that wraps a provided Credentials,
// and works with an on disk cache to speed up credential usage when the cached copy is not expired.
// If there are any problems accessing or initializing the cache, an error will be returned, and
// callers should just use the existing credentials provider.
func NewFileCacheProvider(clusterID, profile, roleARN string, creds *credentials.Credentials) (FileCacheProvider, error) {
if creds == nil {
return FileCacheProvider{}, errors.New("no underlying Credentials object provided")
}
filename := CacheFilename()
cacheKey := cacheKey{clusterID, profile, roleARN}
cachedCredential := cachedCredential{}
// ensure path to cache file exists
_ = f.MkdirAll(filepath.Dir(filename), 0700)
if info, err := f.Stat(filename); err == nil {
if info.Mode()&0077 != 0 {
// cache file has secret credentials and should only be accessible to the user, refuse to use it.
return FileCacheProvider{}, fmt.Errorf("cache file %s is not private", filename)
}
// do file locking on cache to prevent inconsistent reads
lock := newFlock(filename)
defer lock.Unlock()
// wait up to a second for the file to lock
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
defer cancel()
ok, err := lock.TryRLockContext(ctx, 250*time.Millisecond) // try to lock every 1/4 second
if !ok {
// unable to lock the cache, something is wrong, refuse to use it.
return FileCacheProvider{}, fmt.Errorf("unable to read lock file %s: %v", filename, err)
}
cache, err := readCacheWhileLocked(filename)
if err != nil {
// can't read or parse cache, refuse to use it.
return FileCacheProvider{}, err
}
cachedCredential = cache.Get(cacheKey)
} else {
if errors.Is(err, fs.ErrNotExist) {
// cache file is missing. maybe this is the very first run? continue to use cache.
_, _ = fmt.Fprintf(os.Stderr, "Cache file %s does not exist.\n", filename)
} else {
return FileCacheProvider{}, fmt.Errorf("couldn't stat cache file: %w", err)
}
}
return FileCacheProvider{
creds,
cacheKey,
cachedCredential,
}, nil
}
// Retrieve() implements the Provider interface, returning the cached credential if is not expired,
// otherwise fetching the credential from the underlying Provider and caching the results on disk
// with an expiration time.
func (f *FileCacheProvider) Retrieve() (credentials.Value, error) {
if !f.cachedCredential.IsExpired() {
// use the cached credential
return f.cachedCredential.Credential, nil
} else {
_, _ = fmt.Fprintf(os.Stderr, "No cached credential available. Refreshing...\n")
// fetch the credentials from the underlying Provider
credential, err := f.credentials.Get()
if err != nil {
return credential, err
}
if expiration, err := f.credentials.ExpiresAt(); err == nil {
// underlying provider supports Expirer interface, so we can cache
filename := CacheFilename()
// do file locking on cache to prevent inconsistent writes
lock := newFlock(filename)
defer lock.Unlock()
// wait up to a second for the file to lock
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
defer cancel()
ok, err := lock.TryLockContext(ctx, 250*time.Millisecond) // try to lock every 1/4 second
if !ok {
// can't get write lock to create/update cache, but still return the credential
_, _ = fmt.Fprintf(os.Stderr, "Unable to write lock file %s: %v\n", filename, err)
return credential, nil
}
f.cachedCredential = cachedCredential{
credential,
expiration,
nil,
}
// don't really care about read error. Either read the cache, or we create a new cache.
cache, _ := readCacheWhileLocked(filename)
cache.Put(f.cacheKey, f.cachedCredential)
err = writeCacheWhileLocked(filename, cache)
if err != nil {
// can't write cache, but still return the credential
_, _ = fmt.Fprintf(os.Stderr, "Unable to update credential cache %s: %v\n", filename, err)
err = nil
} else {
_, _ = fmt.Fprintf(os.Stderr, "Updated cached credential\n")
}
} else {
// credential doesn't support expiration time, so can't cache, but still return the credential
_, _ = fmt.Fprintf(os.Stderr, "Unable to cache credential: %v\n", err)
err = nil
}
return credential, err
}
}
// IsExpired() implements the Provider interface, deferring to the cached credential first,
// but fall back to the underlying Provider if it is expired.
func (f *FileCacheProvider) IsExpired() bool {
return f.cachedCredential.IsExpired() && f.credentials.IsExpired()
}
// ExpiresAt implements the Expirer interface, and gives access to the expiration time of the credential
func (f *FileCacheProvider) ExpiresAt() time.Time {
return f.cachedCredential.Expiration
}
// CacheFilename returns the name of the credential cache file, which can either be
// set by environment variable, or use the default of ~/.kube/cache/aws-iam-authenticator/credentials.yaml
func CacheFilename() string {
if filename, ok := e.LookupEnv(cacheFileNameEnv); ok {
return filename
} else {
return filepath.Join(UserHomeDir(), ".kube", "cache", "aws-iam-authenticator", "credentials.yaml")
}
}
// UserHomeDir returns the home directory for the user the process is
// running under.
func UserHomeDir() string {
if runtime.GOOS == "windows" { // Windows
return e.Getenv("USERPROFILE")
}
// *nix
return e.Getenv("HOME")
}