Skip to content

Commit e74af2c

Browse files
committed
Export 0227e09160af8ab885b05661d46360e8ea2c4ff4
Export: 0227e09160af8ab885b05661d46360e8ea2c4ff4
1 parent b3480dd commit e74af2c

File tree

2 files changed

+155
-43
lines changed

2 files changed

+155
-43
lines changed

auth/token/token.go

Lines changed: 112 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"os"
1313
"path/filepath"
1414
"runtime"
15+
"slices"
16+
"sort"
1517
"strings"
1618
"time"
1719

@@ -27,15 +29,41 @@ const (
2729

2830
var (
2931
AllKinds = []Kind{KindAccess, KindRefresh}
30-
)
3132

32-
var (
3333
parentDir = "chainguard"
3434
)
3535

36+
type token struct {
37+
alias string
38+
}
39+
40+
type Option func(*token)
41+
42+
// WithAlias allows callers to organize tokens into subdirectories
43+
// under an audience to manage multiple tokens for the same audience
44+
// without overwriting.
45+
func WithAlias(a string) Option {
46+
return func(token *token) {
47+
token.alias = a
48+
}
49+
}
50+
51+
func newToken(opts ...Option) token {
52+
t := token{}
53+
for _, o := range opts {
54+
o(&t)
55+
}
56+
return t
57+
}
58+
3659
// Save saves the given token to cache/audience
37-
func Save(token []byte, kind Kind, audience string) error {
38-
path, err := Path(kind, audience)
60+
func Save(token []byte, kind Kind, audience string, opts ...Option) error {
61+
t := newToken(opts...)
62+
return t.save(token, kind, audience)
63+
}
64+
65+
func (t token) save(token []byte, kind Kind, audience string) error {
66+
path, err := t.path(kind, audience)
3967
if err != nil {
4068
return err
4169
}
@@ -48,8 +76,13 @@ func Save(token []byte, kind Kind, audience string) error {
4876

4977
// Load returns the token for the given audience if it exists,
5078
// or an error if it doesn't.
51-
func Load(kind Kind, audience string) ([]byte, error) {
52-
path, err := Path(kind, audience)
79+
func Load(kind Kind, audience string, opts ...Option) ([]byte, error) {
80+
t := newToken(opts...)
81+
return t.load(kind, audience)
82+
}
83+
84+
func (t token) load(kind Kind, audience string) ([]byte, error) {
85+
path, err := t.path(kind, audience)
5386
if err != nil {
5487
return nil, err
5588
}
@@ -63,8 +96,13 @@ func Load(kind Kind, audience string) ([]byte, error) {
6396

6497
// Delete removes the token for the given audience, if it exists.
6598
// No error is returned if the token doesn't exist.
66-
func Delete(kind Kind, audience string) error {
67-
path, err := Path(kind, audience)
99+
func Delete(kind Kind, audience string, opts ...Option) error {
100+
t := newToken(opts...)
101+
return t.delete(kind, audience)
102+
}
103+
104+
func (t token) delete(kind Kind, audience string) error {
105+
path, err := t.path(kind, audience)
68106
if err != nil {
69107
return err
70108
}
@@ -81,56 +119,96 @@ func DeleteAll() error {
81119
if err != nil {
82120
return fmt.Errorf("error locating Chainguard token dir: %w", err)
83121
}
84-
files, err := os.ReadDir(base)
85-
if err != nil {
86-
return fmt.Errorf("error reading Chainguard token dir: %w", err)
87-
}
122+
88123
// Token directory is expected to be structured as group of audience-specific
89-
// directories, with a single file containing the token
124+
// directories, with token files (oidc-token, refresh-token) or alias directories
125+
// nested within.
90126
//
91127
// $ tree ~/Library/Caches/chainguard
92128
// /Users/foo/Library/Caches/chainguard
93129
// ├── https:--console-api.enforce.dev
94-
// │ └── oidc-token
95-
// ├── https:--cgr.dev
96-
// └── oidc-token
97-
for _, file := range files {
98-
if !file.IsDir() {
99-
// Encountered a file in the directory. Skip.
100-
continue
130+
// │ ├── oidc-token
131+
// │ ├── refresh-token
132+
// │ └── foo
133+
// │ ├── oidc-token
134+
// │ └── refresh-token
135+
// ├── cgr.dev
136+
// └── oidc-token
137+
var dirs []string
138+
if err = filepath.WalkDir(base, func(path string, d fs.DirEntry, err error) error {
139+
// Return errors encountered reading the base directory.
140+
if err != nil {
141+
return err
101142
}
102-
for _, kind := range AllKinds {
103-
// Try to remove a token, ignore file not exist errors
104-
tokenFile := filepath.Join(base, file.Name(), string(kind))
105-
if err := os.Remove(tokenFile); err != nil && !errors.Is(err, fs.ErrNotExist) {
106-
return fmt.Errorf("failed to remove %s: %w", tokenFile, err)
143+
144+
switch {
145+
case path == base:
146+
// Skip the base directory, we don't want to remove it
147+
case d.IsDir():
148+
// Keep track of directories we'll want to delete, if they end up empty.
149+
dirs = append(dirs, path)
150+
case slices.Contains(AllKinds, Kind(d.Name())):
151+
// Remove recognized token files.
152+
if err := os.Remove(path); err != nil {
153+
return fmt.Errorf("removing file %s: %w", path, err)
107154
}
108155
}
109-
// Remove the (hopefully empty) audience directory.
110-
// Ignore failures since other tools may have stored files in this cache.
111-
dir := filepath.Join(base, file.Name())
112-
_ = os.Remove(dir)
156+
return nil
157+
}); err != nil {
158+
return err
113159
}
160+
161+
// Sort directories from longest to shortest to remove nested dirs first.
162+
sort.Slice(dirs, func(i, j int) bool {
163+
return len(dirs[i]) > len(dirs[j])
164+
})
165+
166+
// Remove empty directories
167+
for _, d := range dirs {
168+
ents, err := os.ReadDir(d)
169+
if err != nil {
170+
return fmt.Errorf("reading %s: %w", d, err)
171+
}
172+
// Remove the directory if it is empty
173+
if len(ents) == 0 {
174+
if err := os.Remove(d); err != nil {
175+
return fmt.Errorf("removing directory %s: %w", d, err)
176+
}
177+
}
178+
}
179+
114180
return nil
115181
}
116182

117183
// Path is the filepath of the token for the given audience.
118-
func Path(kind Kind, audience string) (string, error) {
184+
func Path(kind Kind, audience string, opts ...Option) (string, error) {
185+
t := newToken(opts...)
186+
return t.path(kind, audience)
187+
}
188+
189+
func (t token) path(kind Kind, audience string) (string, error) {
119190
a := strings.ReplaceAll(audience, "/", "-")
120191
// Windows does not allow : as a valid character for directory names.
121192
// For backwards compatibility, keep : in directory names for non-Windows systems.
122193
// Ref: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
123194
if runtime.GOOS == "windows" {
124195
a = strings.ReplaceAll(a, ":", "-")
125196
}
126-
fp := filepath.Join(a, string(kind))
197+
// NB: empty elements in Join are ignored, so we don't need to
198+
// check the existence of t.alias here.
199+
fp := filepath.Join(a, t.alias, string(kind))
127200
return cacheFilePath(fp)
128201
}
129202

130203
// RemainingLife returns the amount of time remaining before the token for
131204
// the given audience expires. Returns 0 for expired and non-existent tokens.
132-
func RemainingLife(kind Kind, audience string, less time.Duration) time.Duration {
133-
tok, err := Load(kind, audience)
205+
func RemainingLife(kind Kind, audience string, less time.Duration, opts ...Option) time.Duration {
206+
t := newToken(opts...)
207+
return t.remainingLife(kind, audience, less)
208+
}
209+
210+
func (t token) remainingLife(kind Kind, audience string, less time.Duration) time.Duration {
211+
tok, err := t.load(kind, audience)
134212
if err != nil {
135213
// Not a big deal, life is zero.
136214
return 0
@@ -156,10 +234,7 @@ var timeUntil = time.Until
156234
// Safe calculation for duration remaining from a given time, less the given duration.
157235
func subtractOrZero(expiry time.Time, less time.Duration) time.Duration {
158236
life := timeUntil(expiry.Add(less * -1))
159-
if life < 0 {
160-
return 0
161-
}
162-
return life
237+
return max(0, life)
163238
}
164239

165240
func cacheFilePath(file string) (string, error) {

auth/token/token_test.go

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ func TestSave(t *testing.T) {
6767
name string
6868
kind Kind
6969
audience string
70+
opts []Option
7071
wantPath string
7172
}{{
7273
name: "sans audience",
@@ -83,13 +84,19 @@ func TestSave(t *testing.T) {
8384
kind: KindRefresh,
8485
audience: "https://audience.unit.test",
8586
wantPath: filepath.Join(cacheDir, parentDir, "https:--audience.unit.test", string(KindRefresh)),
87+
}, {
88+
name: "with alias",
89+
kind: KindAccess,
90+
audience: "audience",
91+
opts: []Option{WithAlias("alias")},
92+
wantPath: filepath.Join(cacheDir, parentDir, "audience", "alias", string(KindAccess)),
8693
}}
8794

8895
for _, test := range tests {
8996
t.Run(test.name, func(t *testing.T) {
9097
// Save a token
9198
tokenContents := []byte("mytoken")
92-
if err := Save(tokenContents, test.kind, test.audience); err != nil {
99+
if err := Save(tokenContents, test.kind, test.audience, test.opts...); err != nil {
93100
t.Fatalf("Save() unexpected error=%v", err)
94101
}
95102

@@ -122,6 +129,7 @@ func TestLoad(t *testing.T) {
122129
name string
123130
kind Kind
124131
audience string
132+
alias string
125133
}{{
126134
name: "sans audience",
127135
kind: KindAccess,
@@ -134,14 +142,19 @@ func TestLoad(t *testing.T) {
134142
name: "with audience (with replacement)",
135143
kind: KindRefresh,
136144
audience: "https://audience.unit.test",
145+
}, {
146+
name: "with alias",
147+
kind: KindAccess,
148+
audience: "audience",
149+
alias: "alias",
137150
}}
138151

139152
for _, test := range tests {
140153
t.Run(test.name, func(t *testing.T) {
141154
// Manually save a token
142155
tokenContents := []byte("mytoken")
143156
mutatedAud := strings.ReplaceAll(test.audience, "/", "-")
144-
path := filepath.Join(cacheDir, parentDir, mutatedAud)
157+
path := filepath.Join(cacheDir, parentDir, mutatedAud, test.alias)
145158
if err := os.MkdirAll(path, 0777); err != nil {
146159
t.Fatalf("Unexpected error creating temp dir: %v", err)
147160
}
@@ -150,7 +163,8 @@ func TestLoad(t *testing.T) {
150163
}
151164

152165
// Load the token, check its contents
153-
got, err := Load(test.kind, test.audience)
166+
// NB: WithAlias("") is a no-op.
167+
got, err := Load(test.kind, test.audience, WithAlias(test.alias))
154168
if err != nil {
155169
t.Fatalf("Load() unexpected error=%v", err)
156170
}
@@ -171,6 +185,7 @@ func TestDelete(t *testing.T) {
171185
tests := []struct {
172186
name string
173187
audience string
188+
alias string
174189
kind Kind
175190
exists bool
176191
wantErr bool
@@ -191,14 +206,21 @@ func TestDelete(t *testing.T) {
191206
exists: true,
192207
kind: KindAccess,
193208
wantErr: false,
209+
}, {
210+
name: "with alias",
211+
audience: "audience",
212+
alias: "alias",
213+
exists: true,
214+
kind: KindAccess,
215+
wantErr: false,
194216
}}
195217

196218
for _, test := range tests {
197219
t.Run(test.name, func(t *testing.T) {
198220
// Manually save a token if it should exist
199221
if test.exists {
200222
tokenContents := []byte("mytoken")
201-
path := filepath.Join(cacheDir, parentDir, test.audience)
223+
path := filepath.Join(cacheDir, parentDir, test.audience, test.alias)
202224
if err := os.MkdirAll(path, 0777); err != nil {
203225
t.Fatalf("Unexpected error creating temp dir: %v", err)
204226
}
@@ -208,7 +230,8 @@ func TestDelete(t *testing.T) {
208230
}
209231

210232
// Attempt to delete the token.
211-
err := Delete(test.kind, test.audience)
233+
// NB: WithAlias("") is a no-op.
234+
err := Delete(test.kind, test.audience, WithAlias(test.alias))
212235
if (err != nil) != test.wantErr {
213236
t.Fatalf("Delete() error return mismatch, want=%t got=%v", test.wantErr, err)
214237
}
@@ -222,6 +245,7 @@ func TestDeleteAll(t *testing.T) {
222245
auds []string // Create sub-dirs for each of these in cacheDir
223246
audsWithTokens []string // Dirs from above that should also have a token (subset of auds)
224247
extraFiles []string // Any extra files outside of audience dirs to include (in root of cacheDir)
248+
alias string
225249
wantErr bool
226250
}{{
227251
name: "no tokens, no extra files/dirs",
@@ -259,6 +283,13 @@ func TestDeleteAll(t *testing.T) {
259283
audsWithTokens: []string{"audience1", "audience2"},
260284
extraFiles: []string{filepath.Join("audience1", "extra1"), filepath.Join("some-dir", "extra2")},
261285
wantErr: false,
286+
}, {
287+
name: "with alias",
288+
auds: []string{"audience1", "audience2"},
289+
audsWithTokens: []string{"audience1", "audience2"},
290+
alias: "alias",
291+
extraFiles: nil,
292+
wantErr: false,
262293
}}
263294

264295
for _, test := range tests {
@@ -276,7 +307,7 @@ func TestDeleteAll(t *testing.T) {
276307
}
277308
// Create audience directories
278309
for _, aud := range test.auds {
279-
dir := filepath.Join(cacheDir, aud)
310+
dir := filepath.Join(cacheDir, aud, test.alias)
280311
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
281312
t.Fatalf("failed to create dir %s: %s", dir, err.Error())
282313
}
@@ -288,6 +319,12 @@ func TestDeleteAll(t *testing.T) {
288319
if _, err := os.Create(tok); err != nil {
289320
t.Fatalf("failed to create fake token %s: %s", tok, err.Error())
290321
}
322+
if test.alias != "" {
323+
tok := filepath.Join(cacheDir, aud, test.alias, string(kind))
324+
if _, err := os.Create(tok); err != nil {
325+
t.Fatalf("failed to create fake token %s: %s", tok, err.Error())
326+
}
327+
}
291328
}
292329
}
293330
// Create extra files

0 commit comments

Comments
 (0)