Skip to content

Commit

Permalink
Add mock identity provider for serverless (#170852)
Browse files Browse the repository at this point in the history
Related to [#166340](#166340)

## Summary

Add mock identity provider and utils to test serverless user roles.

## Screenshot

### 1. Login selector

<img width="767" alt="Screenshot 2023-11-08 at 15 18 18"
src="https://github.com/elastic/kibana/assets/190132/82b4a29f-65b4-45d2-bed3-6d9f74043c48">

### 2. Single sign on screen

<img width="437" alt="Screenshot 2023-11-09 at 12 30 46"
src="https://github.com/elastic/kibana/assets/190132/3d5b6f26-5409-4169-a627-bcf6d09836d9">

### 3. User profile page

<img width="1041" alt="Screenshot 2023-11-08 at 17 36 22"
src="https://github.com/elastic/kibana/assets/190132/50bd4a5a-f9a8-4643-9384-9a352701b011">

## Testing

SAML is only supported by ES when running in SSL mode. 

1. To test the mock identity provider run a serverless project in SSL
mode using:

```bash
yarn es serverless --ssl
yarn start --serverless=es --ssl
```

2. Then access Kibana and login in using "Continue as Test User".

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
Co-authored-by: Dzmitry Lemechko <dzmitry.lemechko@elastic.co>
  • Loading branch information
4 people committed Nov 15, 2023
1 parent fd09c26 commit 1fb0313
Show file tree
Hide file tree
Showing 20 changed files with 710 additions and 41 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,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/obs-ux-infra_services-team
x-pack/plugins/monitoring @elastic/obs-ux-infra_services-team
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,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",
Expand Down
3 changes: 2 additions & 1 deletion packages/kbn-es/src/cli_commands/serverless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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). [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
Expand Down Expand Up @@ -73,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,
Expand Down
3 changes: 3 additions & 0 deletions packages/kbn-es/src/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
106 changes: 101 additions & 5 deletions packages/kbn-es/src/utils/docker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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]);
Expand All @@ -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();
Expand Down Expand Up @@ -423,6 +429,66 @@ 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()', () => {
Expand Down Expand Up @@ -463,21 +529,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('<xml/>');

const volumeCmd = await setupServerlessVolumes(log, { basePath: baseEsPath, ssl: true });
const volumeCmd = await setupServerlessVolumes(log, {
basePath: baseEsPath,
ssl: true,
kibanaUrl: 'https://localhost:5603/',
});

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([]);
});

Expand Down Expand Up @@ -543,6 +617,7 @@ describe('runServerlessEsNode()', () => {

describe('runServerlessCluster()', () => {
test('should start 3 serverless nodes', async () => {
waitUntilClusterReadyMock.mockResolvedValue();
mockFs({
[baseEsPath]: {},
});
Expand All @@ -567,7 +642,27 @@ 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]: {},
Expand All @@ -580,6 +675,7 @@ describe('runServerlessCluster()', () => {
});

test(`should not wait for the security index when security is disabled`, async () => {
waitUntilClusterReadyMock.mockResolvedValue();
mockFs({
[baseEsPath]: {},
});
Expand Down
Loading

0 comments on commit 1fb0313

Please sign in to comment.