fix(login): point headless users at the file token store, report real backend · Entire

fix(login): point headless users at the file token store, report real backend

e616b4c→main·

peyton-alt·3d ago·9 files·+408 added/-11 removed

Closes the residual gap in #1036: the mechanisms (ENTIRE_TOKEN_STORE=file, ENTIRE_TOKEN) already exist, but a locked/absent OS keyring during entire login surfaced a raw store error with no way forward, and entire auth status claimed "stored in OS keychain" regardless of the configured backend.

Fault injection in tests uses the existing UseFailingBackendForTesting deterministic under root), loose-permission fixtures chmod explicitly (immune to hardened umask), and both store-write sites, both BackendDescription branches, and the default token path are pinned.

Fixes #1036

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

Sessions

01KXGTT2YYMN1Y1ZYB26M2NZT4View transcript

Changes

9

16 unmodified lines
17  
18  
19  
...  
394 unmodified lines
418  
419  
420  
420  
421  
422  
423  
424  
"github.com/entireio/cli/cmd/entire/cli/palette"  
"github.com/entireio/cli/internal/coreapi"  
"github.com/entireio/cli/internal/entireclient/contexts"  
"github.com/entireio/cli/internal/entireclient/tokenstore"  
"github.com/spf13/cobra"
  
394 unmodified lines
  
if t.activeContext != "" {  
    writeAuthStatusLine(w, "Context:", t.activeContext)  
}  
writeAuthStatusLine(w, "Token:", "stored in OS keychain")  
writeAuthStatusLine(w, "Token:", "stored in "+tokenstore.BackendDescription())

// Active sessions on this core. The token is already known good, so a
// listing failure is non-fatal — note it and carry on.
Mcmd/entire/cli/auth.go+2/-1  
19 unmodified lines
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30...  

// so a conservative non-zero value is enough to keep the entry usable. const defaultContextTokenTTL = time.Hour

// ErrCredentialStoreWrite marks a failure writing tokens to the configured // credential backend (OS keyring or file store), as opposed to claim // validation or contexts.json failures. Login UX branches on it via // errors.Is to decide whether pointing the user at the file token store // would actually help. var ErrCredentialStoreWrite = errors.New("credential store write failed")

// credStoreWriteError tags an underlying store error with // ErrCredentialStoreWrite without changing its message. type credStoreWriteError struct{ inner error }

func (e *credStoreWriteError) Error() string { return e.inner.Error() } func (e *credStoreWriteError) Unwrap() []error { return []error{e.inner, ErrCredentialStoreWrite} }

// RecordLoginContext records a freshly obtained login token in the // shared contexts.json credential model: it derives the issuer (core // URL), handle, and expiry from the token's own claims, stores the token

...


refreshSlot := tokenstore.RefreshService(keychainService) if refreshToken != "" { if err := tokenstore.Set(refreshSlot, handle, refreshToken); err != nil { return "", fmt.Errorf("store refresh token in keyring: %w", err) return "", fmt.Errorf("store refresh token in credential store: %w", &credStoreWriteError{err}) } } else { _ = tokenstore.Delete(refreshSlot, handle) //nolint:errcheck // best-effort cleanup of a stale refresh token }


encoded := tokenstore.EncodeTokenWithExpiration(rawToken, expiresIn) if err := tokenstore.Set(keychainService, handle, encoded); err != nil { return "", fmt.Errorf("store login token in keyring: %w", err) return "", fmt.Errorf("store login token in credential store: %w", &credStoreWriteError{err}) }

var name string


// The Token: provenance line must reflect the configured credential backend: // with ENTIRE_TOKEN_STORE=file the token lives in a JSON file, not the OS // keychain, and claiming otherwise misleads exactly the headless users the // file backend exists for (#1036). func TestRunAuthStatus_FileTokenStoreProvenance(t *testing.T) { t.Setenv("ENTIRE_TOKEN_STORE", "file") t.Setenv("ENTIRE_TOKEN_STORE_PATH", "/ci/secrets/tokens.json")

target := statusTarget{coreURL: testCoreURL, token: "tok", activeContext: "core"} listSessions := func(context.Context, string, string) ([]api.AuthSession, error) { return nil, nil }

var out bytes.Buffer if err := runAuthStatus(context.Background(), &out, okProfile, listSessions, target); err != nil { t.Fatalf("unexpected error: %v", err) } got := out.String() if !strings.Contains(got, "stored in file /ci/secrets/tokens.json") { t.Fatalf("output = %q, want the file-backend provenance line", got) } if strings.Contains(got, "OS keychain") { t.Fatalf("output = %q, must not claim the OS keychain when the file backend is configured", got) } }