Skip to content

Commit f78dcbd

Browse files
Merge pull request #367 from natalie-o-perret/fix/migrate-oauth-token-cache-key
fix(auth): migrate legacy token cache keys on first use after upgrade
2 parents c6dbac3 + 1e35277 commit f78dcbd

5 files changed

Lines changed: 121 additions & 2 deletions

File tree

internal/cli/api_auth.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,9 @@ func (c *CLI) operationSetForAPI(ctx context.Context, apiName string, apiCfg *co
794794
}
795795
if err != nil || s == nil {
796796
if !spec.HasLocalSpecFiles(apiCfg.SpecFiles) {
797+
if err == nil && !forceRefresh && (apiCfg.BaseURL != "" || apiCfg.SpecURL != "" || len(apiCfg.SpecFiles) > 0) {
798+
c.hintf("spec cache for API %q is empty; run \"restish api sync %s\" to populate it", apiName, apiName)
799+
}
797800
return spec.OperationSet{}, false, err
798801
}
799802
s, err = spec.Discover(ctx, spec.DiscoverConfig{

internal/cli/auth.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,11 +299,29 @@ func (c *CLI) cachedOAuthTokenEntry(authType string, cacheKey, apiName, profileN
299299
if cacheKey == ":" || cacheKey == "" {
300300
return nil
301301
}
302-
cached, err := auth.NewTokenCache(c.tokenCachePath()).Get(cacheKey)
302+
tc := auth.NewTokenCache(c.tokenCachePath())
303+
cached, err := tc.Get(cacheKey)
303304
if err != nil {
304305
return nil
305306
}
306-
return cached
307+
if cached != nil {
308+
return cached
309+
}
310+
// The cache key format changed from "apiName:profileName" to "oauth:HASH" at
311+
// some point, silently invalidating existing tokens on upgrade. Fall back to
312+
// the legacy key here; if found, migrate it to the new key and drop the old one.
313+
legacyKey := c.apiCacheNamespace(apiName, profileName)
314+
if legacyKey == cacheKey || legacyKey == ":" || legacyKey == "" {
315+
return nil
316+
}
317+
legacy, err := tc.Get(legacyKey)
318+
if err != nil || legacy == nil {
319+
return nil
320+
}
321+
if setErr := tc.Set(cacheKey, *legacy); setErr == nil {
322+
_ = tc.Delete(legacyKey)
323+
}
324+
return legacy
307325
}
308326

309327
func (c *CLI) cachedOAuthAuthCodeUsable(authType, cacheKey, apiName, profileName string) bool {

internal/cli/auth_internal_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,49 @@ func TestInlineAuthCacheKeyDeduplicatesAbsoluteAuthCodeEndpoints(t *testing.T) {
197197
}
198198
}
199199

200+
func TestCachedOAuthTokenEntryMigratesLegacyInlineAuthCodeCacheKey(t *testing.T) {
201+
cacheFile := filepath.Join(t.TempDir(), "tokens.cbor")
202+
c := New()
203+
c.Hooks().TokenCachePath = cacheFile
204+
205+
ac := &config.AuthConfig{
206+
Type: "oauth-authorization-code",
207+
Params: map[string]string{
208+
"client_id": "restish",
209+
"authorize_url": "https://dex.example.com/auth",
210+
"token_url": "https://dex.example.com/token",
211+
},
212+
}
213+
cacheKey := inlineAuthCacheKey("demo:default", ac, "https://api.example.com")
214+
if cacheKey == "" || cacheKey == "demo:default" {
215+
t.Fatalf("inline auth code cache key = %q, want hashed oauth key", cacheKey)
216+
}
217+
218+
tc := auth.NewTokenCache(cacheFile)
219+
if err := tc.Set("demo:default", auth.CachedToken{AccessToken: "legacy-token"}); err != nil {
220+
t.Fatalf("seed legacy token: %v", err)
221+
}
222+
223+
got := c.cachedOAuthTokenEntry("oauth-authorization-code", cacheKey, "demo", "default")
224+
if got == nil || got.AccessToken != "legacy-token" {
225+
t.Fatalf("migrated token = %+v, want legacy-token", got)
226+
}
227+
migrated, err := tc.Get(cacheKey)
228+
if err != nil {
229+
t.Fatalf("read migrated token: %v", err)
230+
}
231+
if migrated == nil || migrated.AccessToken != "legacy-token" {
232+
t.Fatalf("migrated cache entry = %+v, want legacy-token", migrated)
233+
}
234+
legacy, err := tc.Get("demo:default")
235+
if err != nil {
236+
t.Fatalf("read legacy token: %v", err)
237+
}
238+
if legacy != nil {
239+
t.Fatalf("legacy cache entry still present: %+v", legacy)
240+
}
241+
}
242+
200243
func TestInlineAuthCacheKeyDeduplicatesIssuerAuthCodeEndpoints(t *testing.T) {
201244
ac := &config.AuthConfig{
202245
Type: "oauth-authorization-code",
@@ -363,3 +406,50 @@ func TestAuthHandlerForOAuthUsesThemeCallbackColors(t *testing.T) {
363406
t.Fatalf("failure color = %q, want #ff0000", oauthHandler.CallbackFailureColor)
364407
}
365408
}
409+
410+
func TestOperationSetForAPIHintsAPISync_WhenCacheEmpty(t *testing.T) {
411+
var stderr bytes.Buffer
412+
c := New()
413+
c.Stderr = &stderr
414+
c.Hooks().SpecCachePath = t.TempDir() // empty cache dir
415+
416+
apiCfg := &config.APIConfig{BaseURL: "https://api.example.com"}
417+
set, ok, err := c.operationSetForAPI(context.Background(), "example", apiCfg, "default", false)
418+
if err != nil {
419+
t.Fatalf("unexpected error: %v", err)
420+
}
421+
if ok || len(set.Operations) > 0 {
422+
t.Fatal("expected empty operation set from cold cache")
423+
}
424+
if !strings.Contains(stderr.String(), "api sync example") {
425+
t.Fatalf("expected api sync hint on stderr, got: %q", stderr.String())
426+
}
427+
}
428+
429+
func TestOperationSetForAPINoHintWhenForceRefresh(t *testing.T) {
430+
// forceRefresh=true means an explicit sync is already in progress; no hint.
431+
var stderr bytes.Buffer
432+
c := New()
433+
c.Stderr = &stderr
434+
c.Hooks().SpecCachePath = t.TempDir()
435+
436+
apiCfg := &config.APIConfig{BaseURL: "https://203.0.113.1"}
437+
_, _, _ = c.operationSetForAPI(context.Background(), "example", apiCfg, "default", true)
438+
if strings.Contains(stderr.String(), "api sync") {
439+
t.Fatalf("expected no api sync hint during forceRefresh, got: %q", stderr.String())
440+
}
441+
}
442+
443+
func TestOperationSetForAPINoHintWhenNoSpecSource(t *testing.T) {
444+
// An API with no BaseURL, no SpecURL, no SpecFiles configured has nothing to sync.
445+
var stderr bytes.Buffer
446+
c := New()
447+
c.Stderr = &stderr
448+
c.Hooks().SpecCachePath = t.TempDir()
449+
450+
apiCfg := &config.APIConfig{}
451+
_, _, _ = c.operationSetForAPI(context.Background(), "bare", apiCfg, "default", false)
452+
if strings.Contains(stderr.String(), "api sync") {
453+
t.Fatalf("expected no api sync hint for API with no spec source, got: %q", stderr.String())
454+
}
455+
}

internal/cli/hooks.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ func (c *CLI) resolveTLSSigner(opts request.Options) (request.Options, error) {
8282
return opts, nil
8383
}
8484
}
85+
if opts.TLSSignerName == "pkcs11" {
86+
return opts, fmt.Errorf("tls signer plugin %q not found; install the restish-pkcs11 binary and make sure it is on your PATH (see https://github.com/rest-sh/restish)", opts.TLSSignerName)
87+
}
8588
return opts, fmt.Errorf("tls signer plugin %q not found", opts.TLSSignerName)
8689
}
8790

internal/config/migrate.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ func convertLegacyAPIConfig(name string, legacy *legacyAPIConfig) (*APIConfig, [
228228
}
229229
}
230230

231+
var migratedPKCS11 bool
231232
if legacy.TLS != nil && legacy.TLS.PKCS11 != nil {
232233
if api.Profiles == nil {
233234
api.Profiles = map[string]*ProfileConfig{}
@@ -249,6 +250,7 @@ func convertLegacyAPIConfig(name string, legacy *legacyAPIConfig) (*APIConfig, [
249250
if legacy.TLS.PKCS11.Label != "" && prof.TLSSignerParams["label"] == "" {
250251
prof.TLSSignerParams["label"] = legacy.TLS.PKCS11.Label
251252
}
253+
migratedPKCS11 = true
252254
}
253255

254256
if legacy.TLS != nil && (legacy.TLS.Cert != "" || legacy.TLS.Key != "") {
@@ -272,6 +274,9 @@ func convertLegacyAPIConfig(name string, legacy *legacyAPIConfig) (*APIConfig, [
272274
if warning != "" {
273275
warnings = append(warnings, warning)
274276
}
277+
if migratedPKCS11 {
278+
warnings = append(warnings, fmt.Sprintf("api %q: migrated PKCS#11 TLS config; install the restish-pkcs11 plugin to continue using it (see https://github.com/rest-sh/restish)", name))
279+
}
275280
return api, warnings
276281
}
277282

0 commit comments

Comments
 (0)