Skip to content

Commit

Permalink
Verification code CLI (#111707)
Browse files Browse the repository at this point in the history
* Add verification code CLI

* Added suggestion from code review

* Fixed types

* Added extra test

* Added CLI dist scripts

* Fixed typo

* Write code to data instead of config directory
  • Loading branch information
thomheymann authored Sep 14, 2021
1 parent 34581ff commit db5cf95
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 9 deletions.
9 changes: 9 additions & 0 deletions scripts/kibana_verification_code.js
Original file line number Diff line number Diff line change
@@ -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.
*/

require('../src/cli_verification_code/dev');
39 changes: 39 additions & 0 deletions src/cli_verification_code/cli_verification_code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 { kibanaPackageJson, getDataPath } from '@kbn/utils';
import path from 'path';
import fs from 'fs';
import chalk from 'chalk';

import Command from '../cli/command';

const program = new Command('bin/kibana-verification-code');

program
.version(kibanaPackageJson.version)
.description('Tool to get Kibana verification code')
.action(() => {
const fpath = path.join(getDataPath(), 'verification_code');
try {
const code = fs.readFileSync(fpath).toString();
console.log(
`Your verification code is: ${chalk.black.bgCyanBright(
` ${code.substr(0, 3)} ${code.substr(3)} `
)}`
);
} catch (error) {
console.log(`Couldn't find verification code.
If Kibana hasn't been configured yet, restart Kibana to generate a new code.
Otherwise, you can safely ignore this message and start using Kibana.`);
}
});

program.parse(process.argv);
10 changes: 10 additions & 0 deletions src/cli_verification_code/dev.js
Original file line number Diff line number Diff line change
@@ -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.
*/

require('../setup_node_env');
require('./cli_verification_code');
10 changes: 10 additions & 0 deletions src/cli_verification_code/dist.js
Original file line number Diff line number Diff line change
@@ -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.
*/

require('../setup_node_env/dist');
require('./cli_verification_code');
29 changes: 29 additions & 0 deletions src/dev/build/tasks/bin/scripts/kibana-verification-code
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/sh
SCRIPT=$0

# SCRIPT may be an arbitrarily deep series of symlinks. Loop until we have the concrete path.
while [ -h "$SCRIPT" ] ; do
ls=$(ls -ld "$SCRIPT")
# Drop everything prior to ->
link=$(expr "$ls" : '.*-> \(.*\)$')
if expr "$link" : '/.*' > /dev/null; then
SCRIPT="$link"
else
SCRIPT=$(dirname "$SCRIPT")/"$link"
fi
done

DIR="$(dirname "${SCRIPT}")/.."
CONFIG_DIR=${KBN_PATH_CONF:-"$DIR/config"}
NODE="${DIR}/node/bin/node"
test -x "$NODE"
if [ ! -x "$NODE" ]; then
echo "unable to find usable node.js executable."
exit 1
fi

if [ -f "${CONFIG_DIR}/node.options" ]; then
KBN_NODE_OPTS="$(grep -v ^# < ${CONFIG_DIR}/node.options | xargs)"
fi
NODE_OPTIONS="$KBN_NODE_OPTS $NODE_OPTIONS" "${NODE}" "${DIR}/src/cli_verification_code/dist" "$@"
35 changes: 35 additions & 0 deletions src/dev/build/tasks/bin/scripts/kibana-verification-code.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@echo off

SETLOCAL ENABLEDELAYEDEXPANSION

set SCRIPT_DIR=%~dp0
for %%I in ("%SCRIPT_DIR%..") do set DIR=%%~dpfI

set NODE=%DIR%\node\node.exe

If Not Exist "%NODE%" (
Echo unable to find usable node.js executable.
Exit /B 1
)

set CONFIG_DIR=%KBN_PATH_CONF%
If ["%KBN_PATH_CONF%"] == [] (
set "CONFIG_DIR=%DIR%\config"
)

IF EXIST "%CONFIG_DIR%\node.options" (
for /F "usebackq eol=# tokens=*" %%i in ("%CONFIG_DIR%\node.options") do (
If [!NODE_OPTIONS!] == [] (
set "NODE_OPTIONS=%%i"
) Else (
set "NODE_OPTIONS=!NODE_OPTIONS! %%i"
)
)
)

TITLE Kibana Verification Code
"%NODE%" "%DIR%\src\cli_verification_code\dist" %*

:finally

ENDLOCAL
5 changes: 5 additions & 0 deletions src/plugins/interactive_setup/public/single_chars_field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import React, { useEffect, useRef } from 'react';
import useList from 'react-use/lib/useList';
import useUpdateEffect from 'react-use/lib/useUpdateEffect';

import { i18n } from '@kbn/i18n';
import { euiThemeVars } from '@kbn/ui-shared-deps/theme';

export interface SingleCharsFieldProps {
Expand Down Expand Up @@ -124,6 +125,10 @@ export const SingleCharsField: FunctionComponent<SingleCharsFieldProps> = ({
maxLength={1}
isInvalid={isInvalid}
style={{ textAlign: 'center' }}
aria-label={i18n.translate('interactiveSetup.singleCharsField.digitLabel', {
defaultMessage: 'Digit {index}',
values: { index: i + 1 },
})}
/>
</EuiFlexItem>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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 { fireEvent, render, waitFor } from '@testing-library/react';
import React from 'react';

import { coreMock } from 'src/core/public/mocks';

import { Providers } from './plugin';
import { VerificationCodeForm } from './verification_code_form';

jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));

describe('VerificationCodeForm', () => {
jest.setTimeout(20_000);

it('calls enrollment API when submitting form', async () => {
const coreStart = coreMock.createStart();
coreStart.http.post.mockResolvedValue({});

const onSuccess = jest.fn();

const { findByRole, findByLabelText } = render(
<Providers http={coreStart.http}>
<VerificationCodeForm onSuccess={onSuccess} />
</Providers>
);
fireEvent.input(await findByLabelText('Digit 1'), {
target: { value: '1' },
});
fireEvent.input(await findByLabelText('Digit 2'), {
target: { value: '2' },
});
fireEvent.input(await findByLabelText('Digit 3'), {
target: { value: '3' },
});
fireEvent.input(await findByLabelText('Digit 4'), {
target: { value: '4' },
});
fireEvent.input(await findByLabelText('Digit 5'), {
target: { value: '5' },
});
fireEvent.input(await findByLabelText('Digit 6'), {
target: { value: '6' },
});
fireEvent.click(await findByRole('button', { name: 'Verify', hidden: true }));

await waitFor(() => {
expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/interactive_setup/verify', {
body: JSON.stringify({ code: '123456' }),
});
expect(onSuccess).toHaveBeenCalled();
});
});

it('validates form', async () => {
const coreStart = coreMock.createStart();
const onSuccess = jest.fn();

const { findAllByText, findByRole, findByLabelText } = render(
<Providers http={coreStart.http}>
<VerificationCodeForm onSuccess={onSuccess} />
</Providers>
);

fireEvent.click(await findByRole('button', { name: 'Verify', hidden: true }));

await findAllByText(/Enter a verification code/i);

fireEvent.input(await findByLabelText('Digit 1'), {
target: { value: '1' },
});

await findAllByText(/Enter all six digits/i);
});
});
10 changes: 7 additions & 3 deletions src/plugins/interactive_setup/public/verification_code_form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {
EuiButton,
EuiCallOut,
EuiCode,
EuiEmptyPrompt,
EuiForm,
EuiFormRow,
Expand Down Expand Up @@ -69,8 +70,8 @@ export const VerificationCodeForm: FunctionComponent<VerificationCodeFormProps>
}),
});
} catch (error) {
if (error.response?.status === 403) {
form.setError('code', error.body?.message);
if ((error as IHttpFetchError).response?.status === 403) {
form.setError('code', (error as IHttpFetchError).body?.message);
return;
} else {
throw error;
Expand Down Expand Up @@ -111,7 +112,10 @@ export const VerificationCodeForm: FunctionComponent<VerificationCodeFormProps>
<p>
<FormattedMessage
id="interactiveSetup.verificationCodeForm.codeDescription"
defaultMessage="Copy the verification code from Kibana server."
defaultMessage="Copy the code from the Kibana server or run {command} to retrieve it."
values={{
command: <EuiCode lang="bash">./bin/kibana-verification-code</EuiCode>,
}}
/>
</p>
</EuiText>
Expand Down
21 changes: 15 additions & 6 deletions src/plugins/interactive_setup/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { ConfigSchema, ConfigType } from './config';
import { ElasticsearchService } from './elasticsearch_service';
import { KibanaConfigWriter } from './kibana_config_writer';
import { defineRoutes } from './routes';
import { VerificationCode } from './verification_code';
import { VerificationService } from './verification_service';

// List of the Elasticsearch hosts Kibana uses by default.
const DEFAULT_ELASTICSEARCH_HOSTS = [
Expand All @@ -29,7 +29,7 @@ const DEFAULT_ELASTICSEARCH_HOSTS = [
export class InteractiveSetupPlugin implements PrebootPlugin {
readonly #logger: Logger;
readonly #elasticsearch: ElasticsearchService;
readonly #verificationCode: VerificationCode;
readonly #verification: VerificationService;

#elasticsearchConnectionStatusSubscription?: Subscription;

Expand All @@ -47,7 +47,7 @@ export class InteractiveSetupPlugin implements PrebootPlugin {
this.#elasticsearch = new ElasticsearchService(
this.initializerContext.logger.get('elasticsearch')
);
this.#verificationCode = new VerificationCode(
this.#verification = new VerificationService(
this.initializerContext.logger.get('verification')
);
}
Expand All @@ -73,6 +73,14 @@ export class InteractiveSetupPlugin implements PrebootPlugin {
return;
}

const verificationCode = this.#verification.setup();
if (!verificationCode) {
this.#logger.error(
'Interactive setup mode could not be activated. Ensure Kibana has permission to write to its config folder.'
);
return;
}

let completeSetup: (result: { shouldReloadConfig: boolean }) => void;
core.preboot.holdSetupUntilResolved(
'Validating Elasticsearch connection configuration…',
Expand All @@ -93,6 +101,7 @@ export class InteractiveSetupPlugin implements PrebootPlugin {
elasticsearch: core.elasticsearch,
connectionCheckInterval: this.#getConfig().connectionCheck.interval,
});

this.#elasticsearchConnectionStatusSubscription = elasticsearch.connectionStatus$.subscribe(
(status) => {
if (status === ElasticsearchConnectionStatus.Configured) {
Expand All @@ -104,10 +113,9 @@ export class InteractiveSetupPlugin implements PrebootPlugin {
this.#logger.debug(
'Starting interactive setup mode since Kibana cannot to connect to Elasticsearch at http://localhost:9200.'
);
const { code } = this.#verificationCode;
const pathname = core.http.basePath.prepend('/');
const { protocol, hostname, port } = core.http.getServerInfo();
const url = `${protocol}://${hostname}:${port}${pathname}?code=${code}`;
const url = `${protocol}://${hostname}:${port}${pathname}?code=${verificationCode.code}`;

// eslint-disable-next-line no-console
console.log(`
Expand Down Expand Up @@ -135,7 +143,7 @@ Go to ${chalk.cyanBright.underline(url)} to get started.
preboot: { ...core.preboot, completeSetup },
kibanaConfigWriter: new KibanaConfigWriter(configPath, this.#logger.get('kibana-config')),
elasticsearch,
verificationCode: this.#verificationCode,
verificationCode,
getConfig: this.#getConfig.bind(this),
});
});
Expand All @@ -155,5 +163,6 @@ Go to ${chalk.cyanBright.underline(url)} to get started.
}

this.#elasticsearch.stop();
this.#verification.stop();
}
}
Loading

0 comments on commit db5cf95

Please sign in to comment.