From 13a3831dab46170277780e600c1b7dbb70c315f7 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Wed, 8 Nov 2023 11:58:43 +0000 Subject: [PATCH 01/20] Add mock identity provider for serverless --- package.json | 1 + .../kbn-es/src/cli_commands/serverless.ts | 1 + packages/kbn-es/src/paths.ts | 3 + packages/kbn-es/src/utils/docker.ts | 150 +++++++++--- .../kbn-mock-idp-plugin/common/constants.ts | 24 ++ packages/kbn-mock-idp-plugin/common/index.ts | 10 + packages/kbn-mock-idp-plugin/common/utils.ts | 230 ++++++++++++++++++ packages/kbn-mock-idp-plugin/kibana.jsonc | 11 + packages/kbn-mock-idp-plugin/metadata.xml | 42 ++++ packages/kbn-mock-idp-plugin/package.json | 6 + packages/kbn-mock-idp-plugin/server/index.ts | 9 + packages/kbn-mock-idp-plugin/server/plugin.ts | 110 +++++++++ packages/kbn-mock-idp-plugin/tsconfig.json | 16 ++ src/cli/serve/serve.js | 13 +- tsconfig.base.json | 4 + yarn.lock | 4 + 16 files changed, 602 insertions(+), 32 deletions(-) create mode 100644 packages/kbn-mock-idp-plugin/common/constants.ts create mode 100644 packages/kbn-mock-idp-plugin/common/index.ts create mode 100644 packages/kbn-mock-idp-plugin/common/utils.ts create mode 100644 packages/kbn-mock-idp-plugin/kibana.jsonc create mode 100644 packages/kbn-mock-idp-plugin/metadata.xml create mode 100644 packages/kbn-mock-idp-plugin/package.json create mode 100644 packages/kbn-mock-idp-plugin/server/index.ts create mode 100644 packages/kbn-mock-idp-plugin/server/plugin.ts create mode 100644 packages/kbn-mock-idp-plugin/tsconfig.json diff --git a/package.json b/package.json index e5213d00d20d4..fc9dd8a5b6775 100644 --- a/package.json +++ b/package.json @@ -1223,6 +1223,7 @@ "@kbn/managed-vscode-config": "link:packages/kbn-managed-vscode-config", "@kbn/managed-vscode-config-cli": "link:packages/kbn-managed-vscode-config-cli", "@kbn/management-storybook-config": "link:packages/kbn-management/storybook/config", + "@kbn/mock-idp-plugin": "link:packages/kbn-mock-idp-plugin", "@kbn/openapi-generator": "link:packages/kbn-openapi-generator", "@kbn/optimizer": "link:packages/kbn-optimizer", "@kbn/optimizer-webpack-helpers": "link:packages/kbn-optimizer-webpack-helpers", diff --git a/packages/kbn-es/src/cli_commands/serverless.ts b/packages/kbn-es/src/cli_commands/serverless.ts index 87a573a8810dd..abb8c41a66dd3 100644 --- a/packages/kbn-es/src/cli_commands/serverless.ts +++ b/packages/kbn-es/src/cli_commands/serverless.ts @@ -38,6 +38,7 @@ export const serverless: Command = { --host Publish ES docker container on additional host IP --port The port to bind to on 127.0.0.1 [default: ${DEFAULT_PORT}] --ssl Enable HTTP SSL on the ES cluster + --kibanaUrl Fully qualified URL where Kibana is hosted (including base path). Required for mock identity provider to function --skipTeardown If this process exits, leave the ES cluster running in the background --waitForReady Wait for the ES cluster to be ready to serve requests --resources Overrides resources under ES 'config/' directory, which are by default diff --git a/packages/kbn-es/src/paths.ts b/packages/kbn-es/src/paths.ts index d9b4be41aa15b..4da4448573384 100644 --- a/packages/kbn-es/src/paths.ts +++ b/packages/kbn-es/src/paths.ts @@ -8,6 +8,7 @@ import Os from 'os'; import { resolve } from 'path'; +import { REPO_ROOT } from '@kbn/repo-info'; function maybeUseBat(bin: string) { return Os.platform().startsWith('win') ? `${bin}.bat` : bin; @@ -51,6 +52,8 @@ export const SERVERLESS_SECRETS_SSL_PATH = resolve( export const SERVERLESS_JWKS_PATH = resolve(__dirname, './serverless_resources/jwks.json'); +export const SERVERLESS_IDP_METADATA_PATH = resolve(REPO_ROOT, '.es', 'idp_metadata.xml'); + export const SERVERLESS_RESOURCES_PATHS = [ SERVERLESS_OPERATOR_USERS_PATH, SERVERLESS_ROLE_MAPPING_PATH, diff --git a/packages/kbn-es/src/utils/docker.ts b/packages/kbn-es/src/utils/docker.ts index 4a54045fb0924..2724b1f209463 100644 --- a/packages/kbn-es/src/utils/docker.ts +++ b/packages/kbn-es/src/utils/docker.ts @@ -14,12 +14,17 @@ import { Client, ClientOptions, HttpConnection } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/tooling-log'; import { kibanaPackageJson as pkg, REPO_ROOT } from '@kbn/repo-info'; +import { CA_CERT_PATH, ES_P12_PASSWORD, ES_P12_PATH } from '@kbn/dev-utils'; import { - CA_CERT_PATH, - ES_P12_PASSWORD, - ES_P12_PATH, - kibanaDevServiceAccount, -} from '@kbn/dev-utils'; + MOCK_IDP_REALM_NAME, + MOCK_IDP_ENTITY_ID, + MOCK_IDP_ATTRIBUTE_PRINCIPAL, + MOCK_IDP_ATTRIBUTE_ROLES, + MOCK_IDP_ATTRIBUTE_EMAIL, + MOCK_IDP_ATTRIBUTE_NAME, + ensureSAMLRoleMapping, + createMockIdpMetadata, +} from '@kbn/mock-idp-plugin/common'; import { createCliError } from '../errors'; import { EsClusterExecOptions } from '../cluster_exec_options'; @@ -27,6 +32,7 @@ import { SERVERLESS_RESOURCES_PATHS, SERVERLESS_SECRETS_PATH, SERVERLESS_JWKS_PATH, + SERVERLESS_IDP_METADATA_PATH, SERVERLESS_CONFIG_PATH, SERVERLESS_FILES_PATH, SERVERLESS_SECRETS_SSL_PATH, @@ -68,6 +74,8 @@ export interface ServerlessOptions extends EsClusterExecOptions, BaseOptions { background?: boolean; /** Wait for the ES cluster to be ready to serve requests */ waitForReady?: boolean; + /** Fully qualified URL where Kibana is hosted (including base path). Required for mock identity provider to function */ + kibanaUrl?: string; /** * Resource file(s) to overwrite * (see list of files that can be overwritten under `packages/kbn-es/src/serverless_resources/users`) @@ -445,6 +453,48 @@ export function resolveEsArgs( }); } + // Configure mock identify provider + if ('kibanaUrl' in options && options.kibanaUrl) { + esArgs.set('xpack.security.authc.token.enabled', 'true'); + esArgs.set(`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.order`, '0'); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.metadata.path`, + `${SERVERLESS_CONFIG_PATH}secrets/idp_metadata.xml` + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.entity_id`, + MOCK_IDP_ENTITY_ID + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.entity_id`, + options.kibanaUrl + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.acs`, + `${options.kibanaUrl}/api/security/saml/callback` + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.logout`, + `${options.kibanaUrl}/logout` + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.principal`, + MOCK_IDP_ATTRIBUTE_PRINCIPAL + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.groups`, + MOCK_IDP_ATTRIBUTE_ROLES + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.name`, + MOCK_IDP_ATTRIBUTE_EMAIL + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.mail`, + MOCK_IDP_ATTRIBUTE_NAME + ); + } + if (customEsArgs) { const args = typeof customEsArgs === 'string' ? [customEsArgs] : customEsArgs; @@ -550,6 +600,16 @@ export async function setupServerlessVolumes(log: ToolingLog, options: Serverles ); } + // Create and add meta data for mock identity provider + if (options.kibanaUrl) { + const metadata = await createMockIdpMetadata(options.kibanaUrl); + await Fsp.writeFile(SERVERLESS_IDP_METADATA_PATH, metadata); + volumeCmds.push( + '--volume', + `${SERVERLESS_IDP_METADATA_PATH}:${SERVERLESS_CONFIG_PATH}secrets/idp_metadata.xml:z` + ); + } + volumeCmds.push( ...getESp12Volume(), ...serverlessResources, @@ -558,7 +618,6 @@ export async function setupServerlessVolumes(log: ToolingLog, options: Serverles `${ ssl ? SERVERLESS_SECRETS_SSL_PATH : SERVERLESS_SECRETS_PATH }:${SERVERLESS_CONFIG_PATH}secrets/secrets.json:z`, - '--volume', `${SERVERLESS_JWKS_PATH}:${SERVERLESS_CONFIG_PATH}secrets/jwks.json:z` ); @@ -660,33 +719,62 @@ export async function runServerlessCluster(log: ToolingLog, options: ServerlessO process.on('SIGINT', () => teardownServerlessClusterSync(log, options)); } + const esNodeUrl = `${options.ssl ? 'https' : 'http'}://${portCmd[1].substring( + 0, + portCmd[1].lastIndexOf(':') + )}`; + + const client = getESClient({ + node: esNodeUrl, + auth: { + username: ELASTIC_SERVERLESS_SUPERUSER, + password: ELASTIC_SERVERLESS_SUPERUSER_PASSWORD, + }, + ...(options.ssl + ? { + tls: { + ca: [fs.readFileSync(CA_CERT_PATH)], + // NOTE: Even though we've added ca into the tls options, we are using 127.0.0.1 instead of localhost + // for the ip which is not validated. As such we are getting the error + // Hostname/IP does not match certificate's altnames: IP: 127.0.0.1 is not in the cert's list: + // To work around that we are overriding the function checkServerIdentity too + checkServerIdentity: () => { + return undefined; + }, + }, + } + : {}), + }); + const readyPromise = waitUntilClusterReady({ client, expectedStatus: 'green', log }).then(() => { + if (!options.ssl) { + return; + } + + if (!options.kibanaUrl) { + log.warning( + `Unable to configure mock identity provider. + + Run Elasticsearch with ${chalk.bold.cyan( + '--kibanaUrl ' + )} option to be able to login using ${chalk.bold.cyan(MOCK_IDP_REALM_NAME)} realm` + ); + return; + } + + const roleMappingPromise = ensureSAMLRoleMapping(client); + + log.success( + `Created role mapping for mock identity provider. You can now login using ${chalk.bold.cyan( + MOCK_IDP_REALM_NAME + )} realm` + ); + + return roleMappingPromise; + }); + if (options.waitForReady) { log.info('Waiting until ES is ready to serve requests...'); - - const esNodeUrl = `${options.ssl ? 'https' : 'http'}://${portCmd[1].substring( - 0, - portCmd[1].lastIndexOf(':') - )}`; - - const client = getESClient({ - node: esNodeUrl, - auth: { bearer: kibanaDevServiceAccount.token }, - ...(options.ssl - ? { - tls: { - ca: [fs.readFileSync(CA_CERT_PATH)], - // NOTE: Even though we've added ca into the tls options, we are using 127.0.0.1 instead of localhost - // for the ip which is not validated. As such we are getting the error - // Hostname/IP does not match certificate's altnames: IP: 127.0.0.1 is not in the cert's list: - // To work around that we are overriding the function checkServerIdentity too - checkServerIdentity: () => { - return undefined; - }, - }, - } - : {}), - }); - await waitUntilClusterReady({ client, expectedStatus: 'green', log }); + await readyPromise; } if (!options.background) { diff --git a/packages/kbn-mock-idp-plugin/common/constants.ts b/packages/kbn-mock-idp-plugin/common/constants.ts new file mode 100644 index 0000000000000..6fb5475587574 --- /dev/null +++ b/packages/kbn-mock-idp-plugin/common/constants.ts @@ -0,0 +1,24 @@ +/* + * 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 { resolve } from 'path'; + +export const MOCK_IDP_PLUGIN_PATH = resolve(__dirname, '..'); +export const MOCK_IDP_METADATA_PATH = resolve(MOCK_IDP_PLUGIN_PATH, 'metadata.xml'); + +export const MOCK_IDP_LOGIN_PATH = '/mock_idp/login'; +export const MOCK_IDP_LOGOUT_PATH = '/mock_idp/logout'; + +export const MOCK_IDP_REALM_NAME = 'mock-idp'; +export const MOCK_IDP_ENTITY_ID = 'urn:mock-idp'; // Must match `entityID` in `metadata.xml` +export const MOCK_IDP_ROLE_MAPPING_NAME = 'mock-idp-mapping'; + +export const MOCK_IDP_ATTRIBUTE_PRINCIPAL = 'http://saml.elastic-cloud.com/attributes/principal'; +export const MOCK_IDP_ATTRIBUTE_ROLES = 'http://saml.elastic-cloud.com/attributes/roles'; +export const MOCK_IDP_ATTRIBUTE_EMAIL = 'http://saml.elastic-cloud.com/attributes/email'; +export const MOCK_IDP_ATTRIBUTE_NAME = 'http://saml.elastic-cloud.com/attributes/name'; diff --git a/packages/kbn-mock-idp-plugin/common/index.ts b/packages/kbn-mock-idp-plugin/common/index.ts new file mode 100644 index 0000000000000..9fed036fab99a --- /dev/null +++ b/packages/kbn-mock-idp-plugin/common/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export * from './constants'; +export * from './utils'; diff --git a/packages/kbn-mock-idp-plugin/common/utils.ts b/packages/kbn-mock-idp-plugin/common/utils.ts new file mode 100644 index 0000000000000..58b7acb9cb462 --- /dev/null +++ b/packages/kbn-mock-idp-plugin/common/utils.ts @@ -0,0 +1,230 @@ +/* + * 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 { Client } from '@elastic/elasticsearch'; +import { SignedXml } from 'xml-crypto'; +import { KBN_KEY_PATH, KBN_CERT_PATH } from '@kbn/dev-utils'; +import { readFile } from 'fs/promises'; +import zlib from 'zlib'; +import { promisify } from 'util'; +import { parseString } from 'xml2js'; +import { X509Certificate } from 'crypto'; + +import { + MOCK_IDP_REALM_NAME, + MOCK_IDP_ENTITY_ID, + MOCK_IDP_ROLE_MAPPING_NAME, + MOCK_IDP_ATTRIBUTE_PRINCIPAL, + MOCK_IDP_ATTRIBUTE_ROLES, + MOCK_IDP_ATTRIBUTE_EMAIL, + MOCK_IDP_ATTRIBUTE_NAME, + MOCK_IDP_LOGIN_PATH, + MOCK_IDP_LOGOUT_PATH, +} from './constants'; + +const inflateRawAsync = promisify(zlib.inflateRaw); +const parseStringAsync = promisify(parseString); + +/** + * Creates XML metadata for our mock identity provider. + * + * This can be saved to file and used to configure Elasticsearch SAML realm. + * + * @param kibanaUrl Fully qualified URL where Kibana is hosted (including base path) + */ +export async function createMockIdpMetadata(kibanaUrl: string) { + const signingKey = await readFile(KBN_CERT_PATH); + const cert = new X509Certificate(signingKey); + + return ` + + + + + + ${cert.raw.toString('base64')} + + + + + + + + + + `; +} + +/** + * Creates a SAML response that can be passed directly to the Kibana ACS endpoint to authenticate a user. + * + * @example Create a SAML response. + * + * ```ts + * const samlResponse = await createSAMLResponse({ + * username: '1234567890', + * email: 'mail@elastic.co', + * fullname: 'Test User', + * roles: ['t1_analyst', 'editor'], + * }) + * ``` + * + * @example Authenticate user with SAML response. + * + * ```ts + * fetch('/api/security/saml/callback', { + * method: 'POST', + * body: JSON.stringify({ SAMLResponse: samlResponse }), + * redirect: 'manual' + * }) + * ``` + */ +export async function createSAMLResponse(options: { + /** Fully qualified URL where Kibana is hosted (including base path) */ + kibanaUrl: string; + /** ID from SAML authentication request */ + authnRequestId?: string; + username: string; + fullname?: string; + email?: string; + roles: string[]; +}) { + const issueInstant = new Date().toISOString(); + const notOnOrAfter = new Date(Date.now() + 3600 * 1000).toISOString(); + + const samlAssertionTemplateXML = ` + + ${MOCK_IDP_ENTITY_ID} + + _643ec1b3f5673583b9f9a1e9e73a36daa2a3748f + + + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified + + + + + ${options.username} + + + ${options.roles + .map( + (role) => `${role}` + ) + .join('')} + + ${ + options.email + ? ` + ${options.email} + ` + : '' + } + ${ + options.fullname + ? ` + ${options.fullname} + ` + : '' + } + + + `; + + const signature = new SignedXml(); + signature.signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; + signature.signingKey = await readFile(KBN_KEY_PATH); + + // Adds a reference to a `Assertion` xml element and an array of transform algorithms to be used during signing. + signature.addReference( + `//*[local-name(.)='Assertion']`, + [ + 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', + 'http://www.w3.org/2001/10/xml-exc-c14n#', + ], + 'http://www.w3.org/2001/04/xmlenc#sha256' + ); + + signature.computeSignature(samlAssertionTemplateXML, { + location: { reference: `//*[local-name(.)='Issuer']`, action: 'after' }, + }); + + const value = await Buffer.from( + ` + + ${MOCK_IDP_ENTITY_ID} + + + ${signature.getSignedXml()} + + ` + ).toString('base64'); + + return value; +} + +/** + * Creates the role mapping required for developers to authenticate using SAML. + */ +export async function ensureSAMLRoleMapping(client: Client) { + return client.transport.request({ + method: 'PUT', + path: `/_security/role_mapping/${MOCK_IDP_ROLE_MAPPING_NAME}`, + body: { + enabled: true, + role_templates: [ + { + template: '{"source":"{{#tojson}}groups{{/tojson}}"}', + format: 'json', + }, + ], + rules: { + all: [ + { + field: { + 'realm.name': MOCK_IDP_REALM_NAME, + }, + }, + ], + }, + }, + }); +} + +interface SAMLAuthnRequest { + 'saml2p:AuthnRequest': { + $: { + AssertionConsumerServiceURL: string; + Destination: string; + ID: string; + IssueInstant: string; + }; + }; +} + +export async function parseSAMLAuthnRequest(samlRequest: string) { + const inflatedSAMLRequest = (await inflateRawAsync(Buffer.from(samlRequest, 'base64'))) as Buffer; + const parsedSAMLRequest = (await parseStringAsync( + inflatedSAMLRequest.toString() + )) as SAMLAuthnRequest; + return parsedSAMLRequest['saml2p:AuthnRequest'].$; +} diff --git a/packages/kbn-mock-idp-plugin/kibana.jsonc b/packages/kbn-mock-idp-plugin/kibana.jsonc new file mode 100644 index 0000000000000..929d7b9b990db --- /dev/null +++ b/packages/kbn-mock-idp-plugin/kibana.jsonc @@ -0,0 +1,11 @@ +{ + "type": "plugin", + "id": "@kbn/mock-idp-plugin", + "owner": "@elastic/kibana-security", + "devOnly": true, + "plugin": { + "id": "mockIdpPlugin", + "server": true, + "browser": false + } +} \ No newline at end of file diff --git a/packages/kbn-mock-idp-plugin/metadata.xml b/packages/kbn-mock-idp-plugin/metadata.xml new file mode 100644 index 0000000000000..c6819d1daf974 --- /dev/null +++ b/packages/kbn-mock-idp-plugin/metadata.xml @@ -0,0 +1,42 @@ + + + + + + + + MIIDYjCCAkqgAwIBAgIUZ2p8K7GMXGk6xwCS9S91BUl1JnAwDQYJKoZIhvcNAQEL + BQAwNDEyMDAGA1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5l + cmF0ZWQgQ0EwIBcNMjMwOTIzMTUyMDE0WhgPMjA3MzA5MTAxNTIwMTRaMBExDzAN + BgNVBAMTBmtpYmFuYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMOU + r52dbZ5dY0BoP2p7CEnOpG+qHTNrOAqZO/OJfniPMtpGmwAMl3WZDca6u2XkV2KE + qQyevQ2ADk6G3o8S2RU8mO/+UweuCDF7LHuSdxEGTpucidZErmVhEGUOFosL5UeB + AtIDWxvWwgK+W9Yzt5IEN2HzNCZ6h0dOSk2r9EjVMG5yF4Q6kuqOYxBT7jxoaOtO + OCrgBRummtUga4T13WZ/ZIyyHpXj2+JD4YEmrDyoTa7NLaphv0hnVhHXYoYBI/c6 + 2SwwAoBlmtDmlinwSACQ3o/8eLWk0tqkIP14rc3oFh3m7D2c3c2m2HXuyoSDMfGW + beG2IE1Q3idcGmeG3qsCAwEAAaOBjDCBiTAdBgNVHQ4EFgQUMOUM7w5jmIozDvnq + RpM779m5GigwHwYDVR0jBBgwFoAUMEwqwI5b0MYpNxwaHJ9Tw1Lp3p4wPAYDVR0R + BDUwM4IUaG9zdC5kb2NrZXIuaW50ZXJuYWyCCWxvY2FsaG9zdIIEZXMwM4IEZXMw + MoIEZXMwMTAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCxqvQYXSKqgpdl + SP4gXgwipAnYsoW9qkgWQODTvSBEzUdOWme0d3j7i2l6Ur/nVSv5YjkqAv1hf/yJ + Hrk9h+j29ZO/aQ/KDh5i/gTEUnPw3Bxbw47dfn23tjMWO7NCU1fr5HNztRsa/gQr + e9s07g25u/gTfTi9Fyu0lcRe3bXOLS/mFVcuC5oxuS65R9OlbIsiORkZ2EfwuNUf + wAAYOGPIjM2VlQCvBitefsd/SzRKHdxSPy6KSjkO6MGEGo87fr7B7Nx1qp1DVrK7 + q9XeP1Cuygjg9WTcnsvWvNw8CssyuFM6X/3tGjpPasXwLvNUoG2AairK2AYTWhvS + foE31cFg + + + + + + + + + \ No newline at end of file diff --git a/packages/kbn-mock-idp-plugin/package.json b/packages/kbn-mock-idp-plugin/package.json new file mode 100644 index 0000000000000..456a2cfa5ce32 --- /dev/null +++ b/packages/kbn-mock-idp-plugin/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/mock-idp-plugin", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/kbn-mock-idp-plugin/server/index.ts b/packages/kbn-mock-idp-plugin/server/index.ts new file mode 100644 index 0000000000000..59be4a2359a9b --- /dev/null +++ b/packages/kbn-mock-idp-plugin/server/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export * from './plugin'; diff --git a/packages/kbn-mock-idp-plugin/server/plugin.ts b/packages/kbn-mock-idp-plugin/server/plugin.ts new file mode 100644 index 0000000000000..7fec5281146a8 --- /dev/null +++ b/packages/kbn-mock-idp-plugin/server/plugin.ts @@ -0,0 +1,110 @@ +/* + * 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 type { PluginInitializer, Plugin } from '@kbn/core/server'; +import { schema } from '@kbn/config-schema'; + +import { + MOCK_IDP_LOGIN_PATH, + MOCK_IDP_LOGOUT_PATH, + createSAMLResponse, + parseSAMLAuthnRequest, +} from '../common'; + +export const plugin: PluginInitializer = (): Plugin => ({ + setup(core) { + core.http.resources.register( + { + path: MOCK_IDP_LOGIN_PATH, + validate: { + query: schema.object({ + SAMLRequest: schema.string(), + }), + }, + options: { authRequired: false }, + }, + async (context, request, response) => { + let samlRequest: Awaited>; + try { + samlRequest = await parseSAMLAuthnRequest(request.query.SAMLRequest); + } catch (error) { + return response.badRequest({ + body: '[request query.SAMLRequest]: value is not valid SAMLRequest.', + }); + } + + const samlResponse = await createSAMLResponse({ + authnRequestId: samlRequest.ID, + kibanaUrl: samlRequest.AssertionConsumerServiceURL, + username: 'test_user', + roles: ['superuser'], + }); + + return response.renderHtml({ + body: ` + + Kibana SAML Login + + +
${samlRequest.ID}
+
+ + +
+ + `, + }); + } + ); + + core.http.resources.register( + { + path: `${MOCK_IDP_LOGIN_PATH}/submit.js`, + validate: false, + options: { authRequired: false }, + }, + (context, request, response) => { + return response.renderJs({ body: 'document.getElementById("loginForm").submit();' }); + } + ); + + core.http.resources.register( + { + path: MOCK_IDP_LOGOUT_PATH, + validate: false, + options: { authRequired: false }, + }, + async (context, request, response) => { + return response.redirected({ headers: { location: '/logout?SAMLResponse=something' } }); + } + ); + + let attemptsCounter = 0; + core.http.resources.register( + { + path: '/mock_idp/never_login', + validate: false, + options: { authRequired: false }, + }, + async (context, request, response) => { + return response.renderHtml({ + body: ` + + Kibana SAML Login + + + Attempt #${++attemptsCounter} + + `, + }); + } + ); + }, + start() {}, + stop() {}, +}); diff --git a/packages/kbn-mock-idp-plugin/tsconfig.json b/packages/kbn-mock-idp-plugin/tsconfig.json new file mode 100644 index 0000000000000..c95db79fa9363 --- /dev/null +++ b/packages/kbn-mock-idp-plugin/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/core" + ] +} \ No newline at end of file diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 911eecd45a9fb..a28ac1a5bf5bd 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -15,6 +15,7 @@ import { isKibanaDistributable } from '@kbn/repo-info'; import { readKeystore } from '../keystore/read_keystore'; import { compileConfigStack } from './compile_config_stack'; import { getConfigFromFiles } from '@kbn/config'; +import { MOCK_IDP_PLUGIN_PATH, MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-plugin/common'; const DEV_MODE_PATH = '@kbn/cli-dev-mode'; const DEV_MODE_SUPPORTED = canRequire(DEV_MODE_PATH); @@ -107,8 +108,18 @@ export function applyConfigOverrides(rawConfig, opts, extraCliOptions) { delete extraCliOptions.env; if (opts.dev) { - if (opts.serverless) { + if (opts.serverless && opts.ssl) { setServerlessKibanaDevServiceAccountIfPossible(get, set, opts); + + // Load mock identity provider plugin and configure realm + set('plugins.paths', _.compact([].concat(get('plugins.paths'), MOCK_IDP_PLUGIN_PATH))); + set(`xpack.security.authc.providers.saml.${MOCK_IDP_REALM_NAME}`, { + order: Number.MAX_SAFE_INTEGER, + realm: MOCK_IDP_REALM_NAME, + icon: 'user', + description: 'Continue as Test User', + hint: 'Allows testing serverless user roles', + }); } if (!has('elasticsearch.serviceAccountToken') && opts.devCredentials !== false) { diff --git a/tsconfig.base.json b/tsconfig.base.json index 79e998206483d..b71ea5055a37c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1062,6 +1062,8 @@ "@kbn/ml-trained-models-utils/*": ["x-pack/packages/ml/trained_models_utils/*"], "@kbn/ml-url-state": ["x-pack/packages/ml/url_state"], "@kbn/ml-url-state/*": ["x-pack/packages/ml/url_state/*"], + "@kbn/mock-idp-plugin": ["packages/kbn-mock-idp-plugin"], + "@kbn/mock-idp-plugin/*": ["packages/kbn-mock-idp-plugin/*"], "@kbn/monaco": ["packages/kbn-monaco"], "@kbn/monaco/*": ["packages/kbn-monaco/*"], "@kbn/monitoring-collection-plugin": ["x-pack/plugins/monitoring_collection"], @@ -1316,6 +1318,8 @@ "@kbn/serverless/*": ["x-pack/plugins/serverless/*"], "@kbn/serverless-common-settings": ["packages/serverless/settings/common"], "@kbn/serverless-common-settings/*": ["packages/serverless/settings/common/*"], + "@kbn/serverless-mock-identity-provider": ["packages/serverless/mock_identity_provider"], + "@kbn/serverless-mock-identity-provider/*": ["packages/serverless/mock_identity_provider/*"], "@kbn/serverless-observability": ["x-pack/plugins/serverless_observability"], "@kbn/serverless-observability/*": ["x-pack/plugins/serverless_observability/*"], "@kbn/serverless-observability-settings": ["packages/serverless/settings/observability_project"], diff --git a/yarn.lock b/yarn.lock index 28df3eac7c3f2..dda2dc077dd5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5028,6 +5028,10 @@ version "0.0.0" uid "" +"@kbn/mock-idp-plugin@link:packages/kbn-mock-idp-plugin": + version "0.0.0" + uid "" + "@kbn/monaco@link:packages/kbn-monaco": version "0.0.0" uid "" From bc21dd20115a13374800f6d10428ee46e60e2755 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:46:35 +0000 Subject: [PATCH 02/20] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- tsconfig.base.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/tsconfig.base.json b/tsconfig.base.json index efdf3d6fe4c64..3c5eac80abd25 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1314,8 +1314,6 @@ "@kbn/serverless/*": ["x-pack/plugins/serverless/*"], "@kbn/serverless-common-settings": ["packages/serverless/settings/common"], "@kbn/serverless-common-settings/*": ["packages/serverless/settings/common/*"], - "@kbn/serverless-mock-identity-provider": ["packages/serverless/mock_identity_provider"], - "@kbn/serverless-mock-identity-provider/*": ["packages/serverless/mock_identity_provider/*"], "@kbn/serverless-observability": ["x-pack/plugins/serverless_observability"], "@kbn/serverless-observability/*": ["x-pack/plugins/serverless_observability/*"], "@kbn/serverless-observability-settings": ["packages/serverless/settings/observability_project"], From b48a2c8b91b79662be198ff440ed1c5576499b65 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:53:11 +0000 Subject: [PATCH 03/20] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- packages/kbn-es/tsconfig.json | 3 ++- packages/kbn-mock-idp-plugin/tsconfig.json | 6 ++++-- src/cli/tsconfig.json | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/kbn-es/tsconfig.json b/packages/kbn-es/tsconfig.json index 75059c2ef69cd..69d048a7be47a 100644 --- a/packages/kbn-es/tsconfig.json +++ b/packages/kbn-es/tsconfig.json @@ -11,6 +11,7 @@ "@kbn/dev-proc-runner", "@kbn/ci-stats-reporter", "@kbn/jest-serializers", - "@kbn/repo-info" + "@kbn/repo-info", + "@kbn/mock-idp-plugin" ] } diff --git a/packages/kbn-mock-idp-plugin/tsconfig.json b/packages/kbn-mock-idp-plugin/tsconfig.json index c95db79fa9363..e10840c0e3db5 100644 --- a/packages/kbn-mock-idp-plugin/tsconfig.json +++ b/packages/kbn-mock-idp-plugin/tsconfig.json @@ -11,6 +11,8 @@ "target/**/*" ], "kbn_references": [ - "@kbn/core" + "@kbn/core", + "@kbn/dev-utils", + "@kbn/config-schema" ] -} \ No newline at end of file +} diff --git a/src/cli/tsconfig.json b/src/cli/tsconfig.json index ebbbc19f75c79..3b3c0854975d3 100644 --- a/src/cli/tsconfig.json +++ b/src/cli/tsconfig.json @@ -17,6 +17,7 @@ "@kbn/config", "@kbn/dev-utils", "@kbn/apm-config-loader", + "@kbn/mock-idp-plugin", ], "exclude": [ "target/**/*", From 12cf10e06baf501578adad667db3de9d41033977 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:00:40 +0000 Subject: [PATCH 04/20] [CI] Auto-commit changed files from 'node scripts/generate codeowners' --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1dc3be1a66d6d..6c785f37b2eb2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -532,6 +532,7 @@ x-pack/packages/ml/runtime_field_utils @elastic/ml-ui x-pack/packages/ml/string_hash @elastic/ml-ui x-pack/packages/ml/trained_models_utils @elastic/ml-ui x-pack/packages/ml/url_state @elastic/ml-ui +packages/kbn-mock-idp-plugin @elastic/kibana-security packages/kbn-monaco @elastic/appex-sharedux x-pack/plugins/monitoring_collection @elastic/infra-monitoring-ui x-pack/plugins/monitoring @elastic/infra-monitoring-ui From 0fec2e07f898ba0bb8ac4652ec0bdcda742d476f Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Wed, 8 Nov 2023 14:47:15 +0000 Subject: [PATCH 05/20] Fix errors --- packages/kbn-es/tsconfig.json | 13 +++++-- packages/kbn-mock-idp-plugin/metadata.xml | 42 ---------------------- packages/kbn-mock-idp-plugin/tsconfig.json | 4 ++- tsconfig.base.json | 2 -- 4 files changed, 13 insertions(+), 48 deletions(-) delete mode 100644 packages/kbn-mock-idp-plugin/metadata.xml diff --git a/packages/kbn-es/tsconfig.json b/packages/kbn-es/tsconfig.json index 75059c2ef69cd..466d030cff7ef 100644 --- a/packages/kbn-es/tsconfig.json +++ b/packages/kbn-es/tsconfig.json @@ -3,14 +3,21 @@ "compilerOptions": { "outDir": "target/types" }, - "include": ["**/*.ts", "**/*.js", "**/*.json"], - "exclude": ["target/**/*"], + "include": [ + "**/*.ts", + "**/*.js", + "**/*.json" + ], + "exclude": [ + "target/**/*" + ], "kbn_references": [ "@kbn/tooling-log", "@kbn/dev-utils", "@kbn/dev-proc-runner", "@kbn/ci-stats-reporter", + "@kbn/mock-idp-plugin", "@kbn/jest-serializers", "@kbn/repo-info" ] -} +} \ No newline at end of file diff --git a/packages/kbn-mock-idp-plugin/metadata.xml b/packages/kbn-mock-idp-plugin/metadata.xml deleted file mode 100644 index c6819d1daf974..0000000000000 --- a/packages/kbn-mock-idp-plugin/metadata.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - MIIDYjCCAkqgAwIBAgIUZ2p8K7GMXGk6xwCS9S91BUl1JnAwDQYJKoZIhvcNAQEL - BQAwNDEyMDAGA1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5l - cmF0ZWQgQ0EwIBcNMjMwOTIzMTUyMDE0WhgPMjA3MzA5MTAxNTIwMTRaMBExDzAN - BgNVBAMTBmtpYmFuYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMOU - r52dbZ5dY0BoP2p7CEnOpG+qHTNrOAqZO/OJfniPMtpGmwAMl3WZDca6u2XkV2KE - qQyevQ2ADk6G3o8S2RU8mO/+UweuCDF7LHuSdxEGTpucidZErmVhEGUOFosL5UeB - AtIDWxvWwgK+W9Yzt5IEN2HzNCZ6h0dOSk2r9EjVMG5yF4Q6kuqOYxBT7jxoaOtO - OCrgBRummtUga4T13WZ/ZIyyHpXj2+JD4YEmrDyoTa7NLaphv0hnVhHXYoYBI/c6 - 2SwwAoBlmtDmlinwSACQ3o/8eLWk0tqkIP14rc3oFh3m7D2c3c2m2HXuyoSDMfGW - beG2IE1Q3idcGmeG3qsCAwEAAaOBjDCBiTAdBgNVHQ4EFgQUMOUM7w5jmIozDvnq - RpM779m5GigwHwYDVR0jBBgwFoAUMEwqwI5b0MYpNxwaHJ9Tw1Lp3p4wPAYDVR0R - BDUwM4IUaG9zdC5kb2NrZXIuaW50ZXJuYWyCCWxvY2FsaG9zdIIEZXMwM4IEZXMw - MoIEZXMwMTAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCxqvQYXSKqgpdl - SP4gXgwipAnYsoW9qkgWQODTvSBEzUdOWme0d3j7i2l6Ur/nVSv5YjkqAv1hf/yJ - Hrk9h+j29ZO/aQ/KDh5i/gTEUnPw3Bxbw47dfn23tjMWO7NCU1fr5HNztRsa/gQr - e9s07g25u/gTfTi9Fyu0lcRe3bXOLS/mFVcuC5oxuS65R9OlbIsiORkZ2EfwuNUf - wAAYOGPIjM2VlQCvBitefsd/SzRKHdxSPy6KSjkO6MGEGo87fr7B7Nx1qp1DVrK7 - q9XeP1Cuygjg9WTcnsvWvNw8CssyuFM6X/3tGjpPasXwLvNUoG2AairK2AYTWhvS - foE31cFg - - - - - - - - - \ No newline at end of file diff --git a/packages/kbn-mock-idp-plugin/tsconfig.json b/packages/kbn-mock-idp-plugin/tsconfig.json index c95db79fa9363..542ee12f5e412 100644 --- a/packages/kbn-mock-idp-plugin/tsconfig.json +++ b/packages/kbn-mock-idp-plugin/tsconfig.json @@ -11,6 +11,8 @@ "target/**/*" ], "kbn_references": [ - "@kbn/core" + "@kbn/core", + "@kbn/config-schema", + "@kbn/dev-utils" ] } \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json index efdf3d6fe4c64..3c5eac80abd25 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1314,8 +1314,6 @@ "@kbn/serverless/*": ["x-pack/plugins/serverless/*"], "@kbn/serverless-common-settings": ["packages/serverless/settings/common"], "@kbn/serverless-common-settings/*": ["packages/serverless/settings/common/*"], - "@kbn/serverless-mock-identity-provider": ["packages/serverless/mock_identity_provider"], - "@kbn/serverless-mock-identity-provider/*": ["packages/serverless/mock_identity_provider/*"], "@kbn/serverless-observability": ["x-pack/plugins/serverless_observability"], "@kbn/serverless-observability/*": ["x-pack/plugins/serverless_observability/*"], "@kbn/serverless-observability-settings": ["packages/serverless/settings/observability_project"], From 478cd79033c5a5216daa457c521ade52b403d5f6 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Wed, 8 Nov 2023 15:00:07 +0000 Subject: [PATCH 06/20] Explicit exports --- packages/kbn-mock-idp-plugin/common/index.ts | 21 ++++++++++++++++++-- packages/kbn-mock-idp-plugin/server/index.ts | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/kbn-mock-idp-plugin/common/index.ts b/packages/kbn-mock-idp-plugin/common/index.ts index 9fed036fab99a..aaaffc15f10f8 100644 --- a/packages/kbn-mock-idp-plugin/common/index.ts +++ b/packages/kbn-mock-idp-plugin/common/index.ts @@ -6,5 +6,22 @@ * Side Public License, v 1. */ -export * from './constants'; -export * from './utils'; +export { + MOCK_IDP_PLUGIN_PATH, + MOCK_IDP_METADATA_PATH, + MOCK_IDP_LOGIN_PATH, + MOCK_IDP_LOGOUT_PATH, + MOCK_IDP_REALM_NAME, + MOCK_IDP_ENTITY_ID, + MOCK_IDP_ROLE_MAPPING_NAME, + MOCK_IDP_ATTRIBUTE_PRINCIPAL, + MOCK_IDP_ATTRIBUTE_ROLES, + MOCK_IDP_ATTRIBUTE_EMAIL, + MOCK_IDP_ATTRIBUTE_NAME, +} from './constants'; +export { + createMockIdpMetadata, + createSAMLResponse, + ensureSAMLRoleMapping, + parseSAMLAuthnRequest, +} from './utils'; diff --git a/packages/kbn-mock-idp-plugin/server/index.ts b/packages/kbn-mock-idp-plugin/server/index.ts index 59be4a2359a9b..db807851d4564 100644 --- a/packages/kbn-mock-idp-plugin/server/index.ts +++ b/packages/kbn-mock-idp-plugin/server/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export * from './plugin'; +export { plugin } from './plugin'; From e4f17ac06037919b78962572017b37f7da375f9d Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:10:22 +0000 Subject: [PATCH 07/20] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- packages/kbn-es/tsconfig.json | 2 +- packages/kbn-mock-idp-plugin/tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kbn-es/tsconfig.json b/packages/kbn-es/tsconfig.json index 99de4f01b93e6..ddd551cafe88b 100644 --- a/packages/kbn-es/tsconfig.json +++ b/packages/kbn-es/tsconfig.json @@ -21,4 +21,4 @@ "@kbn/repo-info", "@kbn/mock-idp-plugin" ] -} \ No newline at end of file +} diff --git a/packages/kbn-mock-idp-plugin/tsconfig.json b/packages/kbn-mock-idp-plugin/tsconfig.json index 542ee12f5e412..b2e23a254929b 100644 --- a/packages/kbn-mock-idp-plugin/tsconfig.json +++ b/packages/kbn-mock-idp-plugin/tsconfig.json @@ -15,4 +15,4 @@ "@kbn/config-schema", "@kbn/dev-utils" ] -} \ No newline at end of file +} From 4f99a7464289edeb6c1c8d3d88862da7b2a9459f Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Wed, 8 Nov 2023 17:03:24 +0000 Subject: [PATCH 08/20] Fix logout flow and login selector validation --- packages/kbn-mock-idp-plugin/server/plugin.ts | 23 +------------------ src/cli/serve/serve.js | 7 ++++++ 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/packages/kbn-mock-idp-plugin/server/plugin.ts b/packages/kbn-mock-idp-plugin/server/plugin.ts index 7fec5281146a8..30c20baf39bf4 100644 --- a/packages/kbn-mock-idp-plugin/server/plugin.ts +++ b/packages/kbn-mock-idp-plugin/server/plugin.ts @@ -80,28 +80,7 @@ export const plugin: PluginInitializer = (): Plugin => ({ options: { authRequired: false }, }, async (context, request, response) => { - return response.redirected({ headers: { location: '/logout?SAMLResponse=something' } }); - } - ); - - let attemptsCounter = 0; - core.http.resources.register( - { - path: '/mock_idp/never_login', - validate: false, - options: { authRequired: false }, - }, - async (context, request, response) => { - return response.renderHtml({ - body: ` - - Kibana SAML Login - - - Attempt #${++attemptsCounter} - - `, - }); + return response.redirected({ headers: { location: '/' } }); } ); }, diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index a28ac1a5bf5bd..da231ee5bf6cb 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -120,6 +120,13 @@ export function applyConfigOverrides(rawConfig, opts, extraCliOptions) { description: 'Continue as Test User', hint: 'Allows testing serverless user roles', }); + // Add basic realm since defaults won't be applied when a provider has been configured + if (!has('xpack.security.authc.providers.basic')) { + set('xpack.security.authc.providers.basic.basic', { + order: 0, + enabled: true, + }); + } } if (!has('elasticsearch.serviceAccountToken') && opts.devCredentials !== false) { From 0e60429dad0a7eafb06e4967ed6570bea9140225 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Wed, 8 Nov 2023 17:33:23 +0000 Subject: [PATCH 09/20] Add user roles --- packages/kbn-mock-idp-plugin/server/plugin.ts | 51 +++++++++++++++---- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/kbn-mock-idp-plugin/server/plugin.ts b/packages/kbn-mock-idp-plugin/server/plugin.ts index 30c20baf39bf4..f72710f142495 100644 --- a/packages/kbn-mock-idp-plugin/server/plugin.ts +++ b/packages/kbn-mock-idp-plugin/server/plugin.ts @@ -38,12 +38,31 @@ export const plugin: PluginInitializer = (): Plugin => ({ }); } - const samlResponse = await createSAMLResponse({ - authnRequestId: samlRequest.ID, - kibanaUrl: samlRequest.AssertionConsumerServiceURL, - username: 'test_user', - roles: ['superuser'], - }); + const userRoles = [ + ['superuser', 'elastic_serverless'], + ['system_indices_superuser', 'system_indices_superuser'], + ['t1_analyst', 't1_analyst'], + ['t2_analyst', 't2_analyst'], + ['t3_analyst', 't3_analyst'], + ['threat_intelligence_analyst', 'threat_intelligence_analyst'], + ['rule_author', 'rule_author'], + ['soc_manager', 'soc_manager'], + ['detections_admin', 'detections_admin'], + ['platform_engineer', 'platform_engineer'], + ['endpoint_operations_analyst', 'endpoint_operations_analyst'], + ['endpoint_policy_manager', 'endpoint_policy_manager'], + ] as const; + + const samlResponses = await Promise.all( + userRoles.map((entries) => + createSAMLResponse({ + authnRequestId: samlRequest.ID, + kibanaUrl: samlRequest.AssertionConsumerServiceURL, + username: entries[0], + roles: [entries[1]], + }) + ) + ); return response.renderHtml({ body: ` @@ -51,10 +70,22 @@ export const plugin: PluginInitializer = (): Plugin => ({ Kibana SAML Login -
${samlRequest.ID}
-
- - +

Mock Identity Provider

+ +
    + ${userRoles + .map( + (entries, i) => + ` +
  • + +
  • + ` + ) + .join('')} +
`, From f863db39f5d2a6815de3c7f2774b15ac1a281849 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Wed, 8 Nov 2023 21:33:48 +0000 Subject: [PATCH 10/20] Fix cyclic project reference --- packages/kbn-mock-idp-plugin/server/plugin.ts | 2 +- packages/kbn-mock-idp-plugin/tsconfig.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kbn-mock-idp-plugin/server/plugin.ts b/packages/kbn-mock-idp-plugin/server/plugin.ts index f72710f142495..4a7d3220f23ee 100644 --- a/packages/kbn-mock-idp-plugin/server/plugin.ts +++ b/packages/kbn-mock-idp-plugin/server/plugin.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { PluginInitializer, Plugin } from '@kbn/core/server'; +import type { PluginInitializer, Plugin } from '@kbn/core-plugins-server'; import { schema } from '@kbn/config-schema'; import { diff --git a/packages/kbn-mock-idp-plugin/tsconfig.json b/packages/kbn-mock-idp-plugin/tsconfig.json index b2e23a254929b..a6bfa74544b0d 100644 --- a/packages/kbn-mock-idp-plugin/tsconfig.json +++ b/packages/kbn-mock-idp-plugin/tsconfig.json @@ -11,8 +11,8 @@ "target/**/*" ], "kbn_references": [ - "@kbn/core", + "@kbn/core-plugins-server", "@kbn/config-schema", "@kbn/dev-utils" ] -} +} \ No newline at end of file From 4882634f0538ab5827e19ece3d68aacdbab858db Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:40:17 +0000 Subject: [PATCH 11/20] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- packages/kbn-mock-idp-plugin/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-mock-idp-plugin/tsconfig.json b/packages/kbn-mock-idp-plugin/tsconfig.json index a6bfa74544b0d..1420a34208f13 100644 --- a/packages/kbn-mock-idp-plugin/tsconfig.json +++ b/packages/kbn-mock-idp-plugin/tsconfig.json @@ -15,4 +15,4 @@ "@kbn/config-schema", "@kbn/dev-utils" ] -} \ No newline at end of file +} From 022f61b56366765880cf3552c6aa8fad3728af3d Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Thu, 9 Nov 2023 12:28:29 +0000 Subject: [PATCH 12/20] Fix unit tests --- .../kbn-es/src/cli_commands/serverless.ts | 4 +- packages/kbn-es/src/utils/docker.test.ts | 3 + packages/kbn-es/src/utils/docker.ts | 101 ++++++++---------- packages/kbn-mock-idp-plugin/common/utils.ts | 9 +- packages/kbn-mock-idp-plugin/server/plugin.ts | 20 ++-- scripts/es.js | 1 + src/cli/serve/serve.js | 36 ++++--- 7 files changed, 87 insertions(+), 87 deletions(-) diff --git a/packages/kbn-es/src/cli_commands/serverless.ts b/packages/kbn-es/src/cli_commands/serverless.ts index abb8c41a66dd3..0743cf2e7b5b6 100644 --- a/packages/kbn-es/src/cli_commands/serverless.ts +++ b/packages/kbn-es/src/cli_commands/serverless.ts @@ -38,7 +38,7 @@ export const serverless: Command = { --host Publish ES docker container on additional host IP --port The port to bind to on 127.0.0.1 [default: ${DEFAULT_PORT}] --ssl Enable HTTP SSL on the ES cluster - --kibanaUrl Fully qualified URL where Kibana is hosted (including base path). Required for mock identity provider to function + --kibanaUrl Fully qualified URL where Kibana is hosted (including base path). [default: https://localhost:5601/] --skipTeardown If this process exits, leave the ES cluster running in the background --waitForReady Wait for the ES cluster to be ready to serve requests --resources Overrides resources under ES 'config/' directory, which are by default @@ -74,7 +74,7 @@ export const serverless: Command = { files: 'F', }, - string: ['tag', 'image', 'basePath', 'resources', 'host'], + string: ['tag', 'image', 'basePath', 'resources', 'host', 'kibanaUrl'], boolean: ['clean', 'ssl', 'kill', 'background', 'skipTeardown', 'waitForReady'], default: defaults, diff --git a/packages/kbn-es/src/utils/docker.test.ts b/packages/kbn-es/src/utils/docker.test.ts index 2d71a4e628e11..b1a2a4d530c2d 100644 --- a/packages/kbn-es/src/utils/docker.test.ts +++ b/packages/kbn-es/src/utils/docker.test.ts @@ -543,6 +543,7 @@ describe('runServerlessEsNode()', () => { describe('runServerlessCluster()', () => { test('should start 3 serverless nodes', async () => { + waitUntilClusterReadyMock.mockResolvedValue(); mockFs({ [baseEsPath]: {}, }); @@ -568,6 +569,7 @@ describe('runServerlessCluster()', () => { }); test(`should wait for the security index`, async () => { + waitUntilClusterReadyMock.mockResolvedValue(); waitForSecurityIndexMock.mockResolvedValue(); mockFs({ [baseEsPath]: {}, @@ -580,6 +582,7 @@ describe('runServerlessCluster()', () => { }); test(`should not wait for the security index when security is disabled`, async () => { + waitUntilClusterReadyMock.mockResolvedValue(); mockFs({ [baseEsPath]: {}, }); diff --git a/packages/kbn-es/src/utils/docker.ts b/packages/kbn-es/src/utils/docker.ts index 8a757a10f1236..ad6f1d2e63113 100644 --- a/packages/kbn-es/src/utils/docker.ts +++ b/packages/kbn-es/src/utils/docker.ts @@ -75,7 +75,7 @@ export interface ServerlessOptions extends EsClusterExecOptions, BaseOptions { background?: boolean; /** Wait for the ES cluster to be ready to serve requests */ waitForReady?: boolean; - /** Fully qualified URL where Kibana is hosted (including base path). Required for mock identity provider to function */ + /** Fully qualified URL where Kibana is hosted (including base path) */ kibanaUrl?: string; /** * Resource file(s) to overwrite @@ -452,48 +452,50 @@ export function resolveEsArgs( DEFAULT_SSL_ESARGS.forEach((arg) => { esArgs.set(arg[0], arg[1]); }); - } - // Configure mock identify provider - if ('kibanaUrl' in options && options.kibanaUrl) { - esArgs.set('xpack.security.authc.token.enabled', 'true'); - esArgs.set(`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.order`, '0'); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.metadata.path`, - `${SERVERLESS_CONFIG_PATH}secrets/idp_metadata.xml` - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.entity_id`, - MOCK_IDP_ENTITY_ID - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.entity_id`, - options.kibanaUrl - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.acs`, - `${options.kibanaUrl}/api/security/saml/callback` - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.logout`, - `${options.kibanaUrl}/logout` - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.principal`, - MOCK_IDP_ATTRIBUTE_PRINCIPAL - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.groups`, - MOCK_IDP_ATTRIBUTE_ROLES - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.name`, - MOCK_IDP_ATTRIBUTE_EMAIL - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.mail`, - MOCK_IDP_ATTRIBUTE_NAME - ); + // Configure mock identify provider (ES only supports SAML when running in SSL mode) + if ('kibanaUrl' in options && options.kibanaUrl) { + const trimTrailingSlash = (url: string) => (url.endsWith('/') ? url.slice(0, -1) : url); + + esArgs.set('xpack.security.authc.token.enabled', 'true'); + esArgs.set(`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.order`, '0'); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.metadata.path`, + `${SERVERLESS_CONFIG_PATH}secrets/idp_metadata.xml` + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.entity_id`, + MOCK_IDP_ENTITY_ID + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.entity_id`, + trimTrailingSlash(options.kibanaUrl) + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.acs`, + `${trimTrailingSlash(options.kibanaUrl)}/api/security/saml/callback` + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.logout`, + `${trimTrailingSlash(options.kibanaUrl)}/logout` + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.principal`, + MOCK_IDP_ATTRIBUTE_PRINCIPAL + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.groups`, + MOCK_IDP_ATTRIBUTE_ROLES + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.name`, + MOCK_IDP_ATTRIBUTE_EMAIL + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.mail`, + MOCK_IDP_ATTRIBUTE_NAME + ); + } } if (customEsArgs) { @@ -602,7 +604,7 @@ export async function setupServerlessVolumes(log: ToolingLog, options: Serverles } // Create and add meta data for mock identity provider - if (options.kibanaUrl) { + if (options.ssl && options.kibanaUrl) { const metadata = await createMockIdpMetadata(options.kibanaUrl); await Fsp.writeFile(SERVERLESS_IDP_METADATA_PATH, metadata); volumeCmds.push( @@ -747,18 +749,7 @@ export async function runServerlessCluster(log: ToolingLog, options: ServerlessO : {}), }); const readyPromise = waitUntilClusterReady({ client, expectedStatus: 'green', log }).then(() => { - if (!options.ssl) { - return; - } - - if (!options.kibanaUrl) { - log.warning( - `Unable to configure mock identity provider. - - Run Elasticsearch with ${chalk.bold.cyan( - '--kibanaUrl ' - )} option to be able to login using ${chalk.bold.cyan(MOCK_IDP_REALM_NAME)} realm` - ); + if (!options.ssl || !options.kibanaUrl) { return; } diff --git a/packages/kbn-mock-idp-plugin/common/utils.ts b/packages/kbn-mock-idp-plugin/common/utils.ts index 58b7acb9cb462..5d55fbc565685 100644 --- a/packages/kbn-mock-idp-plugin/common/utils.ts +++ b/packages/kbn-mock-idp-plugin/common/utils.ts @@ -40,6 +40,7 @@ const parseStringAsync = promisify(parseString); export async function createMockIdpMetadata(kibanaUrl: string) { const signingKey = await readFile(KBN_CERT_PATH); const cert = new X509Certificate(signingKey); + const trimTrailingSlash = (url: string) => (url.endsWith('/') ? url.slice(0, -1) : url); return ` + Location="${trimTrailingSlash(kibanaUrl)}${MOCK_IDP_LOGOUT_PATH}" /> + Location="${trimTrailingSlash(kibanaUrl)}${MOCK_IDP_LOGOUT_PATH}" /> + Location="${trimTrailingSlash(kibanaUrl)}${MOCK_IDP_LOGIN_PATH}" /> + Location="${trimTrailingSlash(kibanaUrl)}${MOCK_IDP_LOGIN_PATH}" /> `; diff --git a/packages/kbn-mock-idp-plugin/server/plugin.ts b/packages/kbn-mock-idp-plugin/server/plugin.ts index 4a7d3220f23ee..e24e7418cce62 100644 --- a/packages/kbn-mock-idp-plugin/server/plugin.ts +++ b/packages/kbn-mock-idp-plugin/server/plugin.ts @@ -38,8 +38,7 @@ export const plugin: PluginInitializer = (): Plugin => ({ }); } - const userRoles = [ - ['superuser', 'elastic_serverless'], + const userRoles: Array<[string, string]> = [ ['system_indices_superuser', 'system_indices_superuser'], ['t1_analyst', 't1_analyst'], ['t2_analyst', 't2_analyst'], @@ -51,15 +50,15 @@ export const plugin: PluginInitializer = (): Plugin => ({ ['platform_engineer', 'platform_engineer'], ['endpoint_operations_analyst', 'endpoint_operations_analyst'], ['endpoint_policy_manager', 'endpoint_policy_manager'], - ] as const; + ]; const samlResponses = await Promise.all( - userRoles.map((entries) => + userRoles.map(([username, role]) => createSAMLResponse({ authnRequestId: samlRequest.ID, kibanaUrl: samlRequest.AssertionConsumerServiceURL, - username: entries[0], - roles: [entries[1]], + username, + roles: [role], }) ) ); @@ -67,20 +66,21 @@ export const plugin: PluginInitializer = (): Plugin => ({ return response.renderHtml({ body: ` - Kibana SAML Login + Mock Identity Provider -

Mock Identity Provider

+

Mock Identity Provider

+

Pick a role:

    ${userRoles .map( - (entries, i) => + ([username], i) => `
  • - +
  • ` ) diff --git a/scripts/es.js b/scripts/es.js index 1fcd221c97904..1cee27b7685b5 100644 --- a/scripts/es.js +++ b/scripts/es.js @@ -20,6 +20,7 @@ kbnEs 'source-path': resolve(__dirname, '../../elasticsearch'), 'base-path': resolve(__dirname, '../.es'), ssl: false, + kibanaUrl: 'https://localhost:5601/', }) .catch(function (e) { console.error(e); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index da231ee5bf6cb..adb951def861a 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -108,24 +108,26 @@ export function applyConfigOverrides(rawConfig, opts, extraCliOptions) { delete extraCliOptions.env; if (opts.dev) { - if (opts.serverless && opts.ssl) { + if (opts.serverless) { setServerlessKibanaDevServiceAccountIfPossible(get, set, opts); - // Load mock identity provider plugin and configure realm - set('plugins.paths', _.compact([].concat(get('plugins.paths'), MOCK_IDP_PLUGIN_PATH))); - set(`xpack.security.authc.providers.saml.${MOCK_IDP_REALM_NAME}`, { - order: Number.MAX_SAFE_INTEGER, - realm: MOCK_IDP_REALM_NAME, - icon: 'user', - description: 'Continue as Test User', - hint: 'Allows testing serverless user roles', - }); - // Add basic realm since defaults won't be applied when a provider has been configured - if (!has('xpack.security.authc.providers.basic')) { - set('xpack.security.authc.providers.basic.basic', { - order: 0, - enabled: true, + // Load mock identity provider plugin and configure realm if supported (ES only supports SAML when run with SSL) + if (opts.ssl) { + set('plugins.paths', _.compact([].concat(get('plugins.paths'), MOCK_IDP_PLUGIN_PATH))); + set(`xpack.security.authc.providers.saml.${MOCK_IDP_REALM_NAME}`, { + order: Number.MAX_SAFE_INTEGER, + realm: MOCK_IDP_REALM_NAME, + icon: 'user', + description: 'Continue as Test User', + hint: 'Allows testing serverless user roles', }); + // Add basic realm since defaults won't be applied when a provider has been configured + if (!has('xpack.security.authc.providers.basic')) { + set('xpack.security.authc.providers.basic.basic', { + order: 0, + enabled: true, + }); + } } } @@ -292,7 +294,9 @@ export default function (program) { // We can tell users they only have to run with `yarn start --run-examples` to get those // local links to work. Similar to what we do for "View in Console" links in our // elastic.co links. - basePath: opts.runExamples ? false : !!opts.basePath, + // We also want to run without base path when running in serverless mode so that Elasticsearch can + // connect to Kibana's mock identity provider. + basePath: opts.runExamples || isServerlessMode ? false : !!opts.basePath, optimize: !!opts.optimize, disableOptimizer: !opts.optimizer, oss: !!opts.oss, From ebbf87d4ab3ea1b12c09f551e7262050fc2b11f2 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Fri, 10 Nov 2023 10:28:50 +0000 Subject: [PATCH 13/20] Add unit tests --- packages/kbn-es/src/utils/docker.test.ts | 105 ++++++++++++++++++- packages/kbn-es/src/utils/docker.ts | 124 ++++++++++++----------- packages/kbn-es/tsconfig.json | 5 +- 3 files changed, 167 insertions(+), 67 deletions(-) diff --git a/packages/kbn-es/src/utils/docker.test.ts b/packages/kbn-es/src/utils/docker.test.ts index b1a2a4d530c2d..7b4b701fe8746 100644 --- a/packages/kbn-es/src/utils/docker.test.ts +++ b/packages/kbn-es/src/utils/docker.test.ts @@ -32,15 +32,17 @@ import { ServerlessOptions, } from './docker'; import { ToolingLog, ToolingLogCollectingWriter } from '@kbn/tooling-log'; -import { ES_P12_PATH } from '@kbn/dev-utils'; +import { CA_CERT_PATH, ES_P12_PATH } from '@kbn/dev-utils'; import { SERVERLESS_CONFIG_PATH, SERVERLESS_RESOURCES_PATHS, SERVERLESS_SECRETS_PATH, SERVERLESS_JWKS_PATH, + SERVERLESS_IDP_METADATA_PATH, } from '../paths'; import * as waitClusterUtil from './wait_until_cluster_ready'; import * as waitForSecurityIndexUtil from './wait_for_security_index'; +import * as mockIdpPluginUtil from '@kbn/mock-idp-plugin/common'; jest.mock('execa'); const execa = jest.requireMock('execa'); @@ -58,6 +60,8 @@ jest.mock('./wait_for_security_index', () => ({ waitForSecurityIndex: jest.fn(), })); +jest.mock('@kbn/mock-idp-plugin/common'); + const log = new ToolingLog(); const logWriter = new ToolingLogCollectingWriter(); log.setWriters([logWriter]); @@ -69,6 +73,8 @@ const serverlessObjectStorePath = `${baseEsPath}/${serverlessDir}`; const waitUntilClusterReadyMock = jest.spyOn(waitClusterUtil, 'waitUntilClusterReady'); const waitForSecurityIndexMock = jest.spyOn(waitForSecurityIndexUtil, 'waitForSecurityIndex'); +const ensureSAMLRoleMappingMock = jest.spyOn(mockIdpPluginUtil, 'ensureSAMLRoleMapping'); +const createMockIdpMetadataMock = jest.spyOn(mockIdpPluginUtil, 'createMockIdpMetadata'); beforeEach(() => { jest.resetAllMocks(); @@ -423,6 +429,68 @@ describe('resolveEsArgs()', () => { ] `); }); + + test('should add SAML realm args when kibanaUrl and SSL are passed', () => { + const esArgs = resolveEsArgs([], { + ssl: true, + kibanaUrl: 'https://localhost:5601/', + }); + + expect(esArgs).toHaveLength(28); + expect(esArgs).toMatchInlineSnapshot(` + Array [ + "--env", + "xpack.security.http.ssl.enabled=true", + "--env", + "xpack.security.http.ssl.keystore.path=/usr/share/elasticsearch/config/certs/elasticsearch.p12", + "--env", + "xpack.security.http.ssl.verification_mode=certificate", + "--env", + "xpack.security.authc.token.enabled=true", + "--env", + "xpack.security.authc.realms.saml.mock-idp.order=0", + "--env", + "xpack.security.authc.realms.saml.mock-idp.idp.metadata.path=/usr/share/elasticsearch/config/secrets/idp_metadata.xml", + "--env", + "xpack.security.authc.realms.saml.mock-idp.idp.entity_id=urn:mock-idp", + "--env", + "xpack.security.authc.realms.saml.mock-idp.sp.entity_id=https://localhost:5601", + "--env", + "xpack.security.authc.realms.saml.mock-idp.sp.acs=https://localhost:5601/api/security/saml/callback", + "--env", + "xpack.security.authc.realms.saml.mock-idp.sp.logout=https://localhost:5601/logout", + "--env", + "xpack.security.authc.realms.saml.mock-idp.attributes.principal=http://saml.elastic-cloud.com/attributes/principal", + "--env", + "xpack.security.authc.realms.saml.mock-idp.attributes.groups=http://saml.elastic-cloud.com/attributes/roles", + "--env", + "xpack.security.authc.realms.saml.mock-idp.attributes.name=http://saml.elastic-cloud.com/attributes/email", + "--env", + "xpack.security.authc.realms.saml.mock-idp.attributes.mail=http://saml.elastic-cloud.com/attributes/name", + ] + `); + }); + + test('should not add SAML realm args when security is disabled', () => { + const esArgs = resolveEsArgs([['xpack.security.enabled', 'false']], { + ssl: true, + kibanaUrl: 'https://localhost:5601/', + }); + + expect(esArgs).toHaveLength(8); + expect(esArgs).toMatchInlineSnapshot(` + Array [ + "--env", + "xpack.security.enabled=false", + "--env", + "xpack.security.http.ssl.enabled=true", + "--env", + "xpack.security.http.ssl.keystore.path=/usr/share/elasticsearch/config/certs/elasticsearch.p12", + "--env", + "xpack.security.http.ssl.verification_mode=certificate", + ] + `); + }); }); describe('setupServerlessVolumes()', () => { @@ -463,21 +531,29 @@ describe('setupServerlessVolumes()', () => { expect(existsSync(`${serverlessObjectStorePath}/cluster_state/lease`)).toBe(false); }); - test('should add SSL volumes when ssl is passed', async () => { + test('should add SSL and IDP metadata volumes when ssl and kibanaUrl are passed', async () => { mockFs(existingObjectStore); + createMockIdpMetadataMock.mockResolvedValue(''); + + const volumeCmd = await setupServerlessVolumes(log, { + basePath: baseEsPath, + ssl: true, + kibanaUrl: 'https://localhost:5603/', + }); - const volumeCmd = await setupServerlessVolumes(log, { basePath: baseEsPath, ssl: true }); + expect(createMockIdpMetadataMock).toHaveBeenCalledTimes(1); + expect(createMockIdpMetadataMock).toHaveBeenCalledWith('https://localhost:5603/'); const requiredPaths = [ `${baseEsPath}:/objectstore:z`, + SERVERLESS_IDP_METADATA_PATH, ES_P12_PATH, ...SERVERLESS_RESOURCES_PATHS, ]; const pathsNotIncludedInCmd = requiredPaths.filter( (path) => !volumeCmd.some((cmd) => cmd.includes(path)) ); - - expect(volumeCmd).toHaveLength(20); + expect(volumeCmd).toHaveLength(22); expect(pathsNotIncludedInCmd).toEqual([]); }); @@ -568,6 +644,25 @@ describe('runServerlessCluster()', () => { expect(waitUntilClusterReadyMock.mock.calls[0][0].readyTimeout).toEqual(undefined); }); + test(`should create SAML role mapping when ssl and kibanaUrl are passed`, async () => { + waitUntilClusterReadyMock.mockResolvedValue(); + mockFs({ + [CA_CERT_PATH]: '', + [baseEsPath]: {}, + }); + execa.mockImplementation(() => Promise.resolve({ stdout: '' })); + createMockIdpMetadataMock.mockResolvedValue(''); + + await runServerlessCluster(log, { + basePath: baseEsPath, + waitForReady: true, + ssl: true, + kibanaUrl: 'https://localhost:5601/', + }); + + expect(ensureSAMLRoleMappingMock).toHaveBeenCalledTimes(1); + }); + test(`should wait for the security index`, async () => { waitUntilClusterReadyMock.mockResolvedValue(); waitForSecurityIndexMock.mockResolvedValue(); diff --git a/packages/kbn-es/src/utils/docker.ts b/packages/kbn-es/src/utils/docker.ts index ad6f1d2e63113..75a6cdde3c0c5 100644 --- a/packages/kbn-es/src/utils/docker.ts +++ b/packages/kbn-es/src/utils/docker.ts @@ -452,50 +452,6 @@ export function resolveEsArgs( DEFAULT_SSL_ESARGS.forEach((arg) => { esArgs.set(arg[0], arg[1]); }); - - // Configure mock identify provider (ES only supports SAML when running in SSL mode) - if ('kibanaUrl' in options && options.kibanaUrl) { - const trimTrailingSlash = (url: string) => (url.endsWith('/') ? url.slice(0, -1) : url); - - esArgs.set('xpack.security.authc.token.enabled', 'true'); - esArgs.set(`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.order`, '0'); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.metadata.path`, - `${SERVERLESS_CONFIG_PATH}secrets/idp_metadata.xml` - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.entity_id`, - MOCK_IDP_ENTITY_ID - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.entity_id`, - trimTrailingSlash(options.kibanaUrl) - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.acs`, - `${trimTrailingSlash(options.kibanaUrl)}/api/security/saml/callback` - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.logout`, - `${trimTrailingSlash(options.kibanaUrl)}/logout` - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.principal`, - MOCK_IDP_ATTRIBUTE_PRINCIPAL - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.groups`, - MOCK_IDP_ATTRIBUTE_ROLES - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.name`, - MOCK_IDP_ATTRIBUTE_EMAIL - ); - esArgs.set( - `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.mail`, - MOCK_IDP_ATTRIBUTE_NAME - ); - } } if (customEsArgs) { @@ -512,6 +468,55 @@ export function resolveEsArgs( esArgs.set('ELASTIC_PASSWORD', password); } + // Configure mock identify provider (ES only supports SAML when running in SSL mode) + if ( + ssl && + 'kibanaUrl' in options && + options.kibanaUrl && + esArgs.get('xpack.security.enabled') !== 'false' + ) { + const trimTrailingSlash = (url: string) => (url.endsWith('/') ? url.slice(0, -1) : url); + + esArgs.set('xpack.security.authc.token.enabled', 'true'); + esArgs.set(`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.order`, '0'); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.metadata.path`, + `${SERVERLESS_CONFIG_PATH}secrets/idp_metadata.xml` + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.entity_id`, + MOCK_IDP_ENTITY_ID + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.entity_id`, + trimTrailingSlash(options.kibanaUrl) + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.acs`, + `${trimTrailingSlash(options.kibanaUrl)}/api/security/saml/callback` + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.logout`, + `${trimTrailingSlash(options.kibanaUrl)}/logout` + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.principal`, + MOCK_IDP_ATTRIBUTE_PRINCIPAL + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.groups`, + MOCK_IDP_ATTRIBUTE_ROLES + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.name`, + MOCK_IDP_ATTRIBUTE_EMAIL + ); + esArgs.set( + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.mail`, + MOCK_IDP_ATTRIBUTE_NAME + ); + } + return Array.from(esArgs).flatMap((e) => ['--env', e.join('=')]); } @@ -532,7 +537,7 @@ export function getDockerFileMountPath(hostPath: string) { * Setup local volumes for Serverless ES */ export async function setupServerlessVolumes(log: ToolingLog, options: ServerlessOptions) { - const { basePath, clean, ssl, files, resources } = options; + const { basePath, clean, ssl, kibanaUrl, files, resources } = options; const objectStorePath = resolve(basePath, 'stateless'); log.info(chalk.bold(`Checking for local serverless ES object store at ${objectStorePath}`)); @@ -604,8 +609,8 @@ export async function setupServerlessVolumes(log: ToolingLog, options: Serverles } // Create and add meta data for mock identity provider - if (options.ssl && options.kibanaUrl) { - const metadata = await createMockIdpMetadata(options.kibanaUrl); + if (ssl && kibanaUrl) { + const metadata = await createMockIdpMetadata(kibanaUrl); await Fsp.writeFile(SERVERLESS_IDP_METADATA_PATH, metadata); volumeCmds.push( '--volume', @@ -748,21 +753,22 @@ export async function runServerlessCluster(log: ToolingLog, options: ServerlessO } : {}), }); - const readyPromise = waitUntilClusterReady({ client, expectedStatus: 'green', log }).then(() => { - if (!options.ssl || !options.kibanaUrl) { - return; - } - const roleMappingPromise = ensureSAMLRoleMapping(client); + const readyPromise = waitUntilClusterReady({ client, expectedStatus: 'green', log }).then( + async () => { + if (!options.ssl || !options.kibanaUrl) { + return; + } - log.success( - `Created role mapping for mock identity provider. You can now login using ${chalk.bold.cyan( - MOCK_IDP_REALM_NAME - )} realm` - ); + await ensureSAMLRoleMapping(client); - return roleMappingPromise; - }); + log.success( + `Created role mapping for mock identity provider. You can now login using ${chalk.bold.cyan( + MOCK_IDP_REALM_NAME + )} realm` + ); + } + ); if (options.waitForReady) { log.info('Waiting until ES is ready to serve requests...'); diff --git a/packages/kbn-es/tsconfig.json b/packages/kbn-es/tsconfig.json index ddd551cafe88b..466d030cff7ef 100644 --- a/packages/kbn-es/tsconfig.json +++ b/packages/kbn-es/tsconfig.json @@ -18,7 +18,6 @@ "@kbn/ci-stats-reporter", "@kbn/mock-idp-plugin", "@kbn/jest-serializers", - "@kbn/repo-info", - "@kbn/mock-idp-plugin" + "@kbn/repo-info" ] -} +} \ No newline at end of file From 936d0fc0b9ac56d218e284a545126bbe90908053 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 10 Nov 2023 10:37:09 +0000 Subject: [PATCH 14/20] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- packages/kbn-es/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-es/tsconfig.json b/packages/kbn-es/tsconfig.json index 466d030cff7ef..b40ca33825562 100644 --- a/packages/kbn-es/tsconfig.json +++ b/packages/kbn-es/tsconfig.json @@ -20,4 +20,4 @@ "@kbn/jest-serializers", "@kbn/repo-info" ] -} \ No newline at end of file +} From e1958ae1f468af9727081301bcc4065b1d8c9d3c Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Tue, 14 Nov 2023 11:22:06 +0000 Subject: [PATCH 15/20] Update packages/kbn-es/src/utils/docker.ts Co-authored-by: Aleh Zasypkin --- packages/kbn-es/src/utils/docker.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kbn-es/src/utils/docker.ts b/packages/kbn-es/src/utils/docker.ts index 75a6cdde3c0c5..73e5e1fc77288 100644 --- a/packages/kbn-es/src/utils/docker.ts +++ b/packages/kbn-es/src/utils/docker.ts @@ -477,7 +477,6 @@ export function resolveEsArgs( ) { const trimTrailingSlash = (url: string) => (url.endsWith('/') ? url.slice(0, -1) : url); - esArgs.set('xpack.security.authc.token.enabled', 'true'); esArgs.set(`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.order`, '0'); esArgs.set( `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.metadata.path`, From 6324b49b0d6996f541a4d0f6b1c8061111c39043 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Tue, 14 Nov 2023 13:06:25 +0000 Subject: [PATCH 16/20] Fix unit test --- packages/kbn-es/src/utils/docker.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kbn-es/src/utils/docker.test.ts b/packages/kbn-es/src/utils/docker.test.ts index 7b4b701fe8746..b574447a20508 100644 --- a/packages/kbn-es/src/utils/docker.test.ts +++ b/packages/kbn-es/src/utils/docker.test.ts @@ -436,7 +436,7 @@ describe('resolveEsArgs()', () => { kibanaUrl: 'https://localhost:5601/', }); - expect(esArgs).toHaveLength(28); + expect(esArgs).toHaveLength(26); expect(esArgs).toMatchInlineSnapshot(` Array [ "--env", @@ -446,8 +446,6 @@ describe('resolveEsArgs()', () => { "--env", "xpack.security.http.ssl.verification_mode=certificate", "--env", - "xpack.security.authc.token.enabled=true", - "--env", "xpack.security.authc.realms.saml.mock-idp.order=0", "--env", "xpack.security.authc.realms.saml.mock-idp.idp.metadata.path=/usr/share/elasticsearch/config/secrets/idp_metadata.xml", From 257ad5dbd4ed3831e1a45ff240394419a4c19461 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Mon, 20 Nov 2023 15:55:14 +0000 Subject: [PATCH 17/20] Change to dynamic import --- src/cli/serve/serve.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index adb951def861a..75a18378f56b3 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -15,7 +15,6 @@ import { isKibanaDistributable } from '@kbn/repo-info'; import { readKeystore } from '../keystore/read_keystore'; import { compileConfigStack } from './compile_config_stack'; import { getConfigFromFiles } from '@kbn/config'; -import { MOCK_IDP_PLUGIN_PATH, MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-plugin/common'; const DEV_MODE_PATH = '@kbn/cli-dev-mode'; const DEV_MODE_SUPPORTED = canRequire(DEV_MODE_PATH); @@ -112,7 +111,20 @@ export function applyConfigOverrides(rawConfig, opts, extraCliOptions) { setServerlessKibanaDevServiceAccountIfPossible(get, set, opts); // Load mock identity provider plugin and configure realm if supported (ES only supports SAML when run with SSL) - if (opts.ssl) { + if (opts.ssl && canRequire('@kbn/mock-idp-plugin/common')) { + // Ensure the plugin is loaded in dynamically to exclude from production build + const { + MOCK_IDP_PLUGIN_PATH, + MOCK_IDP_REALM_NAME, + } = require('@kbn/mock-idp-plugin/common'); + + if (has('server.basePath')) { + console.log( + `Custom base path is not supported when running in Serverless, it will be removed.` + ); + _.unset(rawConfig, 'server.basePath'); + } + set('plugins.paths', _.compact([].concat(get('plugins.paths'), MOCK_IDP_PLUGIN_PATH))); set(`xpack.security.authc.providers.saml.${MOCK_IDP_REALM_NAME}`, { order: Number.MAX_SAFE_INTEGER, From 3aaacf728d120a20e2f3489e1d3c1db3a3c11f0c Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Mon, 20 Nov 2023 18:29:40 +0100 Subject: [PATCH 18/20] Fixes type error --- packages/kbn-mock-idp-plugin/server/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-mock-idp-plugin/server/plugin.ts b/packages/kbn-mock-idp-plugin/server/plugin.ts index e24e7418cce62..20c115d80cf2c 100644 --- a/packages/kbn-mock-idp-plugin/server/plugin.ts +++ b/packages/kbn-mock-idp-plugin/server/plugin.ts @@ -16,7 +16,7 @@ import { parseSAMLAuthnRequest, } from '../common'; -export const plugin: PluginInitializer = (): Plugin => ({ +export const plugin: PluginInitializer = async (): Promise => ({ setup(core) { core.http.resources.register( { From 64e9018fc5c105dd03b1af0ae8bc6ed4c94e21aa Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Tue, 21 Nov 2023 12:15:30 +0100 Subject: [PATCH 19/20] Use fs/promises to read CA_CERT --- packages/kbn-es/src/utils/docker.ts | 33 ++++++++++++++++------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/kbn-es/src/utils/docker.ts b/packages/kbn-es/src/utils/docker.ts index d9d275c119a97..9064b20cfea53 100644 --- a/packages/kbn-es/src/utils/docker.ts +++ b/packages/kbn-es/src/utils/docker.ts @@ -7,7 +7,6 @@ */ import chalk from 'chalk'; import execa from 'execa'; -import fs from 'fs'; import Fsp from 'fs/promises'; import { resolve, basename, join } from 'path'; import { Client, ClientOptions, HttpConnection } from '@elastic/elasticsearch'; @@ -738,26 +737,30 @@ export async function runServerlessCluster(log: ToolingLog, options: ServerlessO portCmd[1].lastIndexOf(':') )}`; + let clientExtOptions = {}; + if (options.ssl) { + const ca = await Fsp.readFile(CA_CERT_PATH); + clientExtOptions = { + tls: { + ca, + // NOTE: Even though we've added ca into the tls options, we are using 127.0.0.1 instead of localhost + // for the ip which is not validated. As such we are getting the error + // Hostname/IP does not match certificate's altnames: IP: 127.0.0.1 is not in the cert's list: + // To work around that we are overriding the function checkServerIdentity too + checkServerIdentity: () => { + return undefined; + }, + }, + }; + } + const client = getESClient({ node: esNodeUrl, auth: { username: ELASTIC_SERVERLESS_SUPERUSER, password: ELASTIC_SERVERLESS_SUPERUSER_PASSWORD, }, - ...(options.ssl - ? { - tls: { - ca: [fs.readFileSync(CA_CERT_PATH)], - // NOTE: Even though we've added ca into the tls options, we are using 127.0.0.1 instead of localhost - // for the ip which is not validated. As such we are getting the error - // Hostname/IP does not match certificate's altnames: IP: 127.0.0.1 is not in the cert's list: - // To work around that we are overriding the function checkServerIdentity too - checkServerIdentity: () => { - return undefined; - }, - }, - } - : {}), + ...clientExtOptions, }); const readyPromise = waitUntilClusterReady({ client, expectedStatus: 'green', log }).then( From e6e019895e351173fba24cb3610130d38c618b9c Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Tue, 21 Nov 2023 15:01:36 +0100 Subject: [PATCH 20/20] Revert "Use fs/promises to read CA_CERT" This reverts commit 64e9018fc5c105dd03b1af0ae8bc6ed4c94e21aa. --- packages/kbn-es/src/utils/docker.ts | 33 +++++++++++++---------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/kbn-es/src/utils/docker.ts b/packages/kbn-es/src/utils/docker.ts index e60298d6951d8..73e5e1fc77288 100644 --- a/packages/kbn-es/src/utils/docker.ts +++ b/packages/kbn-es/src/utils/docker.ts @@ -7,6 +7,7 @@ */ import chalk from 'chalk'; import execa from 'execa'; +import fs from 'fs'; import Fsp from 'fs/promises'; import { resolve, basename, join } from 'path'; import { Client, ClientOptions, HttpConnection } from '@elastic/elasticsearch'; @@ -730,30 +731,26 @@ export async function runServerlessCluster(log: ToolingLog, options: ServerlessO portCmd[1].lastIndexOf(':') )}`; - let clientExtOptions = {}; - if (options.ssl) { - const ca = await Fsp.readFile(CA_CERT_PATH); - clientExtOptions = { - tls: { - ca, - // NOTE: Even though we've added ca into the tls options, we are using 127.0.0.1 instead of localhost - // for the ip which is not validated. As such we are getting the error - // Hostname/IP does not match certificate's altnames: IP: 127.0.0.1 is not in the cert's list: - // To work around that we are overriding the function checkServerIdentity too - checkServerIdentity: () => { - return undefined; - }, - }, - }; - } - const client = getESClient({ node: esNodeUrl, auth: { username: ELASTIC_SERVERLESS_SUPERUSER, password: ELASTIC_SERVERLESS_SUPERUSER_PASSWORD, }, - ...clientExtOptions, + ...(options.ssl + ? { + tls: { + ca: [fs.readFileSync(CA_CERT_PATH)], + // NOTE: Even though we've added ca into the tls options, we are using 127.0.0.1 instead of localhost + // for the ip which is not validated. As such we are getting the error + // Hostname/IP does not match certificate's altnames: IP: 127.0.0.1 is not in the cert's list: + // To work around that we are overriding the function checkServerIdentity too + checkServerIdentity: () => { + return undefined; + }, + }, + } + : {}), }); const readyPromise = waitUntilClusterReady({ client, expectedStatus: 'green', log }).then(