Skip to content

feat(auth): support Personal and Service Access Tokens (DD_PAT / DD_SAT)#497

Draft
srosenthal-dd wants to merge 3 commits into
DataDog:mainfrom
srosenthal-dd:stephen.rosenthal/pup-pat-auth
Draft

feat(auth): support Personal and Service Access Tokens (DD_PAT / DD_SAT)#497
srosenthal-dd wants to merge 3 commits into
DataDog:mainfrom
srosenthal-dd:stephen.rosenthal/pup-pat-auth

Conversation

@srosenthal-dd
Copy link
Copy Markdown
Member

@srosenthal-dd srosenthal-dd commented May 12, 2026

Summary

Adds Personal and Service Access Tokens as a third auth tier between OAuth2 (preferred) and DD_API_KEY + DD_APP_KEY (mildly discouraged). PATs and SATs are wire-identical; both go in Authorization: Bearer on OAuth-compatible endpoints and DD-APPLICATION-KEY on OAuth-excluded ones.

Recommended order:

  1. OAuth2 -- preferred
  2. PAT / SAT (DD_PAT / DD_SAT) -- supported anywhere DD_API_KEY + DD_APP_KEY is
  3. DD_API_KEY + DD_APP_KEY -- works everywhere; mildly discouraged

Note: env var names (DD_PAT / DD_SAT) are not standardized across Datadog tooling. Worth confirming the naming before merge.

Notable behavior

  • Runtime precedence when multiple credentials are set: DD_ACCESS_TOKEN > stored OAuth > DD_PAT > DD_SAT > DD_API_KEY + DD_APP_KEY.
  • --org no longer silently swaps an explicit env-supplied PAT/SAT/access-token for a stored OAuth session.
  • Canonical OAuth-excluded endpoint table extracted to src/oauth_excluded.rs so the WASM build shares one source of truth.

Test plan

  • cargo fmt, cargo clippy --all-targets -- -D warnings, cargo test -- --test-threads=1 (1092 pass)
  • Manual: PAT against a staging org for at least one OAuth-excluded endpoint -- not yet exercised live

Add Personal Access Token and Service Access Token as a third
authentication tier, sitting between OAuth2 (preferred) and the
classic DD_API_KEY + DD_APP_KEY (mildly discouraged). PATs and SATs
share the same wire protocol -- sent as Authorization: Bearer on
OAuth-compatible endpoints, and as DD-APPLICATION-KEY on the
OAuth-excluded endpoints (api_keys, application_keys, ddsql-editor)
using Datadog's migration form. This means a PAT or SAT covers every
endpoint that API+App Key covers.

Runtime auth precedence: OAuth access_token > PAT/SAT > API+App Key.
If both DD_PAT and DD_SAT are set, DD_PAT wins. The PatKind enum
records which env var supplied the token so pup auth status can
display "Personal Access Token" vs "Service Access Token".

Docs updated:
- README.md restructured Authentication into three tiers, with PAT
  and SAT both shown as preferred over API+App Key.
- docs/OAUTH2.md comparison table is now a three-way table with
  PAT/SAT pairs treated as one column.
- Environment variables and WASM sections list DD_PAT and DD_SAT.
Audited PAT/SAT support across all auth code paths and fixed the
following issues raised by Claude, Codex, and Gemini reviewers:

- apply_org_override no longer clobbers an explicit env-supplied
  PAT/SAT (or DD_ACCESS_TOKEN) with a stored OAuth session.
  Otherwise --org could silently switch the request principal.
- raw_post_lenient and raw_delete now route through the shared
  apply_auth so PAT/SAT callers reach debugger and cost-CCM paths.
- client.rs and make_dd_config now check PAT before API+App Key on
  OAuth-excluded paths, matching documented precedence
  (OAuth > PAT/SAT > API+App Key).
- Config::from_env now checks all env vars before any file values,
  so DD_SAT in env beats pat: in the config file (and vice versa).
- make_dd_config populates apiKeyAuth=PAT alongside appKeyAuth=PAT
  for PAT-only callers, since some SDK ops require the slot.
- commands/api.rs, commands/incidents.rs, commands/bits.rs, and
  commands/acp.rs no longer have PAT-blind auth blocks.
- commands/api.rs and src/api.rs route PAT through DD-APPLICATION-KEY
  for OAuth-excluded paths instead of Authorization: Bearer.
- extensions/exec.rs forwards DD_PAT or DD_SAT to child processes
  based on cfg.pat_kind and clears stale values otherwise.
- Extracted OAUTH_EXCLUDED_ENDPOINTS and requires_api_key_fallback
  to a new dependency-free src/oauth_excluded.rs so both client.rs
  and the browser/WASM api.rs share one canonical list (previously
  drifted to 11 vs 52 entries).

Tests added for each new branch (PAT preserved across --org, env
override of file PAT/SAT, PAT routed to DD-APPLICATION-KEY on excluded
endpoints, raw_post_lenient + raw_delete with PAT).
Eliminated four near-duplicate auth-routing ladders by making
client::apply_auth pub and delegating from the command-level helpers.
Net diff: -80 LoC.

- commands/api.rs::run -- replaced 45-line inline auth ladder with a
  single apply_auth call.
- commands/incidents.rs::attachments_delete -- ditto.
- commands/bits.rs::resolve_agent_id -- changed signature from three
  Option<&str> params to &Config, eliminating the call-site PAT-into-
  access_token coalesce.
- AuthType::AccessToken now carries PatKind directly instead of
  Option<PatKind>. The None arm was unreachable since pat and pat_kind
  are always set together.
- commands/auth.rs::build_non_oauth_status uses kind.display_name()
  instead of re-implementing the Personal/Service -> label mapping.
- config.rs::apply_org_override uses PatKind::env_var() instead of
  hardcoded "DD_PAT"/"DD_SAT" strings.
- extensions/exec.rs::inject_auth_env collapses the two PatKind match
  arms into a single env-set + unconditional unset pair.
- Removed unused Config::has_pat helper.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant