Skip to content

fix: collect --password once up-front and honor -S consistently across subcommands#201

Merged
inureyes merged 4 commits into
mainfrom
fix/issue-200-collect-password-once
May 15, 2026
Merged

fix: collect --password once up-front and honor -S consistently across subcommands#201
inureyes merged 4 commits into
mainfrom
fix/issue-200-collect-password-once

Conversation

@inureyes
Copy link
Copy Markdown
Member

@inureyes inureyes commented May 15, 2026

Fixes #200.

Problem

Two distinct defects in credential collection both surface in:

bssh -C orin -l lablup --password -S ping

Defect 1 — --password was collected lazily, per-connection

ssh/auth.rs::password_auth() called rpassword::prompt_password() inside each per-node authentication task. The executor fans out N parallel connection attempts, so multiple tasks raced for stdin: the prompt could be missed (drowned by the indicatif progress UI), repeated per node, or interleaved with the progress bar rendering.

The -S (sudo-password) path already did this correctly — it collected once in the dispatcher, wrapped the value in Arc<SudoPassword>, and shared it across tasks. --password now follows the same pattern.

Defect 2 — -S was silently dropped by ping, upload, download

The dispatcher only read cli.sudo_password in the exec and interactive branches. Users could pass -S to ping/upload/download without any error, but the flag had no effect.

Changes

New Password wrapper — src/security/password.rs

Mirrors the existing SudoPassword design:

  • Backed by secrecy::SecretString (auto-zeroized on drop via the secrecy crate's SecretString machinery).
  • prompt_password() uses a non-host-specific prompt: "Enter SSH password (used for all hosts): ".
  • get_password_from_env() reads BSSH_PASSWORD (discouraged for production but useful for automation).
  • get_password(warn_env) combines env + interactive prompt.
  • Designed to be wrapped in Arc<Password> and shared across parallel tasks without copying the secret.

Dispatcher hoists --password collection — src/app/dispatcher.rs

The prompt now runs once at the top of dispatch_command, before any ParallelExecutor is built and before any indicatif MultiProgress is initialized. This avoids the terminal-state pitfall where indicatif corrupts the prompt rendering. The resulting Arc<Password> is threaded through:

  • ping_nodes(...) — new parameter
  • FileTransferParams (upload + download) — new field
  • ExecuteCommandParams — new field
  • InteractiveCommand — new field
  • SSH-mode interactive path

-S semantics for non-applicable subcommands

The dispatcher now emits tracing::warn!("--sudo-password (-S) has no effect for the {subcommand} subcommand and will be ignored") when -S is passed to ping, upload, download, list, or cache-stats. Picked a warning over a hard reject because:

  • Matches typical CLI UX (ssh, git, etc. tend to warn rather than fail on flags that are harmless-but-inapplicable).
  • Existing scripts that happen to pass -S to non-applicable subcommands continue to work — they just see a visible warning instead of silent no-op.
  • exec and interactive paths continue to honor -S exactly as before.

Threading through the auth machinery

  • AuthContext (src/ssh/auth.rs) gains a password: Option<Arc<Password>> field and with_pre_collected_password() builder.
  • password_auth() now consumes the pre-collected value when available. The in-task rpassword::prompt_password() call is retained only as the fallback for the OpenSSH-style "all key methods failed → prompt for password" path (which is opportunistic and has no dispatcher-side collection point).
  • ParallelExecutor::with_ssh_password(), ExecutionConfig::ssh_password, ConnectionConfig::ssh_password, and the SFTP *_with_jump_hosts helpers all carry the Option<Arc<Password>> through to the auth layer.
  • Every per-node task clones the Arc so all tasks observe the same secret without duplicating the underlying string.

Tests

New: src/security/password.rs — 9 unit tests

Mirror src/security/sudo.rs test structure:

  • test_password_creation, test_password_empty_rejection, test_password_debug_redaction
  • test_clone_independence, test_arc_sharing
  • Env var tests: _empty, _valid, _not_set
  • test_get_password_dispatcher_collection_pattern — explicitly models the dispatcher-level collection pattern: one get_password() call, wrapped in Arc<Password>, observably shared across three simulated per-node tasks. Asserts all three see the same password and Arc::strong_count == 4.

New: src/ssh/auth.rs — 2 unit tests

  • test_auth_context_with_pre_collected_password — verifies the builder and Arc sharing semantics.
  • test_password_auth_uses_pre_collected_password — critically, verifies password_auth() returns immediately without prompting when the pre-collected value is set (which means it never blocks on stdin in non-interactive test environments).

Existing tests

All 1233 library unit tests and the integration test suites continue to pass. Pre-existing failing doctest in src/server/filter/mod.rs is unrelated to this PR (verified by running cargo test --doc on main).

Verification

cargo build --release      # passes
cargo test --lib           # 1233 lib tests pass
cargo clippy --all-targets --all-features -- -D warnings  # passes
cargo fmt --check          # passes

Behavior comparison

Scenario Before After
bssh -C cluster --password exec "uptime" Multiple prompts race for stdin or interleave with progress bars; possibly missed entirely One clean prompt up-front, value reused for all N nodes
bssh -C cluster --password ping Same broken behavior One clean prompt up-front
bssh -C cluster --password upload f.txt /tmp/ Same broken behavior One clean prompt up-front
bssh -S ping Flag silently ignored WARN: --sudo-password (-S) has no effect for the 'ping' subcommand and will be ignored
bssh -S exec "sudo apt update" Worked correctly Unchanged — still works
BSSH_PASSWORD=secret bssh -C cluster --password ping Not supported Reads from env (with warning), still single value shared across nodes

Acceptance criteria

  • Running bssh -C <cluster> --password <subcommand> prompts exactly once, before any connection attempt or progress UI output.
  • The same password is used for every node in the cluster.
  • Password memory is zeroized after use (via secrecy::SecretString in Password).
  • -S either applies to applicable subcommands or warns when passed to subcommands where it has no effect.
  • Existing tests pass.
  • A unit test covers the dispatcher-level password collection (test_get_password_dispatcher_collection_pattern).

Fixes two defects in credential collection that surfaced together in `bssh -C orin -l lablup --password -S ping` (issue #200).

Defect 1 — `--password` is collected lazily per-connection. The previous implementation prompted via `rpassword::prompt_password()` inside each per-node authentication task (`ssh/auth.rs::password_auth()`). The executor fans out N parallel connection attempts, so multiple tasks raced for stdin: the prompt could be missed, repeated per node, or interleaved with the indicatif progress UI rendering. The `-S` (sudo-password) path already did this correctly — collect once in the dispatcher, wrap in `Arc<SudoPassword>`, share across tasks — and now `--password` follows the same pattern.

Defect 2 — `-S` was silently dropped by `ping`, `upload`, and `download`. The dispatcher only read `cli.sudo_password` in the `exec` and interactive branches. Users could pass `-S` to `ping`/`upload`/`download` without any error, but the flag had no effect.

Changes:

- Add `src/security/password.rs` mirroring the `SudoPassword` design: a `Password` wrapper backed by `secrecy::SecretString` (auto-zeroized on drop), `prompt_password()` with a non-host-specific prompt ("Enter SSH password (used for all hosts): "), `get_password_from_env()` reading `BSSH_PASSWORD`, and `get_password(warn_env)` combining both. Wrapped in `Arc<Password>` for sharing across per-node tasks without duplicating secret material.
- Hoist `--password` collection to `dispatch_command` in `src/app/dispatcher.rs`. Prompt runs once, before any `ParallelExecutor` is built and before any indicatif `MultiProgress` is initialized, so the terminal is in a clean state when reading the password. The resulting `Arc<Password>` is threaded through `ping_nodes`, `FileTransferParams` (upload/download), `ExecuteCommandParams`, `InteractiveCommand`, and the SSH-mode interactive path.
- Extend `AuthContext` in `src/ssh/auth.rs` with a `password: Option<Arc<Password>>` field and a `with_pre_collected_password()` builder. `password_auth()` now consumes the pre-collected value when available; the in-task `rpassword::prompt_password()` call remains only as the fallback for the OpenSSH-style "all key methods failed, prompt for password" path (which has no dispatcher-side collection because it is opportunistic).
- Thread `ssh_password: Option<Arc<Password>>` through the executor (`ParallelExecutor::with_ssh_password`), `ExecutionConfig`, `ConnectionConfig`, and the SFTP `*_with_jump_hosts` helpers. Every per-node task clones the `Arc` so all tasks observe the same secret without copying it.
- For `-S`: emit `tracing::warn!` in the dispatcher when the flag is passed to a subcommand where it has no effect (`ping`, `upload`, `download`, `list`, `cache-stats`). Chose a warning over a hard reject to match typical CLI UX — existing scripts that happen to pass `-S` to non-applicable subcommands keep working but the user gets visible feedback. `exec` and interactive paths continue to honor `-S` as before.

Tests:

- 9 unit tests in `src/security/password.rs` mirror the structure of `src/security/sudo.rs`: creation, empty rejection, debug redaction, clone independence, `Arc` sharing, env var handling (empty/valid/unset), and a dispatcher-collection-pattern test that verifies a single `get_password()` call yields an `Arc<Password>` observably shared across three simulated per-node tasks.
- 2 new tests in `src/ssh/auth.rs`: `test_auth_context_with_pre_collected_password` checks the builder and Arc sharing semantics; `test_password_auth_uses_pre_collected_password` verifies `password_auth()` returns immediately without prompting when the pre-collected value is set (critical: it never blocks on stdin in non-interactive test environments).
- All existing tests (1233 lib tests + integration suites) continue to pass. `cargo build --release`, `cargo clippy --all-targets --all-features -- -D warnings`, and `cargo fmt --check` all pass.

Closes #200
@inureyes inureyes added type:bug Something isn't working priority:high High priority issue status:review Under review labels May 15, 2026
@inureyes inureyes force-pushed the fix/issue-200-collect-password-once branch from 314ee84 to 8ee5e8b Compare May 15, 2026 06:35
@inureyes inureyes changed the title fix: collect --password once up-front; honor -S consistently fix: collect --password once up-front and honor -S consistently across subcommands May 15, 2026
inureyes added 3 commits May 15, 2026 15:59
…nd paths

Addresses pr-reviewer findings on PR #201:
- jump::chain::auth::determine_auth_method now consumes Arc<Password>
  instead of prompting per-call (issue #200 C1)
- ssh::client::SshClient::connect_and_execute_with_host_check accepts
  Arc<Password> so the download glob-resolution step reuses the
  dispatcher's single prompt (issue #200 C2)
- commands::exec::execute_command_with_forwarding now builds
  AuthMethod::with_password from the pre-collected secret instead of
  erroring after the user has already entered a password (issue #200 M1)
`tracing::warn!` lands on stdout under the default tracing subscriber,
so the `-S has no effect` warning was being mixed into stdout —
breaking `bssh ... ping | grep ...` style pipelines and contradicting
the PR description's contract.

Switch to `eprintln!` to match the existing pattern used for the
`BSSH_PASSWORD` env warning in `src/security/password.rs:168-171`.
Add CHANGELOG entry for #201 (collect --password once up-front, -S
warning to stderr). Document the new BSSH_PASSWORD environment variable
in the README environment-variables section and in the bssh.1 man page.
Update the --password flag description in the man page to describe the
single-prompt / Arc-shared behavior introduced by #201.
@inureyes inureyes added status:done Completed and removed status:review Under review labels May 15, 2026
@inureyes
Copy link
Copy Markdown
Member Author

PR Finalization Complete

Tests

All targeted tests pass. The existing test_get_password_dispatcher_collection_pattern test in src/security/password.rs already covers the M2 gap flagged by the pr-reviewer: it models the exact dispatcher pattern (collect once via get_password(), wrap in Arc, clone to three simulated per-node tasks, verify identical password observed). No additional dispatcher-branch tests were needed.

Final test counts:

  • cargo test --lib security: 76 passed, 0 failed
  • cargo test --lib ssh::auth: 13 passed, 0 failed
  • cargo test --lib jump::chain: 15 passed, 0 failed

Documentation

Added in commit aeca1f4:

  • CHANGELOG.md[2.1.5] - Unreleased section with Fixed entries for the single-prompt --password behavior and the -S stderr warning routing.
  • README.md — Updated "Password Authentication" section to describe the once-up-front collection and added a new "SSH Password Variable" subsection documenting BSSH_PASSWORD with security warning, parallel to the existing BSSH_SUDO_PASSWORD subsection.
  • docs/man/bssh.1 — Updated --password flag description to describe the single-prompt / Arc-shared behavior; added BSSH_PASSWORD entry to the ENVIRONMENT section.

Lint / Format

  • cargo fmt --all -- --check: clean
  • cargo clippy --lib --tests -- -D warnings: clean
  • cargo check --lib --tests: clean

Labels

The PR was not merged. Ready for merge review.

@inureyes inureyes merged commit 99979e7 into main May 15, 2026
2 checks passed
@inureyes inureyes deleted the fix/issue-200-collect-password-once branch May 15, 2026 07:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority:high High priority issue status:done Completed type:bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: collect --password once up-front instead of per-connection; honor -S consistently across subcommands

1 participant