-
Notifications
You must be signed in to change notification settings - Fork 10
perf(cli): avoid duplicate load_session in auth status (#146) #153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| import time | ||
| from dataclasses import dataclass | ||
| from typing import Literal, NoReturn | ||
|
|
||
|
|
@@ -32,7 +33,14 @@ | |
| describe_missing_oauth_vars, | ||
| ensure_public_graphql_configured, | ||
| ) | ||
| from pipefy_cli.oauth import RefreshError, ensure_fresh_session, load_session | ||
| from pipefy_cli.oauth import ( | ||
| RefreshError, | ||
| StoredSession, | ||
| ensure_fresh_session, | ||
| load_session, | ||
| ) | ||
|
|
||
| _SESSION_FRESHNESS_LEEWAY_S = 60 | ||
|
|
||
| AuthSource = Literal[ | ||
| "flag-token", | ||
|
|
@@ -108,9 +116,19 @@ def _cache_key( | |
| ) | ||
|
|
||
|
|
||
| def _session_is_fresh( | ||
| session: StoredSession, *, leeway_s: int = _SESSION_FRESHNESS_LEEWAY_S | ||
| ) -> bool: | ||
| """Return whether the access token is outside the refresh leeway window.""" | ||
| expires_in = session.expires_in or 0 | ||
| return time.time() < session.obtained_at + expires_in - leeway_s | ||
|
|
||
|
|
||
| def detect_all_sources( | ||
| pipefy_settings: PipefySettings, | ||
| auth: AuthContext, | ||
| *, | ||
| cached_stored_session: bool | None = None, | ||
| ) -> list[AuthSource]: | ||
| """Return every configured credential source, highest-precedence first. | ||
|
|
||
|
|
@@ -125,24 +143,31 @@ def detect_all_sources( | |
| ) | ||
| if not describe_missing_oauth_vars(pipefy_settings): | ||
| sources.append("service-account") | ||
| if ( | ||
| auth.oidc_client is not None | ||
| and load_session( | ||
| issuer=auth.oidc_client.issuer_url, | ||
| client_id=auth.oidc_client.client_id, | ||
| if auth.oidc_client is not None: | ||
| has_stored = ( | ||
| cached_stored_session | ||
| if cached_stored_session is not None | ||
| else load_session( | ||
| issuer=auth.oidc_client.issuer_url, | ||
| client_id=auth.oidc_client.client_id, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: correct (the if cached_stored_session is None:
cached_stored_session = (
load_session(issuer=auth.oidc_client.issuer_url, client_id=auth.oidc_client.client_id)
is not None
)
if cached_stored_session:
sources.append("stored-session") |
||
| ) | ||
| is not None | ||
| ) | ||
| is not None | ||
| ): | ||
| sources.append("stored-session") | ||
| if has_stored: | ||
| sources.append("stored-session") | ||
| return sources | ||
|
|
||
|
|
||
| def detect_auth_source( | ||
| pipefy_settings: PipefySettings, | ||
| auth: AuthContext, | ||
| *, | ||
| cached_stored_session: bool | None = None, | ||
| ) -> AuthSource: | ||
| """Return the precedence winner among configured sources, or ``"none"``.""" | ||
| sources = detect_all_sources(pipefy_settings, auth) | ||
| sources = detect_all_sources( | ||
| pipefy_settings, auth, cached_stored_session=cached_stored_session | ||
| ) | ||
| return sources[0] if sources else "none" | ||
|
|
||
|
|
||
|
|
@@ -169,6 +194,8 @@ def _raise_internal_error(detail: str) -> NoReturn: | |
| def get_authenticated_client( | ||
| pipefy_settings: PipefySettings, | ||
| auth: AuthContext, | ||
| *, | ||
| prefetched_session: StoredSession | None = None, | ||
| ) -> PipefyClient: | ||
| """Return a facade client using the highest-precedence available auth source. | ||
|
|
||
|
|
@@ -179,6 +206,9 @@ def get_authenticated_client( | |
| tier 4 (stored user session keyed by issuer + client id). Tier 3 | ||
| (service-account client credentials) is resolved from | ||
| ``pipefy_settings`` alone. | ||
| prefetched_session: When set (e.g. by ``pipefy auth status`` after a | ||
| single keychain read + refresh), skips a second | ||
| ``ensure_fresh_session`` round-trip for the stored-session branch. | ||
|
|
||
| Returns: | ||
| Shared in-process instance when settings match a prior call; otherwise | ||
|
|
@@ -196,7 +226,11 @@ def get_authenticated_client( | |
| typer.echo(str(exc), err=True) | ||
| raise typer.Exit(2) from exc | ||
|
|
||
| source = detect_auth_source(pipefy_settings, auth) | ||
| source = detect_auth_source( | ||
| pipefy_settings, | ||
| auth, | ||
| cached_stored_session=True if prefetched_session is not None else None, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick (if-minor): |
||
| ) | ||
|
|
||
| if source == "none": | ||
| typer.echo(_missing_auth_message(pipefy_settings), err=True) | ||
|
|
@@ -216,24 +250,25 @@ def get_authenticated_client( | |
| _raise_internal_error( | ||
| "auth source 'stored-session' detected but no OIDC client is configured." | ||
| ) | ||
| try: | ||
| session = ensure_fresh_session( | ||
| issuer=auth.oidc_client.issuer_url, | ||
| client_id=auth.oidc_client.client_id, | ||
| ) | ||
| except RefreshError as exc: | ||
| typer.echo( | ||
| f"Stored Pipefy session could not be refreshed: {exc}. " | ||
| "Run `pipefy auth login` to sign in again.", | ||
| err=True, | ||
| ) | ||
| raise typer.Exit(2) from exc | ||
| # ``detect_auth_source`` already confirmed a session is present; if it | ||
| # vanished between the two reads, fall through to the missing-auth path. | ||
| if session is None: | ||
| typer.echo(_missing_auth_message(pipefy_settings), err=True) | ||
| raise typer.Exit(2) | ||
| effective_bearer = session.access_token | ||
| if prefetched_session is not None: | ||
| effective_bearer = prefetched_session.access_token | ||
| else: | ||
| try: | ||
| session = ensure_fresh_session( | ||
| issuer=auth.oidc_client.issuer_url, | ||
| client_id=auth.oidc_client.client_id, | ||
| ) | ||
| except RefreshError as exc: | ||
| typer.echo( | ||
| f"Stored Pipefy session could not be refreshed: {exc}. " | ||
| "Run `pipefy auth login` to sign in again.", | ||
| err=True, | ||
| ) | ||
| raise typer.Exit(2) from exc | ||
| if session is None: | ||
| typer.echo(_missing_auth_message(pipefy_settings), err=True) | ||
| raise typer.Exit(2) | ||
| effective_bearer = session.access_token | ||
|
|
||
| key = _cache_key(pipefy_settings, effective_bearer) | ||
| if _cached_client is not None and _cached_signature == key: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ | |
| AuthContext, | ||
| AuthSource, | ||
| OidcClient, | ||
| _session_is_fresh, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: importing |
||
| detect_all_sources, | ||
| get_authenticated_client, | ||
| ) | ||
|
|
@@ -30,6 +31,7 @@ | |
| DiscoveryPolicy, | ||
| LoginError, | ||
| RefreshError, | ||
| StoredSession, | ||
| ensure_fresh_session, | ||
| keychain_backend_name, | ||
| load_session, | ||
|
|
@@ -319,22 +321,37 @@ class _StatusExit(Exception): | |
| stderr: str | None = None | ||
|
|
||
|
|
||
| def _populate_stored_session(report: AuthStatusReport, oidc: OidcClient) -> None: | ||
| """Populate stored-session fields on ``report``; raise ``_StatusExit`` on failure.""" | ||
| def _populate_stored_session( | ||
| report: AuthStatusReport, | ||
| oidc: OidcClient, | ||
| *, | ||
| loaded_session: StoredSession | None = None, | ||
| ) -> StoredSession: | ||
| """Populate stored-session fields on ``report``; raise ``_StatusExit`` on failure. | ||
|
|
||
| Returns the (possibly refreshed) session for reuse by :func:`_fetch_identity`. | ||
| """ | ||
| report.issuer = oidc.issuer_url | ||
| report.keychain_backend = keychain_backend_name() | ||
| try: | ||
| fresh_session = ensure_fresh_session( | ||
| issuer=oidc.issuer_url, client_id=oidc.client_id | ||
| ) | ||
| if loaded_session is not None and _session_is_fresh(loaded_session): | ||
| fresh_session = loaded_session | ||
| else: | ||
| fresh_session = ensure_fresh_session( | ||
| issuer=oidc.issuer_url, client_id=oidc.client_id | ||
| ) | ||
| except RefreshError as exc: | ||
| # Branch on the RFC 6749 ``error`` field, not on ``str(exc)``. The | ||
| # message text is for humans; ``error_code`` is the machine contract. | ||
| report.state = ( | ||
| "refresh-expired" if exc.error_code == "invalid_grant" else "needs-login" | ||
| ) | ||
| # Best-effort expiry from the pre-refresh blob so users see *why*. | ||
| stale = load_session(issuer=oidc.issuer_url, client_id=oidc.client_id) | ||
| stale = ( | ||
| loaded_session | ||
| if loaded_session is not None | ||
| else load_session(issuer=oidc.issuer_url, client_id=oidc.client_id) | ||
| ) | ||
| if stale is not None: | ||
| report.access_expires_at = _iso_expiry(stale.obtained_at, stale.expires_in) | ||
| report.refresh_expires_at = _iso_expiry( | ||
|
|
@@ -363,13 +380,20 @@ def _populate_stored_session(report: AuthStatusReport, oidc: OidcClient) -> None | |
| report.refresh_expires_at = _iso_expiry( | ||
| fresh_session.obtained_at, fresh_session.refresh_expires_in | ||
| ) | ||
| return fresh_session | ||
|
|
||
|
|
||
| def _fetch_identity( | ||
| report: AuthStatusReport, settings: PipefySettings, auth: AuthContext | ||
| report: AuthStatusReport, | ||
| settings: PipefySettings, | ||
| auth: AuthContext, | ||
| *, | ||
| prefetched_session: StoredSession | None = None, | ||
| ) -> None: | ||
| """Populate ``report.identity``; raise ``_StatusExit`` on transport / 401.""" | ||
| client = get_authenticated_client(settings, auth) | ||
| client = get_authenticated_client( | ||
| settings, auth, prefetched_session=prefetched_session | ||
| ) | ||
| try: | ||
| report.identity = asyncio.run(client.get_me()) | ||
| except TransportServerError as exc: | ||
|
|
@@ -399,7 +423,19 @@ def auth_status( | |
| ) -> None: | ||
| """Print which auth source is active, the authenticated identity, and session expiry.""" | ||
| settings, auth = settings_and_auth_from_ctx(ctx) | ||
| detected = detect_all_sources(settings, auth) | ||
| loaded_session: StoredSession | None = None | ||
| if auth.oidc_client is not None: | ||
| loaded_session = load_session( | ||
| issuer=auth.oidc_client.issuer_url, | ||
| client_id=auth.oidc_client.client_id, | ||
| ) | ||
| detected = detect_all_sources( | ||
| settings, | ||
| auth, | ||
| cached_stored_session=loaded_session is not None | ||
| if auth.oidc_client is not None | ||
| else None, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (if-minor): the cached_stored_session=loaded_session is not None,is equivalent and one line shorter. |
||
| ) | ||
| source: AuthSource = detected[0] if detected else "none" | ||
| report = AuthStatusReport(auth_source=source, detected_sources=detected) | ||
| # Surface masking env vars whenever a stored session exists — that's the | ||
|
|
@@ -421,8 +457,12 @@ def auth_status( | |
| "but no OIDC client is configured. Please file an issue." | ||
| ), | ||
| ) | ||
| _populate_stored_session(report, auth.oidc_client) | ||
| _fetch_identity(report, settings, auth) | ||
| prefetched = _populate_stored_session( | ||
| report, auth.oidc_client, loaded_session=loaded_session | ||
| ) | ||
| _fetch_identity(report, settings, auth, prefetched_session=prefetched) | ||
| else: | ||
| _fetch_identity(report, settings, auth) | ||
| except _StatusExit as exit_: | ||
| _render(exit_.report, json_out=json_out) | ||
| if exit_.stderr and not json_out: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (non-blocking): duplicates
_LEEWAY_S = 60fromoauth/refresh.py:13. If either drifts, the fresh-path predicate here and the stale-check insideensure_fresh_sessionwill silently disagree.suggestion: have
ensure_fresh_sessionaccept an optionalsession: StoredSession | None = Noneand skip the innerload_sessionwhen supplied. Then_populate_stored_sessionjust callsensure_fresh_session(session=loaded_session, ...)— freshness predicate + leeway stay in one place,_session_is_freshgoes away, and the stale path collapses to 1 load too (currently still 2).