diff --git a/rust/src/pat/guides/auth.md b/rust/src/pat/guides/auth.md index 95e52c5b..9a67a97b 100644 --- a/rust/src/pat/guides/auth.md +++ b/rust/src/pat/guides/auth.md @@ -21,16 +21,10 @@ Your browser opens to the GoDaddy login screen. After you approve the CLI, an ac ### Creating a PAT -1. Sign in to the GoDaddy Developer Portal. -2. Open **Personal Access Tokens**. -3. Choose a name, the scopes you need, and an expiration (up to 365 days). -4. Copy the plaintext token immediately — it is shown only once. - -The token looks like: - -```text -gd_pat__<8-hex-crc> -``` +1. Sign in to the [Personal Access Token page](https://developer.godaddy.com/personal-access-token). +2. Click **+ Generate Token**. +3. In the **Generate personal access token** dialog, fill in a **Name**, an **Expiration** (in days), and the **Scopes** the token needs (see the [PAT scopes reference](https://developer.godaddy.com/en/docs/api-users/auth#pat-scopes) — e.g. `domains.domain:read`, `domains.dns:update`). A write-scoped token also satisfies reads for the same resource; a read-scoped token is refused on writes. +4. Click **Generate Token**. The token is shown once in a "Copy your new token" dialog — copy it immediately. You can't retrieve it again from the Personal Access Token page; if you lose it, revoke it and generate a new one. ### Storing a PAT in the CLI @@ -62,7 +56,7 @@ Remove the PAT for an environment: gddy pat remove --env prod ``` -Removing the PAT from the CLI does **not** revoke it in the Developer Portal. +Removing the PAT from the CLI does **not** revoke it in the Developer Portal. To revoke it for real, go to the [Personal Access Token page](https://developer.godaddy.com/personal-access-token), click the trash icon next to the token, and confirm. ### Using PATs in CI @@ -104,5 +98,5 @@ The GoDaddy API gateway exchanges the PAT for a short-lived access token and enf - Treat PATs like passwords. Do not commit them to source control. - Prefer `GDDY_PAT_` environment variables in CI over storing PATs in the registry file. -- Regenerate leaked or suspected PATs in the Developer Portal immediately. -- The CLI validates PAT format before storing, but does **not** contact the API to verify a PAT is still active. +- Regenerate leaked PATs on the [Personal Access Token page](https://developer.godaddy.com/personal-access-token) immediately. +- The CLI only validates PAT format before storing, but it does **not** contact the API to verify the PAT is still active or has the scopes you need. diff --git a/rust/src/pat/mod.rs b/rust/src/pat/mod.rs index 3c33d659..4367a80c 100644 --- a/rust/src/pat/mod.rs +++ b/rust/src/pat/mod.rs @@ -44,11 +44,7 @@ output_schema!(PatRemoveResult { }); /// PAT prefix advertised by GoDaddy. A valid PAT starts with this string. -pub const PAT_PREFIX: &str = "gd_pat_"; - -/// The base portion of the advertised prefix, used when validating the -/// `gd_pat__` structure. -const PAT_PREFIX_BODY: &str = "gd_pat"; +const PAT_PREFIX: &str = "gd_pat_"; /// Default env-var PAT applied to any environment. pub const PAT_ENV_VAR: &str = "GDDY_PAT"; @@ -122,21 +118,17 @@ fn save_registry(path: &std::path::Path, registry: &PatRegistry) -> Result<(), C registry.save(path) } -/// Validate that `token` looks like a GoDaddy PAT: `gd_pat__<8 hex crc>`. +/// Validate that `token` looks like a GoDaddy PAT. This only checks for the +/// `gd_pat_` prefix plus at least one character of content after it with no +/// embedded whitespace, so that we are not overly tied to the implementation +/// details of the token format while still rejecting untrimmed values (e.g. +/// `gd_pat_abc\n` from an env var or file with a trailing newline) that would +/// otherwise be sent as a garbage Bearer token. #[must_use] pub fn is_valid_pat(token: &str) -> bool { - let Some((body, crc)) = token.rsplit_once('_') else { - return false; - }; - if crc.len() != 8 || !crc.chars().all(|c| c.is_ascii_hexdigit()) { - return false; - } - let Some((prefix, entropy)) = body.rsplit_once('_') else { - return false; - }; - prefix == PAT_PREFIX_BODY - && !entropy.is_empty() - && entropy.chars().all(|c| c.is_ascii_alphanumeric()) + token + .strip_prefix(PAT_PREFIX) + .is_some_and(|rest| !rest.is_empty() && !rest.chars().any(char::is_whitespace)) } /// Returns the last four characters of a PAT. If the token is four characters or @@ -350,9 +342,9 @@ fn add_command() -> RuntimeCommandSpec { let name = string_arg(&ctx.args, "name"); let token = resolve_token_arg(&ctx.args).await?; if !is_valid_pat(&token) { - return Err(CliCoreError::message(format!( - "token does not look like a GoDaddy PAT (expected `{PAT_PREFIX}...`); refusing to store it" - ))); + return Err(CliCoreError::message( + "token doesn't look like a GoDaddy PAT; refusing to store it. Run `gddy guide auth` for details on creating PATs.", + )); } let entry = PatEntry { token, name }; @@ -484,23 +476,37 @@ mod tests { use super::*; #[test] - fn rejects_malformed_pats() { + fn rejects_tokens_without_the_pat_prefix() { assert!(!is_valid_pat("")); assert!(!is_valid_pat("notapat")); - assert!(!is_valid_pat("gd_pat_")); - assert!(!is_valid_pat("gd_pat_abc")); // missing crc - assert!(!is_valid_pat("gd_pat_abc_xyz")); // crc too short - assert!(!is_valid_pat("gd_pat_abc_zzzzzzzz")); // crc not hex - assert!(!is_valid_pat("gd_pat_ab!c_12345678")); // invalid char in entropy + assert!(!is_valid_pat("gd_pat")); // missing trailing underscore + assert!(!is_valid_pat("gd_pat_")); // nothing after the prefix + } + + #[test] + fn rejects_tokens_containing_whitespace_after_the_prefix() { + // An untrimmed env var or file (e.g. a trailing newline) should not be + // treated as valid — it would otherwise be sent as a garbage Bearer + // token. This covers both whitespace-only tails and whitespace + // embedded alongside real-looking content. + assert!(!is_valid_pat("gd_pat_ ")); + assert!(!is_valid_pat("gd_pat_\n")); + assert!(!is_valid_pat("gd_pat_\t\t")); + assert!(!is_valid_pat("gd_pat_abc123\n")); + assert!(!is_valid_pat("gd_pat_ abc123")); + assert!(!is_valid_pat("gd_pat_abc 123")); } #[test] - fn accepts_well_formed_pat() { + fn accepts_any_non_empty_value_after_the_prefix() { + // The CLI does not assume a stable shape beyond the prefix, so these + // (including the exact strings from DEVEX-889's bug report) all pass. assert!(is_valid_pat("gd_pat_abc123_1234abcd")); assert!(is_valid_pat("gd_pat_aA0bB1cC2_abcdef12")); - // entropy may be long + assert!(is_valid_pat("gd_pat_1234567890abcdef1234567890abcdef")); + assert!(is_valid_pat("gd_pat_YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=")); assert!(is_valid_pat( - "gd_pat_abcdefghijklmnopqrstuvwxyz0123456789_abcdef12" + "gd_pat_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH" )); }