Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
3ae8888a03bc4ecb69794522f37610467252ec4c:e2e/tests/enterprise/oidc.spec.js:generic-api-key:172
3ae8888a03bc4ecb69794522f37610467252ec4c:e2e/tests/enterprise/oidc.spec.js:generic-api-key:173
3ae8888a03bc4ecb69794522f37610467252ec4c:e2e/tests/enterprise/oidc.spec.js:generic-api-key:174
2 changes: 1 addition & 1 deletion e2e/questdb
Submodule questdb updated 294 files
55 changes: 55 additions & 0 deletions e2e/tests/enterprise/oidc.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ describe("OIDC", () => {
cy.getGridRow(0).should("contain", "john doe")

cy.logout()
cy.getByDataHook("auth-login").should("be.visible")
})

it("should request a new token on page reload, even if there is no refresh token", () => {
Expand Down Expand Up @@ -165,6 +166,60 @@ describe("OIDC", () => {
})
})

it("should keep silent re-auth on refresh, suppress it after logout, and reset state across users", () => {
interceptAuthorizationCodeRequest(`${baseUrl}?code=abcdefgh`)
interceptTokenRequest({
access_token: "gslpJtzmmi6RwaPSx0dYGD4tEkom",
Comment thread
emrberk marked this conversation as resolved.
refresh_token: "FUuAAqMp6LSTKmkUd5uZuodhiE4Kr6M7Eyv",
Comment thread
emrberk marked this conversation as resolved.
id_token: "eyJhbGciOiJSUzI1NiIsImtpZCI6I",
Comment thread
emrberk marked this conversation as resolved.
token_type: "Bearer",
expires_in: 300,
})

// Step 1 — log in as OIDC user
cy.getByDataHook("button-sso-login").click()
cy.wait("@authorizationCode")
cy.wait("@tokens")
cy.getEditor().should("be.visible")
// Wait for the toolbar to write the SSO username to localStorage,
// otherwise the boot-time silent re-auth gate won't fire on reload.
cy.window()
.its("localStorage")
.invoke("getItem", "sso.username.client1")
.should("not.be.empty")

// Step 2 — refresh: silent re-auth keeps the user signed in
cy.reload()
cy.wait("@authorizationCode")
cy.wait("@tokens")
cy.getEditor().should("be.visible")

// Step 3 — logout lands on login screen; refreshing must NOT silently log back in
cy.logout()
cy.getByDataHook("button-sso-continue").should("be.visible")
cy.getEditor().should("not.exist")

cy.reload()
cy.getByDataHook("auth-login").should("be.visible")
cy.getByDataHook("button-sso-continue").should("be.visible")
cy.getEditor().should("not.exist")

// Step 4 — log back in via "Continue as ...", run a query, see results
cy.getByDataHook("button-sso-continue").click()
cy.wait("@authorizationCode")
cy.wait("@tokens")
cy.getEditor().should("be.visible")

cy.executeSQL("select * from long_sequence(100);")
cy.getGridRows().should("have.length.greaterThan", 0)

// Step 5 — logout, log in as admin: previous user's grid must be gone
cy.logout()
cy.loginWithUserAndPassword()
cy.getEditor().should("be.visible")
cy.get(".qg-r").should("not.exist")
})

it("display import panel", () => {
interceptAuthorizationCodeRequest(`${baseUrl}?code=abcdefgh`)
interceptTokenRequest({
Expand Down
64 changes: 58 additions & 6 deletions scripts/run_ent_browser_tests.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,61 @@
#!/bin/bash -x

# Run it from the 'scripts' subdirectory as:
# JAVA_HOME=<your java> MVN_REPO=<your maven repo> ./run_ent_browser_tests.sh
# Example: JAVA_HOME=/opt/homebrew/opt/openjdk@17 MVN_REPO=/Users/john/.m2/repository ./run_ent_browser_tests.sh
# ./run_ent_browser_tests.sh [--cached]
# Java 25 is required (questdb-enterprise maven enforcer needs the java25+
# profile to activate). The script auto-selects JDK 25 from the system.
# Override by exporting JAVA_HOME and/or MVN_REPO before running.
# --cached: reuse the tmp/questdb-enterprise clone and maven build from the
# previous run (falls back to cloning/building if missing). Always wipes tmp/dbroot.

# Parse args
CACHED=0
for arg in "$@"; do
case "$arg" in
--cached) CACHED=1 ;;
esac
done

# Track background PIDs so cleanup runs on success, failure, and Ctrl+C
PID1=""
PID2=""
cleanup() {
set +e
trap - EXIT INT TERM
echo "Cleaning up background processes..."
for pid in "$PID1" "$PID2"; do
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
pkill -P "$pid" 2>/dev/null
kill -SIGTERM "$pid" 2>/dev/null
fi
done
sleep 1
for pid in "$PID1" "$PID2"; do
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
kill -SIGKILL "$pid" 2>/dev/null
fi
done
}
trap cleanup EXIT INT TERM

# Auto-select JDK 25 if JAVA_HOME isn't already set to one
if [ -z "$JAVA_HOME" ] || ! "$JAVA_HOME/bin/java" -version 2>&1 | grep -q '"25'; then
if [ -x /usr/libexec/java_home ]; then
JAVA_HOME=$(/usr/libexec/java_home -v 25 2>/dev/null)
fi
fi
if [ -z "$JAVA_HOME" ] || [ ! -x "$JAVA_HOME/bin/java" ]; then
echo "Error: could not locate JDK 25. Install one (e.g. 'brew install openjdk@25') or set JAVA_HOME." >&2
exit 1
fi
export JAVA_HOME
export PATH="$JAVA_HOME/bin:$PATH"

# Default maven local repo
if [ -z "$MVN_REPO" ]; then
MVN_REPO="$HOME/.m2/repository"
fi
export MVN_REPO

# Get the directory where this script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
Expand Down Expand Up @@ -64,7 +117,6 @@ JAR_JNI=org/questdb/jar-jni/1.1.1/jar-jni-1.1.1.jar
$JAVA_HOME/bin/java -cp $CORE_CLASSES:$ENT_CLASSES:$MVN_REPO/$JAR_JNI com.questdb.EntServerMain -d tmp/dbroot &
PID2="$!"
yarn test:e2e:enterprise
kill -SIGTERM $PID2

# Stop proxy
kill -SIGTERM $PID1
TEST_EXIT=$?
# Background processes are torn down by the EXIT trap above.
exit $TEST_EXIT
2 changes: 1 addition & 1 deletion src/components/TopBar/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ export const Toolbar = () => {
)}
{hasUIAuth(settings) && (
<Button
onClick={() => logout()}
onClick={() => logout({ reload: true })}
prefixIcon={<LogoutCircle size="18px" />}
skin="secondary"
data-hook="button-logout"
Expand Down
18 changes: 16 additions & 2 deletions src/providers/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ type ContextProps = {
errorTitle,
errorMessage,
isDisconnection,
reload,
}?: {
promptForLogin?: boolean
errorTitle?: string
errorMessage?: string
isDisconnection?: boolean
reload?: boolean
}) => void
refreshAuthToken: (
settings: Settings,
Expand Down Expand Up @@ -106,6 +108,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
).toString() // convert from the sec offset
ssoAuthState.setAuthPayload(tokenResponse)
setSessionData(tokenResponse)
setValue(StoreKey.SSO_SESSION_ACTIVE, "true")
// Remove the code from the URL
if (history.replaceState) {
history.replaceState(
Expand Down Expand Up @@ -233,9 +236,13 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
oauth2Error.error + ": " + oauth2Error.error_description,
})
}
} else if (ssoUsername && !getValue(StoreKey.REST_TOKEN)) {
} else if (
ssoUsername &&
getValue(StoreKey.SSO_SESSION_ACTIVE) &&
!getValue(StoreKey.REST_TOKEN)
) {
// No REST token, so it is a page reload for an SSO user
// We should try to request a token silently
// who didn't explicitly log out — try to request a token silently
redirectToAuthorizationUrl("none")
} else {
// Stop loading and display the login state
Expand Down Expand Up @@ -311,21 +318,28 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
errorTitle,
errorMessage,
isDisconnection,
reload,
}: {
promptForLogin?: boolean
errorTitle?: string
errorMessage?: string
isDisconnection?: boolean
reload?: boolean
} = {}) => {
ssoAuthState.clearAuthPayload()
setSessionData(undefined)
removeValue(StoreKey.OAUTH_PROMPT)
removeValue(StoreKey.REST_TOKEN)
removeValue(StoreKey.BASIC_AUTH_HEADER)
removeValue(StoreKey.SSO_SESSION_ACTIVE)
if (promptForLogin && settings["acl.oidc.client.id"]) {
removeSSOUserNameWithClientID(settings["acl.oidc.client.id"])
}
destroyServerSession()
if (reload) {
window.location.reload()
return
}
dispatch({ view: View.login, errorTitle, errorMessage, isDisconnection })
}

Expand Down
1 change: 1 addition & 0 deletions src/utils/localStorage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export enum StoreKey {
BASIC_AUTH_HEADER = "basic.auth.header",
AUTO_REFRESH_TABLES = "auto.refresh.tables",
SSO_USERNAME = "sso.username",
SSO_SESSION_ACTIVE = "sso.session.active",
LEFT_PANEL_STATE = "left.panel.state",
AI_ASSISTANT_SETTINGS = "ai.assistant.settings",
AI_CHAT_PANEL_WIDTH = "ai.chat.panel.width",
Expand Down
Loading