Skip to content

Improve bfabric-cli auth command group#558

Merged
leoschwarz merged 35 commits into
mainfrom
feature/auth-default-interactive-picker
Jul 17, 2026
Merged

Improve bfabric-cli auth command group#558
leoschwarz merged 35 commits into
mainfrom
feature/auth-default-interactive-picker

Conversation

@leoschwarz

@leoschwarz leoschwarz commented Jul 15, 2026

Copy link
Copy Markdown
Member

Makes environment and scope selection in bfabric-cli auth interactive and rounds out the command group for managing configured environments. Builds on #562 (core OAuth API now requires explicit client_id/scope); the CLI-level defaults and scope presets live in bfabric_scripts/cli/login/_constants.py.

Adds questionary, behind a reusable cli.interactive helper (resolve_choice / select_choice / select_or_input / text_input / confirm) with non-interactive fallbacks, plus config_writer.remove_environment_from_config.

🤖 Prepared with assistance from Claude Opus 4.8 via Claude Code.

Collapse `auth default set [ENV]` into the bare `auth default`: with no
argument it opens an arrow-key picker (type to filter, Enter to select),
each row showing the environment's host and auth method. A positional
`ENV` still sets the default non-interactively, and with no TTY the
command lists the environments instead of prompting.

Add a small reusable `cli.interactive` helper around questionary
(`resolve_choice` / `select_choice` / `select_or_input`) covering the
value-on-CLI / menu / type-a-new-value pattern for future commands.
…or auth login

The `auth pkce` / `auth device-code` / `auth pat` commands no longer hardcode
`config_env="PRODUCTION"`. When `--config-env` is omitted they prompt to pick an
existing environment or type a new name (prefilled with the current default, Tab
to autocomplete); non-interactively they target the current default env, else
PRODUCTION.

`auth pkce` / `auth device-code` gain named `--scope` presets — `read-only`,
`read-write`, `read-write-upload` (adds `tus`) — that expand to the real scope
string; a raw scope string still passes through. Omitting `--scope` opens a
picker of the presets plus a Custom option that prompts for raw scopes.

Shared resolution and the presets live in cli/login/_common.py so the three
commands don't duplicate them; cli.interactive gains a text_input helper and a
Tab-autocomplete hint on select_or_input.
…tus, logout removal, set-default prompt)

- auth list: list configured environments (host + auth method), marking the default
- auth status: for OAuth, report cached-token freshness and the granted scope,
  annotated with the matching named preset
- auth logout: rework into "remove environment" — interactive picker (preselecting
  the current default), deletes the config entry and any cached OAuth tokens, guarded
  by a confirmation prompt (--no-confirm to skip; required to remove non-interactively)
- login (pkce/device-code/pat): unless --set-default/--no-set-default is given, ask
  (preselected yes) whether the new environment should become the config default
- consolidate the shared auth-group rendering/resolution into cli/login/_common.py;
  add cli.interactive.confirm and config_writer.remove_environment_from_config
  (also clears a dangling default so the file stays loadable)
Extract the repeated OAuth token/session boilerplate and the thrice-copied
BFABRICPY_CONFIG_ENV cleanup into a login-dir conftest fixture set, and drop
the redundant repeated ask()-is-Any comments from the interactive wrappers.
No behavior change; login + interactive suites (83 tests) green.
…lpers

- auth logout: when removing the current default while other environments
  remain, warn in the confirmation prompt and point to 'auth default' after,
  instead of silently leaving the config with no default.
- auth logout: remove the config entry before clearing the token cache, so a
  failed write can't strand an environment without its token.
- print_environments: guard the width computation against an empty mapping.

Review fixes from the pre-PR readiness pass.
@leoschwarz
leoschwarz marked this pull request as ready for review July 15, 2026 09:55
Comment thread bfabric_scripts/src/bfabric_scripts/cli/login/_common.py Outdated
@leoschwarz
leoschwarz requested a review from Caushi July 15, 2026 09:57
@leoschwarz leoschwarz changed the title Round out the bfabric-cli auth command group Improve bfabric-cli auth command group Jul 15, 2026
The core bfabric OAuth entry points no longer bake in a "CLI" client ID or a
default scope. connect_oauth/connect_pkce/connect_device_code and the _oauth
helpers now require client_id and scope explicitly. The CLI-facing defaults move
to bfabric_scripts (cli/login/_constants.py). connect() with auth_method: oauth
now raises if the config lacks client_id instead of falling back to "CLI".
…it-client-id-scope

# Conflicts:
#	bfabric/docs/changelog.md
#	bfabric/src/bfabric/_oauth/credential_provider.py
Model login scope presets in cli/login/_constants.py as ScopePreset named
tuples (name, scope, description), oriented by use case: read-only (api:read),
read-write (api:write, which implies api:read), and upload (api:write tus).
`auth login` / `auth device-code` now default to the read-write preset instead
of the broad OIDC-inclusive scope. Client/webapp registration keeps
DEFAULT_OAUTH_SCOPE (webapps need the OIDC claims). The presets + by-name index
+ default live in _constants.py for the interactive scope picker to consume as
a follow-up.
The CLI login default is now the read-write preset (api:write), not the broad
OIDC scope, so the troubleshooting doc no longer claims groups is in the default
(employees must request it explicitly for file access). Rename the CLI
registration-scope constant DEFAULT_OAUTH_SCOPE -> DEFAULT_REGISTRATION_SCOPE to
say what it is, and fix a stale comment in transfer/tokens.py that referenced the
removed core constant.
…ng scope

The re-auth hint (_PKCE_SCOPE_HINT) and the upload examples requested the full
OIDC scope set, which the operation never needs. Reduce them to "api:write
{scope}" (api:write implies api:read) — for upload that is exactly the `upload`
preset (api:write tus). Also fix the changelog preset name (upload, not
read-write-upload).
…moval

Reverts the CLI scope presets (7f15ffd), the DEFAULT_OAUTH_SCOPE rename +
troubleshooting-doc edits (58d4de1), and the upload/hint scope changes
(48cd276). #562 stays scoped to removing baked-in OAuth defaults from the core
library; the CLI keeps its original DEFAULT_OAUTH_SCOPE / DEFAULT_CLIENT_ID and
behavior unchanged. Scope presets belong in the interactive-picker follow-up.
…pt is cancelled

Ctrl-C at the interactive "Set '<env>' as the default environment?" prompt made
questionary's .ask() print "Cancelled by user" and return None; confirm() collapsed
that None into False, so login silently proceeded as if --no-set-default were given
instead of aborting.

confirm() now surfaces cancellation as None (consistent with the other interactive
wrappers), resolve_set_default propagates it, and the pkce / device-code / pat login
commands abort with "Login aborted." before touching the OAuth flow or the config.
Adds regression tests for all three commands.

🤖 Prepared with assistance from Claude Opus 4.8 via Claude Code.
@leoschwarz
leoschwarz marked this pull request as draft July 15, 2026 14:43
Integrates the core-defaults removal (#562) and the auth login rename (#561)
into the interactive auth command group.

Conflict/breakage resolution:
- Relocate DEFAULT_CLIENT_ID import from the deleted bfabric._oauth._constants
  to bfabric_scripts.cli.login._constants (pkce, device_code, logout, status).
- Apply the #561 rename: cmd_login_pkce -> cmd_auth_login (auth login);
  test_cmd_login_pkce.py -> test_cmd_auth_login.py; changelog auth pkce -> auth login.
- Adopt #562's use-case scope presets (read-only / read-write / upload) in
  _constants.py (ScopePreset) and consume them from _common.py, replacing the
  OIDC-inclusive presets #558 had built on the (now CLI-owned) DEFAULT_OAUTH_SCOPE.
- Repoint bfabric_scripts login tests at _constants; update preset expansion
  assertions (upload = "api:write tus"; login default = DEFAULT_LOGIN_SCOPE).
…licitly

The CLI login presets (read-only / read-write / upload) are minimal API scopes
and no longer include `groups` or the OIDC scopes. Update the OAuth troubleshooting
doc, the transfer/tokens.py PKCE hint comment, and the bfabric_scripts changelog so
employees know to request `groups` explicitly for file access.
…tration scope

Move the CLI OAuth scope policy into #562 (was reverted in b4175b5 as an
interactive-picker follow-up; now homed here so #562 owns scope policy and #558
owns only the interactive picker).

- cli/login/_constants.py: add use-case-oriented scope presets (read-only=api:read,
  read-write=api:write, upload=api:write tus) with a by-name index and read-write
  default; rename the broad registration-scope constant DEFAULT_OAUTH_SCOPE ->
  DEFAULT_REGISTRATION_SCOPE to say what it is now that login differs.
- auth login / auth device-code default to the minimal read-write scope (api:write)
  instead of the broad OIDC set; registration keeps DEFAULT_REGISTRATION_SCOPE.
- Simplify upload/re-auth scope hints (_PKCE_SCOPE_HINT, upload examples/docs) to
  'api:write {scope}' since api:write implies api:read.
@leoschwarz
leoschwarz changed the base branch from main to refactor/oauth-explicit-client-id-scope July 16, 2026 07:59
…uth-default-interactive-picker

# Conflicts:
#	bfabric/src/bfabric/transfer/tokens.py
#	bfabric_scripts/docs/changelog.md
#	bfabric_scripts/src/bfabric_scripts/cli/login/_constants.py
#	bfabric_scripts/src/bfabric_scripts/cli/login/device_code.py
#	bfabric_scripts/src/bfabric_scripts/cli/login/pkce.py
Drop the ScopePreset NamedTuple and SCOPE_PRESETS_BY_NAME index in favour of a
plain {name: scope} dict — shorter, and it removes the three same-typed positional
args that made preset construction easy to get wrong. Human-readable descriptions
are UI copy and move to the interactive picker (bfabric_scripts _common).
In register_client / register_webapp the required 'scope' sat after the defaulted
'service_user'. Legal for keyword-only args, but list the required one first for
consistency; docstrings reordered to match. No call-site impact (keyword-only).
Revert the name->scope dict switch (8c089b1). The NamedTuple keeps each preset's
human-readable description co-located with its name and scope — which the interactive
picker (bfabric_scripts _common) consumes — instead of splitting descriptions into a
parallel map. Restores ScopePreset, SCOPE_PRESETS, and SCOPE_PRESETS_BY_NAME.
@leoschwarz
leoschwarz marked this pull request as ready for review July 16, 2026 11:49
No behaviour change; removes indirection that wasn't earning its keep.

- Delete cli.interactive.resolve_choice: it was used fully by only one
  caller (auth default) and half-bypassed by resolve_config_env, which
  pre-checks the explicit value and the TTY itself and then called it with
  value=None (two of its four branches dead on that path). Both callers now
  use select_choice / select_or_input directly. Drops its 4 unit tests;
  behaviour stays covered by the auth-default and login-common suites.
- Inline three single-use _common helpers into their only callers:
  _existing_environments -> resolve_config_env, auth_method_label ->
  environment_summary (also dropping a stale "mirrors auth status" note,
  since status keeps its own copy), environment_line -> print_environments.

Net -38 production lines / -64 with tests. Tests + basedpyright green.
…LOGIN_SCOPE)

Make --scope required on `auth login` / `auth device-code` and delete the
DEFAULT_LOGIN_SCOPE constant, so the CLI no longer bakes in a default scope
(matching the core library). Removes the [default: ...] hint from --help.
SCOPE_PRESETS stays as the documented preset catalog.
Base automatically changed from refactor/oauth-explicit-client-id-scope to main July 16, 2026 14:53
…icker

Reconcile the "require explicit --scope / drop DEFAULT_LOGIN_SCOPE" cleanup
with this branch's interactive scope picker:

- Keep --scope OPTIONAL on `auth login` / `auth device-code` so the picker
  runs when it is omitted in a terminal (this branch's feature), rather than
  making it a required argument.
- Honor the dropped baked-in default: resolve_scope no longer falls back to
  DEFAULT_LOGIN_SCOPE non-interactively (returns None -> login aborts, so a
  headless run must pass --scope); the picker preselects the least-privilege
  preset and the Custom prompt starts empty.
- Restore SCOPE_PRESETS_BY_NAME in _constants.py (still needed by the picker);
  DEFAULT_LOGIN_SCOPE / DEFAULT_SCOPE_PRESET stay removed.
- Update the changelog + the resolve_scope test for the no-default behavior.
Same reconciliation as the previous merge: main's #562 ("require explicit
client_id and scope", no CLI default) vs this branch's interactive scope picker.
Keep the picker.

- pkce / device_code: keep --scope OPTIONAL (picker runs when omitted); discard
  main's required-scope signature.
- _constants: keep SCOPE_PRESETS_BY_NAME (needed by the picker); our file is
  main's plus that one binding.
- logout / status: keep our feature imports; main's DEFAULT_CLIENT_ID relocation
  is already present in our version.
- changelog: keep the picker + no-default entry; drop main's "require --scope" line.
The prompt kit always passes use_jk_keys=False (arrow keys are the sole
navigation), but the select_choice test still asserted True. It lives under
cli/ rather than cli/login/, so the login test runs never caught it.
cmd_auth_login (browser/PKCE) and cmd_login_device_code were ~90% identical:
same resolve-or-abort preamble and same token-persist + config-write tail. Merge
them into cli/login/oauth_login.py with two local helpers (_resolve_params,
_persist) and shared cyclopts help constants. Cuts ~45 production lines and one
module without loading _common (behavior and --help output unchanged).
list / default / status / logout all load the config file and act on a named
environment, and duplicated the config-load block (x4) and the interactive env
picker (x2). Merge the four into cli/login/manage.py with local _load_config and
_select_environment helpers, and move the display helpers (environment_summary,
print_environments, describe_scope, describe_token_cache) there from _common,
since these commands are their only consumers.

_common.py is now single-purpose (login-parameter resolution); the login package
drops from 11 modules to 8 and ~48 lines. Behavior and --help output unchanged.
@leoschwarz
leoschwarz merged commit 72dca9c into main Jul 17, 2026
24 checks passed
@leoschwarz
leoschwarz deleted the feature/auth-default-interactive-picker branch July 17, 2026 09:58
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