Skip to content

Commit 4ee855b

Browse files
authored
Merge branch 'main' into nw_markdown_export
2 parents e397c0d + 3990acf commit 4ee855b

File tree

15 files changed

+202
-7
lines changed

15 files changed

+202
-7
lines changed

packages/browser-tests/cypress.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = defineConfig({
1313
screenshotOnRunFailure: false,
1414
video: false,
1515
baseUrl: baseUrl,
16+
chromeWebSecurity: false, //if it is true, cypress does not allow redirects
1617
viewportWidth: 1280,
1718
viewportHeight: 720,
1819
specPattern: "cypress/integration/**/*.spec.js",

packages/browser-tests/cypress/commands.js

+18
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,24 @@ Cypress.Commands.add("loadConsoleWithAuth", (clearWarnings) => {
265265
}
266266
});
267267

268+
Cypress.Commands.add("loadConsoleAsAdminAndCreateSSOGroup", (group, externalGroup = undefined) => {
269+
cy.loadConsoleWithAuth(true);
270+
cy.executeSQL(`CREATE GROUP ${group} WITH EXTERNAL ALIAS ${externalGroup || group};`);
271+
cy.executeSQL(`GRANT HTTP TO ${group};`);
272+
cy.logout();
273+
});
274+
275+
Cypress.Commands.add("logout", () => {
276+
cy.getByDataHook("button-logout").click();
277+
cy.getByDataHook("auth-login").should("be.visible");
278+
});
279+
280+
Cypress.Commands.add("executeSQL", (sql) => {
281+
cy.clearEditor();
282+
cy.typeQuery(sql);
283+
cy.clickRun();
284+
});
285+
268286
Cypress.Commands.add("refreshSchema", () => {
269287
// toggle between auto-refresh modes to trigger a schema refresh
270288
cy.getByDataHook("schema-auto-refresh-button").click();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/// <reference types="cypress" />
2+
3+
const contextPath = process.env.QDB_HTTP_CONTEXT_WEB_CONSOLE || ""
4+
const baseUrl = `http://localhost:9999${contextPath}`;
5+
const settingsUrl = `${baseUrl}/settings`;
6+
7+
const oidcProviderUrl = "http://localhost:9032";
8+
const oidcAuthorizationCodeUrl = `${oidcProviderUrl}/authorization`;
9+
const oidcTokenUrl = `${oidcProviderUrl}/token`;
10+
11+
const interceptSettings = (payload) => {
12+
cy.intercept({ method: "GET", url: settingsUrl }, payload).as(
13+
"settings"
14+
);
15+
};
16+
17+
const interceptAuthorizationCodeRequest = (redirectUrl) => {
18+
cy.intercept("GET", `${oidcAuthorizationCodeUrl}?**`, (req) => {
19+
req.redirect(redirectUrl);
20+
}).as('authorizationCode');
21+
};
22+
23+
const interceptTokenRequest = (payload) => {
24+
cy.intercept({ method: "POST", url: oidcTokenUrl }, payload).as(
25+
"tokens"
26+
);
27+
};
28+
29+
describe("OIDC authentication", () => {
30+
before(() => {
31+
// setup SSO group mappings
32+
cy.loadConsoleAsAdminAndCreateSSOGroup("group1");
33+
});
34+
35+
beforeEach(() => {
36+
// load login page
37+
interceptSettings({
38+
"release.type": "EE",
39+
"release.version": "1.2.3",
40+
"acl.enabled": true,
41+
"acl.basic.auth.realm.enabled": false,
42+
"acl.oidc.enabled": true,
43+
"acl.oidc.client.id": "client1",
44+
"acl.oidc.authorization.endpoint": oidcAuthorizationCodeUrl,
45+
"acl.oidc.token.endpoint": oidcTokenUrl,
46+
"acl.oidc.pkce.required": true,
47+
"acl.oidc.state.required": false,
48+
"acl.oidc.groups.encoded.in.token": false,
49+
});
50+
cy.visit(baseUrl);
51+
52+
cy.wait("@settings");
53+
cy.getByDataHook("auth-login").should("be.visible");
54+
cy.getByDataHook("button-sso-login").should("be.visible");
55+
cy.getEditor().should("not.exist");
56+
});
57+
58+
it("should login via OIDC", () => {
59+
interceptAuthorizationCodeRequest(`${baseUrl}?code=abcdefgh`);
60+
cy.getByDataHook("button-sso-login").click();
61+
cy.wait("@authorizationCode");
62+
63+
interceptTokenRequest({
64+
"access_token": "gslpJtzmmi6RwaPSx0dYGD4tEkom",
65+
"refresh_token": "FUuAAqMp6LSTKmkUd5uZuodhiE4Kr6M7Eyv",
66+
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6I",
67+
"token_type": "Bearer",
68+
"expires_in": 300
69+
});
70+
cy.wait("@tokens");
71+
cy.getEditor().should("be.visible");
72+
73+
cy.executeSQL("select current_user();");
74+
cy.getGridRow(0).should("contain", "user1");
75+
76+
cy.logout();
77+
});
78+
79+
it("should force authentication if token expired, and there is no refresh token", () => {
80+
interceptAuthorizationCodeRequest(`${baseUrl}?code=abcdefgh`);
81+
cy.getByDataHook("button-sso-login").click();
82+
cy.wait("@authorizationCode");
83+
84+
interceptTokenRequest({
85+
"access_token": "gslpJtzmmi6RwaPSx0dYGD4tEkom",
86+
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6I",
87+
"token_type": "Bearer",
88+
"expires_in": 0
89+
});
90+
cy.wait("@tokens");
91+
cy.getEditor().should("be.visible");
92+
93+
cy.reload();
94+
cy.getByDataHook("button-log-in").should("be.visible");
95+
96+
cy.getByDataHook("button-log-in").click()
97+
cy.getEditor().should("be.visible");
98+
});
99+
});

packages/browser-tests/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"scripts": {
66
"test": "cypress run --env failOnSnapshotDiff=false --env requireSnapshots=false --spec 'cypress/integration/console/*.spec.js'",
77
"test:auth": "cypress run --env failOnSnapshotDiff=false --env requireSnapshots=false --spec 'cypress/integration/auth/*.spec.js'",
8+
"test:enterprise": "cypress run --env failOnSnapshotDiff=false --env requireSnapshots=false --spec 'cypress/integration/enterprise/*.spec.js'",
89
"test:update": "yarn run test --env updateSnapshots=true"
910
},
1011
"devDependencies": {

packages/browser-tests/questdb

Submodule questdb updated 146 files

packages/web-console/CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ and this project adheres to
1616
- `Fixed` for any bug fixes.
1717
- `Security` in case of vulnerabilities.
1818

19+
## 0.7.5 - 2025.02.13
20+
21+
### Fixed
22+
23+
- Fix floating-point regex capturing words beginning with 'E' [#394](https://github.com/questdb/ui/pull/394)
24+
- Prevent login loop when token expires without a valid refresh token [#395](https://github.com/questdb/ui/pull/395)
25+
1926
## 0.7.4 - 2025.02.05
2027

2128
### Added

packages/web-console/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@questdb/web-console",
3-
"version": "0.7.4",
3+
"version": "0.7.5",
44
"license": "Apache-2.0",
55
"description": "QuestDB Console",
66
"files": [

packages/web-console/serve-dist.js

+10
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ const server = http.createServer((req, res) => {
3232
})
3333

3434
req.pipe(proxyReq, { end: true })
35+
} else if (
36+
reqPathName.startsWith("/userinfo")
37+
) {
38+
res.writeHead(200, { 'Content-Type': 'application/json' })
39+
// TODO: should be able to set the response from the test
40+
// add something like /setUserInfo?info={sub: "user2", groups: "bla"}
41+
res.end(JSON.stringify({
42+
sub: "user1",
43+
groups: ["group1", "group2"]
44+
}))
3545
} else {
3646
// serve static files from /dist folder
3747
const filePath = path.join(

packages/web-console/src/components/TopBar/toolbar.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ export const Toolbar = () => {
143143
onClick={() => logout()}
144144
prefixIcon={<LogoutCircle size="18px" />}
145145
skin="secondary"
146+
data-hook="button-logout"
146147
>
147148
Log out
148149
</Button>

packages/web-console/src/modules/OAuth2/views/error.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const Error = ({
1818
<Text color="foreground">{errorMessage}</Text>
1919
{!basicAuthEnabled && (
2020
<Button
21+
data-hook="button-login-with-other-account"
2122
skin="secondary"
2223
prefixIcon={<User size="18px" />}
2324
onClick={() => onLogout()}

packages/web-console/src/modules/OAuth2/views/login.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,10 @@ export const Login = ({
308308
validationSchema={schema}
309309
>
310310
<Form.Item name="username" label="User name">
311-
<Form.Input name="username" placeholder={"johndoe"} />
311+
<Form.Input
312+
name="username"
313+
placeholder={"johndoe"}
314+
/>
312315
</Form.Item>
313316
<Form.Item name="password" label="Password">
314317
<Form.Input

packages/web-console/src/modules/OAuth2/views/logout.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const Logout = ({ onLogout }: { onLogout: () => void }) => (
88
<Box gap="1rem">
99
<Text color="foreground">You have been logged out.</Text>
1010
<Button
11+
data-hook="button-log-in"
1112
prefixIcon={<LoginCircle size={18} />}
1213
skin="secondary"
1314
onClick={onLogout}

packages/web-console/src/providers/AuthProvider.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
180180
await refreshAuthToken(settings)
181181
} else {
182182
// if there is no refresh token, user has to re-authenticate to get fresh tokens
183-
dispatch({ view: View.loggedOut })
183+
logout(true)
184184
}
185185
} else {
186186
setSessionData(tokenResponse)
@@ -193,7 +193,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
193193
removeValue(StoreKey.OAUTH_STATE)
194194
const stateParam = urlParams.get("state")
195195
if (!stateParam || state !== stateParam) {
196-
dispatch({ view: View.loggedOut })
196+
logout(true)
197197
return
198198
}
199199
}

run_browser_tests.sh

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ yarn workspace browser-tests test:auth
4343

4444
read -p "Press any key to continue... " -n1 -s
4545

46-
# Switch authentication on
46+
# Switch OSS authentication on
4747
export QDB_HTTP_USER=admin
4848
export QDB_HTTP_PASSWORD=quest
4949

50-
# Running tests which assume authentication is on
50+
# Running tests which assume that OSS authentication is on
5151
./tmp/questdb-*/bin/questdb.sh start -d tmp/dbroot
5252
yarn workspace browser-tests test
5353
./tmp/questdb-*/bin/questdb.sh stop

run_ent_browser_tests.sh

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/bin/bash -x
2+
3+
# Run it from the 'ui' directory as:
4+
# ./run_ent_browser_tests.sh
5+
6+
# Cleanup
7+
rm -rf packages/browser-tests/cypress/snapshots/*
8+
rm -rf tmp/dbroot
9+
rm -rf tmp/questdb-*
10+
11+
# Clone questdb-enterprise
12+
git clone https://github.com/questdb/questdb-enterprise.git tmp/questdb-enterprise
13+
cd tmp/questdb-enterprise || exit 1
14+
git submodule init
15+
git submodule update
16+
cd ../..
17+
18+
# Build server
19+
mvn clean package -e -f tmp/questdb-enterprise/pom.xml -DskipTests -P build-ent-binaries 2>&1
20+
21+
# Unpack server
22+
tar xzf tmp/questdb-enterprise/questdb-ent/target/questdb-enterprise-*-rt-*.tar.gz -C tmp/
23+
mkdir tmp/dbroot
24+
25+
# Build web console
26+
yarn install --immutable --immutable-cache
27+
yarn workspace @questdb/react-components run build
28+
yarn workspace @questdb/web-console run build
29+
30+
# Start proxy
31+
node packages/web-console/serve-dist.js &
32+
PID1="$!"
33+
echo "Proxy started, PID=$PID1"
34+
35+
# Switch dev mode on
36+
export QDB_DEV_MODE_ENABLED=true
37+
38+
# OIDC config
39+
export QDB_ACL_OIDC_ENABLED=true
40+
export QDB_ACL_OIDC_TLS_ENABLED=false
41+
export QDB_ACL_OIDC_GROUPS_CLAIM=groups
42+
export QDB_ACL_OIDC_CLIENT_ID=clientId
43+
export QDB_ACL_OIDC_HOST=localhost
44+
export QDB_ACL_OIDC_PORT=9999
45+
export QDB_ACL_OIDC_USERINFO_ENDPOINT=/userinfo
46+
47+
# Running tests which assume authentication is off
48+
./tmp/questdb-*/bin/questdb.sh start -d tmp/dbroot
49+
yarn workspace browser-tests test:enterprise
50+
./tmp/questdb-*/bin/questdb.sh stop
51+
52+
# Stop proxy
53+
kill -SIGTERM $PID1

0 commit comments

Comments
 (0)