Add Azure DevOps authentication dialog for CI panel#22
Merged
bluestreak01 merged 4 commits intomasterfrom Apr 16, 2026
Merged
Conversation
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>
# Conflicts: # src/app.rs
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
ain the CI panel (or it auto-opens when auth is needed). Dialog has three tabs:security) or Linux keyring (secret-tool).AZURE_DEVOPS_CLIENT_ID. Tenant is entered in the dialog and persisted.az account get-access-token. Reuses your existingaz loginsession — no OAuth dance, no app registration. Account status fetched async so tab switches stay fast; rapid switching cancels in-flight checks.azconfiguredAZURE_DEVOPS_PATenv var still works for CI/headless environments.Dialog UX
field_focus: usizewith zones (bar / input / OK / Cancel). Tab cycles, Left/Right on bar switches mode, Alt+Left/Right always switches.TextInput::handle_action— selection, undo/redo, clipboard, cursor movement all work.error_fg,dialog_hint_fg) — no hardcodedColor::Red/Color::DarkGray.CI panel changes
query_azure_steps,query_azure_test_results, and Azure log downloads now send PAT (Basic) or Bearer auth headers.curl -wand surfaced in the error message — distinguishes "no auth" from "auth rejected".a: Azure Auth; all footers switched fromC-e/A-UptoCtrl+E/Alt+Upfor readability.Cross-platform
openssl(present on all target platforms; pure-Rust fallback can be added later if needed).Pre-existing cleanup
While here, made
cargo clippy --all-targets -- -D warningspass on master:fs_opstests,gcs,nfs_client,editor, andintegration_remote.theme::dialog_text_style,fs_ops::{create_directory, rename_entry},github::checks_url,PrInfo::title,ParquetViewerState::error.#[allow(dead_code)]on VT methods used only by unit tests (those are legitimately part of the public API).Test plan
cargo fmt --checkpassescargo clippy --all-targets -- -D warningspassescargo test— 908 tests passaz logindone, click Fetch, verify token stored🤖 Generated with Claude Code