c6c2c525a6
Also fix some tests that were broken by bumping golang and dependencies in the previous commits. Note that in addition to changes made to satisfy the linter which do not impact the behavior of the code, this commit also adds ReadHeaderTimeout to all usages of http.Server to satisfy the linter (and because it seemed like a good suggestion).
127 lines
4.0 KiB
Go
127 lines
4.0 KiB
Go
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package execcredcache
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"time"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
|
"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
var (
|
|
// errUnsupportedVersion is returned (internally) when we encounter a version of the cache file that we
|
|
// don't understand how to handle (such as one produced by a future version of Pinniped).
|
|
errUnsupportedVersion = fmt.Errorf("unsupported credential cache version")
|
|
)
|
|
|
|
const (
|
|
// apiVersion is the Kubernetes-style API version of the credential cache file object.
|
|
apiVersion = "config.supervisor.pinniped.dev/v1alpha1"
|
|
|
|
// apiKind is the Kubernetes-style Kind of the credential cache file object.
|
|
apiKind = "CredentialCache"
|
|
|
|
// maxCacheDuration is how long a credential can remain in the cache even if it's still otherwise valid.
|
|
maxCacheDuration = 1 * time.Hour
|
|
)
|
|
|
|
type (
|
|
// credCache is the object which is YAML-serialized to form the contents of the cache file.
|
|
credCache struct {
|
|
metav1.TypeMeta
|
|
Entries []entry `json:"credentials"`
|
|
}
|
|
|
|
// entry is a single credential in the cache file.
|
|
entry struct {
|
|
Key string `json:"key"`
|
|
CreationTimestamp metav1.Time `json:"creationTimestamp"`
|
|
LastUsedTimestamp metav1.Time `json:"lastUsedTimestamp"`
|
|
Credential *clientauthenticationv1beta1.ExecCredentialStatus `json:"credential"`
|
|
}
|
|
)
|
|
|
|
// readCache loads a credCache from a path on disk. If the requested path does not exist, it returns an empty cache.
|
|
func readCache(path string) (*credCache, error) {
|
|
cacheYAML, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
// If the file was not found, generate a freshly initialized empty cache.
|
|
return emptyCache(), nil
|
|
}
|
|
// Otherwise bubble up the error.
|
|
return nil, fmt.Errorf("could not read cache file: %w", err)
|
|
}
|
|
|
|
// If we read the file successfully, unmarshal it from YAML.
|
|
var cache credCache
|
|
if err := yaml.Unmarshal(cacheYAML, &cache); err != nil {
|
|
return nil, fmt.Errorf("invalid cache file: %w", err)
|
|
}
|
|
|
|
// Validate that we're reading a version of the config we understand how to parse.
|
|
if !(cache.TypeMeta.APIVersion == apiVersion && cache.TypeMeta.Kind == apiKind) {
|
|
return nil, fmt.Errorf("%w: %#v", errUnsupportedVersion, cache.TypeMeta)
|
|
}
|
|
return &cache, nil
|
|
}
|
|
|
|
// emptyCache returns an empty, initialized credCache.
|
|
func emptyCache() *credCache {
|
|
return &credCache{
|
|
TypeMeta: metav1.TypeMeta{APIVersion: apiVersion, Kind: apiKind},
|
|
Entries: make([]entry, 0, 1),
|
|
}
|
|
}
|
|
|
|
// writeTo writes the cache to the specified file path.
|
|
func (c *credCache) writeTo(path string) error {
|
|
// Marshal the cache back to YAML and save it to the file.
|
|
cacheYAML, err := yaml.Marshal(c)
|
|
if err == nil {
|
|
err = os.WriteFile(path, cacheYAML, 0600)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// normalized returns a copy of the credCache with stale entries removed and entries sorted in a canonical order.
|
|
func (c *credCache) normalized() *credCache {
|
|
result := emptyCache()
|
|
|
|
// Clean up expired/invalid tokens.
|
|
now := time.Now()
|
|
result.Entries = make([]entry, 0, len(c.Entries))
|
|
|
|
for _, e := range c.Entries {
|
|
// Eliminate any cache entries that are missing a credential or an expiration timestamp.
|
|
if e.Credential == nil || e.Credential.ExpirationTimestamp == nil {
|
|
continue
|
|
}
|
|
|
|
// Eliminate any expired credentials.
|
|
if e.Credential.ExpirationTimestamp.Time.Before(time.Now()) {
|
|
continue
|
|
}
|
|
|
|
// Eliminate any entries older than maxCacheDuration.
|
|
if e.CreationTimestamp.Time.Before(now.Add(-maxCacheDuration)) {
|
|
continue
|
|
}
|
|
result.Entries = append(result.Entries, e)
|
|
}
|
|
|
|
// Sort the entries by creation time.
|
|
sort.SliceStable(result.Entries, func(i, j int) bool {
|
|
return result.Entries[i].CreationTimestamp.Before(&result.Entries[j].CreationTimestamp)
|
|
})
|
|
|
|
return result
|
|
}
|