Gandalf operator policy, structured step-up redirect, and step-up token fixes - #20
Draft
1linkovdim wants to merge 1 commit into
Draft
Conversation
…en fixes
Three related changes to the heap dump authority path.
1. Escalation is decided by a gandalf policy, not a stale group claim.
checkAuthorityForCurrentUser fell back to UserEntity#isAdmin when the
per-application policy denied. isAdmin is resolved once at login from the SSO
group list (UserServiceImpl#makeUser) and then baked into the jifa jwt, so
membership changes only take effect when the token is reissued, and the
escalation leaves no gandalf audit trail. It now evaluates a new
fc.operatorPolicy per request, reusing the AuthorizationClient and the gandalf
subject that are already built a few lines above.
2. The step-up redirect url is returned as a field.
The frontend recovered the stairmaster url by splitting the error message on
spaces and taking words[9]. That message is authored by the stairmaster client,
not by us, so any rewording sends the browser to "undefined&redirect=...".
The server now extracts the url once, in StepUpRequiredException, and
ErrorUtil emits it as "redirectUrl" alongside errorCode and message.
Analysis.vue reads the field and falls back to showing the message.
3. The step-up token reaches the enforcer on the REST path, and stops leaking
between sessions.
The authority check runs on the REST path (GET /jifa-api/files/{target} ->
ReadOnlyFileServiceImpl#getFileByUniqueName), but the only writers of
STEP_UP_TOKEN were on the STOMP path, so the header the frontend attaches was
dropped and the enforcer was always called with a null token. StepUpTokenFilter
now publishes it for the duration of the request and always clears it.
The CONNECT frame interceptor in StompConfigurer set the same thread local on a
pooled clientInboundChannel thread and only ever cleared it when a later CONNECT
arrived on that thread without a token, so one user's token stayed visible to
whichever session that thread served next. Scope binding limits this to the same
instanceId, which is the common case rather than the exotic one. The interceptor
is removed: the frontend sends the token on every publish, so
AnalysisApiStompController already reads it per message and clears it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Four related changes to the step-up escalation path. They are separable if you would rather take them one at a time.
1. Gandalf operator policy instead of
isAdminNetflixGandalfUserAccessService.checkAuthorityForCurrentUsergates the step-up escalation onUserEntity#isAdmin. That flag is resolved once, inUserServiceImpl.makeUser, from the SSO group list at login, and then baked into the jifa JWT — so a membership change does not take effect until the token is reissued, in either direction.Replaced with
isOperator(subject), a per-request gandalf call against${fc.operatorPolicy}. This is also what the FlameCommander trigger side is moving to, so triggering a heap dump and inspecting one agree on who an operator is.fc.adminGroupstays as-is;UserServiceImplstill uses it to populateUserEntity#isAdmin.Requires
fc.operatorPolicyin the config (companion change in the wrapper repo,OBS-flamecommander-operators), otherwise the escalation branch denies everyone.2. Structured
redirectUrlinstead ofwords[9]Analysis.vuerecovered the stairmaster URL by splitting the exception message on whitespace and taking index 9. That message is authored by the stairmaster client, so any wording change silently breaks the redirect.New
StepUpRequiredExceptioncarries the URL as a field.ErrorUtil.toJsonemits it asredirectUrl, and the frontend reads the field.The scrape still exists, but it now lives in one place (
StepUpRequiredException.fromMessage) with a regex rather than a word index, and a TODO to read a real property if a future stairmaster release exposes one.3. STEP_UP_TOKEN never reached the enforcer on the REST path
checkAuthorityForCurrentUseris called fromReadOnlyFileServiceImpl.getFileByUniqueName, which is reached over REST. Nothing populatedSTEP_UP_TOKENon that path — onlyAnalysisApiStompControllerset it, and only for STOMP messages. So the header the frontend sent was dropped, and the enforcer saw no token.New
StepUpTokenFilter(OncePerRequestFilter, registered forHTTP_API_PREFIX/*atInteger.MAX_VALUE - 1, i.e. after the security filter chain and before the JWT refresh filter) reads the header, sets the ThreadLocal, and clears it in afinally.4. Cross-session step-up token leak
StompConfigurer's inboundChannelInterceptorsetSTEP_UP_TOKENfrom the CONNECT frame. That interceptor runs on a pooledclientInboundChannelthread (corePoolSize = cores * 2), and the ThreadLocal was only cleared when a later, tokenless CONNECT happened to land on the same thread. Between those two events, one user's step-up token was visible to whichever session that thread served next.Removed the interceptor write entirely. Nothing is lost: the frontend sends the header on every publish (
analysis-api-requester.ts), soAnalysisApiStompControllerreads it per message and clears it in afinally, and the authority check itself runs on the REST path covered by the new filter.Also documented the remaining boundary in
AnalysisApiStompController: itsfinallyclears the token as soon asapiService.invokereturns its future, so async work continuing on another thread does not see it. Nothing depends on that window today.Not built
I have not compiled this — the gradle build needs internal artifactory, and I could not get
stairmaster-enforcement-client-springlocally, so theStepUpConstantsusage is unverified against the real jar. Please treat CI as the first real check.🤖 Generated with Claude Code