Skip to content

Add Azure DevOps authentication dialog for CI panel#22

Merged
bluestreak01 merged 4 commits intomasterfrom
vi_az
Apr 16, 2026
Merged

Add Azure DevOps authentication dialog for CI panel#22
bluestreak01 merged 4 commits intomasterfrom
vi_az

Conversation

@bluestreak01
Copy link
Copy Markdown
Member

Summary

The CI panel silently failed to expand Azure DevOps checks when the user had no credentials, and there was no in-app way to authenticate. This adds a full auth dialog with three methods, retry-on-success, and proper macOS Keychain support.

Auth methods

Press a in the CI panel (or it auto-opens when auth is needed). Dialog has three tabs:

Tab How it works Best for
PAT Paste a Personal Access Token. Stored in macOS Keychain (security) or Linux keyring (secret-tool). Universal — always works, no prerequisites
Browser OAuth2 authorization code flow with PKCE. Binds a random localhost port, opens the browser, captures the redirect. Uses the Azure CLI's public client ID; override with AZURE_DEVOPS_CLIENT_ID. Tenant is entered in the dialog and persisted. Interactive sign-in without leaving the app
az CLI Shells out to az account get-access-token. Reuses your existing az login session — no OAuth dance, no app registration. Account status fetched async so tab switches stay fast; rapid switching cancels in-flight checks. If you already have az configured

AZURE_DEVOPS_PAT env var still works for CI/headless environments.

Dialog UX

  • SSH-dialog-style navigation — field_focus: usize with zones (bar / input / OK / Cancel). Tab cycles, Left/Right on bar switches mode, Alt+Left/Right always switches.
  • Text input reuses TextInput::handle_action — selection, undo/redo, clipboard, cursor movement all work.
  • Centered tab bar with bracketed active tab; whole row gets the focus background when focused.
  • ✓ indicator on tabs/body showing which methods have stored credentials.
  • Theme-aware colors (error_fg, dialog_hint_fg) — no hardcoded Color::Red / Color::DarkGray.
  • Auto-opens only when no credentials exist. When stored credentials are rejected, the error (with HTTP status) goes to the status bar instead of relooping the dialog.

CI panel changes

  • query_azure_steps, query_azure_test_results, and Azure log downloads now send PAT (Basic) or Bearer auth headers.
  • HTTP status code captured via curl -w and surfaced in the error message — distinguishes "no auth" from "auth rejected".
  • On auth failure, the affected check is remembered so the expansion auto-retries once the user authenticates. No need to press Right again.
  • Footer shows a: Azure Auth; all footers switched from C-e/A-Up to Ctrl+E/Alt+Up for readability.

Cross-platform

  • macOS Keychain added alongside the pre-existing Linux keyring support.
  • PKCE SHA-256 via openssl (present on all target platforms; pure-Rust fallback can be added later if needed).
  • No new crate dependencies.
  • Works on all four targets (macOS arm64/x86_64, Linux musl arm64/x86_64).

Pre-existing cleanup

While here, made cargo clippy --all-targets -- -D warnings pass on master:

  • Fixed 10 pre-existing clippy lints in fs_ops tests, gcs, nfs_client, editor, and integration_remote.
  • Removed 6 truly-dead items: theme::dialog_text_style, fs_ops::{create_directory, rename_entry}, github::checks_url, PrInfo::title, ParquetViewerState::error.
  • Kept #[allow(dead_code)] on VT methods used only by unit tests (those are legitimately part of the public API).

Test plan

  • cargo fmt --check passes
  • cargo clippy --all-targets -- -D warnings passes
  • cargo test — 908 tests pass
  • Manual: PAT mode — paste token, verify keychain storage, CI expansion succeeds
  • Manual: Browser mode — with a registered tenant, click Login, verify browser opens and token is stored
  • Manual: az CLI mode — with az login done, click Fetch, verify token stored
  • Manual: Tab/Shift+Tab/Alt+Left-Right navigation, Esc closes, Enter submits
  • Manual: auth failure surfaces HTTP status in status bar (no loop)
  • Manual: stored-credential ✓ indicators appear correctly on tabs

🤖 Generated with Claude Code

bluestreak01 and others added 4 commits April 16, 2026 19:38
The CI panel silently failed to expand Azure DevOps checks when the user
had no credentials, and there was no in-app way to authenticate. This
adds a full auth dialog with three methods, retry-on-success, and
proper macOS Keychain support.

## Azure auth

- **PAT mode** — paste a Personal Access Token, stored in macOS Keychain
  (`security`) or Linux keyring (`secret-tool`). Pre-existing env var
  `AZURE_DEVOPS_PAT` still works.
- **Browser mode** — OAuth2 authorization code flow with PKCE. Binds a
  random localhost port, opens the browser, captures the redirect, and
  exchanges the code for a token. Uses the Azure CLI's public client ID
  by default; override with `AZURE_DEVOPS_CLIENT_ID`. Tenant is entered
  in the dialog and persisted to keychain.
- **az CLI mode** — shells out to `az account get-access-token` to reuse
  an existing `az login` session. No OAuth, no redirect URI, no app
  registration required — just works if `az` is set up. Account status
  (user / tenant) is fetched asynchronously so tab switching stays
  responsive; switching away cancels any in-flight check.

## Dialog UX

- SSH-dialog-style navigation: `field_focus: usize` with zones (tab bar,
  input, OK, Cancel). Tab/Shift+Tab cycles, Left/Right on bar switches
  mode, Alt+Left/Right always switches mode.
- Text input reuses `TextInput::handle_action` via a catch-all — full
  selection, undo/redo, clipboard, cursor movement for free.
- Centered tab bar with bracketed active tab; whole row gets the focus
  background when focused.
- ✓ indicator on tabs/body when credentials are stored.
- Theme-aware colors (`error_fg`, `dialog_hint_fg`) — no hardcoded
  `Color::Red` / `Color::DarkGray`.
- Auto-opens only when no credentials exist; shows status-bar error
  instead of relooping when stored credentials are rejected.
- Manual open: press `a` while the CI panel is focused.

## CI panel changes

- `query_azure_steps`, `query_azure_test_results`, and log download all
  send PAT (Basic) or Bearer auth headers.
- HTTP status code captured via `curl -w` and surfaced in the error
  message, so rejection vs. missing-auth is distinguishable.
- On auth failure, the affected check is remembered so the expansion
  auto-retries once the user authenticates (no need to press Right
  again).

## Cross-platform

- macOS Keychain support added via `security find/add/delete-generic-password`.
- No new runtime dependencies (PKCE SHA-256 shells out to `openssl`, which
  is present on all target platforms; SHA-256 pure-Rust fallback could
  be added later if needed).
- Works on all four targets (macOS arm64/x86_64, Linux musl arm64/x86_64).

## Pre-existing cleanup

- `cargo clippy --all-targets -- -D warnings` now passes on master —
  fixed 10 pre-existing lints in fs_ops tests, gcs, nfs_client, editor,
  and integration_remote.
- Removed 6 truly-dead items: `theme::dialog_text_style`,
  `fs_ops::{create_directory, rename_entry}`, `github::checks_url`,
  `PrInfo::title`, `ParquetViewerState::error`. Kept `#[allow(dead_code)]`
  on VT methods used only by unit tests (public API).

## Footer

- CI panel footer now shows `a: Azure Auth`.
- All footers switched from terse `C-e`/`A-Up` to `Ctrl+E`/`Alt+Up` for
  readability. Fits 85+ column terminals.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Fixes for the review on the Azure DevOps auth dialog:

Correctness / safety
- azure_auth.rs: replace .unwrap() on stream.try_clone() with a match that
  always produces an AuthResult::Error, so the dialog can never hang on
  "Waiting for browser login..." when fd cloning fails. Extracted the
  redirect-handling logic into a dedicated handle_redirect helper.
- azure_auth.rs: add OAuth2 state parameter (RFC 6749 §10.12) for CSRF
  protection. Generated per flow, included in the auth URL, and validated
  against the received state before accepting the code.
- azure_auth.rs: replace xorshift entropy with /dev/urandom-backed
  fill_random(). Used for both the PKCE code verifier and the new state
  nonce. Falls back to the old seeded xorshift only if /dev/urandom is
  unavailable (logged to debug).
- azure_auth.rs: drop three dead set_nonblocking(false) calls inside the
  worker thread; call set_nonblocking(true) once before spawning so the
  error can propagate to the caller.

UX
- app.rs: reset field_focus to the Cancel zone when browser_flow
  transitions from Some to None on error (storage failure or
  AuthResult::Error). Without this, focus silently jumped onto the tenant
  input because the zone layout changed under the stored index.

Dedup / cleanup
- app.rs: promote Action::OpenAzureAuth to a global early intercept next
  to ToggleSettings; remove the duplicated CI-scoped arm.
- azure_auth.rs + ci.rs: hoist SPINNER to a single pub(crate) const in
  azure_auth.rs and reuse from ci.rs.
- ci.rs + azure_auth.rs: use security add-generic-password -U on macOS
  (upsert) instead of delete-then-add, halving the argv-visibility
  window, and document the argv-leak tradeoff.
- azure_auth.rs: unify query-string extraction into a single query_param
  helper used by extract_code/state/error_from_request.

Help text
- ui/help_dialog.rs: replace the misleading "Set AZURE_DEVOPS_TENANT to
  enable OAuth2 login" entry with actual dialog keybindings (Tab, Alt+
  Left/Right, Enter, Esc) and document all three env vars
  (AZURE_DEVOPS_PAT, AZURE_DEVOPS_TENANT, AZURE_DEVOPS_CLIENT_ID).

Tests
- app.rs: add azure_auth_dialog_tests covering AzureAuthMode next/prev
  round-trip, has_input, max_focus for each (mode × in_flight) combo,
  per-zone focus predicates, the invariant that exactly one focus
  predicate is true at every zone of every mode, and active_input_mut
  routing by mode. 11 new tests. Added Debug derive on AzureAuthMode
  so assert_eq!/format! work in the tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- azure_auth: BrowserAuthFlow now carries an Arc<AtomicBool> and Drop
  impl so the worker thread exits promptly on cancel instead of holding
  the bound localhost port for the full 2-minute timeout.
- azure_auth: exchange_code pipes the POST body (authorization code +
  PKCE verifier) through curl's stdin via --data-binary @- instead of
  -d <body>, so OAuth secrets no longer appear in argv / ps output.
- app, azure_auth_dialog: get_token_via_az_cli now runs on a background
  thread. az_fetching is actually toggled true while the fetch is in
  flight (was dead state), the tick loop polls az_token_rx and closes
  the dialog on success, and the renderer shows a spinner + "Fetching
  token from az CLI..." while waiting.
- help_dialog: key column width is computed from the widest key in
  HELP_SECTIONS via OnceLock instead of a fixed :>20, so long keys like
  AZURE_DEVOPS_CLIENT_ID (22 chars) no longer push the description
  column out of alignment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bluestreak01 bluestreak01 merged commit 4a610b8 into master Apr 16, 2026
5 checks passed
@bluestreak01 bluestreak01 deleted the vi_az branch April 16, 2026 23:15
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