diff --git a/CHANGELOG.md b/CHANGELOG.md index 55a75f3..5cd955e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ commands, `--json` output and exit codes. Each release's notes start with its se ## [Unreleased] +### Fixed + +- Usage errors no longer list `--api-url` or `--profile` in the `Usage:` line when only + `HODEISHIELD_API_URL` or `HODEISHIELD_PROFILE` sets them. The variables still work the same and + `--help` still names them. + ([#201](https://github.com/Hodeitek/hodeishield-cli/issues/201)) + ## [0.5.0] - 2026-10-15 ### Added diff --git a/crates/hodeishield-cli/src/cli.rs b/crates/hodeishield-cli/src/cli.rs index 0505f85..0e80849 100644 --- a/crates/hodeishield-cli/src/cli.rs +++ b/crates/hodeishield-cli/src/cli.rs @@ -37,24 +37,22 @@ Exit codes: #[derive(Debug, Parser)] #[command(name = "hodeishield", version, propagate_version = true, after_help = AFTER_HELP)] pub struct Cli { - /// Configuration profile to use. + /// Configuration profile to use. [env: HODEISHIELD_PROFILE] + // Read from the environment in `commands::run`, not by clap: clap would list the flag in every + // usage line while the variable is set. #[arg( long, global = true, help_heading = "Global options", - env = "HODEISHIELD_PROFILE", - hide_env_values = true, value_name = "NAME" )] pub profile: Option, - /// Base URL of the API, overriding the profile. + /// Base URL of the API, overriding the profile. [env: HODEISHIELD_API_URL] #[arg( long, global = true, help_heading = "Global options", - env = "HODEISHIELD_API_URL", - hide_env_values = true, value_name = "URL" )] pub api_url: Option, @@ -149,7 +147,7 @@ With --device, you get a short code to type in on any other device instead. On the consent screen you choose the one tenant the CLI may read; the token covers only that \ tenant, and only reads. The scopes asked for are the ones the CLI's commands need (see \ `config set oauth_scopes` to ask for fewer). Revoke the access with `hodeishield logout`, or in the \ -app under Account → Application access."; +app under Account → Applications."; #[derive(Debug, Args)] pub struct LoginArgs { diff --git a/crates/hodeishield-cli/src/commands/mod.rs b/crates/hodeishield-cli/src/commands/mod.rs index a585398..528b091 100644 --- a/crates/hodeishield-cli/src/commands/mod.rs +++ b/crates/hodeishield-cli/src/commands/mod.rs @@ -112,11 +112,16 @@ impl Context { } } +/// A flag's environment fallback; an empty or blank value counts as unset. +fn env_value(name: &str) -> Option { + std::env::var(name).ok().filter(|v| !v.trim().is_empty()) +} + pub fn run(cli: Cli, out: &mut dyn Write) -> Result<()> { let ctx = Context { overrides: Overrides { - profile: cli.profile, - api_url: cli.api_url, + profile: cli.profile.or_else(|| env_value("HODEISHIELD_PROFILE")), + api_url: cli.api_url.or_else(|| env_value("HODEISHIELD_API_URL")), }, json: cli.json, verbose: cli.verbose, diff --git a/crates/hodeishield-cli/src/commands/session.rs b/crates/hodeishield-cli/src/commands/session.rs index d868ac1..f9591ea 100644 --- a/crates/hodeishield-cli/src/commands/session.rs +++ b/crates/hodeishield-cli/src/commands/session.rs @@ -162,7 +162,7 @@ fn unstored_notice(revoked: &Result<()>) -> String { Err(revocation) => format!( "The session could not be stored on this machine, and the app did not confirm \ revoking the tokens issued for it ({}). Revoke the CLI's access in the app under \ - \"Application access\".", + Account → Applications.", clean(&revocation.message) ), } @@ -1321,7 +1321,7 @@ mod tests { fn the_notice_for_unrevoked_tokens_points_to_the_app() { let notice = unstored_notice(&Err(Failure::general("the app refused to revoke"))); assert!( - notice.contains("did not confirm") && notice.contains("Application access"), + notice.contains("did not confirm") && notice.contains("Account → Applications"), "{notice}" ); } diff --git a/crates/hodeishield-cli/tests/cli.rs b/crates/hodeishield-cli/tests/cli.rs index 15bc171..fbd1223 100644 --- a/crates/hodeishield-cli/tests/cli.rs +++ b/crates/hodeishield-cli/tests/cli.rs @@ -108,6 +108,50 @@ fn assert_no_key(output: &std::process::Output) { ); } +/// A variable that only sets a global option must not turn that option into part of the usage line. +#[test] +fn usage_line_does_not_list_options_set_by_the_environment() { + let env = Env::new(); + let output = env + .cmd() + .env("HODEISHIELD_API_URL", "http://127.0.0.1:9") + .env("HODEISHIELD_PROFILE", "default") + .args(["vendors", "get"]) + .output() + .expect("run"); + let stderr = String::from_utf8_lossy(&output.stderr); + let usage = stderr + .lines() + .find(|l| l.starts_with("Usage:")) + .expect(&stderr); + assert!( + !usage.contains("--api-url") && !usage.contains("--profile"), + "{usage}" + ); +} + +/// The flag wins over the variable, and the variable still selects the profile when there is no flag. +#[test] +fn profile_flag_beats_the_environment() { + let env = Env::new(); + let missing = "Profile `does-not-exist` is not defined"; + let with_flag = env + .cmd() + .env("HODEISHIELD_PROFILE", "does-not-exist") + .args(["--profile", "default", "config", "show"]) + .output() + .expect("run"); + assert!(with_flag.status.success(), "{with_flag:?}"); + assert!(!String::from_utf8_lossy(&with_flag.stderr).contains(missing)); + let without_flag = env + .cmd() + .env("HODEISHIELD_PROFILE", "does-not-exist") + .args(["config", "show"]) + .output() + .expect("run"); + assert!(String::from_utf8_lossy(&without_flag.stderr).contains(missing)); +} + /// `--help` names the variables but never prints their values, which end up in terminals and CI logs. #[test] fn help_does_not_print_the_values_of_the_environment() { diff --git a/deny.toml b/deny.toml index 6132258..c7b30e4 100644 --- a/deny.toml +++ b/deny.toml @@ -17,6 +17,9 @@ feature-depth = 1 # (.github/scripts/advisory-exceptions.py), and cargo audit ignores exactly the same list. [advisories] ignore = [ + { id = "RUSTSEC-2026-0370", reason = "Unmaintained notice, not a vulnerability: the code of zvariant 5.15.0 was merged into zbus 6.0.0. Reached only through zbus 5 <- secret-service 5.2.0 <- zbus-secret-service-keyring-store 1.0.1, the Linux Secret Service keychain backend. The latest release of both secret-service and the keyring store still depend on zbus 5, so there is no upgrade yet. Review by 2027-01-31." }, + { id = "RUSTSEC-2026-0371", reason = "Unmaintained notice, not a vulnerability: the code of zbus_names 4.3.4 was merged into zbus 6.0.0. Reached only through zbus 5 <- secret-service 5.2.0 <- zbus-secret-service-keyring-store 1.0.1, the Linux Secret Service keychain backend. The latest release of both secret-service and the keyring store still depend on zbus 5, so there is no upgrade yet. Review by 2027-01-31." }, + { id = "RUSTSEC-2026-0372", reason = "Unmaintained notice, not a vulnerability: the code of zvariant_derive 5.15.0 was merged into zbus 6.0.0. Reached only through zbus 5 <- secret-service 5.2.0 <- zbus-secret-service-keyring-store 1.0.1, the Linux Secret Service keychain backend. The latest release of both secret-service and the keyring store still depend on zbus 5, so there is no upgrade yet. Review by 2027-01-31." }, ] # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html diff --git a/osv-scanner.toml b/osv-scanner.toml new file mode 100644 index 0000000..b25f1f9 --- /dev/null +++ b/osv-scanner.toml @@ -0,0 +1,17 @@ +# OSV-Scanner configuration (read by the scan in .github/workflows/security-tests.yml). +# Same acceptances, reasons and review date as [advisories] ignore in deny.toml. + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0370" +ignoreUntil = 2027-01-31 +reason = "Unmaintained notice, not a vulnerability: the code of zvariant 5.15.0 was merged into zbus 6.0.0. Reached only through zbus 5 <- secret-service 5.2.0 <- zbus-secret-service-keyring-store 1.0.1, the Linux Secret Service keychain backend. The latest release of both secret-service and the keyring store still depend on zbus 5, so there is no upgrade yet. Review by 2027-01-31." + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0371" +ignoreUntil = 2027-01-31 +reason = "Unmaintained notice, not a vulnerability: the code of zbus_names 4.3.4 was merged into zbus 6.0.0. Reached only through zbus 5 <- secret-service 5.2.0 <- zbus-secret-service-keyring-store 1.0.1, the Linux Secret Service keychain backend. The latest release of both secret-service and the keyring store still depend on zbus 5, so there is no upgrade yet. Review by 2027-01-31." + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0372" +ignoreUntil = 2027-01-31 +reason = "Unmaintained notice, not a vulnerability: the code of zvariant_derive 5.15.0 was merged into zbus 6.0.0. Reached only through zbus 5 <- secret-service 5.2.0 <- zbus-secret-service-keyring-store 1.0.1, the Linux Secret Service keychain backend. The latest release of both secret-service and the keyring store still depend on zbus 5, so there is no upgrade yet. Review by 2027-01-31."