feat(auth): read the password from stdin or prompt for it - #17
Merged
Merged
Conversation
--os-password / OS_PASSWORD were the only ways in, and neither is a good place for a secret: a flag value is visible in `ps` and lands in the shell history, and an exported variable is inherited by every child process. Two more sources, both consulted from Authenticate: - --os-password-stdin reads it from standard input, `docker login --password-stdin` style: the whole of stdin less one trailing line ending. koc-native. More than one line is rejected, since that is a whole openrc piped in by mistake and authenticating with its first line is the confusing outcome. It conflicts with an explicitly typed --os-password and with --creds-from-ns / --creds-from-vault, overrides OS_PASSWORD, and outranks a named cloud's stored password the way a typed flag does. - An interactive prompt when nothing else supplied one and stdin is a terminal — python-openstackclient parity, which osc-lib does through getpass — including for a clouds.yaml entry that deliberately stores no password. A non-interactive run still fails with "no credentials found" rather than blocking on a pipe nobody will write to. Neither fires when the request authenticates without a password (application credentials, a pre-issued token), and both are remembered on Options so a second authentication in the same process does not ask twice. Exercised end to end against a mock Keystone: the piped password and one typed at the prompt over a pty each reach /v3/auth/tokens, --debug still redacts them, and the conflict, empty-stdin and multi-line-stdin cases all fail before any request is made. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McdyrP2uagmAnTsE3oMvha
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
--os-password/OS_PASSWORDwere the only ways to supply a Keystone password, and neither is a good place for a secret: a flag value is visible inpsand lands in the shell history, and an exported variable is inherited by every child process.This adds the two sources that close that gap — one koc-native, one upstream parity — both consulted from
Authenticate.--os-password-stdin(koc-native)Reads the password from standard input,
docker login --password-stdinstyle: the whole of stdin less one trailing line ending, so a password may legitimately begin or end with a space.--os-passwordand with--creds-from-ns/--creds-from-vault, which bring their own credentials.OS_PASSWORD(background configuration, not a typed flag) and, viamarkForced, outranks a named cloud's stored password exactly like a typed flag does.OS_*default: consuming stdin is something a run must opt into visibly.Upstream has no equivalent — osc-lib's only non-
OS_PASSWORDroute is the interactive prompt, which a CI job cannot use.Interactive prompt (python-openstackclient parity)
When nothing supplied a password and stdin is a terminal,
kocnow asks on stderr without echo, the way osc-lib does throughgetpass, instead of failing. This covers the--os-cloudcase where theclouds.yamlentry carries ausernamebut deliberately nopassword.A non-interactive run is never prompted: it fails with
no credentials found, whose message now also names--os-password-stdinand the prompt, rather than blocking on a pipe nobody is going to write to.Shared behaviour
Neither source fires when the request authenticates without a password (application credentials, a pre-issued token). Both write the result back to
Options, so a secondAuthenticatein the same process neither re-reads an exhausted stdin nor prompts twice; gophercloud's own reauth replays the stored auth options and never reaches either.Verification
New
internal/auth/password_test.go(20 cases) covers the parsing rules, every conflict, the "authenticates without a password" cases, the reauth replay, and three end-to-end runs throughAuthenticateagainst a mock Keystone — including theclouds.yaml-with-no-password path.The real binary was also exercised against a mock Keystone that records the token-request body:
printf 'piped-pw\n' | koc … --os-password-stdin server list"password":"piped-pw"no credentials found: … pass --os-password-stdin, or run interactively to be promptedPassword:prompt; body carried the typed password--os-password typed --os-password-stdin--debug"password":"<redacted>"Gate:
gofmtclean,go vetclean,go test ./...green,go test -racegreen, offline-mod=vendor GOPROXY=offbuild and test green.golangci-lintcould not be run locally — the available binary is v2.5.0, built with go1.25, and it refuses this module'sgo 1.26.0target — so the pinned v2.13 in CI is the first real lint run on this code.docs/coverage.mdcommand counts are unchanged (no command is added, renamed or removed); the "koc-native global flags" paragraph, which said "one global flag", now lists--os-password-stdinalongside--timeout. README gains a "Where the password comes from" subsection under Authentication.🤖 Generated with Claude Code
https://claude.ai/code/session_01McdyrP2uagmAnTsE3oMvha
Generated by Claude Code