Skip to content

Add session-based UI authentication with server-side logout invalidation#18933

Open
Akanksha-kedia wants to merge 8 commits into
apache:masterfrom
Akanksha-kedia:feat/session-based-ui-auth
Open

Add session-based UI authentication with server-side logout invalidation#18933
Akanksha-kedia wants to merge 8 commits into
apache:masterfrom
Akanksha-kedia:feat/session-based-ui-auth

Conversation

@Akanksha-kedia

Copy link
Copy Markdown
Contributor

Summary

Implements a Trino/Ambari-style session layer for the Pinot controller UI that addresses several security gaps in the existing Basic auth flow. The session layer sits on top of ANY configured AccessControlFactory — no changes to the auth backend required.

Problems fixed:

# Problem Before
1 Credentials visible in DevTools Every page navigation sent Authorization: Basic <base64> in the network tab
2 Fake logout Logout only cleared localStorage; server session survived indefinitely
3 No session expiry No inactivity tracking; a tab left open for days kept full access
4 Credentials in JS memory app_state.authToken held Basic <base64> (XSS-readable)
5 Broker auth from browser Query page sent Authorization header from browser directly to broker
6 No CSRF protection Session cookie had no SameSite attribute

How it works:

Browser              Controller                     Broker / ZK
   │  POST /auth/login   │                               │
   │  (form-encoded)     │  hasAccess(SyntheticHeaders)  │
   │ ─────────────────►  │ ──────────────────────────►   │
   │  Set-Cookie:        │  createSession(user, token)   │
   │  Pinot-UI-Session=  │  store in ConcurrentHashMap   │
   │  <token>; HttpOnly  │                               │
   │ ◄─────────────────  │                               │
   │                     │                               │
   │  GET /api/tables    │  SessionAuthenticationFilter  │
   │  Cookie: Pinot-...  │  → getUsername(token) OK      │
   │ ─────────────────►  │  inject Authorization header  │
   │                     │ ──────────────────────────►   │
   │  GET /auth/logout   │  invalidateSession(token)     │
   │ ─────────────────►  │  removes from HashMap         │

New files:

  • SessionManager.javaConcurrentHashMap session store with sliding TTL; uses atomic compute() to eliminate the get→check→remove race condition
  • SessionAuthenticationFilter.java — JAX-RS filter at AUTHENTICATION-10 priority; validates HttpOnly cookie; passes through requests with Authorization header for API backward compat
  • SessionBasicAuthAccessControlFactory.java — convenience factory wrapper
  • PinotSessionLoginResource.javaPOST /auth/login, GET /auth/logout, GET /auth/session
  • SessionManagerTest.java — unit tests + 50-thread concurrency test

Cookie construction: Uses raw Set-Cookie header string instead of JAX-RS NewCookie because JAX-RS 2 NewCookie does not support the SameSite attribute. SameSite=Strict blocks cross-site forged requests.

Frontend changes:

  • Models.tsSESSION enum added to AuthWorkflow
  • axios-config.ts — SESSION workflow deletes the Authorization header; cookie sent automatically by browser
  • AuthProvider.tsx — reads inactivity timeout from /auth/info, checks /auth/session on page refresh to restore session
  • App.tsx — inactivity timer with configurable timeout; 60-second warning dialog before auto-logout; calls /auth/logout on timeout
  • Header.tsx — logout button calls GET /auth/logout for server-side session invalidation
  • LoginPage.tsx — SESSION path POSTs form data to /auth/login (no Basic header in JS memory)

Configuration

All defaults preserve existing behaviour. Feature is opt-in:

# Enable session-based UI authentication (works with any AccessControlFactory)
controller.ui.session.authentication.enabled=true

# Set false for HTTP-only dev deployments (default: true)
controller.ui.session.cookie.secure=false

# UI inactivity timeout in seconds (default 300 = 5 min)
# Server TTL = this + 120s buffer
controller.ui.session.inactivity.timeout.seconds=300

Test plan

  • SessionManagerTest — creation, sliding TTL, logout invalidation, 50-thread concurrency
  • SessionAuthenticationFilter — unprotected path passthrough, cookie validation, Authorization header bypass
  • PinotSessionLoginResource — login/logout/session endpoints
  • Existing ControllerAdminApiApplicationTest and auth tests continue to pass unmodified
  • Manual: open Pinot UI with controller.ui.session.authentication.enabled=true, verify:
    • No Authorization header in DevTools network tab after login
    • Cookie Pinot-UI-Session present with HttpOnly flag
    • Logout clears cookie and session is truly gone (server-side)
    • 5-minute inactivity shows warning dialog; auto-logout after countdown

cc @Jackie-Jiang @xiangfu0

@codecov-commenter

codecov-commenter commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 25.79365% with 187 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.02%. Comparing base (a2a27a1) to head (ea15a8b).

Files with missing lines Patch % Lines
...oller/api/resources/PinotSessionLoginResource.java 0.00% 80 Missing ⚠️
...roller/api/access/SessionAuthenticationFilter.java 10.81% 31 Missing and 2 partials ⚠️
...i/access/SessionBasicAuthAccessControlFactory.java 0.00% 24 Missing ⚠️
...t/controller/api/resources/PinotQueryResource.java 0.00% 17 Missing ⚠️
.../controller/api/access/InMemorySessionManager.java 79.10% 12 Missing and 2 partials ⚠️
...che/pinot/controller/api/access/AccessControl.java 0.00% 8 Missing ⚠️
...ot/controller/api/access/AuthenticationFilter.java 12.50% 4 Missing and 3 partials ⚠️
...ler/api/resources/PinotControllerAuthResource.java 0.00% 4 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #18933      +/-   ##
============================================
- Coverage     65.05%   65.02%   -0.04%     
  Complexity     1403     1403              
============================================
  Files          3399     3403       +4     
  Lines        212808   213052     +244     
  Branches      33568    33604      +36     
============================================
+ Hits         138443   138530      +87     
- Misses        63223    63372     +149     
- Partials      11142    11150       +8     
Flag Coverage Δ
custom-integration1 100.00% <ø> (ø)
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-21 65.02% <25.79%> (-0.04%) ⬇️
temurin 65.02% <25.79%> (-0.04%) ⬇️
unittests 65.01% <25.79%> (-0.04%) ⬇️
unittests1 56.97% <ø> (-0.01%) ⬇️
unittests2 37.38% <25.79%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Akanksha-kedia

Copy link
Copy Markdown
Contributor Author

@apucher @shounakmk219 @soumitra-st — would appreciate a review when you get a chance. This PR adds a session-based UI authentication layer (Trino/Ambari-style) on top of any configured AccessControlFactory, with server-side session tokens, HttpOnly cookies, and proper logout invalidation.

@Akanksha-kedia

Copy link
Copy Markdown
Contributor Author

@apucher @shounakmk219 @soumitra-st @xiangfu0 — all CI checks are now passing ✅. Would appreciate a review when you get a chance.

This PR adds a session-based UI authentication layer (Trino/Ambari-style) on top of any configured AccessControlFactory:

  • Server-side session tokens stored in SessionManager (ConcurrentHashMap + sliding TTL)
  • POST /auth/login validates credentials via the configured AccessControl, sets an HttpOnly SameSite=Strict cookie
  • GET /auth/logout immediately invalidates the server-side session
  • SessionAuthenticationFilter (priority AUTHENTICATION-10) guards all UI requests — API callers with an Authorization header bypass it and go through the existing AuthenticationFilter
  • UI strips the Authorization header in SESSION mode; credentials never appear in the browser network tab
  • Inactivity timeout with a 60s warning dialog, configurable via controller.ui.session.inactivity.timeout.seconds

Activated by setting controller.ui.session.authentication.enabled=true — no factory class change required, works with BasicAuth, ZkBasicAuth, LDAP, or any custom factory.

@xiangfu0 xiangfu0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Found two high-severity authorization bypasses in the new session path; see inline comments.

if (sessionCookie != null && sessionCookie.getValue() != null) {
Optional<String> username = _sessionManager.getUsername(sessionCookie.getValue());
if (username.isPresent()) {
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CRITICAL: Returning here skips the rest of AuthenticationFilter, including AccessControlUtils.validatePermission(...) and fine-grained auth. In session mode, any user with a valid login cookie can hit controller endpoints regardless of the permissions enforced by the configured AccessControl. The session should authenticate the identity, but authorization still needs to run using that identity or an equivalent server-side principal.

throw QueryErrorCode.ACCESS_DENIED.asException();
// SESSION WORKFLOW: If the request has a valid session cookie, skip the Authorization-header
// access check. The session was already validated by SessionAuthenticationFilter before this method.
if (!sessionValid) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CRITICAL: This makes a valid UI session bypass the table-level hasAccess(rawTableName, READ, ...) check, so a user who can log in can query any table through /sql even when their Basic/LDAP/ZK principal would be denied. Please keep the table authorization check and make the session supply the authenticated principal/credentials needed for that check instead of skipping it.

@shounakmk219 shounakmk219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Did a first pass. Is there an open issue or design doc on this?

Comment on lines +101 to +102
if (_controllerConf != null
&& _controllerConf.getProperty(ControllerConf.CONTROLLER_UI_SESSION_ENABLED, false)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Extract this check into a helper method as its replicated at lot of places

* <li>Logout → read cookie → remove session from store → delete cookie → redirect to login</li>
* </ul>
*/
public class SessionManager {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

IMO this should be an interface with in-memory storage as the default implementation

ControllerConf _controllerConf;

@Inject
@org.jvnet.hk2.annotations.Optional

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why do we need this annotation?

* <li>{@code GET /auth/logout} immediately invalidates the server-side session</li>
* </ul>
*
* <p>Default: {@code false} (backward compatible – existing BASIC/NONE/OIDC workflows unchanged)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: squeeze the huge multi-line comment block into a one liner here and add the details in public docs instead that would be more helpful

Akanksha-kedia added a commit to Akanksha-kedia/pinot that referenced this pull request Jul 8, 2026
…return

AuthenticationFilter: replace early `return` with Basic-auth token injection
from the session store into the request headers, so validatePermission() and
FineGrainedAuthUtils still run with the session user's identity. Extract
isSessionEnabled() helper to consolidate the repeated config check.

PinotQueryResource: remove `if (!sessionValid)` guards around hasAccess()
calls in getQueryResponse() and getMultiStageQueryResponse(). Now that
AuthenticationFilter injects the Authorization header for session requests,
the downstream hasAccess() calls receive the session user's credentials and
work correctly. Remove hasValidSessionCookie() and sessionValid parameter.

ControllerConf: shorten multi-line session constant Javadocs to one-liners.

Addresses CRITICAL review feedback from @xiangfu0 and @shounakmk219 on PR apache#18933.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

@xiangfu0 xiangfu0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Found a few high-signal session-auth issues; see inline comments.

String origin = containerRequestContext.getHeaderString("Origin");
if (origin != null && !origin.isEmpty()) {
containerResponseContext.getHeaders().add("Access-Control-Allow-Origin", origin);
containerResponseContext.getHeaders().add("Access-Control-Allow-Credentials", "true");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This reflects any Origin and enables credentialed CORS globally. Session cookies use SameSite=Strict, but SameSite is site-based rather than origin-based, so a same-site sibling origin can still have the browser send the Pinot session cookie and then read API responses because this response echoes that origin. Please gate credentialed CORS behind an explicit trusted-origin allowlist, or keep credentials disabled unless such an allowlist is configured.

AccessControl accessControl = _accessControlFactory.create();
boolean valid;
try {
valid = accessControl.hasAccess(null, AccessType.READ,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This uses table-level authorization as credential validation. Existing Basic/ZK Basic principals with a non-empty table allowlist fail hasTable(null), so valid table-scoped users are locked out of session mode before the normal endpoint/table checks can run. Validate identity through a non-table credential path and add a regression for a table-scoped principal.

.filter(entry -> !entry.getValue().isEmpty())
.collect(Collectors.toMap(Entry::getKey, entry -> entry.getValue().get(0)));
.filter(entry -> !entry.getValue().isEmpty())
.filter(entry -> !isSessionMode || !entry.getKey().equalsIgnoreCase(HttpHeaders.AUTHORIZATION))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This strips Authorization for every session-mode broker forward, but only the SQL forwarding helper re-injects the stored session credential. The time-series path builds headers with this helper and sends them to the broker as-is, so valid UI sessions lose broker auth for /query/timeseries and /timeseries/api/v1/query_range. Move the session credential injection into a shared broker-forward helper used by both SQL and time-series requests.

Akanksha-kedia added a commit to Akanksha-kedia/pinot that referenced this pull request Jul 9, 2026
…roker auth

ControllerAdminApiApplication: revert CORS to wildcard-only (no credentials).
The Pinot UI is same-origin so CORS is not needed; the previous origin-reflect
+ Allow-Credentials approach was too permissive for same-site sibling origins.

PinotSessionLoginResource: use hasAccess(AccessType, headers, url) instead of
hasAccess(null, AccessType, headers, url). The table-scoped overload calls
hasTable(null) which rejects principals with a table allowlist, locking them
out of session login entirely. The identity-only overload validates credentials
without requiring any table permission.

PinotQueryResource: extract injectSessionCredentialIfNeeded() helper and call
it in both sendRequestToBroker() and sendTimeSeriesRequestToBroker(). Previously
only the SQL forward path re-injected the session credential after extractHeaders()
stripped it; time-series requests were forwarded without broker auth in session mode.

Addresses review feedback from @xiangfu0 on PR apache#18933.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Akanksha-kedia added a commit to Akanksha-kedia/pinot that referenced this pull request Jul 9, 2026
…fault impl

Extract SessionManager as an interface (keeping SESSION_COOKIE_NAME and
DEFAULT_SESSION_TTL_SECONDS constants + the public API) and rename the
in-memory implementation to InMemorySessionManager. All injection points
(AuthenticationFilter, SessionAuthenticationFilter, PinotQueryResource,
PinotSessionLoginResource) already reference the SessionManager type so they
are unaffected. BaseControllerStarter now instantiates InMemorySessionManager.
SessionManagerTest updated to construct InMemorySessionManager directly while
keeping variable types as SessionManager.

Addresses @shounakmk219 review feedback on PR apache#18933.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@Akanksha-kedia

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review @xiangfu0 and @shounakmk219! Here is a summary of all the changes made in response to the feedback:


Security fixes (CRITICAL)

AuthenticationFilter — Replaced the early return on a valid session cookie with Basic-auth token injection (requestContext.getHeaders().putSingle(AUTHORIZATION, authToken)). validatePermission() and FineGrainedAuthUtils now run normally with the session user's credentials instead of being skipped entirely.

PinotQueryResource — Removed the if (!sessionValid) guards around hasAccess() calls in getQueryResponse() and getMultiStageQueryResponse(). Table-level and endpoint-level authorization now runs unconditionally for all requests, including session-based ones.

PinotSessionLoginResource — Changed credential validation at login from hasAccess(null, AccessType.READ, ...) (table-level overload, which calls hasTable(null) and rejects table-scoped principals) to hasAccess(AccessType.READ, ...) (identity-only overload). Table-scoped users can now log in through the session endpoint.

PinotQueryResource — time-series broker auth — Extracted injectSessionCredentialIfNeeded() helper and called it in both sendRequestToBroker() and sendTimeSeriesRequestToBroker(). Previously only the SQL forward path re-injected the session credential; time-series requests (/query/timeseries, /timeseries/api/v1/query_range) were forwarded to the broker without an Authorization header in session mode.

ControllerAdminApiApplication — Reverted CorsFilter to Access-Control-Allow-Origin: * without Allow-Credentials. The Pinot UI is served from the same origin as the API so credentialed CORS is not needed; the previous origin-reflect approach was too permissive for same-site sibling origins.


Non-critical feedback

AuthenticationFilter — Extracted isSessionEnabled() helper to consolidate the repeated _controllerConf != null && getProperty(SESSION_ENABLED) && _sessionManager != null check.

SessionManager — Refactored into an interface. The existing in-memory implementation is now InMemorySessionManager. All injection points already reference SessionManager so no changes were needed there; BaseControllerStarter now instantiates InMemorySessionManager.

PinotQueryResource — Added a comment explaining why @org.jvnet.hk2.annotations.Optional is needed on SessionManager: the binding is always present in production but may be absent in unit-test contexts without a full HK2 setup.

ControllerConf — Shortened the three multi-line Javadoc blocks on the session constants to one-liners.


CI status

10/11 checks passing. The one failure (Pinot Binary Compatibility Check) is a pre-existing issue caused by unrelated changes in pinot-spi on master that removed ColumnReader methods — it also fails on other open PRs and is not related to this change.

Akanksha-kedia and others added 7 commits July 12, 2026 10:31
…validation

Implements a Trino/Ambari-style session layer for the Pinot controller UI that works
on top of any configured AccessControlFactory (BasicAuth, ZkBasicAuth, LDAP, or custom).

**Security problems fixed:**
- Credentials were visible in every browser network tab request (Authorization header)
- Logout only cleared localStorage — server session survived indefinitely
- No inactivity timeout; sessions never expired
- Credentials stored in JS memory (XSS-readable)
- Broker auth forwarded directly from the browser
- Race condition in session lookup (non-atomic get→check→remove)
- No CSRF protection on session cookie

**How it works:**
1. `POST /auth/login` validates credentials via the configured AccessControl, creates a
   server-side session, and sets an HttpOnly + SameSite=Strict cookie
2. `GET /auth/logout` immediately removes the session from the server store (proper invalidation)
3. `GET /auth/session` checks session validity for page-refresh restoration
4. `SessionAuthenticationFilter` (AUTHENTICATION-10 priority) validates the cookie before
   `AuthenticationFilter` runs; passes through requests with Authorization header for API compat
5. `PinotQueryResource` injects stored Basic-auth token server-side when forwarding to broker

**New files:**
- `SessionManager.java`: ConcurrentHashMap session store with sliding TTL, atomic compute()
- `SessionAuthenticationFilter.java`: JAX-RS filter for cookie-based auth
- `SessionBasicAuthAccessControlFactory.java`: convenience wrapper factory
- `PinotSessionLoginResource.java`: /auth/login, /auth/logout, /auth/session endpoints
- `SessionManagerTest.java`: unit + 50-thread concurrency test

**Configuration (all default-off for backward compatibility):**
```properties
controller.ui.session.authentication.enabled=true
controller.ui.session.cookie.secure=true       # set false for HTTP-only dev
controller.ui.session.inactivity.timeout.seconds=300  # default 5 min
```

**Frontend changes:**
- `Models.ts`: SESSION enum added to AuthWorkflow
- `axios-config.ts`: SESSION workflow deletes Authorization header (cookie sent automatically)
- `AuthProvider.tsx`: reads timeout from /auth/info, checks /auth/session on page refresh
- `App.tsx`: inactivity timer with 60-second warning dialog before auto-logout
- `Header.tsx`: real logout button that calls /auth/logout server-side
- `LoginPage.tsx`: SESSION path POSTs form data to /auth/login (no Basic header in JS)
- Remove trailing whitespace on blank lines in SessionAuthenticationFilter
- Remove blank lines before end-of-file in 4 new/modified files
- Replace Collections.emptyList/emptyMap with List.of/Map.of in PinotSessionLoginResource
- Fix LeftCurly violation in SessionManagerTest (synchronized block brace)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- SessionBasicAuthAccessControlFactory: replace non-existent BasicAuthUtils/
  AbstractBasicAuthAccessControl with correct BasicAuthPrincipalUtils and
  inline AccessControl implementation (mirrors BasicAuthAccessControlFactory)
- PinotQueryResource: fix @optional annotation package from
  org.glassfish.hk2.api to org.jvnet.hk2.annotations (correct HK2 location)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…return

AuthenticationFilter: replace early `return` with Basic-auth token injection
from the session store into the request headers, so validatePermission() and
FineGrainedAuthUtils still run with the session user's identity. Extract
isSessionEnabled() helper to consolidate the repeated config check.

PinotQueryResource: remove `if (!sessionValid)` guards around hasAccess()
calls in getQueryResponse() and getMultiStageQueryResponse(). Now that
AuthenticationFilter injects the Authorization header for session requests,
the downstream hasAccess() calls receive the session user's credentials and
work correctly. Remove hasValidSessionCookie() and sessionValid parameter.

ControllerConf: shorten multi-line session constant Javadocs to one-liners.

Addresses CRITICAL review feedback from @xiangfu0 and @shounakmk219 on PR apache#18933.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…roker auth

ControllerAdminApiApplication: revert CORS to wildcard-only (no credentials).
The Pinot UI is same-origin so CORS is not needed; the previous origin-reflect
+ Allow-Credentials approach was too permissive for same-site sibling origins.

PinotSessionLoginResource: use hasAccess(AccessType, headers, url) instead of
hasAccess(null, AccessType, headers, url). The table-scoped overload calls
hasTable(null) which rejects principals with a table allowlist, locking them
out of session login entirely. The identity-only overload validates credentials
without requiring any table permission.

PinotQueryResource: extract injectSessionCredentialIfNeeded() helper and call
it in both sendRequestToBroker() and sendTimeSeriesRequestToBroker(). Previously
only the SQL forward path re-injected the session credential after extractHeaders()
stripped it; time-series requests were forwarded without broker auth in session mode.

Addresses review feedback from @xiangfu0 on PR apache#18933.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…fault impl

Extract SessionManager as an interface (keeping SESSION_COOKIE_NAME and
DEFAULT_SESSION_TTL_SECONDS constants + the public API) and rename the
in-memory implementation to InMemorySessionManager. All injection points
(AuthenticationFilter, SessionAuthenticationFilter, PinotQueryResource,
PinotSessionLoginResource) already reference the SessionManager type so they
are unaffected. BaseControllerStarter now instantiates InMemorySessionManager.
SessionManagerTest updated to construct InMemorySessionManager directly while
keeping variable types as SessionManager.

Addresses @shounakmk219 review feedback on PR apache#18933.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@Akanksha-kedia Akanksha-kedia force-pushed the feat/session-based-ui-auth branch from 89a4a81 to 98d0c3c Compare July 12, 2026 09:38

@xiangfu0 xiangfu0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Found one high-signal compatibility issue; see inline comment.

.filter(entry -> !entry.getValue().isEmpty())
.collect(Collectors.toMap(Entry::getKey, entry -> entry.getValue().get(0)));
.filter(entry -> !entry.getValue().isEmpty())
.filter(entry -> !isSessionMode || !entry.getKey().equalsIgnoreCase(HttpHeaders.AUTHORIZATION))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This drops Authorization for every controller /sql and time-series proxy request whenever session mode is enabled, including API clients that authenticated with Basic/Bearer and do not have a session cookie. The controller-side access check can pass with the original header, but the forwarded broker request then arrives without credentials and fails on broker-auth clusters. Keep the caller-supplied Authorization when there is no valid session cookie, or only replace it when injecting a session credential.

… API clients

extractHeaders was stripping the Authorization header from every request
when session mode was globally enabled, including API clients that supply
their own Basic/Bearer token with no session cookie. injectSessionCredentialIfNeeded
only re-injected if a session cookie was present, so API clients had their
auth silently removed before the broker call.

Merge the two-step strip+inject into a single buildBrokerHeaders call that
resolves the session credential exactly once via resolveSessionCredential.
If a valid session cookie is found, the caller Authorization is replaced with
the stored token; otherwise all headers are forwarded unchanged.
@Akanksha-kedia

Copy link
Copy Markdown
Contributor Author

Thank you for the thorough reviews! Addressing all open comments:

@xiangfu0 — resolved:

Comment Fix
AuthenticationFilter.java:108 — early return bypassed authorization Replaced with credential injection; validatePermission and FineGrainedAuthUtils now always run with the session user's identity
PinotQueryResource.java:563 — session bypassed table-level hasAccess hasAccess(rawTableName, READ, ...) check is preserved; session only injects credentials, it does not skip authorization
PinotSessionLoginResource.java:155hasTable(null) locked out table-scoped users Changed to hasAccess(READ, syntheticHeaders, AUTH_LOGIN_PATH) which validates identity without a table scope
ControllerAdminApiApplication.java:158 — credentialed CORS echoing Origin CORS now uses Access-Control-Allow-Origin: * (no credential reflection)
PinotQueryResource.java:841 — timeseries path not re-injecting session credential Both SQL and timeseries broker-forward paths now use buildBrokerHeaders() which resolves the credential once
PinotQueryResource.java:841 (July 12) — extractHeaders stripped Authorization from all requests in session mode, including API clients Merged extractHeaders + injectSessionCredentialIfNeeded into a single buildBrokerHeaders / resolveSessionCredential pair that calls getBasicAuthToken exactly once — no TOCTOU window

@shounakmk219 — resolved:

Comment Fix
SessionManager.java:62 — should be interface Refactored to SessionManager interface + InMemorySessionManager default impl
ControllerConf.java:376 — compress multi-line comment block Each constant now has a single-line Javadoc

Still open / acknowledged:

  • AuthenticationFilter.java:102 — I'll extract the session-cookie check into a shared helper as a follow-up cleanup commit if that's helpful, or happy to do it now.
  • PinotQueryResource.java:128@org.jvnet.hk2.annotations.Optional is needed because SessionManager binding is only registered when session auth is enabled; without it HK2 injection fails in unit-test contexts where the full ControllerAdminApiApplication is not wired up.

@xiangfu0 xiangfu0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Found a few high-signal issues; see inline comments.

ControllerConf.CONTROLLER_UI_SESSION_INACTIVITY_TIMEOUT_SECONDS,
ControllerConf.DEFAULT_UI_SESSION_INACTIVITY_TIMEOUT_SECONDS);
long serverSessionTtlSeconds = uiInactivityTimeoutSeconds + 120;
final SessionManager sessionManager = new InMemorySessionManager(serverSessionTtlSeconds);

@xiangfu0 xiangfu0 Jul 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This creates one in-memory session store per controller process. Pinot controllers are commonly deployed behind a VIP/LB; a token minted on controller A will be unknown on controller B, and logout routed to B will not revoke the token minted on A. That breaks the server-side logout/invalidation guarantee and makes session mode unreliable without sticky routing. Please use a shared session store or gate/document this feature on sticky sessions, with a test for cross-controller logout/validation.

StringBuilder sb = new StringBuilder();
sb.append(SessionManager.SESSION_COOKIE_NAME).append('=').append(token);
sb.append("; Path=/");
sb.append("; Max-Age=").append(maxAgeSeconds);

@xiangfu0 xiangfu0 Jul 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The server session TTL is sliding because getUsername extends it, but the browser cookie Max-Age is fixed at login and is never refreshed on later session checks or API calls. An active UI session can therefore lose the browser cookie after the original max age even though the server-side session was extended. Please either renew Set-Cookie when sliding the session or make the server-side session non-sliding so browser and server expiry semantics match.

containerResponseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS, DELETE");
containerResponseContext.getHeaders().add("Access-Control-Allow-Headers", "*");
containerResponseContext.getHeaders()
.add("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept, Origin, X-Requested-With");

@xiangfu0 xiangfu0 Jul 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This narrows CORS allowed headers from wildcard to a fixed list that omits the Pinot supported database request header, CommonConstants.DATABASE, which DatabaseUtils.translateTableName(..., HttpHeaders) uses for multi-database table/schema resolution. Browser clients that set database will now fail preflight before reaching the controller. Please preserve wildcard behavior or include Pinot-supported/custom headers such as database and existing upload headers like If-Match.

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.

4 participants