mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[scout] support login with a custom role (#213798)
## Summary Adding custom roles support in Scout UI tests Example: ``` test.beforeEach(async ({ browserAuth, pageObjects }) => { await browserAuth.loginWithCustomRole({ elasticsearch: { cluster: ['manage'], indices: [ { names: ['.siem-signals*', '.lists-*', '.items-*'], privileges: ['read', 'view_index_metadata'], allow_restricted_indices: false, }, { names: ['.alerts*', '.preview.alerts*'], privileges: ['read', 'view_index_metadata'], allow_restricted_indices: false, }, ], }, kibana: [ { base: [], feature: { siemV2: ['read', 'read_alerts'], }, spaces: ['*'], }, ], }); await pageObjects.dashboard.goto(); ``` In `kbn/scout-security` to login as `platform_engineer` we will need to override browser auth fixture with smth like: ```ts const resourcePath = path.resolve(SERVERLESS_ROLES_ROOT_PATH, 'security', 'roles.yml'); const svlRoleDescriptors = new Map<string, any>( Object.entries(readRolesDescriptorsFromResource(resourcePath) as Record<string, unknown>) ); const loginAsPlatformEngineer = async () => { const roleName = 'platform_engineer'; if (!serverless) { const roleDesciptor = svlRoleDescriptors?.get(roleName) as ElasticsearchRoleDescriptor; if (!roleDesciptor) { throw new Error(`No role descriptors found for ${roleName}`); } await samlAuth.setCustomRole(roleDesciptor); return loginAs(samlAuth.customRoleName); } else { await loginAs(roleName); } } ``` This way we gonna use custom role to replicate serverless default roles in Stateful run (and support deployment agnostic approach) --------- Co-authored-by: Cesare de Cal <cesare.decal@elastic.co>
This commit is contained in:
parent
c6b594cfee
commit
ef32357d80
10 changed files with 178 additions and 14 deletions
|
@ -37,6 +37,8 @@ export type {
|
|||
ScoutLogger,
|
||||
ScoutServerConfig,
|
||||
ScoutTestConfig,
|
||||
KibanaRole,
|
||||
ElasticsearchRoleDescriptor,
|
||||
} from './src/types';
|
||||
|
||||
// re-export from Playwright
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { EsClient, KbnClient } from '.';
|
||||
|
||||
export interface KibanaRole {
|
||||
elasticsearch: {
|
||||
cluster: string[];
|
||||
indices: Array<{
|
||||
names: string[];
|
||||
privileges: string[];
|
||||
allow_restricted_indices?: boolean | undefined;
|
||||
}>;
|
||||
};
|
||||
kibana: Array<{
|
||||
base: string[];
|
||||
feature: Record<string, string[]>;
|
||||
spaces: string[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ElasticsearchRoleDescriptor {
|
||||
cluster?: string[];
|
||||
indices?: Array<{
|
||||
names: string[];
|
||||
privileges: string[];
|
||||
allow_restricted_indices?: boolean;
|
||||
}>;
|
||||
applications?: Array<{
|
||||
application: string;
|
||||
privileges: string[];
|
||||
resources: string[];
|
||||
}>;
|
||||
run_as?: string[];
|
||||
}
|
||||
|
||||
export const createCustomRole = async (
|
||||
kbnClient: KbnClient,
|
||||
customRoleName: string,
|
||||
role: KibanaRole
|
||||
) => {
|
||||
const { status } = await kbnClient.request({
|
||||
method: 'PUT',
|
||||
path: `/api/security/role/${customRoleName}`,
|
||||
body: role,
|
||||
});
|
||||
|
||||
if (status !== 204) {
|
||||
throw new Error(`Failed to set custom role with status: ${status}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const createElasticsearchCustomRole = async (
|
||||
client: EsClient,
|
||||
customRoleName: string,
|
||||
role: ElasticsearchRoleDescriptor
|
||||
) => {
|
||||
await client.security.putRole({
|
||||
name: customRoleName,
|
||||
...role,
|
||||
});
|
||||
};
|
|
@ -19,3 +19,5 @@ export type { SamlSessionManager } from '@kbn/test';
|
|||
export type { ScoutLogger } from './logger';
|
||||
export type { KbnClient } from '@kbn/test';
|
||||
export type { Client as EsClient } from '@elastic/elasticsearch';
|
||||
export { createCustomRole, createElasticsearchCustomRole } from './custom_role';
|
||||
export type { ElasticsearchRoleDescriptor, KibanaRole } from './custom_role';
|
||||
|
|
|
@ -42,8 +42,9 @@ const createKibanaHostOptions = (config: ScoutTestConfig): HostOptions => {
|
|||
|
||||
export const createSamlSessionManager = (
|
||||
config: ScoutTestConfig,
|
||||
log: ScoutLogger
|
||||
): SamlSessionManager => {
|
||||
log: ScoutLogger,
|
||||
customRoleName?: string
|
||||
) => {
|
||||
const resourceDirPath = getResourceDirPath(config);
|
||||
const rolesDefinitionPath = path.resolve(resourceDirPath, 'roles.yml');
|
||||
|
||||
|
@ -51,7 +52,8 @@ export const createSamlSessionManager = (
|
|||
string,
|
||||
unknown
|
||||
>;
|
||||
const supportedRoles = Object.keys(supportedRoleDescriptors);
|
||||
|
||||
const supportedRoles = [...Object.keys(supportedRoleDescriptors)].concat(customRoleName || []);
|
||||
|
||||
const sessionManager = new SamlSessionManager({
|
||||
hostOptions: createKibanaHostOptions(config),
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { PROJECT_DEFAULT_ROLES } from '../../../../common';
|
||||
import { ElasticsearchRoleDescriptor, KibanaRole, PROJECT_DEFAULT_ROLES } from '../../../../common';
|
||||
import { coreWorkerFixtures } from '../../worker';
|
||||
|
||||
export type LoginFunction = (role: string) => Promise<void>;
|
||||
|
@ -28,6 +28,12 @@ export interface BrowserAuthFixture {
|
|||
* @returns A Promise that resolves once the cookie in browser is set.
|
||||
*/
|
||||
loginAsPrivilegedUser: () => Promise<void>;
|
||||
/**
|
||||
* Logs in as a user with a custom role.
|
||||
* @param role - A role object that defines the Kibana and ES previleges. Role will re-created if it doesn't exist.
|
||||
* @returns A Promise that resolves once the cookie in browser is set.
|
||||
*/
|
||||
loginWithCustomRole: (role: KibanaRole) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -36,7 +42,7 @@ export interface BrowserAuthFixture {
|
|||
* for the specified role and the "context" fixture to update the cookie with the role-scoped session.
|
||||
*/
|
||||
export const browserAuthFixture = coreWorkerFixtures.extend<{ browserAuth: BrowserAuthFixture }>({
|
||||
browserAuth: async ({ log, context, samlAuth, config, kbnUrl }, use) => {
|
||||
browserAuth: async ({ log, context, samlAuth, config, kbnUrl, esClient }, use) => {
|
||||
const setSessionCookie = async (cookieValue: string) => {
|
||||
await context.clearCookies();
|
||||
await context.addCookies([
|
||||
|
@ -49,11 +55,19 @@ export const browserAuthFixture = coreWorkerFixtures.extend<{ browserAuth: Brows
|
|||
]);
|
||||
};
|
||||
|
||||
const loginAs: LoginFunction = async (role) => {
|
||||
const cookie = await samlAuth.getInteractiveUserSessionCookieWithRoleScope(role);
|
||||
let isCustomRoleCreated = false;
|
||||
|
||||
const loginAs: LoginFunction = async (role: string) => {
|
||||
const cookie = await samlAuth.session.getInteractiveUserSessionCookieWithRoleScope(role);
|
||||
await setSessionCookie(cookie);
|
||||
};
|
||||
|
||||
const loginWithCustomRole = async (role: KibanaRole | ElasticsearchRoleDescriptor) => {
|
||||
await samlAuth.setCustomRole(role);
|
||||
isCustomRoleCreated = true;
|
||||
return loginAs(samlAuth.customRoleName);
|
||||
};
|
||||
|
||||
const loginAsAdmin = () => loginAs('admin');
|
||||
const loginAsViewer = () => loginAs('viewer');
|
||||
const loginAsPrivilegedUser = () => {
|
||||
|
@ -64,6 +78,16 @@ export const browserAuthFixture = coreWorkerFixtures.extend<{ browserAuth: Brows
|
|||
};
|
||||
|
||||
log.serviceLoaded('browserAuth');
|
||||
await use({ loginAsAdmin, loginAsViewer, loginAsPrivilegedUser });
|
||||
await use({
|
||||
loginAsAdmin,
|
||||
loginAsViewer,
|
||||
loginAsPrivilegedUser,
|
||||
loginWithCustomRole,
|
||||
});
|
||||
|
||||
if (isCustomRoleCreated) {
|
||||
log.debug(`Deleting custom role with name ${samlAuth.customRoleName}`);
|
||||
await esClient.security.deleteRole({ name: samlAuth.customRoleName });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -19,6 +19,10 @@ import {
|
|||
KibanaUrl,
|
||||
getLogger,
|
||||
ScoutLogger,
|
||||
createElasticsearchCustomRole,
|
||||
createCustomRole,
|
||||
ElasticsearchRoleDescriptor,
|
||||
KibanaRole,
|
||||
} from '../../../common/services';
|
||||
import type { ScoutTestOptions } from '../../types';
|
||||
import type { ScoutTestConfig } from '.';
|
||||
|
@ -30,6 +34,12 @@ export type { KibanaUrl } from '../../../common/services/kibana_url';
|
|||
export type { ScoutTestConfig } from '../../../types';
|
||||
export type { ScoutLogger } from '../../../common/services/logger';
|
||||
|
||||
export interface SamlAuth {
|
||||
session: SamlSessionManager;
|
||||
customRoleName: string;
|
||||
setCustomRole(role: KibanaRole | ElasticsearchRoleDescriptor): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The coreWorkerFixtures setup defines foundational fixtures that are essential
|
||||
* for running tests in the "kbn-scout" framework. These fixtures provide reusable,
|
||||
|
@ -45,7 +55,7 @@ export const coreWorkerFixtures = base.extend<
|
|||
kbnUrl: KibanaUrl;
|
||||
esClient: Client;
|
||||
kbnClient: KbnClient;
|
||||
samlAuth: SamlSessionManager;
|
||||
samlAuth: SamlAuth;
|
||||
}
|
||||
>({
|
||||
// Provides a scoped logger instance for each worker to use in fixtures and tests.
|
||||
|
@ -114,14 +124,41 @@ export const coreWorkerFixtures = base.extend<
|
|||
|
||||
/**
|
||||
* Creates a SAML session manager, that handles authentication tasks for tests involving
|
||||
* SAML-based authentication.
|
||||
* SAML-based authentication. Exposes a method to set a custom role for the session.
|
||||
*
|
||||
* Note: In order to speedup execution of tests, we cache the session cookies for each role
|
||||
* after first call.
|
||||
*/
|
||||
samlAuth: [
|
||||
({ log, config }, use) => {
|
||||
use(createSamlSessionManager(config, log));
|
||||
({ log, config, esClient, kbnClient }, use, workerInfo) => {
|
||||
let customRoleHash = '';
|
||||
const customRoleName = `custom_role_worker_${workerInfo.parallelIndex}`;
|
||||
const session = createSamlSessionManager(config, log, customRoleName);
|
||||
|
||||
const isCustomRoleSet = (roleHash: string) => roleHash === customRoleHash;
|
||||
|
||||
const isElasticsearchRole = (role: any): role is ElasticsearchRoleDescriptor => {
|
||||
return 'applications' in role;
|
||||
};
|
||||
|
||||
const setCustomRole = async (role: KibanaRole | ElasticsearchRoleDescriptor) => {
|
||||
const newRoleHash = JSON.stringify(role);
|
||||
|
||||
if (isCustomRoleSet(newRoleHash)) {
|
||||
log.info(`Custom role is already set`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isElasticsearchRole(role)) {
|
||||
await createElasticsearchCustomRole(esClient, customRoleName, role);
|
||||
} else {
|
||||
await createCustomRole(kbnClient, customRoleName, role);
|
||||
}
|
||||
|
||||
customRoleHash = newRoleHash;
|
||||
};
|
||||
|
||||
use({ session, customRoleName, setCustomRole });
|
||||
},
|
||||
{ scope: 'worker' },
|
||||
],
|
||||
|
|
|
@ -14,7 +14,7 @@ export type {
|
|||
KibanaUrl,
|
||||
EsClient,
|
||||
KbnClient,
|
||||
SamlSessionManager,
|
||||
SamlAuth,
|
||||
} from './core_fixtures';
|
||||
|
||||
export { esArchiverFixture } from './es_archiver';
|
||||
|
|
|
@ -7,4 +7,12 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type { EsClient, KbnClient, KibanaUrl, SamlSessionManager, ScoutLogger } from '../common';
|
||||
export type {
|
||||
EsClient,
|
||||
KbnClient,
|
||||
KibanaUrl,
|
||||
SamlSessionManager,
|
||||
ScoutLogger,
|
||||
KibanaRole,
|
||||
ElasticsearchRoleDescriptor,
|
||||
} from '../common';
|
||||
|
|
|
@ -320,6 +320,23 @@ describe('SamlSessionManager', () => {
|
|||
expect(email).toBe(cloudEmail);
|
||||
});
|
||||
|
||||
test(`'getSupportedRoles' return empty array when roles by default`, async () => {
|
||||
const samlSessionManager = new SamlSessionManager({
|
||||
...samlSessionManagerOptions,
|
||||
});
|
||||
const roles = samlSessionManager.getSupportedRoles();
|
||||
expect(roles).toEqual([]);
|
||||
});
|
||||
|
||||
test(`'getSupportedRoles' return the correct roles when roles were defined`, async () => {
|
||||
const samlSessionManager = new SamlSessionManager({
|
||||
...samlSessionManagerOptions,
|
||||
supportedRoles,
|
||||
});
|
||||
const roles = samlSessionManager.getSupportedRoles();
|
||||
expect(roles).toBe(supportedRoles.roles);
|
||||
});
|
||||
|
||||
test(`'getUserData' should call security API and return user profile data`, async () => {
|
||||
const testData: UserProfile = {
|
||||
username: '92qab123',
|
||||
|
|
|
@ -211,4 +211,8 @@ Set env variable 'TEST_CLOUD=1' to run FTR against your Cloud deployment`
|
|||
const profileData = await getSecurityProfile({ kbnHost: this.kbnHost, cookie, log: this.log });
|
||||
return profileData;
|
||||
}
|
||||
|
||||
getSupportedRoles() {
|
||||
return this.supportedRoles ? this.supportedRoles.roles : [];
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue