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
10 changes: 5 additions & 5 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ path = "src/main.rs"
async-trait = "0.1"
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
clap = { version = "4.5", features = ["std", "string"] }
cli-engine = { features = ["pkce-auth"], version = "0.1.3" }
cli-engine = { features = ["pkce-auth"], version = "0.2" }
dirs = "6"
fancy-regex = "0.14"
regex = { version = "1", features = ["std"] }
Expand Down
4 changes: 3 additions & 1 deletion rust/src/actions_catalog/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ pub fn module() -> Module {
.with_command(RuntimeCommandSpec::new(
CommandSpec::new("list", "List all available action contracts")
.with_system("actions")
.with_tier(Tier::Read),
.with_tier(Tier::Read)
.no_auth(true),
|_cred, _args| async move {
let actions: Vec<_> = ACTIONS
.iter()
Expand All @@ -79,6 +80,7 @@ pub fn module() -> Module {
CommandSpec::new("describe", "Show detailed schema for an action contract")
.with_system("actions")
.with_tier(Tier::Read)
.no_auth(true)
.with_arg(
clap::Arg::new("action")
.value_name("ACTION")
Expand Down
9 changes: 4 additions & 5 deletions rust/src/api_explorer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ fn list_command() -> RuntimeCommandSpec {
CommandSpec::new("list", "List all API domains")
.with_system("api")
.with_tier(Tier::Read)
.no_auth(true)
.with_default_fields("domain,title,endpoints,baseUrl")
.with_arg(
clap::Arg::new("domain")
Expand Down Expand Up @@ -253,6 +254,7 @@ fn describe_command() -> RuntimeCommandSpec {
)
.with_system("api")
.with_tier(Tier::Read)
.no_auth(true)
.with_arg(
clap::Arg::new("endpoint")
.value_name("ENDPOINT")
Expand Down Expand Up @@ -298,6 +300,7 @@ fn search_command() -> RuntimeCommandSpec {
CommandSpec::new("search", "Search API endpoints by keyword")
.with_system("api")
.with_tier(Tier::Read)
.no_auth(true)
.with_default_fields("domain,method,path,summary")
.with_arg(
clap::Arg::new("query")
Expand Down Expand Up @@ -443,11 +446,7 @@ fn call_command() -> RuntimeCommandSpec {
.get("method")
.and_then(|v| v.as_str())
.unwrap_or("GET");
let token = ctx
.credential
.as_ref()
.map(|c| c.token.clone())
.unwrap_or_default();
let token = ctx.credential().await?.token;
let base_url = crate::application::client::api_url_for_env(&ctx.middleware.env);
let url = format!("{base_url}{endpoint}");

Expand Down
80 changes: 61 additions & 19 deletions rust/src/application/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ use serde_json::json;

use crate::application::client::{ApplicationClient, api_url_for_env};

fn make_client(ctx: &cli_engine::CommandContext) -> cli_engine::Result<ApplicationClient> {
let token = ctx
.credential
.as_ref()
.map(|c| c.token.clone())
.unwrap_or_default();
async fn make_client(ctx: &cli_engine::CommandContext) -> cli_engine::Result<ApplicationClient> {
// Lazily resolve the credential; this triggers the auth flow only for
// commands that actually call the API.
let token = ctx.credential().await?.token;
let base_url = api_url_for_env(&ctx.middleware.env);
Ok(ApplicationClient::new(base_url, token))
}
Expand Down Expand Up @@ -48,7 +46,7 @@ fn list_command() -> RuntimeCommandSpec {
.with_tier(Tier::Read)
.with_default_fields("name,label,status"),
|ctx| async move {
let client = make_client(&ctx)?;
let client = make_client(&ctx).await?;
let data = client.list_applications().await.map_err(client_err)?;
Ok(CommandResult::new(data).with_next_actions(vec![
NextAction::new(
Expand Down Expand Up @@ -80,7 +78,7 @@ fn info_command() -> RuntimeCommandSpec {
),
|ctx| async move {
let name = arg_str(&ctx, "name").to_owned();
let client = make_client(&ctx)?;
let client = make_client(&ctx).await?;
let data = client.get_application(&name).await.map_err(client_err)?;
Ok(
CommandResult::new(data["application"].clone()).with_next_actions(vec![
Expand Down Expand Up @@ -178,7 +176,7 @@ fn init_command() -> RuntimeCommandSpec {
.map(|s| s.split(',').map(|p| p.trim().to_owned()).collect())
.unwrap_or_else(|| vec!["apps.app-registry:read".to_owned()]);

let client = make_client(&ctx)?;
let client = make_client(&ctx).await?;
let data = client
.create_application(json!({
"name": name,
Expand Down Expand Up @@ -235,6 +233,7 @@ fn validate_command() -> RuntimeCommandSpec {
CommandSpec::new("validate", "Validate godaddy.toml config")
.with_system("applications")
.with_tier(Tier::Read)
.no_auth(true)
.with_arg(
clap::Arg::new("config")
.long("config")
Expand Down Expand Up @@ -294,7 +293,7 @@ fn update_command() -> RuntimeCommandSpec {
if let Some(desc) = ctx.args.get("description").and_then(|v| v.as_str()) {
input.insert("description".to_owned(), json!(desc));
}
let client = make_client(&ctx)?;
let client = make_client(&ctx).await?;
let data = client
.update_application(&id, json!(input))
.await
Expand Down Expand Up @@ -356,7 +355,7 @@ fn enable_command() -> RuntimeCommandSpec {
|ctx| async move {
let name = arg_str(&ctx, "name").to_owned();
let store_id = arg_str(&ctx, "store-id").to_owned();
let client = make_client(&ctx)?;
let client = make_client(&ctx).await?;
let data = client
.enable_application(json!({ "applicationName": name, "storeId": store_id }))
.await
Expand Down Expand Up @@ -422,7 +421,7 @@ fn disable_command() -> RuntimeCommandSpec {
|ctx| async move {
let name = arg_str(&ctx, "name").to_owned();
let store_id = arg_str(&ctx, "store-id").to_owned();
let client = make_client(&ctx)?;
let client = make_client(&ctx).await?;
let data = client
.disable_application(json!({ "applicationName": name, "storeId": store_id }))
.await
Expand Down Expand Up @@ -482,7 +481,7 @@ fn archive_command() -> RuntimeCommandSpec {
),
|ctx| async move {
let name = arg_str(&ctx, "name").to_owned();
let client = make_client(&ctx)?;
let client = make_client(&ctx).await?;
let app_data = client.get_application(&name).await.map_err(client_err)?;
let app_id = app_data["application"]["id"]
.as_str()
Expand Down Expand Up @@ -540,7 +539,7 @@ fn release_command() -> RuntimeCommandSpec {
if let Some(desc) = description {
input["description"] = json!(desc);
}
let client = make_client(&ctx)?;
let client = make_client(&ctx).await?;
let data = client.create_release(input).await.map_err(client_err)?;
Ok(
CommandResult::new(data["createRelease"].clone()).with_next_actions(vec![
Expand Down Expand Up @@ -573,11 +572,7 @@ fn deploy_command() -> RuntimeCommandSpec {
.unwrap_or("")
.to_owned();
let env = ctx.middleware.env.clone();
let token = ctx
.credential
.as_ref()
.map(|c| c.token.clone())
.unwrap_or_default();
let token = ctx.credential().await?.token;
let base_url = api_url_for_env(&env);
let client = ApplicationClient::new(base_url, token);

Expand Down Expand Up @@ -831,6 +826,7 @@ pub fn add_group() -> RuntimeGroupSpec {
CommandSpec::new("action", "Add an action to godaddy.toml")
.with_system("applications")
.with_tier(Tier::Mutate)
.no_auth(true)
.with_arg(
clap::Arg::new("name")
.long("name")
Expand Down Expand Up @@ -862,6 +858,7 @@ pub fn add_group() -> RuntimeGroupSpec {
CommandSpec::new("subscription", "Add a webhook subscription to godaddy.toml")
.with_system("applications")
.with_tier(Tier::Mutate)
.no_auth(true)
.with_arg(
clap::Arg::new("name")
.long("name")
Expand Down Expand Up @@ -925,6 +922,7 @@ pub fn add_extension_group() -> RuntimeGroupSpec {
CommandSpec::new("embed", "Add an embed extension")
.with_system("applications")
.with_tier(Tier::Mutate)
.no_auth(true)
.with_arg(
clap::Arg::new("name")
.long("name")
Expand Down Expand Up @@ -974,6 +972,7 @@ pub fn add_extension_group() -> RuntimeGroupSpec {
CommandSpec::new("checkout", "Add a checkout extension")
.with_system("applications")
.with_tier(Tier::Mutate)
.no_auth(true)
.with_arg(
clap::Arg::new("name")
.long("name")
Expand Down Expand Up @@ -1023,6 +1022,7 @@ pub fn add_extension_group() -> RuntimeGroupSpec {
CommandSpec::new("blocks", "Add a blocks extension")
.with_system("applications")
.with_tier(Tier::Mutate)
.no_auth(true)
.with_arg(
clap::Arg::new("source")
.long("source")
Expand Down Expand Up @@ -1052,3 +1052,45 @@ pub fn add_extension_group() -> RuntimeGroupSpec {
},
))
}

#[cfg(test)]
mod tests {
use cli_engine::{Cli, CliConfig};

/// API commands must stay fail-closed: `application list` calls the backend,
/// so it must require authentication. Built with **no auth provider
/// registered**, the engine's default `AuthRequirement::Required` must reject
/// it before the handler runs (no network call). This guards against someone
/// mistakenly marking an API command `no_auth(true)`, which would let it run
/// unauthenticated.
#[tokio::test]
async fn application_list_requires_auth() {
let cli = Cli::new(
CliConfig::new("gddy", "GoDaddy developer CLI", "gddy")
.with_default_auth_provider("godaddy")
.with_module(super::super::module()),
);

let output = cli
.run(["gddy", "application", "list", "--output", "json"])
.await;

// The engine maps auth-resolution failures to exit code 2; together with
// the provider-named error below this proves the command was rejected at
// credential resolution, not by the handler hitting the network.
const AUTH_FAILURE_EXIT: i32 = 2;
assert_eq!(
output.exit_code, AUTH_FAILURE_EXIT,
"application list must fail closed at auth resolution, got: {}",
output.rendered
);
let json: serde_json::Value =
serde_json::from_str(&output.rendered).expect("valid json output");
let message = json["error"]["message"].as_str().unwrap_or_default();
assert!(
message.contains("provider"),
"expected an auth-provider resolution error, got: {}",
output.rendered
);
}
}
41 changes: 38 additions & 3 deletions rust/src/env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ pub fn module() -> Module {
.with_command(RuntimeCommandSpec::new(
CommandSpec::new("list", "List available environments")
.with_system("env")
.with_tier(Tier::Read),
.with_tier(Tier::Read)
.no_auth(true),
|_cred, _args| async move {
let current = get_env().unwrap_or_else(|| DEFAULT_ENV.to_owned());
let envs: Vec<_> = KNOWN_ENVS
Expand All @@ -76,7 +77,8 @@ pub fn module() -> Module {
.with_command(RuntimeCommandSpec::new(
CommandSpec::new("get", "Get the active environment")
.with_system("env")
.with_tier(Tier::Read),
.with_tier(Tier::Read)
.no_auth(true),
|_cred, _args| async move {
let env = get_env().unwrap_or_else(|| DEFAULT_ENV.to_owned());
Ok(CommandResult::new(json!({
Expand All @@ -89,6 +91,7 @@ pub fn module() -> Module {
CommandSpec::new("set", "Set the active environment")
.with_system("env")
.with_tier(Tier::Mutate)
.no_auth(true)
.with_arg(
// Distinct id from the global `--env` flag (also id "env");
// a shared id makes the positional collide with the flag's
Expand Down Expand Up @@ -125,7 +128,8 @@ pub fn module() -> Module {
.with_command(RuntimeCommandSpec::new(
CommandSpec::new("info", "Show details for the active environment")
.with_system("env")
.with_tier(Tier::Read),
.with_tier(Tier::Read)
.no_auth(true),
|_cred, _args| async move {
let env = get_env().unwrap_or_else(|| DEFAULT_ENV.to_owned());
Ok(CommandResult::new(json!({
Expand All @@ -137,3 +141,34 @@ pub fn module() -> Module {
))
})
}

#[cfg(test)]
mod tests {
use cli_engine::{Cli, CliConfig};

/// `env list` is a local-only command: it must run without any auth flow.
///
/// The CLI is built with **no auth provider registered**. Because the engine
/// authenticates fail-closed by default (`AuthRequirement::Required`), a
/// command that forgot to opt out would fail here with "no provider
/// registered". This guards that the `env` commands stay `no_auth(true)`.
#[tokio::test]
async fn env_list_runs_without_auth() {
let cli = Cli::new(
CliConfig::new("gddy", "GoDaddy developer CLI", "gddy").with_module(super::module()),
);

let output = cli.run(["gddy", "env", "list", "--output", "json"]).await;

assert_eq!(output.exit_code, 0, "rendered output: {}", output.rendered);
let json: serde_json::Value =
serde_json::from_str(&output.rendered).expect("valid json output");
let envs = json["data"].as_array().expect("data array");
assert!(
envs.iter()
.any(|e| e["name"] == "ote" || e["name"] == "prod"),
"expected known environments in output, got: {}",
output.rendered
);
}
}
6 changes: 1 addition & 5 deletions rust/src/webhook/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ pub fn module() -> Module {
.with_system("webhooks")
.with_tier(Tier::Read),
|ctx| async move {
let token = ctx
.credential
.as_ref()
.map(|c| c.token.clone())
.unwrap_or_default();
let token = ctx.credential().await?.token;
let base_url = api_url_for_env(&ctx.middleware.env);
let url = format!("{base_url}/v1/apis/webhook-event-types");
let resp = crate::application::client::make_http_client()
Expand Down
Loading