feat(tpagectl): implement credential management #588
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
thwap/thwap-pagesd!588
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/cli-credentials"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Implements Phase 6 credential management.
#254 — OS keychain storage
internal/tpagectl/credentialspackage: aStoreinterface plus a keychain-backed store viagithub.com/99designs/keyring(macOS Keychain, Windows Credential Manager, Linux Secret Service). ReturnsErrUnavailablewhen no backend exists (e.g. headless Linux) so callers fall back.#255 — config-file fallback with permissions
cmd/tpagectl/main.go): explicit config/flag/env token wins, else the stored keychain credential.config.SetTokennow chmods the written file to 0600 ("appropriate permissions" for the plaintext fallback).#256 —
tokencommandinternal/tpagectl/tokenpackage withtoken set <TOKEN>(keychain primary, config fallback onErrUnavailable) andtoken clear(removes from keychain + clears configserver.token), registered on the root.login integration
login(#252) now stores the obtained token through the credential store (keychain primary, config fallback) instead of always writing the config file.Why
Phase 6 roadmap task #257.
Testing
token: set (stored in store), set-missing-arg, set keychain-unavailable→config fallback, clear (store + config cleared), clear with unavailable keychainlogin: stores token via store; keychain-unavailable fallback writes configSetToken0600 permissionstoken,token --helprendersgo test -race ./...passes (24 packages)golangci-lint runcleanBreaking Changes
auth.NewCommandand the newtoken.NewCommandtake acredentials.Store; the root wires the shared keychain store.golang.org/x/oauth2-based login now stores via the credential store.Notes
99designs/keyring(+ transitive: go-keychain, wincred, jose2go, godbus/dbus, go-libsecret, percent) added and vendored. Token storage is keychain-first with a 0600 config-file fallback.Closes #254
Closes #255
Closes #256
Closes #257
Solid feature overall: clean
credentials.Storeabstraction, keychain-first with documented config fallback, and good test coverage for the new commands. One blocking correctness issue; a few suggestions filed as issues.Blocking Issues
cmd/tpagectl/main.go:136– Stale config token shadows the newly stored keychain credential.resolveTokenreturnscfg.Server.Tokenwhenever it is non-empty, but neitherloginnortoken setclears the config token after a successful keychain store. If the config file contains an old token (e.g. written by a pre-keychainlogin), re-runningloginortoken set <NEW>stores the new token in the keychain while the old one stays authoritative, so rotation silently never takes effect and the client keeps using the stale token. Fix: clear the config token (config.SetToken("")or a dedicated removal) after a successful keychain store, or restrict config precedence to explicit--token/env so the keychain credential is preferred.Suggestions
internal/tpagectl/credentials/credentials.go:60–open()maps everykeyring.Openerror toErrUnavailable, so a present-but-broken backend silently triggers the plaintext fallback. DistinguishErrNoAvailBackendfrom real backend failures. Filed as #589.cmd/tpagectl/main.go:140–resolveTokenswallows allGet()errors and returns"", masking genuine keychain read failures. Return empty only forErrUnavailable/ErrNotFound. Filed as #590.internal/tpagectl/config/config.go:105–WriteConfigthenChmod 0600leaves a window where the plaintext token is world-readable. Write temp + chmod + atomic rename. Filed as #591.internal/tpagectl/token/token.go–token set <TOKEN>leaks the token via process list and shell history. Consider stdin/env input. Filed as #592.credentialspackage andresolveTokenhave no direct tests. Filed as #593.internal/tpagectl/auth/login.go:4– package doc still says the token is "stored in the config file"; now keychain-first. Filed as #594.Addressed the requested changes.
Blocking (stale config token shadows keychain):
loginandtoken setnow call the newconfig.ClearToken()after a successful keychain store, so a pre-existingserver.tokenno longer stays authoritative and rotation takes effect.ClearTokenis a no-op when no config file exists.TestTokenSetandTestLoginClearsStaleConfigTokencover it.Also fixed (filed as issues):
credentials.open()maps onlykeyring.ErrNoAvailImpltoErrUnavailable; genuine backend failures are wrapped and propagated so callers don't silently degrade to the plaintext config fallback.resolveTokennow takes the store and returns(string, error), propagating real keychain read failures while treatingErrUnavailable/ErrNotFoundas empty.config.SetToken/ClearTokenwrite atomically (same-dir temp file with 0600 perms, renamed over the target), closing the world-readable window.token setaccepts the token from stdin when the argument is omitted (avoids shell-history/process-list leaks); help text updated.credentialspackage tests (sentinels,KeychainStoresatisfiesStore) andresolveTokentests (config wins, keychain fallback, unavailable/missing → empty, read error propagates).loginpackage doc to reflect keychain-first storage.Verified:
go test -race ./...(25 packages),golangci-lint run, and pre-commit all pass.Re-review complete. All requested changes verified in
c819ef5. The blocking issue is resolved and every filed suggestion was addressed. Approving; two follow-up findings filed as issues.Previous blocking issue — resolved
cmd/tpagectl/main.go– stale config token no longer shadows the keychain. Bothlogin(auth/login.go:106) andtoken set(token/token.go:76) callconfig.ClearToken()after a successful keychain store, and tests cover both paths (TestLoginClearsStaleConfigToken, updatedTestTokenSet).Previously filed suggestions — all addressed
credentials.open()maps onlykeyring.ErrNoAvailImpl→ErrUnavailable; genuine backend errors are wrapped and propagated (#589).resolveTokenreturns(string, error)and propagates real keychain failures while treating unavailable/missing as empty (#590).config.SetToken/ClearTokenwrite atomically via a same-dir temp file with 0600 perms renamed over the target (#591).token setreads the token from stdin when the argument is omitted (#592).credentialspackage andresolveTokentests added (#593).loginpackage doc corrected to keychain-first storage (#594).New findings (non-blocking)
internal/tpagectl/config/config.go:97–ClearTokenis documented as a no-op when no config file exists, butresolveConfigalways returns the default user config path, so thepath == ""guard never fires andClearTokencreates a config file at the default path.TestClearTokenNoFilepasses for the wrong reason (checks the working directory while the file lands in the real user config dir, which also pollutes the developer's config duringgo test). Filed as #595.cmd/tpagectl/main.go:128– Because the rootBeforehook builds the client for every command, propagating genuine keychain read errors now aborts all commands, includingtoken clearandlogin(the recovery paths). Consider resolving the token lazily for commands that need an authenticated client. Filed as #596.