| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
|
⚠️ Please install the Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## master #6687 +/- ##
==========================================
- Coverage 46.92% 46.91% -0.01%
==========================================
Files 416 416
Lines 50549 50545 -4
Branches 7252 7252
==========================================
- Hits 23718 23714 -4
Misses 25157 25157
Partials 1674 1674
*This pull request uses carry forward flags. Click here to find out more.
Continue to review full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Sorry, something went wrong.
There was a problem hiding this comment.
The cache key omits token_refresh_margin_seconds, but the stored deadline is calculated using that value. If two configs share the same IdP/client credentials and use different margins—the new option is explicitly configurable—the stricter config can reuse a token until the looser config's deadline. That violates the requested safety margin. We should include the margin in the key or store the token's true expiry and apply each caller's margin when reading; add a regression that uses both configs without manually clearing the cache.
Sorry, something went wrong.
|
@franciscojavierarceo One behavior change worth flagging: a token already inside the margin now gets cached instead of skipped. Every read still rejects it for that caller so nothing observable changes, and a config with a smaller margin can legitimately use it. The regression exercises both configs against the shared entry without clearing the cache, and also checks the reverse direction stays cheap, with the narrow-margin config reusing what the wide one wrote. I verified it fails on the old code, with the wide config receiving the narrow config's token. I also dropped the manual cache clear from test_refresh_margin_is_configurable, since that clear is what hid this; it now uses distinct credentials to isolate the two margins. 326 permissions tests pass. |
Sorry, something went wrong.
|
@franciscojavierarceo @ntkathole Green and ready for another look. It also still needs a kind/ label, which I can't add as an outside contributor. |
Sorry, something went wrong.
|
@franciscojavierarceo @ntkathole All checks green, no conflicts with current master. It also still needs a kind/ label, which I can't add as an outside contributor. |
Sorry, something went wrong.
|
@larrysingleton007 please resolve the conflicts |
Sorry, something went wrong.
|
@ntkathole done, master merged and pushed. The only conflict was .secrets.baseline, which is generated: master had picked up newer entries while this branch carried two shifted line numbers for test_oidc_auth_client.py plus a newer generated_at. I took master's baseline and re-ran the detect-secrets hook rather than hand-merging, which reapplied exactly the two shifts this branch needs (29 to 44, 31 to 46). Hand-editing generated line numbers would have been guesswork. 326 permissions tests pass on the merged tree, with ruff and mypy clean. The cache-key fix and its regression are unchanged. The other two, #6689 and #6690, merge cleanly against current master, so nothing needed there. |
Sorry, something went wrong.
|
@ntkathole both addressed in 2a73cb7. Pruning. Inserting on a miss now first drops entries whose stored expiry has passed. Keys are credential identities, so the cache was already bounded by the number of distinct configs, but a long-lived process that rotates credentials would have retained every retired one. Revocation. Added OidcAuthClientManager.invalidate_token() plus a transport-agnostic invalidate_auth_token(auth_config) in client_auth_token.py, and wired it into the gRPC interceptor's existing error path: an UNAUTHENTICATED status drops the cached token, so the next call refetches. That bounds the staleness to the one request that was rejected rather than the remainder of the token's life. One deliberate deviation from what you asked, and I'd rather flag it than quietly do less. I did not retry the rejected call. All four interceptor methods share _handle_call, and for the stream variants the request_iterator may already be partially consumed, so a retry there could replay a partial stream. Retry seems like it belongs per-transport, where the caller knows whether the request is replayable, rather than in the shared handler. The invalidation is the part that fixes the correctness problem; the retry only saves the caller one visible error. Happy to add it for the unary paths in this PR if you'd prefer, or as a follow-up covering all three transports, since the Arrow Flight and requests wrappers have no 401 handling at all today. Two new tests: one asserting invalidation forces a refetch and is idempotent, one asserting an expired entry is pruned while an unexpired one survives. 328 permissions tests pass, ruff and mypy clean. |
Sorry, something went wrong.
|
@larrysingleton007 not able to rebase and merge, can you rebase and possibly squash the commit? |
Sorry, something went wrong.
|
@ntkathole rebased onto current master and squashed to a single commit, 202e30d. The branch is no longer behind, and the three merge commits are gone. One note on how I did it, since it affects what you're reviewing. Master had moved three commits past this branch's last merge, so I brought those in first and then collapsed everything onto upstream/master. A straight squash of the old branch tip would have reverted those three. The net diff is unchanged: 6 files, +414/-9, all of it this PR's own work. Re-verified after the rebase, since master's newer commits touched permissions/user.py: 329 permissions tests pass, ruff and mypy clean. |
Sorry, something went wrong.
|
@ntkathole both review threads are resolved now. I'd replied in the conversation rather than inside the threads, which is why they stayed open — my mistake. Summary of where they landed: pruning on cache miss is implemented and covered by a test; eviction on UNAUTHENTICATED is implemented via invalidate_token() and invalidate_auth_token(), wired into the gRPC interceptor. The retry is the one piece I left out, for the stream-replay reason in the thread, and I'm happy to add it either here for the unary paths or as a follow-up across all three transports. Two things still block the merge, neither of them code in this PR. The failing check is unit-test-python (3.10, ubuntu-latest) on test_http_session_manager::test_thread_safety, which asserts len(sessions) == 10 and got 9. It can't be this PR: that test builds its session from NoAuthConfig(), which takes the AuthType.NONE branch and never calls get_auth_token, so the token cache isn't in its code path. Its own assert len(errors) == 0 passed, so no thread raised — one thread just didn't record a result, which is a race in the test's own accounting. The test came in with #5895. Could you re-run that job? @franciscojavierarceo the reviewDecision is still CHANGES_REQUESTED from your 3 August review of the cache key. That was fixed on 5 August: the cache now stores the token's true expiry and applies each config's margin on read, with a regression that I verified fails against the pre-fix code. Would you mind taking another look so the review state clears? |
Sorry, something went wrong.
|
Following up on the red check with evidence rather than just asking for a re-run. unit-test-python fails intermittently on master, on a different test each time:
Both are on master with no PR involved. The failure here, test_http_session_manager::test_thread_safety, is a third instance of the same class: tests that assert on wall-clock timing or thread scheduling, which are sensitive to how loaded the runner is when 8 xdist workers share it. For this PR specifically, that test builds its session from NoAuthConfig(), which takes the AuthType.NONE branch and never calls get_auth_token, so nothing in this change is in its code path. I also ran it 88 times locally, 40 in isolation and 48 under deliberate CPU contention, without a single failure — consistent with it needing a loaded shared runner rather than being deterministic. So a re-run should clear it. Separately, test_performance_bounds_single_entity looks worth fixing on its own: a 0.2s wall-clock bound cannot be made reliable by raising it a little, since the runner's available CPU isn't bounded. Happy to open an issue and PR for that if it's useful. |
Sorry, something went wrong.
|
One more CI follow-up: the DuckDB failure in the latest run is unrelated to this OIDC diff. I found the same extension-install lock race in 2 other PR runs, filed #6743, and put the fix in #6744. #6744 installs DuckDB's delta extension once before the 8 pytest workers start. The cold-cache suite passes locally with 84 tests passed and 5 skipped, and all 18 ordinary PR checks are green. Its DuckDB CI job is waiting for ok-to-test. No change is needed in this PR for that failure. |
Sorry, something went wrong.
On the client_secret branch every outbound RPC built a fresh auth-token factory, manager and OIDCDiscoveryService, paying a discovery GET plus a token POST per call and discarding the token. All three auth interceptors invoke it per RPC, so a batch materialization loop multiplied IdP load by request count and could trip IdP rate limits. Measured before: 5 calls = 5 discovery GETs + 5 token POSTs. After: 1 and 1. Caches IdP tokens in a module-level dict keyed by the token-request identity, since the interceptors build a fresh manager per call and instance state would not survive. Expiry comes from the token's own exp claim, falling back to the token endpoint's expires_in; a token whose expiry is unknowable is not cached, preserving per-call behaviour for opaque tokens. The cache stores true expiry and applies the caller's token_refresh_margin_seconds on read, rather than storing a deadline. The margin is not part of the key, so baking it in let a config with a wider margin reuse a token past its own safety window when another config sharing the same credentials had written the entry. token_refresh_margin_seconds is configurable on OidcClientAuthConfig (default 30, gt=0) rather than hardcoded. Inserting on a miss prunes entries whose stored expiry has passed. Keys are credential identities so the cache is bounded by distinct configs, but a long-lived process rotating credentials would otherwise retain every retired identity. Reuse means a token the IdP revokes mid-life keeps being presented until its own expiry, where fetching per call self-corrected. Adds OidcAuthClientManager.invalidate_token and a transport-agnostic invalidate_auth_token(auth_config), wired into the gRPC interceptor: an UNAUTHENTICATED response drops the cached token so the next call refetches, bounding staleness to the rejected request. The call is not retried, because all four interceptor methods share that path and a stream's request_iterator may already be consumed. 329 permissions tests pass. Signed-off-by: Larry Singleton <166439969+larrysingleton007@users.noreply.github.com>
| Back | FazBrowse Home | New Git URL |
What this PR does / why we need it
This is the client-side sibling of #6683. On the client_secret (client credentials / ROPC) branch, every outbound RPC builds a fresh auth manager and throws away the token it fetched:
So each request pays a discovery GET plus a token POST against the IdP for a token that is typically valid for an hour. Measured with counting mocks on master: 5 outbound calls produce 5 discovery GETs and 5 token POSTs. Beyond the added latency, a batch loop multiplies IdP load by request count, which can trip provider rate limits.
The fix caches issued tokens keyed by the token-request identity (discovery URL, client id and secret, username, password) and reuses each until shortly before expiry, taken from the token's own exp claim with the token response's expires_in as a fallback. After the change the same measurement performs 1 discovery GET and 1 token POST.
The cache is module level because the interceptors construct a fresh manager per RPC, so instance state cannot survive between calls. Only the client_secret branch is affected; static tokens, token_env_var, and mounted service account tokens were already cheap and are untouched.
A few details worth reviewer attention:
One interaction worth flagging: HttpSessionManager.get_session re-calls get_auth_token on every session cache hit, with a comment from #5895 explaining it does so "in case it expired". After this change that call returns a cached token rather than a freshly fetched one. The intent still holds, because the cache never returns a token with less than the margin remaining, but the mechanism changes and it seemed better to say so than to let a reviewer find it.
Testing: 7 new unit tests covering reuse until expiry, refusal to cache inside the refresh margin, the expires_in fallback for opaque tokens, no caching when expiry is unknowable, cache keying across distinct configs, the configurable margin changing reuse behavior, and rejection of non-positive margins. An autouse fixture resets the module cache around every test so no test inherits or leaks a cached token. 315 permissions tests pass; ruff and mypy are clean.
Which issue(s) this PR fixes
Fixes #6684