Skip to content

Commit 132efc4

Browse files
committed
Merge remote-tracking branch 'upstream/main' into fix/remove-multipage
2 parents 8a1f43e + 5a40a8e commit 132efc4

48 files changed

Lines changed: 1884 additions & 641 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2094,6 +2094,8 @@ x-pack/platform/plugins/shared/streams/common/sig_events_tuning_config.ts @elast
20942094
/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/geo_containment @elastic/kibana-presentation
20952095
/x-pack/platform/plugins/shared/stack_alerts/public/rule_types/geo_containment @elastic/kibana-presentation
20962096

2097+
# Visualization
2098+
/x-pack/platform/test/functional/services/lens.ts @elastic/kibana-visualization
20972099

20982100
# Machine Learning
20992101
/x-pack/platform/test/stack_functional_integration/apps/ml @elastic/ml-ui

src/platform/packages/private/kbn-journeys/services/auth.ts

Lines changed: 14 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -8,87 +8,35 @@
88
*/
99

1010
import Url from 'url';
11-
import { format } from 'util';
1211

12+
import { fetchSessionCookie } from '@kbn/ftr-common-functional-services';
1313
import { FtrService } from './ftr_context_provider';
1414

1515
export interface Credentials {
1616
username: string;
1717
password: string;
1818
}
1919

20-
function extractCookieValue(headers: Headers) {
21-
// Headers.getSetCookie() is the Node 22 / undici API; fall back to Headers.get for
22-
// older runtimes (jsdom in the test config).
23-
const headersWithGetSetCookie = headers as Headers & { getSetCookie?: () => string[] };
24-
const firstSetCookie = headersWithGetSetCookie.getSetCookie
25-
? headersWithGetSetCookie.getSetCookie()[0]
26-
: headers.get('set-cookie');
27-
return firstSetCookie?.split(';')[0].split('sid=')[1] ?? '';
28-
}
2920
export class AuthService extends FtrService {
3021
private readonly config = this.ctx.getService('config');
3122
private readonly log = this.ctx.getService('log');
3223
private readonly kibanaServer = this.ctx.getService('kibanaServer');
3324

3425
public async login(credentials?: Credentials) {
35-
const baseUrl = new URL(
36-
Url.format({
37-
protocol: this.config.get('servers.kibana.protocol'),
38-
hostname: this.config.get('servers.kibana.hostname'),
39-
port: this.config.get('servers.kibana.port'),
40-
})
41-
);
42-
const loginUrl = new URL('/internal/security/login', baseUrl);
43-
const provider = this.isCloud() ? 'cloud-basic' : 'basic';
44-
45-
const version = await this.kibanaServer.version.get();
46-
47-
this.log.info('fetching auth cookie from', loginUrl.href);
48-
const authResponse = await fetch(loginUrl, {
49-
method: 'POST',
50-
headers: {
51-
'content-type': 'application/json',
52-
'kbn-version': version,
53-
'sec-fetch-mode': 'cors',
54-
'sec-fetch-site': 'same-origin',
55-
'x-elastic-internal-origin': 'Kibana',
56-
},
57-
body: JSON.stringify({
58-
providerType: 'basic',
59-
providerName: provider,
60-
currentURL: new URL('/login?next=%2F', baseUrl).href,
61-
params: credentials ?? { username: this.getUsername(), password: this.getPassword() },
62-
}),
63-
redirect: 'manual',
26+
const baseUrl = Url.format({
27+
protocol: this.config.get('servers.kibana.protocol'),
28+
hostname: this.config.get('servers.kibana.hostname'),
29+
port: this.config.get('servers.kibana.port'),
6430
});
65-
66-
if (authResponse.status !== 200) {
67-
throw new Error(
68-
`Kibana auth failed: code: ${authResponse.status}, message: ${authResponse.statusText}`
69-
);
70-
}
71-
72-
const cookie = extractCookieValue(authResponse.headers);
73-
if (cookie) {
74-
this.log.info('captured auth cookie');
75-
} else {
76-
this.log.error(
77-
format('unable to determine auth cookie from response', {
78-
status: `${authResponse.status} ${authResponse.statusText}`,
79-
body: await authResponse.text(),
80-
headers: Object.fromEntries(authResponse.headers.entries()),
81-
})
82-
);
83-
84-
throw new Error(`failed to determine auth cookie`);
85-
}
86-
87-
return {
88-
name: 'sid',
89-
value: cookie,
90-
url: baseUrl.href,
91-
};
31+
const provider = this.isCloud() ? 'cloud-basic' : 'basic';
32+
const kbnVersion = await this.kibanaServer.version.get();
33+
const username = credentials?.username ?? this.getUsername();
34+
const password = credentials?.password ?? this.getPassword();
35+
36+
this.log.info(`fetching auth cookie from ${baseUrl}/internal/security/login`);
37+
const cookie = await fetchSessionCookie({ baseUrl, username, password, kbnVersion, provider });
38+
this.log.info('captured auth cookie');
39+
return cookie;
9240
}
9341

9442
public getUsername() {

src/platform/packages/shared/kbn-ftr-common-functional-services/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,5 @@ export { RandomnessService } from './services/randomness';
4747
export { KibanaSupertestProvider, ElasticsearchSupertestProvider } from './services/supertest';
4848
export { retryForSuccess } from './services/retry/retry_for_success';
4949
export { SecurityService } from './services/security';
50+
export { fetchSessionCookie } from './services/cookie_auth';
51+
export type { SessionCookie, FetchSessionCookieParams } from './services/cookie_auth';

src/platform/packages/shared/kbn-ftr-common-functional-services/services/all.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { SupertestWithoutAuthProvider } from './supertest_without_auth';
2222
import { SamlAuthProvider } from './saml_auth';
2323
import { KibanaSupertestProvider, ElasticsearchSupertestProvider } from './supertest';
2424
import { SecurityServiceProvider } from './security';
25+
import { CookieAuthService } from './cookie_auth';
26+
import { BrowserAuthService } from './browser_auth';
2527

2628
export const services = {
2729
es: EsProvider,
@@ -40,4 +42,6 @@ export const services = {
4042
esSupertest: ElasticsearchSupertestProvider,
4143
supertestWithoutAuth: SupertestWithoutAuthProvider,
4244
security: SecurityServiceProvider,
45+
cookieAuth: CookieAuthService,
46+
browserAuth: BrowserAuthService,
4347
};
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { FtrService } from '../ftr_provider_context';
11+
import type { Cookie } from '../cookie_auth';
12+
13+
export interface LoginByCookieOptions {
14+
/** When provided, verifies GET /internal/security/me returns this username. */
15+
expectedUsername?: string;
16+
/** Whether to assert the userMenuButton test-subject is rendered after login. Defaults to true. */
17+
expectUserMenuButton?: boolean;
18+
/**
19+
* Where to navigate after injecting the cookie. Defaults to the Kibana root (`/`).
20+
* Pass the actual target app URL to avoid a second navigation in the caller.
21+
*/
22+
targetUrl?: string;
23+
}
24+
25+
/**
26+
* Browser-side cookie injection service.
27+
*
28+
* Mirrors the pattern used by the serverless `svl_common_page.loginWithRole()`:
29+
* 1. Navigate to /bootstrap-anonymous.js (no-auth endpoint to enter the Kibana origin)
30+
* 2. Clean all browser state (cookies, session storage, local storage)
31+
* 3. Set the `sid` cookie
32+
* 4. Navigate to Kibana home and optionally verify identity
33+
*
34+
* Consumed by SecurityPageObject.login() and TestUser.setRoles() when
35+
* `security.cookieLogin: true` is set in the FTR config.
36+
*
37+
* Note: `browser` and `testSubjects` are not part of the common FTR provider context type —
38+
* they come from @kbn/ftr-common-functional-ui-services, which functional test configs spread
39+
* on top. We use `hasService` + `as any` (same pattern as TestUser) to access them safely.
40+
*/
41+
export class BrowserAuthService extends FtrService {
42+
private readonly log = this.ctx.getService('log');
43+
private readonly deployment = this.ctx.getService('deployment');
44+
private readonly supertestWithoutAuth = this.ctx.getService('supertestWithoutAuth');
45+
46+
// Browser-specific services — only present in functional (UI) test contexts.
47+
48+
private readonly browser: any = this.ctx.hasService('browser')
49+
? this.ctx.getService('browser' as any)
50+
: undefined;
51+
52+
private readonly testSubjects: any = this.ctx.hasService('testSubjects')
53+
? this.ctx.getService('testSubjects' as any)
54+
: undefined;
55+
56+
/**
57+
* Clears all browser state (cookies, session storage, local storage).
58+
*
59+
* Navigates to `/bootstrap-anonymous.js` first only when the browser is not already on the
60+
* Kibana origin — e.g. when starting from `about:blank`. If the browser is already on a
61+
* Kibana page (e.g. `/login` after a redirect) the extra navigation is skipped.
62+
*/
63+
async cleanBrowserState(): Promise<void> {
64+
if (!this.browser) {
65+
throw new Error(
66+
'[browserAuth] browser service is not available — ' +
67+
'cleanBrowserState() can only be called from functional UI test configs.'
68+
);
69+
}
70+
71+
const hostPort = this.deployment.getHostPort();
72+
const currentUrl: string = await this.browser.getCurrentUrl();
73+
74+
if (!currentUrl.startsWith(hostPort)) {
75+
// Not on the Kibana origin yet — navigate to a lightweight no-auth endpoint
76+
// so cookie operations target the correct domain.
77+
this.log.debug('[browserAuth] navigating to /bootstrap-anonymous.js for cookie domain');
78+
await this.browser.get(hostPort + '/bootstrap-anonymous.js');
79+
const alert = await this.browser.getAlert();
80+
if (alert) await alert.accept();
81+
}
82+
83+
this.log.debug('[browserAuth] deleting all cookies and clearing storage');
84+
await this.browser.deleteAllCookies();
85+
await this.browser.clearSessionStorage();
86+
await this.browser.clearLocalStorage();
87+
}
88+
89+
/**
90+
* Injects a Kibana `sid` session cookie into the browser context and navigates to
91+
* `options.targetUrl` (defaults to Kibana root `/`), verifying login succeeded.
92+
*
93+
* Pass `targetUrl` equal to the final app URL to avoid a second navigation in the caller.
94+
*/
95+
async loginByCookie(cookie: Cookie, options: LoginByCookieOptions = {}): Promise<void> {
96+
if (!this.browser || !this.testSubjects) {
97+
throw new Error(
98+
'[browserAuth] browser services are not available — ' +
99+
'loginByCookie() can only be called from functional UI test configs.'
100+
);
101+
}
102+
103+
const { expectUserMenuButton = true, targetUrl } = options;
104+
105+
await this.cleanBrowserState();
106+
107+
// cleanBrowserState() wipes localStorage. Restore the flag that suppresses the
108+
// welcome screen so it does not overlay the UI after the first navigation.
109+
await this.browser.setLocalStorageItem('home:welcome:show', 'false');
110+
111+
this.log.debug(`[browserAuth] injecting 'sid' cookie`);
112+
await this.browser.setCookie('sid', cookie.value);
113+
114+
const destination = targetUrl ?? this.deployment.getHostPort();
115+
this.log.debug(`[browserAuth] navigating to ${destination}`);
116+
await this.browser.get(destination);
117+
118+
if (options.expectedUsername) {
119+
const cookies = await this.browser.getCookies();
120+
const sidValue = (cookies as Array<{ name: string; value: string }>).find(
121+
(c) => c.name === 'sid'
122+
)?.value;
123+
124+
if (sidValue) {
125+
const { body } = await this.supertestWithoutAuth
126+
.get('/internal/security/me')
127+
.set('kbn-xsrf', 'xxx')
128+
.set('Cookie', `sid=${sidValue}`)
129+
.expect(200);
130+
131+
if (body.username !== options.expectedUsername) {
132+
throw new Error(
133+
`[browserAuth] expected username '${options.expectedUsername}' but got '${body.username}'`
134+
);
135+
}
136+
this.log.debug(`[browserAuth] verified session for '${options.expectedUsername}'`);
137+
}
138+
}
139+
140+
if (expectUserMenuButton) {
141+
await this.testSubjects.existOrFail('userMenuButton', { timeout: 10_000 });
142+
this.log.debug('[browserAuth] login confirmed via userMenuButton');
143+
}
144+
}
145+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
export { BrowserAuthService } from './browser_auth_service';
11+
export type { LoginByCookieOptions } from './browser_auth_service';
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import Url from 'url';
11+
import { FtrService } from '../ftr_provider_context';
12+
import { fetchSessionCookie } from './fetch_session_cookie';
13+
14+
export type { SessionCookie as Cookie } from './fetch_session_cookie';
15+
16+
const TEST_USER_NAME = 'test_user';
17+
const TEST_USER_PASSWORD = 'changeme';
18+
19+
/**
20+
* Generates a Kibana session cookie (`sid`) by posting to the internal basic-auth login endpoint.
21+
* Works for both local (`basic` provider) and Cloud (`cloud-basic` provider) deployments.
22+
*
23+
* This is purely HTTP — no browser dependency. Consumed by BrowserAuthService (browser-side
24+
* cookie injection) and directly by API-level test helpers.
25+
*/
26+
export class CookieAuthService extends FtrService {
27+
private readonly config = this.ctx.getService('config');
28+
private readonly log = this.ctx.getService('log');
29+
private readonly kibanaServer = this.ctx.getService('kibanaServer');
30+
31+
private getBaseUrl(): string {
32+
return Url.format({
33+
protocol: this.config.get('servers.kibana.protocol'),
34+
hostname: this.config.get('servers.kibana.hostname'),
35+
port: this.config.get('servers.kibana.port'),
36+
});
37+
}
38+
39+
isCloud(): boolean {
40+
return this.config.get('servers.kibana.hostname') !== 'localhost';
41+
}
42+
43+
async getCookieForUser(username: string, password: string) {
44+
const baseUrl = this.getBaseUrl();
45+
const provider = this.isCloud() ? 'cloud-basic' : 'basic';
46+
const kbnVersion = await this.kibanaServer.version.get();
47+
48+
this.log.info(
49+
`[cookieAuth] fetching auth cookie for '${username}' from ${baseUrl}/internal/security/login`
50+
);
51+
const cookie = await fetchSessionCookie({ baseUrl, username, password, kbnVersion, provider });
52+
this.log.info(`[cookieAuth] captured auth cookie for '${username}'`);
53+
return cookie;
54+
}
55+
56+
/** Cookie for the shared FTR test_user (username: test_user, password: changeme). */
57+
async getCookieForTestUser() {
58+
return this.getCookieForUser(TEST_USER_NAME, TEST_USER_PASSWORD);
59+
}
60+
61+
/** Cookie for the Kibana server user configured in `servers.kibana.username/password`. */
62+
async getCookieForKibanaUser() {
63+
return this.getCookieForUser(
64+
this.config.get('servers.kibana.username'),
65+
this.config.get('servers.kibana.password')
66+
);
67+
}
68+
}

0 commit comments

Comments
 (0)