Skip to content

Gandalf operator policy, structured step-up redirect, and step-up token fixes - #20

Draft
1linkovdim wants to merge 1 commit into
Netflix:sbn-support-3.3from
1linkovdim:operator-gandalf-and-stepup-fixes
Draft

Gandalf operator policy, structured step-up redirect, and step-up token fixes#20
1linkovdim wants to merge 1 commit into
Netflix:sbn-support-3.3from
1linkovdim:operator-gandalf-and-stepup-fixes

Conversation

@1linkovdim

Copy link
Copy Markdown

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 isAdmin

NetflixGandalfUserAccessService.checkAuthorityForCurrentUser gates the step-up escalation on UserEntity#isAdmin. That flag is resolved once, in UserServiceImpl.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.adminGroup stays as-is; UserServiceImpl still uses it to populate UserEntity#isAdmin.

Requires fc.operatorPolicy in the config (companion change in the wrapper repo, OBS-flamecommander-operators), otherwise the escalation branch denies everyone.

2. Structured redirectUrl instead of words[9]

Analysis.vue recovered 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 StepUpRequiredException carries the URL as a field. ErrorUtil.toJson emits it as redirectUrl, 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

checkAuthorityForCurrentUser is called from ReadOnlyFileServiceImpl.getFileByUniqueName, which is reached over REST. Nothing populated STEP_UP_TOKEN on that path — only AnalysisApiStompController set it, and only for STOMP messages. So the header the frontend sent was dropped, and the enforcer saw no token.

New StepUpTokenFilter (OncePerRequestFilter, registered for HTTP_API_PREFIX/* at Integer.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 a finally.

4. Cross-session step-up token leak

StompConfigurer's inbound ChannelInterceptor set STEP_UP_TOKEN from the CONNECT frame. That interceptor runs on a pooled clientInboundChannel thread (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), so AnalysisApiStompController reads it per message and clears it in a finally, and the authority check itself runs on the REST path covered by the new filter.

Also documented the remaining boundary in AnalysisApiStompController: its finally clears the token as soon as apiService.invoke returns 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-spring locally, so the StepUpConstants usage is unverified against the real jar. Please treat CI as the first real check.

🤖 Generated with Claude Code

…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>
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.

1 participant