Skip to content

feat(account): add platform token management via Account Management API#342

Open
christian-kreuzberger-dtx wants to merge 2 commits into
mainfrom
feat/account-token
Open

feat(account): add platform token management via Account Management API#342
christian-kreuzberger-dtx wants to merge 2 commits into
mainfrom
feat/account-token

Conversation

@christian-kreuzberger-dtx

@christian-kreuzberger-dtx christian-kreuzberger-dtx commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds dtctl account login — browser-based PKCE OAuth for the account plane
  • Adds dtctl account status — shows stored token state, expiry, and scopes
  • Adds dtctl account token list/create/revoke — full CRUD for platform tokens (dt0s16.*) via the Account Management API

The account plane uses a different OAuth resource parameter (urn:dtaccount:{uuid}) and separate base URLs per tier, so a dedicated auth layer was added alongside the SDK handler and CLI wrapper.

New commands

dtctl account login [--account-uuid <uuid>]     # stores account token in keyring
dtctl account status                             # show token state/expiry/scopes
dtctl account token list [-o json|yaml|table]
dtctl account token create --name <n> --scope <s> --user-uuid <u> [--expires 30d]
dtctl account token revoke <tokenId>

Required OAuth scopes on the Heimdall client

The following scopes must be present on dt0s12.dtctl-* (tracked in AI-239):

Scope Purpose
account-idm-read list operations
account-idm-write mutating operations
platform-token:tokens:manage list + revoke platform tokens
platform-token:tokens:write create platform tokens

dt0s12.dtctl-dev has all four. dtctl-sprint and dtctl-prod pending AI-239.

How to test (dev environment)

Prerequisites: a dtctl context pointing at a dev environment (e.g. <environment-id>.dev.apps.dynatracelabs.com) and your account UUID.

1. Build

go build -o ~/bin/dtctl .

2. Log in to the account plane

~/bin/dtctl account login --account-uuid <your-account-uuid>
# browser opens → complete login → token stored in keyring

3. Verify token state

~/bin/dtctl account status
# should show: token valid, scopes include platform-token:tokens:manage and platform-token:tokens:write

4. List existing platform tokens

~/bin/dtctl account token list
~/bin/dtctl account token list -o json

5. Create a token

~/bin/dtctl account token create \
  --name test-ci-token \
  --scope "platform-token:tokens:manage" \
  --user-uuid <your-user-uuid> \
  --expires 1d
# output: token secret printed once — note it down

6. Verify it appears in the list

~/bin/dtctl account token list

7. Revoke it

~/bin/dtctl account token revoke <tokenId-from-step-5>
~/bin/dtctl account token list  # should be gone or show REVOKED status

8. Dry-run (no API calls, no credentials needed)

~/bin/dtctl account token create --name x --scope y --user-uuid z --dry-run
~/bin/dtctl account token revoke tok-123 --dry-run

9. Readonly safety check

# switch to a readonly context and attempt create — should be blocked
~/bin/dtctl account token create --name x --scope y --user-uuid z
# expected: safety check error

🤖 Generated with Claude Code

Adds `dtctl account token` subcommands for listing, creating, and revoking
Dynatrace platform tokens (dt0s16.*) via the Account Management API.

Also introduces the account-plane authentication infrastructure needed to
support this and future account-scoped operations.

New commands:
  dtctl account login               # PKCE browser OAuth for account plane
  dtctl account status              # show stored token state and expiry
  dtctl account token list          # list all platform tokens
  dtctl account token create        # create a platform token
  dtctl account token revoke <id>   # revoke a platform token by ID

New files:
  sdk/api/platformtoken/           SDK handler (list/create/revoke)
  pkg/resources/platformtoken/     CLI wrapper
  pkg/auth/account_scopes.go       account OAuth scopes per safety level
  pkg/client/account.go            base URL helpers for account/IAM tiers
  pkg/client/account_discovery.go  UUID auto-discovery via /iam/v1/access-info
  cmd/account.go                   parent command + subcommand registration
  cmd/account_login.go             PKCE browser flow
  cmd/account_status.go            token state display
  cmd/account_token.go             create/list/revoke subcommands

Account UUID is resolved from: --account-uuid flag > DTCTL_ACCOUNT_UUID >
context account-uuid config field > auto-discovery via access-info API.
Account token is stored in the system keyring (or file fallback) by
`dtctl account login` and retrieved transparently by all account commands.

Required OAuth scopes (must be present on the Heimdall client):
  account-idm-read, account-idm-write
  platform-token:tokens:manage, platform-token:tokens:write

NOISSUE
@discostu105

Copy link
Copy Markdown
Collaborator

Code review

Nice PR — the account-plane auth layer, SDK handler + CLI wrapper split, and the safety-check/dry-run plumbing all look solid, and it's well covered by unit, golden, and integration tests. A few issues worth addressing before merge.

1. account login --account-uuid X never persists the UUID, so follow-up commands can't find the token it just stored

cmd/account_login.go:50 resolves the UUID via resolveUUIDNoDiscovery(ctx, uuidFlag) and cmd/account_login.go:102 stores the token under keyring key account-<uuid> — but the UUID is never written to the context config or anywhere durable. setupAccountClient (cmd/root.go:838) then re-resolves with an empty flag — resolveUUIDNoDiscovery(ctx, "") — reading only DTCTL_ACCOUNT_UUID / ctx.AccountUUID.

So if the user supplied the UUID only via the login flag (exactly the flow in this PR's own test steps 2–4, which don't set it in context/env), the UUID resolves to "", resolveAccountToken skips the keyring, and account token list fails with account token required: … run 'dtctl account login' despite a successful login. account status likewise reports Account UUID: (not set).

Fix: persist the UUID to the context on login, or fall back to the keyring/discovery when the flag was the only source.

2. account token create can leak the one-time token secret to stderr under -vv / --debug

setupAccountClient (cmd/root.go:860) enables verbose logging at level (forced to 2 under --debug). At level ≥2 the httpclient dumps full response bodies to stderr, and the create response body contains the secret token (dt0s16…) that is meant to be shown exactly once. So dtctl -vv account token create … writes the secret into logs / terminal scrollback.

The logging mechanism is pre-existing, but account token create is the first command whose response body is itself a credential — consider redacting the token field (or the response body) for this path.

3. Auto-discovery passes the account-scoped token to the IAM access-info endpoint (PLAUSIBLE)

resolveAccountUUID (cmd/root.go:745) calls DiscoverAccountUUID(iamBase, envToken, …) with envToken = accountToken. This path is only reachable when no UUID is configured and DTCTL_ACCOUNT_TOKEN is set; if that token is an account-plane token (resource=urn:dtaccount:{uuid}), IAM will likely reject it (401) since access-info expects the environment-scoped token. Discovery then fails with a confusing "could not resolve account UUID" error. Narrow, but worth a clearer error or documenting that discovery needs the environment token.

4. Nit: test/e2e/account_token_test.go:64

fmt.Sprintf("dtctl-e2e-test-token") has no format directives (staticcheck S1039 — use a plain string literal). Separately, the hard-coded token name risks collisions or orphaned tokens across concurrent/rerun integration tests if a prior run's revoke failed; a unique suffix (e.g. timestamp) would be safer.

🤖 Generated with Claude Code

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.

2 participants