Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,10 +308,10 @@ When a CLI registers auth providers or configures a default provider, `cli_engin

| Command | Behavior |
| --- | --- |
| `auth login --provider NAME --env ENV` | Clears cached credentials for the environment and authenticates. |
| `auth login --provider NAME [--env ENV]` | Clears cached credentials for the explicit environment, or the active middleware environment when omitted, and authenticates. |
| `auth status --provider NAME --env ENV` | Shows cached status for one provider and environment. |
| `auth status` | Shows status for all providers and cached environments. |
| `auth logout --provider NAME --env ENV` | Clears cached credentials for the environment. |
| `auth logout --provider NAME [--env ENV]` | Clears cached credentials for the explicit environment, or the active middleware environment when omitted. |

These commands are implemented with the same `CommandSpec`, middleware, output envelope, and renderers
as application commands.
Expand Down
33 changes: 27 additions & 6 deletions src/auth/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use serde_json::{Value, json};

use super::Dispatcher;
use crate::{
CommandResult, CommandSpec, Credential, GroupSpec, Result, RuntimeCommandSpec,
RuntimeGroupSpec, Tier,
CliCoreError, CommandContext, CommandResult, CommandSpec, Credential, GroupSpec, Result,
RuntimeCommandSpec, RuntimeGroupSpec, Tier,
};

/// Data rendered after a successful `auth login`.
Expand Down Expand Up @@ -48,7 +48,7 @@ pub fn auth_command_group(default_provider: &str, registered_names: &[String]) -
.mutates(true)
.no_auth(true)
.with_arg(provider_arg(&effective_default, registered_names))
.with_arg(Arg::new("env").long("env").value_name("ENV").required(true))
.with_arg(Arg::new("env").long("env").value_name("ENV"))
.with_arg(
Arg::new("scope")
.long("scope")
Expand All @@ -62,7 +62,7 @@ pub fn auth_command_group(default_provider: &str, registered_names: &[String]) -
),
async |context| {
let provider = string_arg(&context.args, "provider");
let env = string_arg(&context.args, "env");
let env = env_arg(&context)?;
let scopes = string_vec_arg(&context.args, "scope");
serde_json::to_value(
login_and_build_with_scopes(&context.middleware.auth, &provider, &env, &scopes)
Expand Down Expand Up @@ -93,10 +93,10 @@ pub fn auth_command_group(default_provider: &str, registered_names: &[String]) -
.mutates(true)
.no_auth(true)
.with_arg(provider_arg(&effective_default, registered_names))
.with_arg(Arg::new("env").long("env").value_name("ENV").required(true)),
.with_arg(Arg::new("env").long("env").value_name("ENV")),
async |context| {
let provider = string_arg(&context.args, "provider");
let env = string_arg(&context.args, "env");
let env = env_arg(&context)?;
logout_result(&context.middleware.auth, &provider, &env)
.await
.map(CommandResult::new)
Expand All @@ -119,6 +119,27 @@ fn string_arg(args: &serde_json::Map<String, Value>, name: &str) -> String {
.to_owned()
}

fn env_arg(context: &CommandContext) -> Result<String> {
if let Some(env) = context.user_args.get("env").and_then(Value::as_str) {
if env.is_empty() {
return Err(missing_env_error());
}
return Ok(env.to_owned());
}

if !context.middleware.env.is_empty() {
return Ok(context.middleware.env.clone());
}

Err(missing_env_error())
}

fn missing_env_error() -> CliCoreError {
CliCoreError::message(
"auth: missing environment; pass --env or configure a default environment",
)
}

/// Reads a repeatable string argument as a `Vec<String>`, accepting either a
/// JSON array (multiple values) or a single string.
fn string_vec_arg(args: &serde_json::Map<String, Value>, name: &str) -> Vec<String> {
Expand Down
172 changes: 161 additions & 11 deletions tests/foundation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2374,6 +2374,108 @@ async fn cli_runtime_auth_login_uses_registered_provider_default() {
);
}

#[tokio::test]
async fn cli_runtime_auth_login_uses_middleware_env_when_env_flag_omitted() {
let cli = auth_cli_with_default_env("dev");

let output = cli
.run(["my-cli", "auth", "login", "--output", "json"])
.await;

assert_eq!(output.exit_code, 0, "{}", output.rendered);
let rendered: serde_json::Value = serde_json::from_str(&output.rendered).expect("valid json");
assert_eq!(
rendered["data"],
json!({
"provider": "primary",
"env": "dev",
"identity": "tester",
"expires_at": "2099-01-01T00:00:00Z"
})
);
}

#[tokio::test]
async fn cli_runtime_auth_login_env_flag_overrides_middleware_env() {
let cli = auth_cli_with_default_env("dev");

let output = cli
.run([
"my-cli", "auth", "login", "--env", "prod", "--output", "json",
])
.await;

assert_eq!(output.exit_code, 0, "{}", output.rendered);
let rendered: serde_json::Value = serde_json::from_str(&output.rendered).expect("valid json");
assert_eq!(rendered["data"]["env"], "prod");
}

#[tokio::test]
async fn cli_runtime_auth_login_empty_env_flag_errors_instead_of_using_middleware_env() {
let cli = auth_cli_with_default_env("dev");

let output = cli
.run(["my-cli", "auth", "login", "--env", "", "--output", "json"])
.await;

assert_ne!(output.exit_code, 0, "{}", output.rendered);
let rendered: serde_json::Value = serde_json::from_str(&output.rendered).expect("valid json");
assert_eq!(
rendered["error"]["message"],
"auth: missing environment; pass --env or configure a default environment"
);
}

#[tokio::test]
async fn cli_runtime_auth_login_errors_when_env_missing() {
let cli = auth_cli_without_default_env();

let output = cli
.run(["my-cli", "auth", "login", "--output", "json"])
.await;

assert_ne!(output.exit_code, 0, "{}", output.rendered);
let rendered: serde_json::Value = serde_json::from_str(&output.rendered).expect("valid json");
assert_eq!(
rendered["error"]["message"],
"auth: missing environment; pass --env or configure a default environment"
);
}

#[tokio::test]
async fn cli_runtime_auth_logout_uses_middleware_env_when_env_flag_omitted() {
let cli = auth_cli_with_default_env("dev");

let output = cli
.run(["my-cli", "auth", "logout", "--output", "json"])
.await;

assert_eq!(output.exit_code, 0, "{}", output.rendered);
let rendered: serde_json::Value = serde_json::from_str(&output.rendered).expect("valid json");
assert_eq!(
rendered["data"],
json!({"provider": "primary", "env": "dev", "status": "logged out"})
);
}

#[tokio::test]
async fn cli_runtime_auth_logout_env_flag_overrides_middleware_env() {
let cli = auth_cli_with_default_env("dev");

let output = cli
.run([
"my-cli", "auth", "logout", "--env", "prod", "--output", "json",
])
.await;

assert_eq!(output.exit_code, 0, "{}", output.rendered);
let rendered: serde_json::Value = serde_json::from_str(&output.rendered).expect("valid json");
assert_eq!(
rendered["data"],
json!({"provider": "primary", "env": "prod", "status": "logged out"})
);
}

#[tokio::test]
async fn cli_runtime_auth_commands_use_init_deps_registered_providers() {
let init_count = Arc::new(AtomicUsize::new(0));
Expand Down Expand Up @@ -4085,18 +4187,66 @@ fn auth_command_group_sets_provider_defaults() {
.contains("one of: [primary, oauth, device]")
);

let logout = group
.commands
.iter()
.find(|command| command.spec.name == "logout")
.expect("logout subcommand should exist");
assert!(
logout
.spec
.args
for command_name in ["login", "logout"] {
let command = group
.commands
.iter()
.any(|arg| arg.get_id() == "env" && arg.is_required_set())
);
.find(|command| command.spec.name == command_name)
.expect("auth subcommand should exist");
assert!(
command
.spec
.args
.iter()
.any(|arg| arg.get_id() == "env" && !arg.is_required_set())
);
}
}

fn auth_cli_with_default_env(env: &'static str) -> Cli {
Cli::new(CliConfig {
name: "my-cli".to_owned(),
short: "Developer tooling".to_owned(),
app_id: "my-cli".to_owned(),
register_flags: Some(Arc::new(move |command: Command| {
command.arg(
Arg::new("env")
.long("env")
.global(true)
.default_value(env)
.value_name("ENV")
.help("Target environment"),
)
})),
apply_flags: Some(Arc::new(|matches, middleware| {
if let Some(env) = matches.get_one::<String>("env") {
middleware.env = env.clone();
}
Ok(())
})),
auth_providers: vec![Arc::new(FakeProvider {
name: "primary".to_owned(),
identity: "tester".to_owned(),
logout_fails: false,
environments: vec!["dev".to_owned(), "prod".to_owned()],
})],
..CliConfig::default()
})
}

fn auth_cli_without_default_env() -> Cli {
Cli::new(CliConfig {
name: "my-cli".to_owned(),
short: "Developer tooling".to_owned(),
app_id: "my-cli".to_owned(),
auth_providers: vec![Arc::new(FakeProvider {
name: "primary".to_owned(),
identity: "tester".to_owned(),
logout_fails: false,
environments: vec!["dev".to_owned(), "prod".to_owned()],
})],
..CliConfig::default()
})
}

#[test]
Expand Down