Skip to content
Closed
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
66 changes: 62 additions & 4 deletions packages/browser-tests/cypress/integration/enterprise/oidc.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ describe("OIDC authentication", () => {
});

beforeEach(() => {
cy.clearLocalStorage();

// load login page
interceptSettings({
"release.type": "EE",
Expand Down Expand Up @@ -76,7 +78,7 @@ describe("OIDC authentication", () => {
cy.logout();
});

it("should force authentication if token expired, and there is no refresh token", () => {
it("should force SSO authentication if token expired, and there is no refresh token", () => {
interceptAuthorizationCodeRequest(`${baseUrl}?code=abcdefgh`);
cy.getByDataHook("button-sso-login").click();
cy.wait("@authorizationCode");
Expand All @@ -91,9 +93,65 @@ describe("OIDC authentication", () => {
cy.getEditor().should("be.visible");

cy.reload();
cy.getByDataHook("button-log-in").should("be.visible");
cy.getByDataHook("button-sso-login").should("be.visible");

cy.getByDataHook("button-sso-login").click();
cy.getEditor().should("be.visible");
});

it("should not force SSO re-authentication with continue button", () => {
interceptAuthorizationCodeRequest(`${baseUrl}?code=abcdefgh`);
cy.getByDataHook("button-sso-login").click();
cy.wait("@authorizationCode");

interceptTokenRequest({
"access_token": "gslpJtzmmi6RwaPSx0dYGD4tEkom",
"refresh_token": "FUuAAqMp6LSTKmkUd5uZuodhiE4Kr6M7Eyv",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6I",
"token_type": "Bearer",
"expires_in": 300
});
cy.wait("@tokens");
cy.getEditor().should("be.visible");

cy.executeSQL("select current_user();");
cy.getGridRow(0).should("contain", "user1");

cy.logout();

cy.getByDataHook("button-sso-continue").click();
cy.wait("@authorizationCode").then((interception) => {
expect(interception.request.url).to.include("/authorization");
const url = new URL(interception.request.url);
expect(url.searchParams.get("prompt")).to.equal(null);
});
});

it("should force SSO re-authentication with choose a different account button", () => {
interceptAuthorizationCodeRequest(`${baseUrl}?code=abcdefgh`);
cy.getByDataHook("button-sso-login").click();
cy.wait("@authorizationCode");

cy.getByDataHook("button-log-in").click()
interceptTokenRequest({
"access_token": "gslpJtzmmi6RwaPSx0dYGD4tEkom",
"refresh_token": "FUuAAqMp6LSTKmkUd5uZuodhiE4Kr6M7Eyv",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6I",
"token_type": "Bearer",
"expires_in": 300
});
cy.wait("@tokens");
cy.getEditor().should("be.visible");

cy.executeSQL("select current_user();");
cy.getGridRow(0).should("contain", "user1");

cy.logout();

cy.getByDataHook("button-sso-login").click();
cy.wait("@authorizationCode").then((interception) => {
expect(interception.request.url).to.include("/authorization");
const url = new URL(interception.request.url);
expect(url.searchParams.get("prompt")).to.equal("login");
});
});
});
});
2 changes: 1 addition & 1 deletion packages/browser-tests/questdb
Submodule questdb updated 26 files
+ core/src/main/bin/linux-aarch64/libjemalloc.so
+1 −1 core/src/main/c/share/dedup.cpp
+38 −10 core/src/main/c/share/ooo.cpp
+3 −3 core/src/main/c/share/ooo.h
+16 −18 core/src/main/c/share/ooo_radix.h
+7 −0 core/src/main/java/io/questdb/PropServerConfiguration.java
+2 −1 core/src/main/java/io/questdb/PropertyKey.java
+2 −0 core/src/main/java/io/questdb/cairo/CairoConfiguration.java
+10 −5 core/src/main/java/io/questdb/cairo/CairoConfigurationWrapper.java
+10 −5 core/src/main/java/io/questdb/cairo/DefaultCairoConfiguration.java
+0 −6 core/src/main/java/io/questdb/cairo/TableUtils.java
+19 −5 core/src/main/java/io/questdb/cairo/TableWriter.java
+6 −12 core/src/main/java/io/questdb/cairo/wal/WalTxnDetails.java
+1 −1 core/src/main/java/io/questdb/cairo/wal/seq/TableSequencerAPI.java
+1 −1 core/src/main/java/io/questdb/cairo/wal/seq/TableWriterPressureControlImpl.java
+8 −3 core/src/main/java/io/questdb/std/Vect.java
+ core/src/main/resources/io/questdb/bin/darwin-aarch64/libquestdb.dylib
+ core/src/main/resources/io/questdb/bin/darwin-x86-64/libquestdb.dylib
+ core/src/main/resources/io/questdb/bin/linux-aarch64/libquestdb.so
+ core/src/main/resources/io/questdb/bin/linux-x86-64/libquestdb.so
+ core/src/main/resources/io/questdb/bin/windows-x86-64/libquestdb.dll
+31 −10 core/src/test/java/io/questdb/test/DynamicPropServerConfigurationTest.java
+2 −0 core/src/test/java/io/questdb/test/cairo/fuzz/AbstractFuzzTest.java
+0 −1 core/src/test/java/io/questdb/test/cairo/wal/WalTableFailureTest.java
+27 −3 core/src/test/java/io/questdb/test/cairo/wal/WalWriterTest.java
+138 −20 core/src/test/java/io/questdb/test/std/VectFuzzTest.java
15 changes: 12 additions & 3 deletions packages/web-console/src/components/TopBar/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { Text } from "../Text"
import { selectors } from "../../store"
import { useSelector } from "react-redux"
import { IconWithTooltip } from "../IconWithTooltip"
import { hasUIAuth } from "../../modules/OAuth2/utils"
import { hasUIAuth, setSSOUserNameWithClientID } from "../../modules/OAuth2/utils"
import { getValue } from "../../utils/localStorage"
import { StoreKey } from "../../utils/localStorage/types"

type ServerDetails = {
instance_name: string | null
Expand Down Expand Up @@ -93,11 +95,18 @@ export const Toolbar = () => {
},
)
if (response.type === QuestDB.Type.DQL && response.count === 1) {
const currentUser = response.data[0].current_user
setServerDetails({
instance_name: response.data[0].instance_name,
instance_rgb: response.data[0].instance_rgb,
current_user: response.data[0].current_user,
current_user: currentUser,
})

// an SSO user is logged in, update the SSO username
const authPayload = getValue(StoreKey.AUTH_PAYLOAD)
if (authPayload && currentUser && settings["acl.oidc.client.id"]) {
setSSOUserNameWithClientID(settings["acl.oidc.client.id"], currentUser)
}
}
} catch (e) {
return
Expand Down Expand Up @@ -145,7 +154,7 @@ export const Toolbar = () => {
skin="secondary"
data-hook="button-logout"
>
Log out
Logout
</Button>
)}
</Box>
Expand Down
13 changes: 13 additions & 0 deletions packages/web-console/src/modules/OAuth2/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Settings } from "../../providers/SettingsProvider/types"
import { StoreKey } from "../../utils/localStorage/types"

type TokenPayload = Partial<{
grant_type: string
Expand Down Expand Up @@ -83,3 +84,15 @@ export const getAuthToken = async (

export const hasUIAuth = (settings: Settings) =>
settings["acl.enabled"] && !settings["acl.basic.auth.realm.enabled"]

export const getSSOUserNameWithClientID = (clientId: string) => {
return localStorage.getItem(`${StoreKey.SSO_USERNAME}.${clientId}`) ?? ""
}

export const setSSOUserNameWithClientID = (clientId: string, value: string) => {
localStorage.setItem(`${StoreKey.SSO_USERNAME}.${clientId}`, value)
}

export const removeSSOUserNameWithClientID = (clientId: string) => {
localStorage.removeItem(`${StoreKey.SSO_USERNAME}.${clientId}`)
}
54 changes: 35 additions & 19 deletions packages/web-console/src/modules/OAuth2/views/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Text } from "../../../components"
import { setValue } from "../../../utils/localStorage"
import { StoreKey } from "../../../utils/localStorage/types"
import { useSettings } from "../../../providers"

import { getSSOUserNameWithClientID } from "../utils"
const Header = styled.div`
position: absolute;
width: 100%;
Expand Down Expand Up @@ -53,16 +53,16 @@ const Container = styled.div`
font-size: 16px;
transition: height 10s ease;
`
const Title = styled.h1`
const Title = styled.h2`
color: white;
text-align: center;
text-align: start;
font-weight: 600;
`

const SSOCard = styled.div`
button {
padding-top: 2rem;
padding-bottom: 2rem;
border-radius: 0 5px 5px 0;
width: 100%;
margin-bottom: 10px;
}
Expand Down Expand Up @@ -199,12 +199,15 @@ export const Login = ({
onOAuthLogin,
onBasicAuthSuccess,
}: {
onOAuthLogin: () => void
onOAuthLogin: (loginWithDifferentAccount?: boolean) => void
onBasicAuthSuccess: () => void
}) => {
const { settings } = useSettings()
const isEE = settings["release.type"] === "EE"
const [errorMessage, setErrorMessage] = React.useState<string | undefined>()
const ssoUsername = settings["acl.oidc.enabled"] && settings["acl.oidc.client.id"]
? getSSOUserNameWithClientID(settings["acl.oidc.client.id"])
: ""

const httpBasicAuthStrategy = isEE
? {
Expand Down Expand Up @@ -284,22 +287,35 @@ export const Login = ({
is absent, we should display generic text as the title contributes to
the page layout.
*/}
<Title>Please Sign In</Title>
{settings["acl.oidc.enabled"] && (
<SSOCard>
<StyledButton
data-hook="button-sso-login"
skin="secondary"
prefixIcon={<User size="18px" />}
onClick={() => onOAuthLogin()}
>
Continue with SSO
</StyledButton>
<Line>
<LineText color="gray2">or</LineText>
</Line>
</SSOCard>
<>
<Title style={{ marginBottom: '4rem' }}>Single Sign-On</Title>
<SSOCard>
{!!ssoUsername && (
<StyledButton
data-hook="button-sso-continue"
skin="primary"
prefixIcon={<User size="18px" />}
onClick={() => onOAuthLogin(false)}
>
Continue as {ssoUsername}
</StyledButton>
)}
<StyledButton
data-hook="button-sso-login"
skin={!!ssoUsername ? "transparent" : "primary"}
prefixIcon={!!ssoUsername ? undefined : <User size="18px" />}
onClick={() => onOAuthLogin(true)}
>
{!!ssoUsername ? "Choose a different account" : "Continue with SSO"}
</StyledButton>
<Line style={{ marginBottom: '4rem', marginTop: '2rem' }}>
<LineText color="gray2">or</LineText>
</Line>
</SSOCard>
</>
)}
<Title>Sign In</Title>
<Card hasError={errorMessage}>
<Form<FormValues>
name="login"
Expand Down
34 changes: 9 additions & 25 deletions packages/web-console/src/providers/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getAuthorisationURL,
getAuthToken,
getTokenExpirationDate,
removeSSOUserNameWithClientID,
} from "../modules/OAuth2/utils"
import {
generateCodeChallenge,
Expand All @@ -29,17 +30,15 @@ import { useSettings } from "./SettingsProvider"

type ContextProps = {
sessionData?: Partial<AuthPayload>
logout: () => void
logout: (removeSSOUsername?: boolean) => void
refreshAuthToken: (settings: Settings) => Promise<AuthPayload>
switchToOAuth: () => void
}

enum View {
ready,
loading,
error,
login,
loggedOut,
}

type State = { view: View; errorMessage?: string }
Expand All @@ -52,7 +51,6 @@ const defaultValues: ContextProps = {
sessionData: undefined,
logout: () => {},
refreshAuthToken: async () => ({} as AuthPayload),
switchToOAuth: () => {},
}

class OAuth2Error {
Expand Down Expand Up @@ -259,32 +257,27 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
}
}

const redirectToAuthorizationUrl = (login?: boolean) => {
const redirectToAuthorizationUrl = (loginWithDifferentAccount?: boolean) => {
const state = generateState(settings)
const code_verifier = generateCodeVerifier(settings)
const code_challenge = generateCodeChallenge(code_verifier)
window.location.href = getAuthorisationURL({
settings,
code_challenge,
state,
login,
login: loginWithDifferentAccount,
redirect_uri: settings["acl.oidc.redirect.uri"] || window.location.href,
})
}

const logout = (noRedirect?: boolean) => {
const logout = (removeSSOUsername?: boolean) => {
removeValue(StoreKey.AUTH_PAYLOAD)
removeValue(StoreKey.REST_TOKEN)
removeValue(StoreKey.BASIC_AUTH_HEADER)
if (noRedirect) {
dispatch({ view: View.loggedOut })
} else {
window.location.reload()
if (removeSSOUsername && settings["acl.oidc.client.id"]) {
removeSSOUserNameWithClientID(settings["acl.oidc.client.id"])
}
}

const switchToOAuth = () => {
redirectToAuthorizationUrl(true)
dispatch({ view: View.login })
}

useEffect(() => {
Expand Down Expand Up @@ -322,7 +315,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
sessionData,
logout,
refreshAuthToken,
switchToOAuth,
}}
>
{children}
Expand All @@ -337,20 +329,12 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
),
[View.login]: () => (
<Login
onOAuthLogin={switchToOAuth}
onOAuthLogin={redirectToAuthorizationUrl}
onBasicAuthSuccess={() => {
dispatch({ view: View.ready })
}}
/>
),
[View.loggedOut]: () => (
<Logout
onLogout={() => {
removeValue(StoreKey.OAUTH_REDIRECT_COUNT)
redirectToAuthorizationUrl(true)
}}
/>
),
}

return <>{views[state.view]()}</>
Expand Down
1 change: 1 addition & 0 deletions packages/web-console/src/utils/localStorage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ export enum StoreKey {
REST_TOKEN = "rest.token",
BASIC_AUTH_HEADER = "basic.auth.header",
AUTO_REFRESH_TABLES = "auto.refresh.tables",
SSO_USERNAME = "sso.username",
}