Revert "Add mock identity provider for serverless (#170852)"

This reverts commit 1fb0313a52.
This commit is contained in:
Brad White 2023-11-15 12:20:16 -07:00
parent 65ebb69a9f
commit d09e47abbc
20 changed files with 41 additions and 710 deletions

1
.github/CODEOWNERS vendored
View file

@ -534,7 +534,6 @@ 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/obs-ux-infra_services-team
x-pack/plugins/monitoring @elastic/obs-ux-infra_services-team

View file

@ -1232,7 +1232,6 @@
"@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",

View file

@ -38,7 +38,6 @@ 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). [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 +73,7 @@ export const serverless: Command = {
files: 'F',
},
string: ['tag', 'image', 'basePath', 'resources', 'host', 'kibanaUrl'],
string: ['tag', 'image', 'basePath', 'resources', 'host'],
boolean: ['clean', 'ssl', 'kill', 'background', 'skipTeardown', 'waitForReady'],
default: defaults,

View file

@ -8,7 +8,6 @@
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;
@ -52,8 +51,6 @@ 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,

View file

@ -32,17 +32,15 @@ import {
ServerlessOptions,
} from './docker';
import { ToolingLog, ToolingLogCollectingWriter } from '@kbn/tooling-log';
import { CA_CERT_PATH, ES_P12_PATH } from '@kbn/dev-utils';
import { 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');
@ -60,8 +58,6 @@ 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]);
@ -73,8 +69,6 @@ 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();
@ -429,66 +423,6 @@ 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(26);
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.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()', () => {
@ -529,29 +463,21 @@ describe('setupServerlessVolumes()', () => {
expect(existsSync(`${serverlessObjectStorePath}/cluster_state/lease`)).toBe(false);
});
test('should add SSL and IDP metadata volumes when ssl and kibanaUrl are passed', async () => {
test('should add SSL volumes when ssl is passed', async () => {
mockFs(existingObjectStore);
createMockIdpMetadataMock.mockResolvedValue('<xml/>');
const volumeCmd = await setupServerlessVolumes(log, {
basePath: baseEsPath,
ssl: true,
kibanaUrl: 'https://localhost:5603/',
});
expect(createMockIdpMetadataMock).toHaveBeenCalledTimes(1);
expect(createMockIdpMetadataMock).toHaveBeenCalledWith('https://localhost:5603/');
const volumeCmd = await setupServerlessVolumes(log, { basePath: baseEsPath, ssl: true });
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(22);
expect(volumeCmd).toHaveLength(20);
expect(pathsNotIncludedInCmd).toEqual([]);
});
@ -617,7 +543,6 @@ describe('runServerlessEsNode()', () => {
describe('runServerlessCluster()', () => {
test('should start 3 serverless nodes', async () => {
waitUntilClusterReadyMock.mockResolvedValue();
mockFs({
[baseEsPath]: {},
});
@ -642,27 +567,7 @@ 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('<xml/>');
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();
mockFs({
[baseEsPath]: {},
@ -675,7 +580,6 @@ describe('runServerlessCluster()', () => {
});
test(`should not wait for the security index when security is disabled`, async () => {
waitUntilClusterReadyMock.mockResolvedValue();
mockFs({
[baseEsPath]: {},
});

View file

@ -14,17 +14,12 @@ 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 {
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';
CA_CERT_PATH,
ES_P12_PASSWORD,
ES_P12_PATH,
kibanaDevServiceAccount,
} from '@kbn/dev-utils';
import { waitForSecurityIndex } from './wait_for_security_index';
import { createCliError } from '../errors';
@ -33,7 +28,6 @@ import {
SERVERLESS_RESOURCES_PATHS,
SERVERLESS_SECRETS_PATH,
SERVERLESS_JWKS_PATH,
SERVERLESS_IDP_METADATA_PATH,
SERVERLESS_CONFIG_PATH,
SERVERLESS_FILES_PATH,
SERVERLESS_SECRETS_SSL_PATH,
@ -75,8 +69,6 @@ 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) */
kibanaUrl?: string;
/**
* Resource file(s) to overwrite
* (see list of files that can be overwritten under `packages/kbn-es/src/serverless_resources/users`)
@ -468,54 +460,6 @@ 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.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('=')]);
}
@ -536,7 +480,7 @@ export function getDockerFileMountPath(hostPath: string) {
* Setup local volumes for Serverless ES
*/
export async function setupServerlessVolumes(log: ToolingLog, options: ServerlessOptions) {
const { basePath, clean, ssl, kibanaUrl, files, resources } = options;
const { basePath, clean, ssl, files, resources } = options;
const objectStorePath = resolve(basePath, 'stateless');
log.info(chalk.bold(`Checking for local serverless ES object store at ${objectStorePath}`));
@ -607,16 +551,6 @@ export async function setupServerlessVolumes(log: ToolingLog, options: Serverles
);
}
// Create and add meta data for mock identity provider
if (ssl && kibanaUrl) {
const metadata = await createMockIdpMetadata(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,
@ -625,6 +559,7 @@ 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`
);
@ -726,52 +661,33 @@ 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(
async () => {
if (!options.ssl || !options.kibanaUrl) {
return;
}
await ensureSAMLRoleMapping(client);
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...');
await readyPromise;
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 });
if (!options.esArgs || !options.esArgs.includes('xpack.security.enabled=false')) {
// If security is not disabled, make sure the security index exists before running the test to avoid flakiness
await waitForSecurityIndex({ client, log });

View file

@ -3,20 +3,13 @@
"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"
]

View file

@ -1,24 +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 { 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';

View file

@ -1,27 +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.
*/
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';

View file

@ -1,231 +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 { 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);
const trimTrailingSlash = (url: string) => (url.endsWith('/') ? url.slice(0, -1) : url);
return `<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
entityID="${MOCK_IDP_ENTITY_ID}">
<md:IDPSSODescriptor WantAuthnRequestsSigned="false"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>${cert.raw.toString('base64')}</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="${trimTrailingSlash(kibanaUrl)}${MOCK_IDP_LOGOUT_PATH}" />
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="${trimTrailingSlash(kibanaUrl)}${MOCK_IDP_LOGOUT_PATH}" />
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="${trimTrailingSlash(kibanaUrl)}${MOCK_IDP_LOGIN_PATH}" />
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="${trimTrailingSlash(kibanaUrl)}${MOCK_IDP_LOGIN_PATH}" />
</md:IDPSSODescriptor>
</md:EntityDescriptor>
`;
}
/**
* 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 = `
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" ID="_RPs1WfOkul8lZ72DtJtes0BKyPgaCamg" IssueInstant="${issueInstant}">
<saml:Issuer>${MOCK_IDP_ENTITY_ID}</saml:Issuer>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:transient">_643ec1b3f5673583b9f9a1e9e73a36daa2a3748f</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="${notOnOrAfter}" ${
options.authnRequestId ? `InResponseTo="${options.authnRequestId}"` : ''
} Recipient="${options.kibanaUrl}" />
</saml:SubjectConfirmation>
</saml:Subject>
<saml:AuthnStatement AuthnInstant="${issueInstant}" SessionIndex="4464894646681600">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<saml:Attribute Name="${MOCK_IDP_ATTRIBUTE_PRINCIPAL}" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xsi:type="xs:string">${options.username}</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="${MOCK_IDP_ATTRIBUTE_ROLES}" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
${options.roles
.map(
(role) => `<saml:AttributeValue xsi:type="xs:string">${role}</saml:AttributeValue>`
)
.join('')}
</saml:Attribute>
${
options.email
? `<saml:Attribute Name="${MOCK_IDP_ATTRIBUTE_EMAIL}" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xsi:type="xs:string">${options.email}</saml:AttributeValue>
</saml:Attribute>`
: ''
}
${
options.fullname
? `<saml:Attribute Name="${MOCK_IDP_ATTRIBUTE_NAME}" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xsi:type="xs:string">${options.fullname}</saml:AttributeValue>
</saml:Attribute>`
: ''
}
</saml:AttributeStatement>
</saml:Assertion>
`;
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(
`
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_bdf1d51245ed0f71aa23" Version="2.0" IssueInstant="${issueInstant}" ${
options.authnRequestId ? `InResponseTo="${options.authnRequestId}"` : ''
} Destination="${options.kibanaUrl}">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${MOCK_IDP_ENTITY_ID}</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>${signature.getSignedXml()}
</samlp:Response>
`
).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'].$;
}

View file

@ -1,11 +0,0 @@
{
"type": "plugin",
"id": "@kbn/mock-idp-plugin",
"owner": "@elastic/kibana-security",
"devOnly": true,
"plugin": {
"id": "mockIdpPlugin",
"server": true,
"browser": false
}
}

View file

@ -1,6 +0,0 @@
{
"name": "@kbn/mock-idp-plugin",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -1,9 +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.
*/
export { plugin } from './plugin';

View file

@ -1,120 +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 type { PluginInitializer, Plugin } from '@kbn/core-plugins-server';
import { schema } from '@kbn/config-schema';
import {
MOCK_IDP_LOGIN_PATH,
MOCK_IDP_LOGOUT_PATH,
createSAMLResponse,
parseSAMLAuthnRequest,
} from '../common';
export const plugin: PluginInitializer<void, void> = async (): Promise<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<ReturnType<typeof parseSAMLAuthnRequest>>;
try {
samlRequest = await parseSAMLAuthnRequest(request.query.SAMLRequest);
} catch (error) {
return response.badRequest({
body: '[request query.SAMLRequest]: value is not valid SAMLRequest.',
});
}
const userRoles: Array<[string, string]> = [
['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'],
];
const samlResponses = await Promise.all(
userRoles.map(([username, role]) =>
createSAMLResponse({
authnRequestId: samlRequest.ID,
kibanaUrl: samlRequest.AssertionConsumerServiceURL,
username,
roles: [role],
})
)
);
return response.renderHtml({
body: `
<!DOCTYPE html>
<title>Mock Identity Provider</title>
<link rel="icon" href="data:,">
<body>
<h2>Mock Identity Provider</h2>
<form id="loginForm" method="post" action="${
samlRequest.AssertionConsumerServiceURL
}">
<h3>Pick a role:<h3>
<ul>
${userRoles
.map(
([username], i) =>
`
<li>
<button name="SAMLResponse" value="${samlResponses[i]}">${username}</button>
</li>
`
)
.join('')}
</ul>
</form>
</body>
`,
});
}
);
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: '/' } });
}
);
},
start() {},
stop() {},
});

View file

@ -1,18 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types"
},
"include": [
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/core-plugins-server",
"@kbn/config-schema",
"@kbn/dev-utils"
]
}

View file

@ -20,7 +20,6 @@ kbnEs
'source-path': resolve(__dirname, '../../elasticsearch'),
'base-path': resolve(__dirname, '../.es'),
ssl: false,
kibanaUrl: 'https://localhost:5601/',
})
.catch(function (e) {
console.error(e);

View file

@ -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);
@ -110,25 +109,6 @@ export function applyConfigOverrides(rawConfig, opts, extraCliOptions) {
if (opts.dev) {
if (opts.serverless) {
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) {
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,
});
}
}
}
if (!has('elasticsearch.serviceAccountToken') && opts.devCredentials !== false) {
@ -294,9 +274,7 @@ 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.
// 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,
basePath: opts.runExamples ? false : !!opts.basePath,
optimize: !!opts.optimize,
disableOptimizer: !opts.optimizer,
oss: !!opts.oss,

View file

@ -17,7 +17,6 @@
"@kbn/config",
"@kbn/dev-utils",
"@kbn/apm-config-loader",
"@kbn/mock-idp-plugin",
],
"exclude": [
"target/**/*",

View file

@ -1062,8 +1062,6 @@
"@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"],

View file

@ -5028,10 +5028,6 @@
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 ""