mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[FTR] [deployment agnostic tests] move saml roles setup to kbn/es (#190489)
## Summary closes #190212 This PR moves realm roles and mapping creation from FTR service to `kbn/es` package in oder to configure ES properly when you start servers with ` node scripts/functional_tests_server.js --config <path/to/stateful_config_with_SAML_setup>` It allows to start servers and investigate test scenario manually (e.g. reproduce failure) Thanks a lot to @azasypkin for ideas how to simplify and minimize the code change. Instead of 2 API calls to ES in FTR service we copy roles.yml to ES config directory before its start and create role mapping via API (but using template) as a part of security setup How to test: ``` node scripts/functional_tests_server.js --config x-pack/test/api_integration/deployment_agnostic/stateful.config.ts curl -X GET "http://localhost:9220/_security/role_mapping" -H "Content-Type: application/json" -u elastic:changeme ``` you should get the valid mapping back Note: if you try to load Kibana manually, it won't work due to missing mock-idp-plugin in stateful #190221 But automated tests are functional and can be run via script. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
This commit is contained in:
parent
90a435cf8f
commit
da1db2cdeb
19 changed files with 58 additions and 148 deletions
|
@ -18,6 +18,7 @@ import { promisify } from 'util';
|
|||
import { CA_CERT_PATH, ES_NOPASSWORD_P12_PATH, extract } from '@kbn/dev-utils';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import treeKill from 'tree-kill';
|
||||
import { MOCK_IDP_REALM_NAME, ensureSAMLRoleMapping } from '@kbn/mock-idp-utils';
|
||||
import { downloadSnapshot, installSnapshot, installSource, installArchive } from './install';
|
||||
import { ES_BIN, ES_PLUGIN_BIN, ES_KEYSTORE_BIN } from './paths';
|
||||
import {
|
||||
|
@ -312,7 +313,7 @@ export class Cluster {
|
|||
*/
|
||||
private exec(installPath: string, opts: EsClusterExecOptions) {
|
||||
const {
|
||||
skipNativeRealmSetup = false,
|
||||
skipSecuritySetup = false,
|
||||
reportTime = () => {},
|
||||
startTime,
|
||||
skipReadyCheck,
|
||||
|
@ -437,8 +438,8 @@ export class Cluster {
|
|||
});
|
||||
}
|
||||
|
||||
// once the cluster is ready setup the native realm
|
||||
if (!skipNativeRealmSetup) {
|
||||
// once the cluster is ready setup the realm
|
||||
if (!skipSecuritySetup) {
|
||||
const nativeRealm = new NativeRealm({
|
||||
log: this.log,
|
||||
elasticPassword: options.password,
|
||||
|
@ -446,8 +447,12 @@ export class Cluster {
|
|||
});
|
||||
|
||||
await nativeRealm.setPasswords(options);
|
||||
}
|
||||
|
||||
const samlRealmConfigPrefix = `authc.realms.saml.${MOCK_IDP_REALM_NAME}.`;
|
||||
if (args.some((arg) => arg.includes(samlRealmConfigPrefix))) {
|
||||
await ensureSAMLRoleMapping(client);
|
||||
}
|
||||
}
|
||||
this.log.success('kbn/es setup complete');
|
||||
});
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
export interface EsClusterExecOptions {
|
||||
skipNativeRealmSetup?: boolean;
|
||||
skipSecuritySetup?: boolean;
|
||||
reportTime?: (...args: any[]) => void;
|
||||
startTime?: number;
|
||||
esArgs?: string[] | string;
|
||||
|
|
|
@ -18,7 +18,7 @@ import { ToolingLog } from '@kbn/tooling-log';
|
|||
import { BASE_PATH, ES_CONFIG, ES_KEYSTORE_BIN } from '../paths';
|
||||
import { Artifact } from '../artifact';
|
||||
import { parseSettings, SettingsFilter } from '../settings';
|
||||
import { log as defaultLog } from '../utils/log';
|
||||
import { log as defaultLog, isFile, copyFileSync } from '../utils';
|
||||
import { InstallArchiveOptions } from './types';
|
||||
|
||||
const isHttpUrl = (str: string) => {
|
||||
|
@ -41,6 +41,7 @@ export async function installArchive(archive: string, options?: InstallArchiveOp
|
|||
log = defaultLog,
|
||||
esArgs = [],
|
||||
disableEsTmpDir = process.env.FTR_DISABLE_ES_TMPDIR?.toLowerCase() === 'true',
|
||||
resources,
|
||||
} = options || {};
|
||||
|
||||
let dest = archive;
|
||||
|
@ -84,6 +85,23 @@ export async function installArchive(archive: string, options?: InstallArchiveOp
|
|||
...parseSettings(esArgs, { filter: SettingsFilter.SecureOnly }),
|
||||
]);
|
||||
|
||||
// copy resources to ES config directory
|
||||
if (resources) {
|
||||
resources.forEach((resource) => {
|
||||
if (!isFile(resource)) {
|
||||
throw new Error(
|
||||
`Invalid resource: '${resource}'.\nOnly valid files can be copied to ES config directory`
|
||||
);
|
||||
}
|
||||
|
||||
const filename = path.basename(resource);
|
||||
const destPath = path.resolve(installPath, 'config', filename);
|
||||
|
||||
copyFileSync(resource, destPath);
|
||||
log.info('moved %s in config to %s', resource, destPath);
|
||||
});
|
||||
}
|
||||
|
||||
return { installPath, disableEsTmpDir };
|
||||
}
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ export async function installSnapshot({
|
|||
log = defaultLog,
|
||||
esArgs,
|
||||
useCached = false,
|
||||
resources,
|
||||
}: InstallSnapshotOptions) {
|
||||
const { downloadPath } = await downloadSnapshot({
|
||||
license,
|
||||
|
@ -68,5 +69,6 @@ export async function installSnapshot({
|
|||
installPath,
|
||||
log,
|
||||
esArgs,
|
||||
resources,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ export async function installSource({
|
|||
installPath = path.resolve(basePath, 'source'),
|
||||
log = defaultLog,
|
||||
esArgs,
|
||||
resources,
|
||||
}: InstallSourceOptions) {
|
||||
log.info('source path: %s', chalk.bold(sourcePath));
|
||||
log.info('install path: %s', chalk.bold(installPath));
|
||||
|
@ -59,6 +60,7 @@ export async function installSource({
|
|||
installPath,
|
||||
log,
|
||||
esArgs,
|
||||
resources,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ export interface InstallSourceOptions {
|
|||
installPath?: string;
|
||||
log?: ToolingLog;
|
||||
esArgs?: string[];
|
||||
resources?: string[];
|
||||
}
|
||||
|
||||
export interface DownloadSnapshotOptions {
|
||||
|
@ -26,6 +27,7 @@ export interface DownloadSnapshotOptions {
|
|||
installPath?: string;
|
||||
log?: ToolingLog;
|
||||
useCached?: boolean;
|
||||
resources?: string[];
|
||||
}
|
||||
|
||||
export interface InstallSnapshotOptions extends DownloadSnapshotOptions {
|
||||
|
@ -42,4 +44,5 @@ export interface InstallArchiveOptions {
|
|||
esArgs?: string[];
|
||||
/** Disable creating a temp directory, allowing ES to write to OS's /tmp directory */
|
||||
disableEsTmpDir?: boolean;
|
||||
resources?: string[];
|
||||
}
|
||||
|
|
|
@ -311,6 +311,7 @@ describe('#installArchive()', () => {
|
|||
esArgs: ['foo=true'],
|
||||
log,
|
||||
disableEsTmpDir: true,
|
||||
resources: ['path/to/resource'],
|
||||
};
|
||||
const cluster = new Cluster({ log });
|
||||
await cluster.installArchive('bar', options);
|
||||
|
|
|
@ -43,11 +43,11 @@ export function extractConfigFiles(
|
|||
return localConfig;
|
||||
}
|
||||
|
||||
function isFile(dest = '') {
|
||||
export function isFile(dest = '') {
|
||||
return fs.existsSync(dest) && fs.statSync(dest).isFile();
|
||||
}
|
||||
|
||||
function copyFileSync(src: string, dest: string) {
|
||||
export function copyFileSync(src: string, dest: string) {
|
||||
const destPath = path.dirname(dest);
|
||||
|
||||
if (!fs.existsSync(destPath)) {
|
||||
|
|
|
@ -10,7 +10,7 @@ export { cache } from './cache';
|
|||
export { log } from './log';
|
||||
export { parseEsLog } from './parse_es_log';
|
||||
export { findMostRecentlyChanged } from './find_most_recently_changed';
|
||||
export { extractConfigFiles } from './extract_config_files';
|
||||
export { extractConfigFiles, isFile, copyFileSync } from './extract_config_files';
|
||||
// @ts-expect-error not typed yet
|
||||
export { NativeRealm, SYSTEM_INDICES_SUPERUSER } from './native_realm';
|
||||
export { buildSnapshot } from './build_snapshot';
|
||||
|
|
|
@ -6,20 +6,12 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import { type Config } from '@kbn/test';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-utils';
|
||||
import { KibanaServer } from '../..';
|
||||
|
||||
import { ServerlessAuthProvider } from './serverless/auth_provider';
|
||||
import { StatefulAuthProvider } from './stateful/auth_provider';
|
||||
import { createRole, createRoleMapping } from './stateful/create_role_mapping';
|
||||
|
||||
const STATEFUL_ADMIN_ROLE_MAPPING_PATH = './stateful/admin_mapping';
|
||||
|
||||
export interface AuthProvider {
|
||||
getSupportedRoleDescriptors(): any;
|
||||
getSupportedRoleDescriptors(): Record<string, unknown>;
|
||||
getDefaultRole(): string;
|
||||
getRolesDefinitionPath(): string;
|
||||
getCommonRequestHeader(): { [key: string]: string };
|
||||
|
@ -28,26 +20,10 @@ export interface AuthProvider {
|
|||
|
||||
export interface AuthProviderProps {
|
||||
config: Config;
|
||||
kibanaServer: KibanaServer;
|
||||
log: ToolingLog;
|
||||
}
|
||||
|
||||
export const getAuthProvider = async (props: AuthProviderProps) => {
|
||||
const { config, log, kibanaServer } = props;
|
||||
export const getAuthProvider = (props: AuthProviderProps) => {
|
||||
const { config } = props;
|
||||
const isServerless = !!props.config.get('serverless');
|
||||
if (isServerless) {
|
||||
return new ServerlessAuthProvider(config);
|
||||
}
|
||||
|
||||
const provider = new StatefulAuthProvider();
|
||||
// TODO: Move it to @kbn-es package, so that roles and its mapping are created before FTR services loading starts.
|
||||
// 'viewer' and 'editor' roles are available by default, but we have to create 'admin' role
|
||||
const adminRoleMapping = JSON.parse(
|
||||
fs.readFileSync(require.resolve(STATEFUL_ADMIN_ROLE_MAPPING_PATH), 'utf8')
|
||||
);
|
||||
await createRole({ roleName: 'admin', roleMapping: adminRoleMapping, kibanaServer, log });
|
||||
const roles = Object.keys(provider.getSupportedRoleDescriptors());
|
||||
// Creating roles mapping for mock-idp
|
||||
await createRoleMapping({ name: MOCK_IDP_REALM_NAME, roles, config, log });
|
||||
return provider;
|
||||
return isServerless ? new ServerlessAuthProvider(config) : new StatefulAuthProvider();
|
||||
};
|
||||
|
|
|
@ -20,14 +20,13 @@ export interface RoleCredentials {
|
|||
cookieHeader: { Cookie: string };
|
||||
}
|
||||
|
||||
export async function SamlAuthProvider({ getService }: FtrProviderContext) {
|
||||
export function SamlAuthProvider({ getService }: FtrProviderContext) {
|
||||
const config = getService('config');
|
||||
const log = getService('log');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const isCloud = !!process.env.TEST_CLOUD;
|
||||
|
||||
const authRoleProvider = await getAuthProvider({ config, kibanaServer, log });
|
||||
const authRoleProvider = getAuthProvider({ config });
|
||||
const supportedRoleDescriptors = authRoleProvider.getSupportedRoleDescriptors();
|
||||
const supportedRoles = Object.keys(supportedRoleDescriptors);
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ export class ServerlessAuthProvider implements AuthProvider {
|
|||
this.rolesDefinitionPath = resolve(SERVERLESS_ROLES_ROOT_PATH, this.projectType, 'roles.yml');
|
||||
}
|
||||
|
||||
getSupportedRoleDescriptors(): any {
|
||||
getSupportedRoleDescriptors(): Record<string, unknown> {
|
||||
return readRolesDescriptorsFromResource(this.rolesDefinitionPath);
|
||||
}
|
||||
getDefaultRole(): string {
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
{
|
||||
"kibana":[
|
||||
{
|
||||
"base":[
|
||||
"all"
|
||||
],
|
||||
"feature":{
|
||||
|
||||
},
|
||||
"spaces":[
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"elasticsearch":{
|
||||
"cluster":[
|
||||
"all"
|
||||
],
|
||||
"indices":[
|
||||
{
|
||||
"names":[
|
||||
"*"
|
||||
],
|
||||
"privileges":[
|
||||
"all"
|
||||
],
|
||||
"allow_restricted_indices":false
|
||||
},
|
||||
{
|
||||
"names":[
|
||||
"*"
|
||||
],
|
||||
"privileges":[
|
||||
"monitor",
|
||||
"read",
|
||||
"read_cross_cluster",
|
||||
"view_index_metadata"
|
||||
],
|
||||
"allow_restricted_indices":true
|
||||
}
|
||||
],
|
||||
"run_as":[
|
||||
|
||||
]
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ import {
|
|||
|
||||
export class StatefulAuthProvider implements AuthProvider {
|
||||
private readonly rolesDefinitionPath = resolve(REPO_ROOT, STATEFUL_ROLES_ROOT_PATH, 'roles.yml');
|
||||
getSupportedRoleDescriptors(): any {
|
||||
getSupportedRoleDescriptors(): Record<string, unknown> {
|
||||
return readRolesDescriptorsFromResource(this.rolesDefinitionPath);
|
||||
}
|
||||
getDefaultRole() {
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Config, createEsClientForFtrConfig } from '@kbn/test';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import { KibanaServer } from '../../..';
|
||||
|
||||
export interface CreateRoleProps {
|
||||
roleName: string;
|
||||
roleMapping: string[];
|
||||
kibanaServer: KibanaServer;
|
||||
log: ToolingLog;
|
||||
}
|
||||
|
||||
export interface CreateRoleMappingProps {
|
||||
name: string;
|
||||
roles: string[];
|
||||
config: Config;
|
||||
log: ToolingLog;
|
||||
}
|
||||
|
||||
export async function createRole(props: CreateRoleProps) {
|
||||
const { roleName, roleMapping, kibanaServer, log } = props;
|
||||
log.debug(`Adding a role: ${roleName}`);
|
||||
const { status, statusText } = await kibanaServer.request({
|
||||
path: `/api/security/role/${roleName}`,
|
||||
method: 'PUT',
|
||||
body: roleMapping,
|
||||
retries: 0,
|
||||
});
|
||||
if (status !== 204) {
|
||||
throw new Error(`Expected status code of 204, received ${status} ${statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createRoleMapping(props: CreateRoleMappingProps) {
|
||||
const { name, roles, config, log } = props;
|
||||
log.debug(`Creating a role mapping: {realm.name: ${name}, roles: ${roles}}`);
|
||||
const esClient = createEsClientForFtrConfig(config);
|
||||
await esClient.security.putRoleMapping({
|
||||
name,
|
||||
roles,
|
||||
enabled: true,
|
||||
// @ts-ignore
|
||||
rules: { field: { 'realm.name': name } },
|
||||
});
|
||||
}
|
|
@ -18,7 +18,6 @@
|
|||
"@kbn/expect",
|
||||
"@kbn/repo-info",
|
||||
"@kbn/es",
|
||||
"@kbn/mock-idp-utils"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -202,6 +202,7 @@ export function createTestEsCluster<
|
|||
license,
|
||||
basePath,
|
||||
esArgs,
|
||||
resources: files,
|
||||
};
|
||||
|
||||
return new (class TestCluster {
|
||||
|
@ -297,7 +298,7 @@ export function createTestEsCluster<
|
|||
// If we have multiple nodes, we shouldn't try setting up the native realm
|
||||
// right away or wait for ES to be green, the cluster isn't ready. So we only
|
||||
// set it up after the last node is started.
|
||||
skipNativeRealmSetup: this.nodes.length > 1 && i < this.nodes.length - 1,
|
||||
skipSecuritySetup: this.nodes.length > 1 && i < this.nodes.length - 1,
|
||||
skipReadyCheck: this.nodes.length > 1 && i < this.nodes.length - 1,
|
||||
onEarlyExit,
|
||||
writeLogsToPath,
|
||||
|
|
|
@ -18,6 +18,10 @@ import {
|
|||
systemIndicesSuperuser,
|
||||
FtrConfigProviderContext,
|
||||
} from '@kbn/test';
|
||||
import path from 'path';
|
||||
// @ts-expect-error we have to check types with "allowJs: false" for now, causing this import to fail
|
||||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
import { STATEFUL_ROLES_ROOT_PATH } from '@kbn/es';
|
||||
import { DeploymentAgnosticCommonServices, services } from '../services';
|
||||
|
||||
interface CreateTestConfigOptions<T extends DeploymentAgnosticCommonServices> {
|
||||
|
@ -85,6 +89,10 @@ export function createStatefulTestConfig<T extends DeploymentAgnosticCommonServi
|
|||
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.name=${MOCK_IDP_ATTRIBUTE_NAME}`,
|
||||
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.mail=${MOCK_IDP_ATTRIBUTE_EMAIL}`,
|
||||
],
|
||||
files: [
|
||||
// Passing the roles that are equivalent to the ones we have in serverless
|
||||
path.resolve(REPO_ROOT, STATEFUL_ROLES_ROOT_PATH, 'roles.yml'),
|
||||
],
|
||||
},
|
||||
|
||||
kbnTestServer: {
|
||||
|
|
|
@ -32,8 +32,6 @@ export function SecuritySolutionServerlessUtils({
|
|||
});
|
||||
|
||||
async function invalidateApiKey(credentials: RoleCredentials) {
|
||||
// load service to call it outside mocha context
|
||||
await svlUserManager.init();
|
||||
await svlUserManager.invalidateM2mApiKeyWithRoleScope(credentials);
|
||||
}
|
||||
|
||||
|
@ -55,8 +53,6 @@ export function SecuritySolutionServerlessUtils({
|
|||
|
||||
const createSuperTest = async (role = 'admin') => {
|
||||
cleanCredentials(role);
|
||||
// load service to call it outside mocha context
|
||||
await svlUserManager.init();
|
||||
const credentials = await svlUserManager.createM2mApiKeyWithRoleScope(role);
|
||||
rolesCredentials.set(role, credentials);
|
||||
|
||||
|
@ -66,8 +62,6 @@ export function SecuritySolutionServerlessUtils({
|
|||
|
||||
return {
|
||||
getUsername: async (role = 'admin') => {
|
||||
// load service to call it outside mocha context
|
||||
await svlUserManager.init();
|
||||
const { username } = await svlUserManager.getUserData(role);
|
||||
|
||||
return username;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue