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
32 changes: 29 additions & 3 deletions rust/src/db/tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//! matching `platform app deploy`.
//!
//! Auth: the CLI mints a short-lived agent token from the hosting API
//! (`POST /v1/hosting/nodejs/apps/:id/agent-token`) using your GoDaddy OAuth
//! (`POST /v1/hosting/apps/NODEJS-:id/agent-token`) using your GoDaddy OAuth
//! credential, stepped up to the dedicated `hosting.database.tunnel:execute`
//! scope alongside deploy-execute — the tunnel scope is a separate grant, so
//! authority to publish a deployment does not by itself grant raw database
Expand All @@ -36,6 +36,7 @@ use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, connect_async_with_conf

use crate::error::GddyError;
use crate::hosting::client::HostingClient;
use crate::hosting::common::HostingAppType;
use crate::http::api_url_for_env;
use crate::scopes::HOSTING_DATABASE_TUNNEL_EXECUTE as DATABASE_TUNNEL;
use crate::scopes::HOSTING_DEPLOYMENT_EXECUTE as DEPLOY_EXECUTE;
Expand Down Expand Up @@ -66,6 +67,11 @@ struct TunnelArgs {
#[arg(long = "app-id", value_name = "APP_ID")]
app_id: String,

/// Hosting product of the app (e.g. `nodejs`). Selects which product's
/// agent mints the tunnel token.
#[arg(long = "product", value_name = "PRODUCT", ignore_case = true)]
product: HostingAppType,
Comment on lines +72 to +73

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you review this finding and resolve through code change or explanation?


/// Local TCP port MySQL clients connect to.
#[arg(long, value_name = "PORT", default_value_t = 3306)]
port: u16,
Expand Down Expand Up @@ -177,7 +183,7 @@ async fn run_tunnel(
sender
.send(json!({ "type": "step", "name": "authorize", "status": "started" }))
.await;
let (agent_url, token) = match mint_agent_token(ctx, &args.app_id).await {
let (agent_url, token) = match mint_agent_token(ctx, &args.app_id, args.product).await {
Ok(pair) => pair,
Err(e) => return Err(fail(sender, e).await),
};
Expand Down Expand Up @@ -305,13 +311,14 @@ async fn run_tunnel(
async fn mint_agent_token(
ctx: &CommandContext,
app_id: &str,
product: HostingAppType,
) -> cli_engine::Result<(String, String)> {
let required = vec![DEPLOY_EXECUTE.to_owned(), DATABASE_TUNNEL.to_owned()];
let token = ctx.credential_with_scopes(&required).await?.token;
let base_url = api_url_for_env(&ctx.middleware.env)?;
let client = HostingClient::new(base_url, token);
let resp = client
.get_agent_token(app_id)
.get_agent_token(app_id, product.as_str())
.await
.map_err(|e| GddyError::from(e).into_cli_error())?;
let agent_url = field_str(&resp, "agentUrl")?;
Expand Down Expand Up @@ -598,6 +605,25 @@ fn map_ws_err(err: tokio_tungstenite::tungstenite::Error) -> GddyError {
mod tests {
use super::build_tunnel_ws_url;

#[test]
fn product_flag_parses_case_insensitively_to_wire_value() {
use crate::hosting::common::HostingAppType;
use clap::Parser;

// Wrap the flattened args so clap can parse them standalone in a test.
#[derive(Parser)]
struct Wrap {
#[command(flatten)]
args: super::TunnelArgs,
}

let wrap = Wrap::try_parse_from(["db-tunnel", "--app-id", "app-1", "--product", "nodejs"])
.expect("--product nodejs should parse");
assert_eq!(wrap.args.product, HostingAppType::Nodejs);
// The lowercase CLI value maps to the uppercase wire value sent as `?appType=`.
assert_eq!(wrap.args.product.as_str(), "NODEJS");
}

#[test]
fn rejects_plaintext_schemes() {
// The agent URL comes from the service, never the operator, so a
Expand Down
16 changes: 11 additions & 5 deletions rust/src/hosting/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,13 +290,19 @@ impl HostingClient {
/// `hosting.database.tunnel:execute` scope in addition to `hosting.deployment:execute`,
/// so publish authority alone does not yield a database-tunnel agent token.
///
/// This mint lives under the Node.js-specific `/v1/hosting/nodejs` path
/// rather than the flattened `/v1/hosting` base the other methods use, so it
/// is spelled out with a leading `/nodejs` segment to reach the server route.
pub async fn get_agent_token(&self, app_id: &str) -> Result<Value, ClientError> {
/// The product rides as a prefix on the app id —
/// `/v1/hosting/apps/{app_type}-{id}/agent-token` (e.g. `NODEJS-{id}`) — rather
/// than a product path segment, matching how the hosting app collection is
/// addressed; the origin strips the prefix back to the raw nanoid. `app_type`
/// is the wire value from `HostingAppType::as_str` (e.g. `NODEJS`).
pub async fn get_agent_token(
&self,
app_id: &str,
app_type: &str,
) -> Result<Value, ClientError> {
// Secret response: the body is a minted bearer token, so it must never
// reach the `--debug transport` trace (cli-engine would print it in full).
self.post_empty_json_secret_response(&format!("/nodejs/apps/{app_id}/agent-token"))
self.post_empty_json_secret_response(&format!("/apps/{app_type}-{app_id}/agent-token"))
.await
}

Expand Down
4 changes: 2 additions & 2 deletions rust/src/hosting/client_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ async fn get_agent_token_posts_empty_body_and_returns_url_and_token() {
let mock = server
.mock_async(|when, then| {
when.method(POST)
.path("/v1/hosting/nodejs/apps/app-1/agent-token")
.path("/v1/hosting/apps/NODEJS-app-1/agent-token")
.header("authorization", "Bearer test-token")
.json_body(json!({}));
then.status(200).json_body(json!({
Expand All @@ -549,7 +549,7 @@ async fn get_agent_token_posts_empty_body_and_returns_url_and_token() {
.await;

let body = client(&server.base_url())
.get_agent_token("app-1")
.get_agent_token("app-1", "NODEJS")
.await
.expect("get agent token");

Expand Down
Loading