Skip to content

Commit 15ad2d6

Browse files
authored
[Scout] Add Browser Authentication article (#243360)
This PR adds a new article on browser authentication to our collection of Scout articles meant to be consumed by an LLM via the FTR-to-Scout AI-assisted [guide](https://github.com/elastic/kibana/tree/main/src/platform/packages/private/kbn-scout-info/llms).
1 parent 4e7c4f9 commit 15ad2d6

File tree

2 files changed

+237
-0
lines changed

2 files changed

+237
-0
lines changed

src/platform/packages/private/kbn-scout-info/llms/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Instructions:
6363
@src/platform/packages/private/kbn-scout-info/llms/generate-scout-page-objects.md contains instructions on how to perform this task
6464
@src/platform/packages/private/kbn-scout-info/llms/what-is-scout.md contains a high-level description of the Scout framework
6565
@src/platform/packages/private/kbn-scout-info/llms/scout-page-objects.md contains a high-level overview of page objects in Scout
66+
@src/platform/packages/private/kbn-scout-info/llms/scout-browser-auth.md contains information on how browser authentication works in Scout
6667
```
6768

6869
**Checkpoint**: the AI will generate or modify Scout page objects. For this step, you may need to manually move these files to the correct directory (either the plugin's page objects folder, or one of the Scout packages), and register them in the `pageObjects` fixture. Refer to the official Scout documentation.
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# Browser authentication in Scout
2+
3+
Scout uses [SAML](https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/saml) as a unified authentication protocol across all deployment types. In this article we'll explore how to authenticate your Playwright tests using the `browserAuth` fixture.
4+
5+
## Log in with the `browserAuth` fixture
6+
7+
The `browserAuth` fixture provides convenient methods to authenticate with different user roles in your tests, regardless of where your tests run:
8+
9+
| Method | Description |
10+
| :-------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------- |
11+
| `loginAsAdmin()` | Logs in with the `admin` role (full Kibana and Elasticsearch access). |
12+
| `loginAsPrivilegedUser()` | Logs in with a privileged non-admin role (`editor`, except for Elasticsearch projects that use the `developer` role). |
13+
| `loginAsViewer()` | Logs in with the `viewer` role (read-only permissions). |
14+
| `loginAs(role: string)` | Logs in a specific built-in role by name. The role must exist in the deployment's role configuration, otherwise an error is thrown. |
15+
| `loginWithCustomRole(role: KibanaRole)` | Creates a custom role with the specified Kibana and Elasticsearch privileges, and then logs in with that role. |
16+
17+
These authentication methods are **asynchronous** and must be awaited.
18+
19+
**Basic usage example:**
20+
21+
```ts
22+
import { expect, tags } from '@kbn/scout';
23+
import { test } from '../fixtures';
24+
25+
// ...
26+
27+
test.describe('My sample test suite', { tag: tags.DEPLOYMENT_AGNOSTIC }, () => {
28+
test.beforeEach(async ({ browserAuth, pageObjects }) => {
29+
// [1]
30+
// log in as an admin
31+
await browserAuth.loginAsAdmin(); // [2]
32+
// ...
33+
});
34+
35+
test('my sample test 1', async ({ pageObjects }) => {
36+
// the browser is already authenticated as admin here
37+
// ...
38+
});
39+
40+
test('my sample test 2', async ({ pageObjects }) => {
41+
// the browser is already authenticated as admin here
42+
// ...
43+
});
44+
});
45+
```
46+
47+
1. We first unpack the **`browserAuth`** fixture from the test context.
48+
2. We then log in with the **`admin`** role before each test in the **`beforeEach`** hook.
49+
50+
> **SAML authentication: local deployment vs Elastic Cloud**
51+
>
52+
> When running tests in **local** stateful and serverless environments, Scout creates **on-demand** SAML identities using a trusted **mock Identity Provider (IdP)**, bypassing the need for pre-provisioned users.
53+
>
54+
> However, when running tests on Elastic Cloud, Scout authenticates with **real Elastic Cloud accounts** using credentials from the `<KIBANA_ROOT>/.ftr/role_users.json` or `<KIBANA_ROOT>/.scout/role_users.json` files through the actual cloud Identity Provider.
55+
56+
### Logging in with a custom role
57+
58+
You can log in with a custom role with the `loginWithCustomRole()` method. This is ideal for testing specific permission sets in your plugin:
59+
60+
```ts
61+
test.describe(
62+
'Discover app with a restricted read-only role',
63+
{ tag: tags.DEPLOYMENT_AGNOSTIC },
64+
() => {
65+
test.beforeAll(async () => {
66+
// load test data
67+
// ...
68+
});
69+
70+
test.beforeEach(async ({ browserAuth, pageObjects }) => {
71+
// log in with custom role
72+
await browserAuth.loginWithCustomRole({
73+
kibana: [
74+
{
75+
base: [], // [1]
76+
feature: {
77+
discover: ['read'],
78+
},
79+
spaces: ['*'],
80+
},
81+
],
82+
elasticsearch: {
83+
cluster: [], // [2]
84+
indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], // [3]
85+
},
86+
});
87+
});
88+
89+
test('should display a disabled save button', async ({ browserAuth }) => {
90+
// navigate to Discover and verify read-only access
91+
// ...
92+
});
93+
}
94+
);
95+
```
96+
97+
1. For testing specific features, use `base: []` and explicit `feature` definitions.
98+
2. We leave this empty as no cluster privileges are needed for this test.
99+
3. We define the minimum index privileges needed to access Discover.
100+
101+
> **Warning: Scout vs FTR**
102+
>
103+
> Scout's `loginWithCustomRole()` method internally uses similar role creation logic to FTR's `samlAuth.setCustomRole()` method, but wraps it with **automatic authentication** and **cleanup**, making it a one-step solution for Playwright tests compared to FTR's multi-step approach.
104+
105+
> **What happens behind the scenes?**
106+
>
107+
> When you call `loginWithCustomRole()`, Scout first creates a uniquely named role in Elasticsearch for each Playwright worker (`custom_role_worker_1`, `custom_role_worker_2`, etc.). If your tests run sequentially, Scout will create just one role named `custom_role_worker_1`.
108+
>
109+
> Scout then:
110+
>
111+
> 1. Creates an authenticated SAML session for the custom role
112+
> 2. Caches the session for performance (subsequent calls with the same role definition reuse the cached session)
113+
> 3. Sets the session cookie in your browser context
114+
> 4. Automatically deletes the custom role after test completion (no matter if the test passed or failed)
115+
>
116+
> If you call `loginWithCustomRole()` again with a different role definition in the same test, Scout **recreates** the role with the new privileges and authenticates with a fresh session.
117+
118+
### Predefined roles vs custom roles
119+
120+
Let's compare **predefined roles** with **custom roles**:
121+
122+
- **Predefined roles** are built-in Elastic roles available to all customers out-of-the-box (like `admin`, `editor`, `viewer`). These work with `loginAs()` and are defined in the appropriate `roles.yml` file. Here's a [list](https://www.elastic.co/docs/deploy-manage/users-roles/cloud-organization/user-roles) for Elastic Cloud.
123+
- **Custom roles** are roles you create specifically for testing with particular permission sets. These require `loginWithCustomRole()` instead.
124+
125+
> **Warning: Scout vs FTR**
126+
>
127+
> **FTR** allows defining custom roles in the test config file under `security.roles`. **Scout** takes a different approach: custom roles are created dynamically using `loginWithCustomRole()`.
128+
129+
### Extending the `browserAuth` fixture
130+
131+
Using `loginWithCustomRole()` works well for one-off cases, but if you need to log in with the same custom role across multiple tests, it's more convenient to extend the `browserAuth` fixture with solution-specific or plugin-specific authentication methods.
132+
133+
#### Solution-scoped extension
134+
135+
The `@kbn/scout-security` package [extends](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/packages/kbn-scout-security/src/playwright/fixtures/test/browser_auth/index.ts) the `browserAuth` fixture to expose Security-specific authentication methods (e.g. `loginAsPlatformEngineer()`):
136+
137+
```ts
138+
// loginAsPlatformEngineer() is available in all tests importing @kbn/scout-security
139+
test.beforeEach(async ({ browserAuth }) => {
140+
await browserAuth.loginAsPlatformEngineer();
141+
});
142+
```
143+
144+
All Security tests importing `@kbn/scout-security` can now use this method to log in as a Platform Engineer without redefining the custom role in each test.
145+
146+
#### Plugin-scoped extension
147+
148+
A plugin can also extend the `browserAuth` fixture by creating a custom fixture in the plugin's `test/scout/ui/fixtures` directory. For example, the Observability APM plugin [exposes](https://github.com/elastic/kibana/blob/main/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/index.ts#L82-L99) `loginAsApmMonitor()`:
149+
150+
```ts
151+
// loginAsApmMonitor() is available in all tests importing the plugin fixture
152+
test.beforeEach(async ({ browserAuth }) => {
153+
await browserAuth.loginAsApmMonitor();
154+
});
155+
```
156+
157+
Create a custom fixture that extends `browserAuth` in your plugin's test directory:
158+
159+
```ts
160+
// x-pack/solutions/<solution>/plugins/<private or shared>/my-plugin/test/scout/ui/fixtures/index.ts
161+
162+
import { scoutTestFixtures } from '@kbn/scout'; // [1]
163+
import type { BrowserAuthFixture } from '@kbn/scout'; // [1]
164+
165+
interface MyPluginAuthFixture extends BrowserAuthFixture {
166+
loginAsMyPluginUser: () => Promise<void>;
167+
}
168+
169+
export const fixtures = scoutTestFixtures.extend<{ browserAuth: MyPluginAuthFixture }>({
170+
browserAuth: async ({ browserAuth }, use) => {
171+
const loginAsMyPluginUser = async () =>
172+
browserAuth.loginWithCustomRole({
173+
// update as necessary
174+
elasticsearch: {
175+
cluster: ['monitor'],
176+
indices: [{ names: ['my-plugin-*'], privileges: ['read', 'write'] }],
177+
},
178+
// update as necessary
179+
kibana: [
180+
{
181+
feature: { myPlugin: ['all'] },
182+
spaces: ['*'],
183+
},
184+
],
185+
});
186+
187+
// make the new method available via the browserAuth fixture
188+
await use({
189+
...browserAuth,
190+
loginAsMyPluginUser,
191+
});
192+
},
193+
});
194+
```
195+
196+
1. Import from `@kbn/scout-oblt` or from `@kbn/scout-security` if your plugin belongs to a specific solution.
197+
198+
Then use it in your tests:
199+
200+
```ts
201+
import { expect, test } from '../fixtures';
202+
203+
test('my plugin feature works', async ({ browserAuth, page }) => {
204+
await browserAuth.loginAsMyPluginUser();
205+
await page.goto('/app/my-plugin');
206+
// Your test logic
207+
});
208+
```
209+
210+
## Best practices
211+
212+
#### Use deployment-agnostic methods
213+
214+
Scout tests are designed to be deployment agnostic by default. Prefer `loginAsPrivilegedUser()` over `loginAs('editor')` when writing deployment-agnostic tests, as it automatically selects the appropriate role for the deployment type.
215+
216+
#### Keep custom roles minimal
217+
218+
Only grant the **minimum privileges** needed for your test. This makes tests more realistic and helps catch permission-related bugs.
219+
220+
#### Reuse custom roles via fixtures
221+
222+
If multiple tests need the same custom role, extend the `browserAuth` fixture instead of repeating `loginWithCustomRole()` calls.
223+
224+
#### Test permission boundaries
225+
226+
Use custom roles to explicitly test both what users **can** and **cannot** do:
227+
228+
```ts
229+
test('viewer cannot save dashboards', async ({ browserAuth, page }) => {
230+
await browserAuth.loginAsViewer();
231+
await page.gotoApp('dashboards');
232+
233+
// Verify save button is disabled
234+
await expect(page.testSubj.locator('dashboardSaveButton')).toBeDisabled();
235+
});
236+
```

0 commit comments

Comments
 (0)