Skip to content

feat(cli): OAuth 2.0 Device Authorization Grant, silent token refresh, and TLS fixes#656

Open
mickume wants to merge 16 commits intojumpstarter-dev:mainfrom
mickume:fix/cli-device-flow
Open

feat(cli): OAuth 2.0 Device Authorization Grant, silent token refresh, and TLS fixes#656
mickume wants to merge 16 commits intojumpstarter-dev:mainfrom
mickume:fix/cli-device-flow

Conversation

@mickume
Copy link
Copy Markdown
Contributor

@mickume mickume commented May 7, 2026

Summary

  • OAuth 2.0 Device Authorization Grant: Add --device-flow / --no-device-flow CLI option to jmp login, enabling headless authentication via the RFC 8628 device code flow. Auto-detected when running inside VS Code Dev Spaces (VSCODE_INJECTION=1) or when JMP_OIDC_DEVICE_FLOW=1 is set.
  • Silent token refresh: Background token-expiry monitor no longer prints noisy status messages; warnings are downgraded to logger.debug/logger.info. Token refresh also works correctly with --insecure-tls now by passing the TLS flag to the OIDC config.
  • Automatic retry on expired tokens: handle_exceptions_with_reauthentication now silently re-authenticates and retries the wrapped function once, instead of asking the user to manually retry.
  • Warning suppression: Suppress urllib3.InsecureRequestWarning (when user already opted into --insecure-tls) and transient authlib.jose deprecation warnings.
  • Lint fixes: Resolved ruff E402 (import ordering), C901 (function complexity), and W293 (trailing whitespace) violations.

Test plan

  • Verify jmp login --device-flow prompts with a verification URI and polls until the user completes browser auth
  • Verify auto-detection in a Dev Spaces environment (VSCODE_INJECTION=1)
  • Verify jmp login without --device-flow still uses the authorization code grant
  • Verify expired-token re-auth + retry works transparently (no "please try again" message)
  • Verify --insecure-tls no longer emits InsecureRequestWarning on every request
  • Run existing OIDC and exception unit tests (oidc_test.py, exceptions_test.py, shell_test.py)

🤖 Generated with Claude Code

mickume and others added 15 commits April 29, 2026 16:46
…ixes #13)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ixes #13)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Silence shell token expiry warnings and success messages — only warn
when refresh fails and the user needs to act. Auto-retry commands after
successful re-authentication instead of telling the user to try again.
_try_refresh_token was not passing insecure_tls to the OIDC Config,
causing silent TLS verification failures when using --insecure-tls.
Also promote the "Token refresh failed" log to INFO so users can see
why refresh fails at the default log level.
Replace nonlocal variables with mutable list containers in
exceptions_test.py to work around ty's lack of nonlocal support.
Add ty overrides for gpiod driver to suppress unresolvable import
and attribute warnings from the Linux-only gpiod C extension.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces OIDC device authorization flow support for CLI authentication, refactors token-expiration reauthentication with single-retry semantics, improves token recovery logging transparency, and enhances QEMU driver cloud-init media handling and binary selection.

Changes

OIDC Device Flow & Exception Handling

Layer / File(s) Summary
Configuration Constants
jumpstarter/config/env.py
Adds JMP_OIDC_DEVICE_FLOW environment variable constant for device-flow auto-detection.
OIDC Core Implementation
jumpstarter-cli-common/jumpstarter_cli_common/oidc.py
Implements Config.device_code_grant() method to perform OIDC device authorization with polling, error handling, and user code printing; adds should_use_device_flow() to select flow based on flag or environment variables; adds warning filters for deprecation and TLS noise; new constants for device-flow grant type and polling interval.
CLI Device Flow Option
jumpstarter-cli-common/jumpstarter_cli_common/oidc.py
Adds --device-flow/--no-device-flow flag to opt_oidc click decorator with auto-detection help text.
Login Command Integration
jumpstarter-cli/jumpstarter_cli/login.py
Integrates device flow into login() command and relogin_client(): conditionally uses device_code_grant() when should_use_device_flow() is true; otherwise falls back to authorization-code grant.
Exception Handler Refactoring
jumpstarter-cli-common/jumpstarter_cli_common/exceptions.py
Refactors _handle_connection_error_with_reauth(), _handle_single_exception_with_reauth(), and _handle_exception_group_with_reauth() to return success booleans; updates handle_exceptions_with_reauthentication decorator to retry wrapped function exactly once after successful re-auth and remap exceptions via _map_cli_exception.
Token Recovery & Logging
jumpstarter-cli/jumpstarter_cli/shell.py
Changes token refresh failure log from debug to info level; removes user-facing green terminal message for successful recovery and logs at debug level instead; replaces token-expiry warning echo with debug log; passes insecure_tls parameter through OIDC config.
OIDC Device Flow Tests
jumpstarter-cli-common/jumpstarter_cli_common/oidc_test.py
Adds async test suite for should_use_device_flow() (flag/environment precedence), device_code_grant() (discovery, polling, error handling, user output), and constants validation.
Exception & Shell Recovery Tests
jumpstarter-cli-common/jumpstarter_cli_common/exceptions_test.py, jumpstarter-cli/jumpstarter_cli/shell_test.py
Tests exception-handler single retry on success and no double-retry on subsequent failure; validates shell token refresh with insecure_tls flag; asserts no user-visible echo output on token recovery or expiry-window handling.

QEMU Driver & Infrastructure Updates

Layer / File(s) Summary
Cloud-Init Media & Binary Selection
jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver.py
Replaces vvfat-based cidata with ISO-based cloud-init media (genisoimage/mkisofs/xorriso); implements dynamic QEMU binary selection (preferring qemu-kvm when available, falling back to qemu-system-); strips whitespace from OCI URLs before validation.
Cleanup & Configuration
jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver.py, jumpstarter-driver-gpiod/pyproject.toml
Removes generated _cidata_iso file during power-off; adds Typer/typing tool overrides for gpiod driver to suppress unresolved-import and possibly-unbound-attribute diagnostics.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • mangelajo
  • bennyz

Poem

🐰 Device codes bloom, retry once with care,
Cloud-init in ISO, through the air,
Auth flows dance with cleaner logs,
QEMU leaps where binary sings,
A hop, a skip, what robustness brings! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title comprehensively captures the three main features: OAuth 2.0 Device Authorization Grant, silent token refresh, and TLS fixes, directly matching the primary changes in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description clearly and comprehensively details the three main changes: OAuth 2.0 Device Authorization Grant implementation, silent token refresh with automatic retry, and TLS/warning suppression improvements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py (1)

261-272: ⚡ Quick win

Use OAuthError.error attribute instead of fragile string matching

The string-based error dispatch is fragile. authlib raises OAuthError with a structured .error attribute containing the RFC 8628 error code (e.g., e.error == "authorization_pending"), which is safer and immune to message-format changes.

Additionally, line 268's or "expired" in error_msg adds only false-positive risk—"expired" is not an RFC 8628 device flow error code; only "expired_token" is. Using the structured .error attribute with just e.error == "expired_token" avoids both the substring-matching fragility and the unnecessary fallback.

♻️ Proposed refactor
+from authlib.integrations.base_client.errors import OAuthError
 
             client = self.client()
             try:
                 token = await run_sync(
                     lambda c=client: c.fetch_token(
                         token_endpoint,
                         grant_type=DEVICE_FLOW_GRANT_TYPE,
                         device_code=device_code,
                     )
                 )
                 return token
-            except Exception as e:
-                error_msg = str(e)
-                if "authorization_pending" in error_msg:
+            except OAuthError as e:
+                if e.error == "authorization_pending":
                     continue
-                if "slow_down" in error_msg:
+                if e.error == "slow_down":
                     interval += 5
                     continue
-                if "expired_token" in error_msg or "expired" in error_msg:
+                if e.error == "expired_token":
                     raise click.ClickException("Device code expired. Please try again.") from e
-                if "access_denied" in error_msg:
+                if e.error == "access_denied":
                     raise click.ClickException("Authorization was denied by the user.") from e
                 raise click.ClickException(f"Device flow token request failed: {e}") from e
+            except Exception as e:
+                raise click.ClickException(f"Device flow token request failed: {e}") from e
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py` around
lines 261 - 272, Replace fragile substring matching in the exception handler
with structured checks against authlib.oauth2.rfc6749.errors.OAuthError.error:
import OAuthError (or the correct authlib exception class), catch OAuthError (or
check isinstance) in the except block and use e.error ==
"authorization_pending"/"slow_down"/"expired_token"/"access_denied" to control
flow (increase interval for "slow_down", continue for "authorization_pending",
raise a ClickException for "expired_token" and "access_denied"); remove the
`"expired" in error_msg` fallback and for any other error re-raise a
ClickException including the original exception for context.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/packages/jumpstarter-cli-common/jumpstarter_cli_common/exceptions.py`:
- Around line 232-239: Extract a shared helper (e.g.,
_invoke_and_map_exceptions(func, *args, login_func=None, **kwargs)) that runs
func(*args, **kwargs) and centralizes exception mapping: catch both
BaseExceptionGroup and Exception/KeyboardInterrupt, iterate leaf_exceptions(eg,
fix_tracebacks=False) for groups and single exceptions alike, call
_map_cli_exception on each leaf (instead of returning True on the first leaf),
and collect/make decisions from those mappings (call login_func if mapping
indicates reauth and return a boolean indicating whether reauth succeeded and
the caller should retry). Replace the duplicated try/except logic in
_handle_exception_group_with_reauth and the retry block so both use this new
helper, ensuring BaseExceptionGroup is handled on both initial and retry
attempts and reducing complexity to satisfy Ruff C901.

In `@python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py`:
- Around line 29-33: The current module-level warnings.filterwarnings call that
silences Unverified HTTPS warnings must be removed and the suppression made
conditional where the insecure_tls option is applied; delete the global
warnings.filterwarnings(...) for InsecureRequestWarning in
jumpstarter_cli_common/oidc.py and instead add a conditional
warnings.filterwarnings("ignore", category=InsecureRequestWarning, ...) inside
the code path that consumes insecure_tls (e.g., inside Config.client() or
wherever Config.client(insecure_tls=...) is invoked) so the warning is only
suppressed when insecure_tls=True; keep the authlib-related filter moved so all
warning filters run after imports.
- Around line 34-38: The E402 lint errors are caused by placing
warnings.filterwarnings(...) before the module imports; move both
warnings.filterwarnings(...) blocks so that all imports (e.g., OAuth2Session,
extract_compact, URL and JMP_OIDC_CALLBACK_PORT/JMP_OIDC_DEVICE_FLOW) appear
first, then add the two warnings.filterwarnings(...) calls after the import
section (they can remain where they are functionally, since the authlib warning
fires at runtime), and run make lint-fix to verify.

In `@python/packages/jumpstarter-cli/jumpstarter_cli/login.py`:
- Around line 338-340: Silent reauth drops an explicit --device-flow choice:
login() respects the flag but relogin_client() recomputes flow from env/VS Code
detection causing headless users to fall back to browser auth; fix by persisting
the chosen flow in the client config (e.g., store a device_flow boolean on the
Client/Config object when login() runs) and update relogin_client() to read that
stored flag instead of recomputing via should_use_device_flow(), or add a
parameter to relogin_client(device_flow=...) and thread the device_flow
selection from login() through client initialization and any code paths that
call relogin_client(); update any helpers that call oidc.device_code_grant()
(references: login(), relogin_client(), should_use_device_flow(),
oidc.device_code_grant) to use the persisted/passed-in flag.

In `@python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver.py`:
- Around line 235-241: The loop that picks qemu_binary (in
jumpstarter_driver_qemu.driver.py) incorrectly treats the bare "qemu-kvm" as
present because "/" not in qemu_binary is true; change the selection logic to
check executability properly: for each candidate in ["/usr/libexec/qemu-kvm",
"/usr/bin/qemu-kvm", "qemu-kvm"] verify either Path(candidate).exists() and is
executable for absolute paths or use shutil.which(candidate) (or os.access on
the resolved path) for the bare name, only break when a valid executable is
found, and if none are valid set qemu_binary = f"qemu-system-{self.parent.arch}"
so the code falls back cleanly when qemu-kvm isn’t installed.
- Around line 378-380: The current assignment self._cidata_iso =
Path(self._cidata.name).parent / "cidata.iso" places the ISO in the shared temp
root causing races; change it to create the ISO under the driver's own temp
directory (e.g. use your driver temp attribute such as self._tmpdir or
self._temp_dir) so each VM gets a unique path, e.g. set self._cidata_iso =
Path(self._tmpdir) / "cidata.iso" (or the equivalent driver temp attribute
already in this class) and ensure subsequent code uses this new attribute.

---

Nitpick comments:
In `@python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py`:
- Around line 261-272: Replace fragile substring matching in the exception
handler with structured checks against
authlib.oauth2.rfc6749.errors.OAuthError.error: import OAuthError (or the
correct authlib exception class), catch OAuthError (or check isinstance) in the
except block and use e.error ==
"authorization_pending"/"slow_down"/"expired_token"/"access_denied" to control
flow (increase interval for "slow_down", continue for "authorization_pending",
raise a ClickException for "expired_token" and "access_denied"); remove the
`"expired" in error_msg` fallback and for any other error re-raise a
ClickException including the original exception for context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c168f8f7-15f1-4f7f-bc0e-87575bb1cb75

📥 Commits

Reviewing files that changed from the base of the PR and between abd5d91 and 921fea5.

📒 Files selected for processing (10)
  • python/packages/jumpstarter-cli-common/jumpstarter_cli_common/exceptions.py
  • python/packages/jumpstarter-cli-common/jumpstarter_cli_common/exceptions_test.py
  • python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py
  • python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc_test.py
  • python/packages/jumpstarter-cli/jumpstarter_cli/login.py
  • python/packages/jumpstarter-cli/jumpstarter_cli/shell.py
  • python/packages/jumpstarter-cli/jumpstarter_cli/shell_test.py
  • python/packages/jumpstarter-driver-gpiod/pyproject.toml
  • python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver.py
  • python/packages/jumpstarter/jumpstarter/config/env.py

Comment on lines +29 to +33
warnings.filterwarnings(
"ignore",
message=r"Unverified HTTPS request is being made.*",
module=r"urllib3\..*",
)
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

InsecureRequestWarning is suppressed globally, not conditionally on --insecure-tls

The comment says "when the user opts into --insecure-tls" but the filter is applied unconditionally at module import time. Any unverified HTTPS request made anywhere in the process — including from unrelated code paths — will silently drop its warning for every user, not just those who opted in.

Since the authlib comment above (lines 18-19) correctly explains why the warning fires at runtime not at import time, the right fix for both warning filters is to move them after all imports. For the InsecureRequestWarning specifically, the suppression should be conditional:

🛡️ Proposed fix: conditional suppression

Remove the module-level InsecureRequestWarning filter entirely, and add it where insecure_tls=True is first used, e.g., in Config.client():

 def client(self, **kwargs):
+    if self.insecure_tls:
+        warnings.filterwarnings(
+            "ignore",
+            message=r"Unverified HTTPS request is being made.*",
+            module=r"urllib3\..*",
+        )
     session = OAuth2Session(client_id=self.client_id, scope=self._scopes(), **kwargs)
     session.verify = False if self.insecure_tls else (os.environ.get("SSL_CERT_FILE") or certifi.where())
     return session
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py` around
lines 29 - 33, The current module-level warnings.filterwarnings call that
silences Unverified HTTPS warnings must be removed and the suppression made
conditional where the insecure_tls option is applied; delete the global
warnings.filterwarnings(...) for InsecureRequestWarning in
jumpstarter_cli_common/oidc.py and instead add a conditional
warnings.filterwarnings("ignore", category=InsecureRequestWarning, ...) inside
the code path that consumes insecure_tls (e.g., inside Config.client() or
wherever Config.client(insecure_tls=...) is invoked) so the warning is only
suppressed when insecure_tls=True; keep the authlib-related filter moved so all
warning filters run after imports.

Comment thread python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py Outdated
Comment on lines +338 to 340
elif should_use_device_flow(device_flow):
tokens = await oidc.device_code_grant()
else:
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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Silent reauth forgets an explicit --device-flow choice.

login() honors --device-flow, but relogin_client() recomputes the flow only from env/VS Code detection. A user who logged in with jmp login --device-flow and did not also export JMP_OIDC_DEVICE_FLOW=1 will fall back to browser auth on token expiry, which breaks the headless workflow this PR is adding. Persist the selected auth flow in the client config, or otherwise thread it into relogin_client().

Also applies to: 388-391

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/packages/jumpstarter-cli/jumpstarter_cli/login.py` around lines 338 -
340, Silent reauth drops an explicit --device-flow choice: login() respects the
flag but relogin_client() recomputes flow from env/VS Code detection causing
headless users to fall back to browser auth; fix by persisting the chosen flow
in the client config (e.g., store a device_flow boolean on the Client/Config
object when login() runs) and update relogin_client() to read that stored flag
instead of recomputing via should_use_device_flow(), or add a parameter to
relogin_client(device_flow=...) and thread the device_flow selection from
login() through client initialization and any code paths that call
relogin_client(); update any helpers that call oidc.device_code_grant()
(references: login(), relogin_client(), should_use_device_flow(),
oidc.device_code_grant) to use the persisted/passed-in flag.

Comment on lines +235 to +241
if self.parent.arch == platform.machine() and os.access("/dev/kvm", os.R_OK | os.W_OK):
# Try common locations for qemu-kvm
for qemu_binary in ["/usr/libexec/qemu-kvm", "/usr/bin/qemu-kvm", "qemu-kvm"]:
if Path(qemu_binary).exists() or "/" not in qemu_binary:
break
else:
qemu_binary = f"qemu-system-{self.parent.arch}"
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fall back cleanly when qemu-kvm is not installed.

This loop always breaks on the bare qemu-kvm candidate because "/" not in qemu_binary" is true, even if that wrapper is missing from PATH. On hosts with KVM access but only qemu-system-<arch>, on() now fails before boot.

Suggested fix
-        if self.parent.arch == platform.machine() and os.access("/dev/kvm", os.R_OK | os.W_OK):
-            # Try common locations for qemu-kvm
-            for qemu_binary in ["/usr/libexec/qemu-kvm", "/usr/bin/qemu-kvm", "qemu-kvm"]:
-                if Path(qemu_binary).exists() or "/" not in qemu_binary:
-                    break
-        else:
-            qemu_binary = f"qemu-system-{self.parent.arch}"
+        qemu_binary = f"qemu-system-{self.parent.arch}"
+        if self.parent.arch == platform.machine() and os.access("/dev/kvm", os.R_OK | os.W_OK):
+            for candidate in ["/usr/libexec/qemu-kvm", "/usr/bin/qemu-kvm", "qemu-kvm"]:
+                if ("/" in candidate and Path(candidate).exists()) or shutil.which(candidate):
+                    qemu_binary = candidate
+                    break
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver.py`
around lines 235 - 241, The loop that picks qemu_binary (in
jumpstarter_driver_qemu.driver.py) incorrectly treats the bare "qemu-kvm" as
present because "/" not in qemu_binary is true; change the selection logic to
check executability properly: for each candidate in ["/usr/libexec/qemu-kvm",
"/usr/bin/qemu-kvm", "qemu-kvm"] verify either Path(candidate).exists() and is
executable for absolute paths or use shutil.which(candidate) (or os.access on
the resolved path) for the bare name, only break when a valid executable is
found, and if none are valid set qemu_binary = f"qemu-system-{self.parent.arch}"
so the code falls back cleanly when qemu-kvm isn’t installed.

Comment on lines +378 to +380
# Create ISO image from cidata directory for cloud-init
# This is more compatible than vvfat which may not be available in qemu-kvm
self._cidata_iso = Path(self._cidata.name).parent / "cidata.iso"
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a VM-scoped path for cidata.iso.

Path(self._cidata.name).parent / "cidata.iso" resolves to the shared temp root, so parallel QEMU instances race on the same file and off() from one VM can delete another VM's cloud-init media. Put the ISO under the driver's own temp directory instead.

Suggested fix
-        self._cidata_iso = Path(self._cidata.name).parent / "cidata.iso"
+        self._cidata_iso = Path(self.parent._tmp_dir.name) / "cidata.iso"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver.py`
around lines 378 - 380, The current assignment self._cidata_iso =
Path(self._cidata.name).parent / "cidata.iso" places the ISO in the shared temp
root causing races; change it to create the ISO under the driver's own temp
directory (e.g. use your driver temp attribute such as self._tmpdir or
self._temp_dir) so each VM gets a unique path, e.g. set self._cidata_iso =
Path(self._tmpdir) / "cidata.iso" (or the equivalent driver temp attribute
already in this class) and ensure subsequent code uses this new attribute.

Move warnings.filterwarnings after imports to fix E402, extract helpers
to reduce complexity below C901 threshold, and remove trailing whitespace.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@raballew raballew self-requested a review May 8, 2026 21:06
Copy link
Copy Markdown
Member

@raballew raballew left a comment

Choose a reason for hiding this comment

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

Some inconsistencies but a good first second try.

Comment on lines +34 to +38
warnings.filterwarnings(
"ignore",
message=r"Unverified HTTPS request is being made.*",
module=r"urllib3\..*",
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The InsecureRequestWarning suppression runs unconditionally at module import time, which means any code that imports oidc silently hides legitimate TLS errors even when the user never passed --insecure-tls.

Consider moving the warnings.filterwarnings call into Config.client() or Config.__init__, gated on self.insecure_tls, so the warning is only suppressed when the user explicitly opted in.

Comment on lines 1 to 21
import ssl
from unittest.mock import AsyncMock, MagicMock, patch

from jumpstarter_cli_common.oidc import Config, _get_ssl_context
import click
import pytest

from jumpstarter_cli_common.oidc import (
DEVICE_FLOW_GRANT_TYPE,
DEVICE_FLOW_POLL_INTERVAL,
Config,
_get_ssl_context,
should_use_device_flow,
)


@pytest.fixture
def anyio_backend():
return "asyncio"


class TestConfigInsecureTls:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No tests covering the expired_token error path or the deadline-exceeded timeout path in device_code_grant. Both are critical failure modes.

@@ -23,3 +38,253 @@ class TestGetSslContext:
def test_returns_ssl_context(self) -> None:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There is no test verifying the interval actually increases. A regression here would silently violate the RFC.

Comment on lines +261 to +272
except Exception as e:
error_msg = str(e)
if "authorization_pending" in error_msg:
continue
if "slow_down" in error_msg:
interval += 5
continue
if "expired_token" in error_msg or "expired" in error_msg:
raise click.ClickException("Device code expired. Please try again.") from e
if "access_denied" in error_msg:
raise click.ClickException("Authorization was denied by the user.") from e
raise click.ClickException(f"Device flow token request failed: {e}") from e
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The authlib library typically provides structured OAuthError with an .error attribute. Consider checking for specific exception types and using structured attributes, falling back to string matching only for unknown types.

while time.monotonic() < deadline:
await anyio.sleep(interval)

client = self.client()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The device flow polling loop creates a new self.client() (OAuth2Session wrapping a requests.Session with a connection pool) on every iteration.

Comment on lines 385 to 386
except Exception:
pass
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When refresh_token_grant fails in relogin_client, a bare except Exception: pass silently swallows the error with no logging. This makes debugging refresh failures during re-authentication very difficult.

lambda: client.fetch_token(config["token_endpoint"], authorization_response=authorization_response)
)

async def device_code_grant(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a lot of responsibility in one class.

Consider extracting grant-specific logic into standalone functions or strategy classes. For example, device_code_grant could be a standalone function that receives a Config and returns tokens.

"user_code": "ABCD-1234",
"verification_uri": "https://auth.example.com/device",
"expires_in": 600,
"interval": 0,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is essentially dead code. Using a non-zero interval (e.g., 5) and verifying mock sleep was called with the correct value would make these tests more meaningful.

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