fix(sso): do not cache a failed Entra ID parent group lookup - #3223
Open
marevol wants to merge 1 commit into
Open
fix(sso): do not cache a failed Entra ID parent group lookup#3223marevol wants to merge 1 commit into
marevol wants to merge 1 commit into
Conversation
The groupCache loader caught IOException itself and returned an empty Pair, so Guava stored that empty result. A throttled or briefly unreachable Microsoft Graph therefore pinned "this group has no parents" for the whole cache TTL (10 minutes by default), and every user who logged in during that window silently lost their parent group permissions. The only trace was a warning. Split the Graph call out into getMemberGroupIds() and let failures propagate: a loader that throws leaves nothing in the cache, so the next login retries. Graph's Request_ResourceNotFound is mapped to an empty array instead, because "this group does not exist" is a real answer and should stay cached. Also widen the catch around groupCache.get() to UncheckedExecutionException. Guava wraps an unchecked exception from the loader in that type, which is not an ExecutionException, so it escaped the handler entirely -- and the Graph JSON parser throws CurlException, a RuntimeException, whenever an error body is not JSON.
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.
Problem
A transient Graph failure is cached as "no parent groups" for 10 minutes
The
groupCacheloader handled its own failures and returned an emptyPair:Guava has no way to tell that apart from a real empty answer, so it stores it for
groupCacheExpiry(10 minutes by default). Throttling (429), a 5xx, or a brief network bliptherefore pins "this group has no parents" for every user who logs in during that window. They
lose their parent-group permissions and see it as "some documents are missing"; the only trace is
a WARN line. The error-response branch had the same shape.
An unchecked exception escapes the handler
Guava wraps a checked exception from the loader in
ExecutionException, but an uncheckedone in
UncheckedExecutionException, which is not a subtype. The Graph JSON parser(
OpenSearchCurl's) throwsCurlException, aRuntimeException, whenever an error body is notJSON — for example an HTML error page from a proxy — so that case escaped this catch entirely.
Fix
getMemberGroupIds(user, id)— the Graph call, extracted. It throwsIOExceptionfortransport failures and for error responses, and returns an empty array for
Request_ResourceNotFound, which is a real answer and should stay cached.loadParentGroup(user, id, depth)— the walk, with failures propagating. A loader thatthrows leaves nothing in the cache, so the next login retries.
ExecutionException | UncheckedExecutionException.Behaviour on failure is otherwise unchanged: a WARN plus an empty result for that one caller.
A note on what this PR does not change
While testing this I found that the nested-group recursion never runs on the success path:
processGroup()adds the id it was asked about before returning, so the guard is false for everygroup it resolved. The recursion only fires when
processGroupthrew anIOException— andmaxGroupDepthhas no effect on the normal path.That is consistent with Graph's
getMemberGroupsalready being transitive (one call returns everyancestor), so it may well be correct as-is. I have not changed it here;
test_getParentGroup_doesNotRecurseOnceTheParentWasResolvedpins the current behaviour so anychange is deliberate.
Tests
EntraIdAuthenticator's unit test class, 5 new tests, driven through a subclass whose Graphaccess is scripted:
test_getParentGroup_cachesASuccessfulLookuptest_getParentGroup_doesNotCacheAFailedLookup— fails, is not cached, and the next callsucceeds
test_getParentGroup_cachesAGenuineEmptyResulttest_getParentGroup_survivesAnUncheckedFailureFromTheLoadertest_getParentGroup_doesNotRecurseOnceTheParentWasResolved— the characterisation aboveFalsification: restoring the swallow-and-cache behaviour fails the second test; narrowing the
catch back to
ExecutionExceptionerrors the fourth.(the whole
org.codelibs.fess.ssopackage)