Skip to content

QPU auth management - #79

Open
sarjevane wants to merge 5 commits into
mainfrom
sg/poc-pasqos-auth
Open

sarjevane wants to merge 5 commits into
mainfrom
sg/poc-pasqos-auth

Conversation

@sarjevane

Copy link
Copy Markdown
Collaborator

No description provided.

@sarjevane sarjevane self-assigned this Jul 29, 2026
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

no critical SBOM vulnerabilities detected

This PR previously reported critical SBOM vulnerabilities, but the latest
CI run found none with CRITICAL severity in generated SBOM artifacts.

@sarjevane
sarjevane force-pushed the sg/poc-pasqos-auth branch 5 times, most recently from 514b4e4 to a84c49e Compare July 31, 2026 12:53
@sarjevane
sarjevane marked this pull request as ready for review July 31, 2026 12:54

@badtst badtst left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM from my (limited) understanding of Keycloak

Comment thread warden/lib/config/config.sample.yaml Outdated
# auth:
# # Keycloak base URL (not the token endpoint; the OIDC path is appended).
# url: http://keycloak:8080
# realm: pasqos

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we leave the pasqos example name visible in user-facing documentation ? At the same time it is the correct realm name

Comment thread warden/lib/qpu_client/auth.py Outdated
request.headers["Authorization"] = f"Bearer {self._sync_token()}"
response = yield request
if response.status_code == httpx.codes.UNAUTHORIZED:
logger.info("QPU API returned 401, refreshing token and retrying once")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should those logs be INFO or should they be hidden in DEBUG level, token failures are documented by raising an exception

Comment thread warden/lib/config/config.py Outdated
return self.tls_verify

@property
def auth_flow(self) -> httpx.Auth | None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: maybe rename to something like qpu_auth to not avoid confusion with the auth_flow method of httpx.Auth

Comment thread warden/lib/config/config.py Outdated
Comment on lines +84 to +91
url: str
realm: str = "pasqos"
# OIDC client_id
id: str
# OIDC client_secret. Provide via WARDEN_QPU_AUTH_SECRET, never in YAML.
secret: str
# Refresh this many seconds before the token actually expires.
leeway_s: float = 30

@badtst badtst Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: Avoid in code config default values ?

Comment on lines +42 to +43
token_requests = [r for r in httpx_mock.get_requests() if str(r.url) == TOKEN_URL]
assert len(token_requests) == 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: IIRC if a mock response does not have is_reusable set to True it will raise an error if it is called more than once (httpx.TimeoutException because httpx_mock will not re-send the response to an already matched request)

If a response is not requestes, an error is raised even if the test passes

Suggested change
token_requests = [r for r in httpx_mock.get_requests() if str(r.url) == TOKEN_URL]
assert len(token_requests) == 1

Comment on lines +104 to +105
qpu_requests = [r for r in httpx_mock.get_requests() if str(r.url) == QPU_URL]
assert len(qpu_requests) == 2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: IIRC not needed

Suggested change
qpu_requests = [r for r in httpx_mock.get_requests() if str(r.url) == QPU_URL]
assert len(qpu_requests) == 2

Comment thread tests/lib/qpu_client/test_auth.py Outdated
Comment on lines +195 to +197
# expires_in 30 with the default leeway_s 30 would clamp to 0 without the
# half-lifespan fallback, disabling the cache entirely and forcing a
# Keycloak round-trip on every request.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: same comment on leeway as above

Comment thread tests/lib/qpu_client/test_auth.py Outdated
Comment on lines +188 to +189
token_requests = [r for r in httpx_mock.get_requests() if str(r.url) == TOKEN_URL]
assert len(token_requests) == 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: same as above

assert response.status_code == 200
assert response.request.headers["Authorization"] == "Bearer fresh"
qpu_requests = [r for r in httpx_mock.get_requests() if str(r.url) == QPU_URL]
assert len(qpu_requests) == 2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit:same as above

Comment thread tests/scheduler/utils.py
# Deliberately generous: this budget is only ever spent by a test that is
# already failing, so it costs nothing on the happy path. Tight per-test budgets
# turned a slow CI runner into a flake instead of catching anything.
JOB_WAIT_TIMEOUT_S = 30

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe it is a bit long for a test fail in a local environment. Maybe we can set it to something lower like 10 in a local dev environment and switch it to 30 in CI ?

Suggested change
JOB_WAIT_TIMEOUT_S = 30
JOB_WAIT_TIMEOUT_S = 30 if "CI" in os.environ else 10

Comment thread warden/lib/qpu_client/auth.py Outdated
Comment thread warden/lib/qpu_client/auth.py Outdated

logger = logging.getLogger(__name__)

TERMINAL_STATUSES: tuple[JobStatus, ...] = ("ERROR", "DONE", "CANCELED")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: why not use the JobStatus enum values instead of strings?

Comment thread warden/scheduler/worker.py Outdated
Comment on lines +57 to +63
if self._status in TERMINAL_STATUSES and not was_terminal:
# Logged here rather than by the caller so that the closing line is
# part of the same JobUpdate - hence the same transaction - as the
# terminal status. Flushed separately, the DB would briefly hold a
# finished job whose logs are truncated, and anything that stops
# polling once the status is terminal reads incomplete logs.
logger.info("Job execution ended with status '%s'", self._status)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: This final log line was moved out of line 169, so it won't appear for job who time out and the cancelation fail because the same logic has not been added to to_error method. Is that intentional?

Comment thread warden/lib/qpu_client/auth.py Outdated
expires_in = float(payload.get("expires_in", 0))
self._token = token
ttl = max(expires_in - self.conf.leeway_s, expires_in / 2)
if expires_in <= self.conf.leeway_s:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The warning log is not emitted for all cases where we use ttl=expires_in/2.

e.g. if expires_in=40, leeway=30 then ttl=20 and yet expires_in>leeway

Suggested change
if expires_in <= self.conf.leeway_s:
if ttl > expires_in - self.conf.leeway_s:

Comment thread warden/lib/qpu_client/retry.py Outdated
NotRetriedHTTPStatus: If the HTTP request returns with a non-retryable error code.
MaxRetryError: If the maximum number of retries without success has been reached.
QPUClientRequestError: Any subclass already classified as non-retryable
by the wrapped function (e.g. TokenRequestError) propagates unchanged.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That's not true in the case of no_retry=True, see line 82

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we raise MaxRetryError in case of no_retry=True then or change the desc ?

@MatthieuMoreau0

Copy link
Copy Markdown
Collaborator

LGTM, I haven't tested locally tbh but I believe Thomas has already confirmed the proper integration with local pasqos using keycloak

@MatthieuMoreau0

Copy link
Copy Markdown
Collaborator

I finally tested locally and confirmed this works as expected 👍

@badtst
badtst force-pushed the sg/poc-pasqos-auth branch from 0982676 to 80b7e93 Compare August 20, 2026 13:49
@sarjevane
sarjevane force-pushed the sg/poc-pasqos-auth branch 2 times, most recently from 5d58cb4 to 847f6aa Compare August 31, 2026 09:24
@badtst badtst self-assigned this Aug 31, 2026
badtst added 4 commits August 31, 2026 15:34
feat(auth): add Keycloak client_credentials httpx auth flow
# This is the 1st commit message:

feat(auth): add QPUAuthConfig for Keycloak client credentials
feat(auth): add Keycloak client_credentials httpx auth flow

# This is the commit message #2:

feat(auth): add QPUAuthConfig for Keycloak client credentials

feat(auth): add Keycloak client_credentials httpx auth flow

feat(auth): attach Keycloak auth flow to QPU API clients

fix(retry): do not rewrap already-classified QPU client errors

docs(auth): document qpu.auth config block

# This is the commit message #3:

feat(auth): add QPUAuthConfig for Keycloak client credentials

feat(auth): add Keycloak client_credentials httpx auth flow

feat(auth): attach Keycloak auth flow to QPU API clients

fix(retry): do not rewrap already-classified QPU client errors

docs(auth): document qpu.auth config block

fix(auth): handle short token lifespans and address review findings

Cache for half the lifespan (with a WARNING) instead of disabling the
cache entirely when expires_in <= leeway_s. Also catch AttributeError
alongside ValueError when parsing a non-object token error body,
narrow the unlocked-cache ponytail comment to the async path where the
race is actually reachable, tighten config.py's auth_flow typing, note
that QPUClientRequestError subclasses can propagate through retry(),
and add coverage for the no-auth-header and POST-401-replay paths.

fix(typechecking): Fix type-checking error

# This is the commit message #4:

feat(auth): add QPUAuthConfig for Keycloak client credentials

feat(auth): add Keycloak client_credentials httpx auth flow

feat(auth): attach Keycloak auth flow to QPU API clients

fix(retry): do not rewrap already-classified QPU client errors

docs(auth): document qpu.auth config block

fix(auth): handle short token lifespans and address review findings

Cache for half the lifespan (with a WARNING) instead of disabling the
cache entirely when expires_in <= leeway_s. Also catch AttributeError
alongside ValueError when parsing a non-object token error body,
narrow the unlocked-cache ponytail comment to the async path where the
race is actually reachable, tighten config.py's auth_flow typing, note
that QPUClientRequestError subclasses can propagate through retry(),
and add coverage for the no-auth-header and POST-401-replay paths.

fix(typechecking): Fix type-checking error

fix(test): Fix test flakiness

feat(auth): add QPUAuthConfig for Keycloak client credentials

feat(auth): add Keycloak client_credentials httpx auth flow

feat(auth): attach Keycloak auth flow to QPU API clients

fix(retry): do not rewrap already-classified QPU client errors

docs(auth): document qpu.auth config block

fix(auth): handle short token lifespans and address review findings

Cache for half the lifespan (with a WARNING) instead of disabling the
cache entirely when expires_in <= leeway_s. Also catch AttributeError
alongside ValueError when parsing a non-object token error body,
narrow the unlocked-cache ponytail comment to the async path where the
race is actually reachable, tighten config.py's auth_flow typing, note
that QPUClientRequestError subclasses can propagate through retry(),
and add coverage for the no-auth-header and POST-401-replay paths.

fix(typechecking): Fix type-checking error

fix(test): Fix test flakiness

# This is the commit message #5:

Fix test and refacto

# This is the commit message #6:

PR comments
feat(auth): add Keycloak client_credentials httpx auth flow

feat(auth): add QPUAuthConfig for Keycloak client credentials

feat(auth): add Keycloak client_credentials httpx auth flow

feat(auth): attach Keycloak auth flow to QPU API clients

fix(retry): do not rewrap already-classified QPU client errors

docs(auth): document qpu.auth config block

feat(auth): add QPUAuthConfig for Keycloak client credentials

feat(auth): add Keycloak client_credentials httpx auth flow

feat(auth): attach Keycloak auth flow to QPU API clients

fix(retry): do not rewrap already-classified QPU client errors

docs(auth): document qpu.auth config block

fix(auth): handle short token lifespans and address review findings

Cache for half the lifespan (with a WARNING) instead of disabling the
cache entirely when expires_in <= leeway_s. Also catch AttributeError
alongside ValueError when parsing a non-object token error body,
narrow the unlocked-cache ponytail comment to the async path where the
race is actually reachable, tighten config.py's auth_flow typing, note
that QPUClientRequestError subclasses can propagate through retry(),
and add coverage for the no-auth-header and POST-401-replay paths.

fix(typechecking): Fix type-checking error

feat(auth): add QPUAuthConfig for Keycloak client credentials

feat(auth): add Keycloak client_credentials httpx auth flow

feat(auth): attach Keycloak auth flow to QPU API clients

fix(retry): do not rewrap already-classified QPU client errors

docs(auth): document qpu.auth config block

fix(auth): handle short token lifespans and address review findings

Cache for half the lifespan (with a WARNING) instead of disabling the
cache entirely when expires_in <= leeway_s. Also catch AttributeError
alongside ValueError when parsing a non-object token error body,
narrow the unlocked-cache ponytail comment to the async path where the
race is actually reachable, tighten config.py's auth_flow typing, note
that QPUClientRequestError subclasses can propagate through retry(),
and add coverage for the no-auth-header and POST-401-replay paths.

fix(typechecking): Fix type-checking error

fix(test): Fix test flakiness

feat(auth): add QPUAuthConfig for Keycloak client credentials

feat(auth): add Keycloak client_credentials httpx auth flow

feat(auth): attach Keycloak auth flow to QPU API clients

fix(retry): do not rewrap already-classified QPU client errors

docs(auth): document qpu.auth config block

fix(auth): handle short token lifespans and address review findings

Cache for half the lifespan (with a WARNING) instead of disabling the
cache entirely when expires_in <= leeway_s. Also catch AttributeError
alongside ValueError when parsing a non-object token error body,
narrow the unlocked-cache ponytail comment to the async path where the
race is actually reachable, tighten config.py's auth_flow typing, note
that QPUClientRequestError subclasses can propagate through retry(),
and add coverage for the no-auth-header and POST-401-replay paths.

fix(typechecking): Fix type-checking error

fix(test): Fix test flakiness

Fix test and refacto

PR comments

test(auth): Add QPU Auth tests

feat(auth): add QPUAuthConfig for Keycloak client credentials
feat(auth): add Keycloak client_credentials httpx auth flow

feat(auth): add QPUAuthConfig for Keycloak client credentials

feat(auth): add Keycloak client_credentials httpx auth flow

feat(auth): attach Keycloak auth flow to QPU API clients

fix(retry): do not rewrap already-classified QPU client errors

docs(auth): document qpu.auth config block

feat(auth): add QPUAuthConfig for Keycloak client credentials

feat(auth): add Keycloak client_credentials httpx auth flow

feat(auth): attach Keycloak auth flow to QPU API clients

fix(retry): do not rewrap already-classified QPU client errors

docs(auth): document qpu.auth config block

fix(auth): handle short token lifespans and address review findings

Cache for half the lifespan (with a WARNING) instead of disabling the
cache entirely when expires_in <= leeway_s. Also catch AttributeError
alongside ValueError when parsing a non-object token error body,
narrow the unlocked-cache ponytail comment to the async path where the
race is actually reachable, tighten config.py's auth_flow typing, note
that QPUClientRequestError subclasses can propagate through retry(),
and add coverage for the no-auth-header and POST-401-replay paths.

fix(typechecking): Fix type-checking error

feat(auth): add QPUAuthConfig for Keycloak client credentials

feat(auth): add Keycloak client_credentials httpx auth flow

feat(auth): attach Keycloak auth flow to QPU API clients

fix(retry): do not rewrap already-classified QPU client errors

docs(auth): document qpu.auth config block

fix(auth): handle short token lifespans and address review findings

Cache for half the lifespan (with a WARNING) instead of disabling the
cache entirely when expires_in <= leeway_s. Also catch AttributeError
alongside ValueError when parsing a non-object token error body,
narrow the unlocked-cache ponytail comment to the async path where the
race is actually reachable, tighten config.py's auth_flow typing, note
that QPUClientRequestError subclasses can propagate through retry(),
and add coverage for the no-auth-header and POST-401-replay paths.

fix(typechecking): Fix type-checking error

fix(test): Fix test flakiness

feat(auth): add QPUAuthConfig for Keycloak client credentials

feat(auth): add Keycloak client_credentials httpx auth flow

feat(auth): attach Keycloak auth flow to QPU API clients

fix(retry): do not rewrap already-classified QPU client errors

docs(auth): document qpu.auth config block

fix(auth): handle short token lifespans and address review findings

Cache for half the lifespan (with a WARNING) instead of disabling the
cache entirely when expires_in <= leeway_s. Also catch AttributeError
alongside ValueError when parsing a non-object token error body,
narrow the unlocked-cache ponytail comment to the async path where the
race is actually reachable, tighten config.py's auth_flow typing, note
that QPUClientRequestError subclasses can propagate through retry(),
and add coverage for the no-auth-header and POST-401-replay paths.

fix(typechecking): Fix type-checking error

fix(test): Fix test flakiness

Fix test and refacto

PR comments

test(auth): Add QPU Auth tests
@badtst
badtst force-pushed the sg/poc-pasqos-auth branch from 847f6aa to fa2b18c Compare August 31, 2026 16:15
Comment on lines +51 to +65
def auth_flow(
self, request: httpx2.Request
) -> Generator[httpx2.Request, httpx2.Response, None]:
if not self._is_fresh():
token_response = yield self._token_request()
self._store(token_response)
assert self._token is not None
request.headers["Authorization"] = f"Bearer {self._token}"
response = yield request
if response.status_code == httpx2.codes.UNAUTHORIZED:
logger.info("QPU API returned 401, refreshing token and retrying once")
token_response = yield self._token_request()
self._store(token_response)
request.headers["Authorization"] = f"Bearer {self._token}"
yield request

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

TODO for someone: improve error reporting here through warden logs

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.

3 participants