Skip to content

Forward impersonation token to BFF modules in federated dev mode#7372

Closed
lucferbux wants to merge 1 commit intoopendatahub-io:mainfrom
lucferbux:rhoaieng-59422
Closed

Forward impersonation token to BFF modules in federated dev mode#7372
lucferbux wants to merge 1 commit intoopendatahub-io:mainfrom
lucferbux:rhoaieng-59422

Conversation

@lucferbux
Copy link
Copy Markdown
Contributor

@lucferbux lucferbux commented Apr 23, 2026

https://issues.redhat.com/browse/RHOAIENG-59422

Description

In federated dev mode (make dev-start-federated), module API calls flow through a double proxy chain:

Browser → Main webpack → Main backend → Module webpack → BFF

The main dashboard backend sets the authorization header on proxied requests — this can carry either the developer's kubectl token or an impersonated user's token (via the dev impersonation toggle). However, each module's webpack dev proxy used a static headers: getProxyHeaders() option that always overwrites x-forwarded-access-token with the developer's kubectl token (resolved once at startup), effectively discarding any impersonation.

Fix: Replace the static headers: with an onProxyReq callback in all module webpack.dev.js files. When the incoming request has an authorization header and AUTH_METHOD === 'user_token' (federated mode), the callback extracts the token and forwards it as x-forwarded-access-token — matching the production convention BFFs already use. In standalone or internal auth modes, it falls back to getProxyHeaders().

Modules changed

Module File Notes
maas packages/maas/frontend/config/webpack.dev.js Single proxy entry
gen-ai packages/gen-ai/frontend/config/webpack.dev.js Single proxy entry
automl packages/automl/frontend/config/webpack.dev.js Single proxy entry
autorag packages/autorag/frontend/config/webpack.dev.js Single proxy entry
mlflow packages/mlflow/frontend/config/webpack.dev.js Single proxy entry; pathRewrite preserved
eval-hub packages/eval-hub/frontend/config/webpack.dev.js Two proxy entries (main API + MLflow)

Note: model-registry is excluded from this PR — its webpack config lives upstream at kubeflow/model-registry. The same fix is being addressed via kubeflow/hub#2544, where we've suggested aligning with this approach.

No Makefile or getProxyHeaders() changes — only the proxy wiring in webpack.dev.js.

Behavior per mode

Mode Result
Federated + impersonating BFF receives impersonated token via x-forwarded-access-token
Federated + no impersonation BFF receives kubectl token (same as before)
Standalone (user_token) Falls back to getProxyHeaders() (same as before)
Mock federated (internal) Falls back to getProxyHeaders() / kubeflow-userid (same as before)

How Has This Been Tested?

Tested on a live cluster with temporary debug logging added to the BFF ExtractRequestIdentity method (logging the first 8 and last 4 chars of the received token). Validated with both maas and model-registry BFFs in federated mode.

Test setup

  1. Main dashboard: npm run dev
  2. MaaS BFF (federated): cd packages/maas && make dev-start-federated
  3. Model Registry BFF (federated): cd packages/model-registry && make dev-start-federated

Results — Admin (no impersonation)

Both BFFs receive the admin kubectl token (sha256~m...6wd8):

Model Registry:

time=2026-04-23T16:34:50.043+02:00 level=INFO msg="DEBUG ExtractRequestIdentity" header=x-forwarded-access-token tokenStart=sha256~m tokenEnd=6wd8
time=2026-04-23T16:34:54.736+02:00 level=INFO msg="user is cluster-admin"

MaaS:

time=2026-04-23T17:00:06.539+02:00 level=INFO msg="DEBUG ExtractRequestIdentity" header=x-forwarded-access-token tokenStart=sha256~m tokenEnd=6wd8

Results — Impersonating dev user

Both BFFs receive a different token (sha256~D...7l94) — the impersonated user's token:

Model Registry:

time=2026-04-23T17:01:06.622+02:00 level=INFO msg="DEBUG ExtractRequestIdentity" header=x-forwarded-access-token tokenStart=sha256~D tokenEnd=7l94
time=2026-04-23T17:01:06.972+02:00 level=INFO msg="user is NOT cluster-admin"

MaaS:

time=2026-04-23T17:01:33.767+02:00 level=INFO msg="DEBUG ExtractRequestIdentity" header=x-forwarded-access-token tokenStart=sha256~D tokenEnd=7l94
time=2026-04-23T17:01:33.893+02:00 level=INFO msg="user is NOT cluster-admin"
time=2026-04-23T17:01:34.512+02:00 level=ERROR msg="failed to list MaaSAuthPolicies: maasauthpolicies.maas.opendatahub.io is forbidden: User \"regularuser1\" cannot list resource \"maasauthpolicies\" in API group \"maas.opendatahub.io\" in the namespace \"models-as-a-service\""

RBAC works end-to-end: the impersonated user is correctly identified as non-admin, and permission-gated resources are properly denied.

Test Impact

This is a dev-only change (webpack dev server proxy configuration). No production code is affected. No new tests needed — the change is verified through manual testing of the dev impersonation flow.

Request review criteria:

Self checklist (all need to be checked):

  • The developer has manually tested the changes and verified that the changes work
  • Testing instructions have been added in the PR body (for PRs involving changes that are not immediately obvious).
  • The developer has added tests or explained why testing cannot be added (unit or cypress tests for related changes)
  • The code follows our Best Practices (React coding standards, PatternFly usage, performance considerations)

After the PR is posted & before it merges:

  • The developer has tested their solution on a cluster by using the image produced by the PR to main

@openshift-ci
Copy link
Copy Markdown
Contributor

openshift-ci Bot commented Apr 23, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign griffin-sullivan for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Six frontend webpack configuration files (automl, autorag, eval-hub, gen-ai, maas, mlflow) replace static proxy header injection from precomputed headers: getProxyHeaders() with dynamic per-request onProxyReq handlers. When an incoming Authorization header exists and AUTH_METHOD is user_token, the handler strips the Bearer prefix and forwards the token via x-forwarded-access-token header. All other scenarios compute getProxyHeaders() at proxy time and apply returned headers individually.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Security Issues

CWE-522 (Insufficiently Protected Credentials): Token extraction logic performs naive string manipulation on bearer tokens without validation.

  • The code assumes Authorization header structure is Bearer <token> and strips the prefix blindly. Malformed headers (e.g., Bearer, Bearer , Bearer\t) could result in invalid or empty tokens being forwarded as x-forwarded-access-token, causing upstream authentication failures or bypasses.

CWE-269 (Improper Access Control): Conditional header forwarding based on AUTH_METHOD lacks explicit validation.

  • No verification that x-forwarded-access-token is only set when the upstream service actually expects it. If upstream doesn't recognize this header, tokens may be silently ignored or cause unexpected behavior.

Missing token presence validation: After stripping Bearer , there is no check that a token actually remains.

  • Setting x-forwarded-access-token to an empty string or whitespace breaks upstream auth without clear error signaling.

Recommendation: Validate Authorization header format before extraction (regex or strict parsing), explicitly check token non-emptiness, and document upstream service expectations for x-forwarded-access-token.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: forwarding impersonation tokens to BFF modules in federated dev mode, which is the core objective of all file modifications.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description comprehensively covers the problem, solution, modules changed, behavior matrix, testing approach, and all required self-checklist items are confirmed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/eval-hub/frontend/config/webpack.dev.js (1)

96-130: ⚠️ Potential issue | 🟠 Major

Cache fallback headers and require Bearer auth before forwarding.

Lines 103 and 125 now run getProxyHeaders() per fallback request; in user_token mode that shells out to kubectl twice on the dev-server request path for both proxies. Lines 97-101 and 119-123 also forward any non-empty Authorization value as x-forwarded-access-token without validating the Bearer scheme (CWE-20), which can disable the kubectl fallback for malformed auth.

Proposed change
 const getProxyHeaders = () => {
   if (AUTH_METHOD === 'internal') {
     return {
       'kubeflow-userid': 'user@example.com',
@@
   }
   return {};
 };
+
+const defaultProxyHeaders = getProxyHeaders();
+
+const getForwardedAccessToken = (req) => {
+  if (AUTH_METHOD !== 'user_token') {
+    return undefined;
+  }
+  const upstreamAuth = req.headers.authorization;
+  const match = typeof upstreamAuth === 'string' ? upstreamAuth.match(/^Bearer\s+(.+)$/i) : null;
+  return match ? match[1].trim() : undefined;
+};
@@
-            onProxyReq: (proxyReq, req) => {
-              const upstreamAuth = req.headers.authorization;
-              if (upstreamAuth && AUTH_METHOD === 'user_token') {
+            onProxyReq: (proxyReq, req) => {
+              const token = getForwardedAccessToken(req);
+              if (token) {
                 // Federated mode: forward the upstream token (may be impersonated) as x-forwarded-access-token
-                const token = upstreamAuth.replace(/^Bearer\s+/i, '');
                 proxyReq.setHeader('x-forwarded-access-token', token);
               } else {
-                const headers = getProxyHeaders();
-                Object.entries(headers).forEach(([key, value]) => {
+                Object.entries(defaultProxyHeaders).forEach(([key, value]) => {
                   proxyReq.setHeader(key, value);
                 });
               }
             },
@@
-            onProxyReq: (proxyReq, req) => {
-              const upstreamAuth = req.headers.authorization;
-              if (upstreamAuth && AUTH_METHOD === 'user_token') {
+            onProxyReq: (proxyReq, req) => {
+              const token = getForwardedAccessToken(req);
+              if (token) {
                 // Federated mode: forward the upstream token (may be impersonated) as x-forwarded-access-token
-                const token = upstreamAuth.replace(/^Bearer\s+/i, '');
                 proxyReq.setHeader('x-forwarded-access-token', token);
               } else {
-                const headers = getProxyHeaders();
-                Object.entries(headers).forEach(([key, value]) => {
+                Object.entries(defaultProxyHeaders).forEach(([key, value]) => {
                   proxyReq.setHeader(key, value);
                 });
               }
             },

As per coding guidelines, “Changes here affect build performance and output for all consumers.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/eval-hub/frontend/config/webpack.dev.js` around lines 96 - 130,
Validate the incoming Authorization header in onProxyReq: only treat it as a
forwardable token if req.headers.authorization matches the Bearer scheme
(/^Bearer\s+/i) before stripping and setting x-forwarded-access-token (use the
same regex used elsewhere), and only call getProxyHeaders() when you actually
need the fallback; cache the result on the request object (e.g.,
req.__cachedProxyHeaders) so subsequent proxy handlers reuse it instead of
invoking getProxyHeaders() twice; update both onProxyReq blocks to check
AUTH_METHOD === 'user_token' AND a Bearer-formatted upstreamAuth before
forwarding, otherwise load and reuse req.__cachedProxyHeaders and set those
headers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/automl/frontend/config/webpack.dev.js`:
- Around line 91-103: The onProxyReq handler currently calls getProxyHeaders()
unnecessarily and forwards any non-empty Authorization value without validating
the Bearer scheme; update onProxyReq to first check that
req.headers.authorization exists and matches the Bearer scheme
(case-insensitive) before extracting the token and setting
x-forwarded-access-token, and otherwise call getProxyHeaders() once and reuse
its return value to set fallback headers; reference onProxyReq, AUTH_METHOD,
getProxyHeaders, and the x-forwarded-access-token header when making these
changes.

In `@packages/autorag/frontend/config/webpack.dev.js`:
- Around line 95-107: The onProxyReq handler currently calls getProxyHeaders()
on every fallback and forwards any non-empty Authorization as
x-forwarded-access-token without validating the Bearer scheme; update onProxyReq
to first check AUTH_METHOD and the Authorization header with a Bearer scheme
(e.g. test /^Bearer\s+/i) and only extract and forward the token as
x-forwarded-access-token when the header is a valid Bearer token (use
upstreamAuth.replace to strip the scheme). If the Authorization header is
missing or not Bearer, call getProxyHeaders() once and apply its returned
headers to proxyReq (avoid calling getProxyHeaders() when a valid Bearer token
is present to prevent unnecessary kubectl shellouts), keeping the existing
proxyReq.setHeader calls.

In `@packages/gen-ai/frontend/config/webpack.dev.js`:
- Around line 85-97: The onProxyReq handler is calling getProxyHeaders() on
every fallback and forwards any non-empty Authorization header as
x-forwarded-access-token without validating the Bearer scheme; fix by (1) moving
a cachedHeaders variable outside/on top of the dev-server proxy scope so
getProxyHeaders() is called once and reused (avoid shelling out repeatedly in
AUTH_METHOD === 'user_token' fallback), and (2) validate that
req.headers.authorization matches the Bearer pattern /^Bearer\s+/i before
extracting and forwarding the token in onProxyReq (only then
setHeader('x-forwarded-access-token', token)); if the Authorization header is
absent or malformed, fall back to using cachedHeaders and set those headers on
proxyReq.

In `@packages/maas/frontend/config/webpack.dev.js`:
- Around line 94-106: The onProxyReq handler currently calls getProxyHeaders()
multiple times and forwards any non-empty Authorization header as
x-forwarded-access-token without validating the Bearer scheme; update onProxyReq
to (1) require the Authorization header to match the Bearer scheme
(/^Bearer\s+/i) before extracting and forwarding the token when AUTH_METHOD ===
'user_token', (2) treat malformed or missing Bearer tokens as a signal to use
the fallback proxy headers instead of attempting to forward, and (3) call
getProxyHeaders() at most once per request (cache its result in a local
variable) so kubectl is not invoked multiple times; reference onProxyReq,
AUTH_METHOD and getProxyHeaders in your change.

In `@packages/mlflow/frontend/config/webpack.dev.js`:
- Around line 90-102: The onProxyReq handler currently forwards any non-empty
req.headers.authorization as x-forwarded-access-token and calls
getProxyHeaders() on the fallback path, which can invoke kubectl twice; update
onProxyReq to first validate the upstreamAuth strictly matches the Bearer scheme
(e.g. /^\s*Bearer\s+/i) before extracting and forwarding the token, and only
call getProxyHeaders() when that validation fails; also ensure getProxyHeaders()
is invoked at most once per request by computing a single headers variable when
needed and reusing it (references: onProxyReq, AUTH_METHOD,
req.headers.authorization, getProxyHeaders, proxyReq.setHeader).

---

Outside diff comments:
In `@packages/eval-hub/frontend/config/webpack.dev.js`:
- Around line 96-130: Validate the incoming Authorization header in onProxyReq:
only treat it as a forwardable token if req.headers.authorization matches the
Bearer scheme (/^Bearer\s+/i) before stripping and setting
x-forwarded-access-token (use the same regex used elsewhere), and only call
getProxyHeaders() when you actually need the fallback; cache the result on the
request object (e.g., req.__cachedProxyHeaders) so subsequent proxy handlers
reuse it instead of invoking getProxyHeaders() twice; update both onProxyReq
blocks to check AUTH_METHOD === 'user_token' AND a Bearer-formatted upstreamAuth
before forwarding, otherwise load and reuse req.__cachedProxyHeaders and set
those headers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 5dec798c-02e6-43f4-8f3b-44c37865491e

📥 Commits

Reviewing files that changed from the base of the PR and between 427b9b4 and 7387086.

⛔ Files ignored due to path filters (1)
  • packages/model-registry/upstream/frontend/config/webpack.dev.js is excluded by !**/upstream/**
📒 Files selected for processing (6)
  • packages/automl/frontend/config/webpack.dev.js
  • packages/autorag/frontend/config/webpack.dev.js
  • packages/eval-hub/frontend/config/webpack.dev.js
  • packages/gen-ai/frontend/config/webpack.dev.js
  • packages/maas/frontend/config/webpack.dev.js
  • packages/mlflow/frontend/config/webpack.dev.js

Comment thread packages/automl/frontend/config/webpack.dev.js
Comment thread packages/autorag/frontend/config/webpack.dev.js
Comment thread packages/gen-ai/frontend/config/webpack.dev.js
Comment thread packages/maas/frontend/config/webpack.dev.js
Comment thread packages/mlflow/frontend/config/webpack.dev.js
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 63.98%. Comparing base (46fc04a) to head (7387086).
⚠️ Report is 15 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #7372      +/-   ##
==========================================
- Coverage   64.97%   63.98%   -0.99%     
==========================================
  Files        2448     2513      +65     
  Lines       76175    77980    +1805     
  Branches    19221    19829     +608     
==========================================
+ Hits        49492    49894     +402     
- Misses      26683    28086    +1403     

see 121 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 46fc04a...7387086. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Replace static `headers: getProxyHeaders()` with `onProxyReq` callback
in all module webpack dev proxies. In federated mode, the main dashboard
backend sets the authorization header (which may carry an impersonated
token). The new callback extracts it and forwards as
x-forwarded-access-token for the BFF, matching production convention.
In standalone/internal auth modes, falls back to getProxyHeaders().

model-registry is excluded — handled upstream via
kubeflow/hub#2544.
@lucferbux
Copy link
Copy Markdown
Contributor Author

Closing this in favor of #7065

@lucferbux lucferbux closed this Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant