Skip to content

feat(auth): request a product-area scope bundle at login, step up only for sensitive scopes - #317

Merged
jpage-godaddy merged 8 commits into
godaddy:mainfrom
nverbos-godaddy:feat/auth-login-scope-bundle
Oct 12, 2026
Merged

jpage-godaddy merged 8 commits into
godaddy:mainfrom
nverbos-godaddy:feat/auth-login-scope-bundle

Conversation

@nverbos-godaddy

@nverbos-godaddy nverbos-godaddy commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • gddy auth login now requests a bundle of every permission scope the CLI uses except a short step-up-only list, instead of just apps.app-registry:read, domains.domain:read, offline_access and the OIDC openid/profile pair.
  • Scopes whose purpose is to spend money, take a site offline, delete a whole billable resource, or open raw data access stay out of the bundle and keep prompting when the command that needs them runs: domains.domain:create, domains.nameserver:update, hosting.application:delete, hosting.database.tunnel:execute, and shopping.checkout:execute + shopping.order:read. Editing scopes that also permit deleting a sub-resource (dns delete, hosting secret delete, hosting domain detach, platform app archive) stay defaults on purpose: the authorization server offers no finer split, and prompting for domains.dns:update would put a consent screen back into every site setup.
  • shopping.catalog:read only reads the public product catalog, so it is a login default. The catalog commands now declare just that scope (SHOPPING_CATALOG_SCOPES) instead of the full shopping trio, so browsing never steps up. The checkout commands declare SHOPPING_PURCHASE_SCOPES (checkout:execute + order:read) so the first purchase prompts once and a later order get is already covered; shopping order get itself declares only shopping.order:read, so reading an order never asks for spending authority. The shopping client helper now resolves its credential from the command's own .with_scopes declaration (ctx.credential(), like domain::make_client) rather than taking a second copy of the scopes.
  • SCOPE_REGISTRY is the single source of truth: each entry's default flag decides login versus step-up, and scopes::DEFAULT_OAUTH_SCOPES is derived from those flags (the hand-maintained environments::DEFAULT_OAUTH_SCOPES list is removed). The step-up policy is written on the registry and pinned by a test naming the six sensitive scopes, so flipping a flag is an explicit change. The wired-to-a-command test now covers every permission scope, not just non-defaults, so a default scope cannot outlive its last command unnoticed. The module's "Adding a scope" steps include the default-versus-step-up decision.
  • gddy auth scope --defaults-only / --non-default report the split straight from the registry.
  • Apart from the shopping split above, no command .with_scopes declarations change. Commands keep declaring what they need so a token obtained before this change still steps up correctly.

Why

A first-time user driving the CLI from an agent through "search for a domain, buy it, buy hosting, deploy a site, connect the domain" hit roughly a dozen OAuth consent screens, one per command that needed a scope the token did not have. Every scope step-up is a full browser round trip through the consent screen, so the fix is to authorize less often.

With this bundle the same journey is three consent screens: login, register the domain, buy hosting. Everything from app create through deploy, secrets, logs, and DNS is covered by the login grant. The consent screen groups scopes by product area with a "Read only" / "Read & manage" label, so the longer login grant renders as a handful of collapsed rows rather than a wall of bullets.

Fewer, more distinctive prompts also mean the prompts that remain get read, which is the point of having them.

What the login screen shows

Group Label Scopes
Sign-in & profile Read only openid, profile
Domains & DNS Read & manage domains.domain:read, domains.dns:update
Hosting Read & manage application read/create/update, deployment execute, source read/write, secret read/write, log read, domain read/write, subscription read/write
Business Email Read & manage email.mailbox:read, email.mailbox:create
Apps & integrations Read & manage apps.app-registry:read, apps.app-registry:write
Website Security Read & manage security.website-security-scan:execute
Shopping & orders Read only shopping.catalog:read
Ongoing access notice offline_access

What still steps up after this PR

Scope Commands Why
domains.domain:create domain purchase Spends money
shopping.checkout:execute shopping checkout create/get/update/complete Spends money
shopping.order:read shopping order get; also granted with checkout Rides along with checkout so a post-purchase order get never prompts; alone it prompts only for the read
domains.nameserver:update domain nameserver set Can take a live site offline
hosting.application:delete hosting app delete Deletes a whole billable resource
hosting.database.tunnel:execute db tunnel Raw database access

Each prompt happens once per environment, not once per use: the granted scope joins the single stored token and stays for the life of the rotating refresh token. gddy auth scope --non-default prints this list from the running binary.

Out of scope / related

  • openid and profile come from feat(auth): upgrade cli-engine to 0.10.1; log in with an ephemeral callback port and OIDC scopes #314 (merged), whose registry entries carry default: true, so they are part of the derived login request here; the consent screen gains the Sign-in & profile row.
  • Wiring cli-engine's ScopeHierarchy (write implies read) and an agent-facing login preset are follow-ups; with the full bundle they change nothing for the current commands.
  • A step-up consent screen still lists the whole cumulative scope set, because cli-engine requests defaults ∪ granted ∪ required into the single stored token. Highlighting the newly requested scope on that screen is a consent-screen change tracked separately.

Known CI failure, not from this PR

cargo audit began failing on 2026-10-12 when RUSTSEC-2026-0338 through -0342 (quinn-proto 0.11.16, a transitive dependency) entered the advisory database; the identical lockfile passed on main earlier the same day and main's next run hits it too. This PR deliberately does not touch Cargo.lock; the bump will be picked up from main once it lands there.

Behavior for existing users

Nothing changes until the next authorization. A stored token keeps refreshing with its narrow scope set; the first step-up it hits (or a gddy auth logout + login) moves it onto the bundle. No migration.

Requirements

  • The CLI's OAuth client must be registered for every scope in scopes::ALL plus offline_access, on every environment. With the wider login request, a single unregistered scope would fail gddy auth login for that environment rather than one command. All 27 permission scopes were already registered on the CLI client (each was already requestable via step-up) and the OTE and prod registrations were confirmed, so no client registration change is needed.

Test plan

  • cargo fmt --check
  • cargo clippy --workspace -- -D warnings
  • cargo test --workspace (1083 passed in godaddy-cli, including the new registry-derivation and step-up-set tests)
  • ./rust/scripts/check-module-size.sh
  • Manual, against a test environment (2026-10-12, branch build): gddy auth logout && gddy auth login produced one consent screen and a token with the 22 bundle scopes (auth status). shopping catalog search, domain available, domain list, dns list, hosting app list and email list all ran without a prompt (some returned API 404/502s from the test account's data, none were auth failures). shopping order get triggered exactly one step-up requesting the 24-scope union (run before the order-read narrowing; it now requests only order:read on top of the bundle); after approval the token held 24 scopes and a second shopping order get did not prompt.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 12, 2026 18:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Several destructive capabilities remain login defaults, and Shopping grants checkout authority during read-only browsing.

2 open findings
What changed in this PR

Expands login-time OAuth scopes while reserving sensitive permissions for step-up authorization.

Changes:

  • Adds a centralized STEP_UP_ONLY scope policy.
  • Expands default login scopes and registry flags.
  • Adds an invariant test for scope classification.
File Description
rust/​src/​scopes.rs Defines step-up policy, updates registry defaults, and adds validation.
rust/​src/​environments/​mod.rs Expands the default OAuth login bundle.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread rust/src/scopes.rs Outdated
Comment thread rust/src/scopes.rs Outdated
…y for sensitive scopes

Login now requests every permission scope the CLI uses except a short
step-up-only list, so a first-time "buy a domain, buy hosting, deploy a
site" session needs three consent screens (login plus one per purchase)
instead of about a dozen. Each scope step-up is a browser round trip
through the consent screen, which now groups scopes by product area with
a read-only / read-and-manage label, so the wider login grant stays
readable.

Scopes that spend money (domain registration, shopping), can take a
site offline (nameserver update), destroy resources (app delete), or
open raw data access (database tunnel) stay out of the bundle and
prompt when the command that needs them runs. `scopes::STEP_UP_ONLY`
records that list and a new test enforces that the login defaults are
exactly every declared scope minus it, plus offline_access.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@nverbos-godaddy
nverbos-godaddy force-pushed the feat/auth-login-scope-bundle branch from 08369a6 to 3808d65 Compare October 12, 2026 18:30
nverbos-godaddy and others added 3 commits October 12, 2026 11:35
…or checkout

`shopping.catalog:read` only reads the public product catalog, so it joins
the login bundle. The catalog commands now declare just that scope instead
of the full shopping trio, so browsing never triggers a step-up; checkout
and order commands keep `shopping.checkout:execute` + `shopping.order:read`
as a pair so the first purchase prompts once and the follow-up order read
does not prompt again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- `shopping order get` declares only `shopping.order:read`; the checkout
  commands keep the checkout + order pair, so reading an order never asks
  for spending authority while a post-purchase `order get` stays covered.
- Shopping `make_client` resolves the credential from the command's own
  `.with_scopes` declaration via `ctx.credential()`, matching
  `domain::make_client`, instead of taking a second copy of the scopes.
- The wired-to-a-command test now covers every permission scope, not only
  non-defaults, so a scope cannot outlive its last command unnoticed.
- The defaults invariant test also compares lengths so a duplicated
  default entry fails instead of being hidden by the set comparison.
- "Adding a scope" docs gain the default-versus-step-up decision; the
  step-up-only constants are marked in their doc comments and the
  block-level rule replaces per-constant "requested at login" notes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@nverbos-godaddy

Copy link
Copy Markdown
Contributor Author

Example of the consent screen with the new default scopes

image

Comment thread rust/src/environments/mod.rs Outdated
Comment thread rust/src/shopping/common.rs Outdated
Comment thread rust/src/scopes.rs Outdated
Comment thread rust/src/scopes.rs
…lags

Per review: SCOPE_REGISTRY is now the single source of truth. Each
entry's `default` flag decides whether a scope is requested at login or
only when a declaring command runs; `scopes::DEFAULT_OAUTH_SCOPES` is
derived from those flags at first use, and the hand-maintained
`environments::DEFAULT_OAUTH_SCOPES` and `STEP_UP_ONLY` lists are gone.
The step-up policy is recorded in the registry docs and pinned by a test
that names the six sensitive scopes, so flipping a flag is an explicit
change rather than a side effect. Also drops a code comment aimed at the
coding session rather than readers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@nverbos-godaddy
nverbos-godaddy force-pushed the feat/auth-login-scope-bundle branch from 8379c31 to 3da29f0 Compare October 12, 2026 19:42

@jpage-godaddy jpage-godaddy left a comment

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.

Beautiful, thanks.

… scopes

Per review: with the login bundle in place, the non-default scopes are
exactly the sensitive ones, so help text that recommended `auth login
--scope` for every non-default scope would defeat the step-up policy.
`gddy auth scope` and the gddy skill now describe non-default as "prompts
on first use, on purpose" and tell agents to plan for those commands
instead. The platform guide and email help no longer tell users to add
`--scope` for scopes that are now login defaults. The registry-derivation
test asserts independent properties instead of repeating the derivation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
nverbos-godaddy and others added 2 commits October 12, 2026 12:59
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-bundle

# Conflicts:
#	rust/src/environments/mod.rs
@jpage-godaddy
jpage-godaddy merged commit cea21f5 into godaddy:main Oct 12, 2026
5 checks passed
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.

3 participants