Improve keystore CLI (#157359)

## Summary

Relates to: #113217

- Add extra documentation to highlight behaviour of the kibana keystore
(for #113217)
- Fix/Tidy-up commands (`create`, `list`) where the extra unused
arguments were preventing the `options` from being passed to the
functions. Also remove unnecessary `async` keyword from the `remove`
command.
- Added new `show` command
```
Usage: bin/kibana-keystore show [options] <key>

Displays the value of a single setting in the keystore. Pass the -o (or --output) parameter to write the setting to a file.

Options:
  -s, --silent         prevent all logging
  -o, --output <file>  output value to a file
  -h, --help           output usage information
```

### Checklist

Delete any items that are not applicable to this PR.

- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Kaarina Tungseth <kaarina.tungseth@elastic.co>
This commit is contained in:
Alex Szabo 2023-05-16 16:21:25 +02:00 committed by GitHub
parent 8181597cc9
commit 6ebfb8aa3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 188 additions and 5 deletions

View file

@ -5,7 +5,12 @@ Some settings are sensitive, and relying on filesystem permissions to protect
their values is not sufficient. For this use case, Kibana provides a
keystore, and the `kibana-keystore` tool to manage the settings in the keystore.
NOTE: All commands here should be run as the user which will run Kibana.
[NOTE]
====
* Run all commands as the user who runs {kib}.
* Only the settings with the `(Secure)` qualifier should be stored in the keystore.
Unsupported, extraneous or invalid JSON-string settings cause {kib} to fail to start up.
====
[float]
[[creating-keystore]]
@ -36,7 +41,8 @@ bin/kibana-keystore list
[[add-string-to-keystore]]
=== Add string settings
NOTE: Your input will be JSON-parsed to allow for object/array input configurations. To enforce string values, use "double quotes" around your input.
NOTE: Your input will be JSON-parsed to allow for object/array input configurations.
To enforce string values, use "double quotes" around your input.
Sensitive string settings, like authentication credentials for Elasticsearch
can be added using the `add` command:
@ -75,3 +81,14 @@ To remove a setting from the keystore, use the `remove` command:
----------------------------------------------------------------
bin/kibana-keystore remove the.setting.name.to.remove
----------------------------------------------------------------
[float]
[[read-settings]]
=== Read settings
To display the configured setting values, use the `show` command:
[source, sh]
----------------------------------------------------------------
bin/kibana-keystore show setting.key
----------------------------------------------------------------

View file

@ -17,6 +17,7 @@ import { createCli } from './create';
import { listCli } from './list';
import { addCli } from './add';
import { removeCli } from './remove';
import { showCli } from './show';
const argv = process.argv.slice();
const program = new Command('bin/kibana-keystore');
@ -31,6 +32,7 @@ createCli(program, keystore);
listCli(program, keystore);
addCli(program, keystore);
removeCli(program, keystore);
showCli(program, keystore);
program
.command('help <command>')

View file

@ -9,7 +9,7 @@
import { Logger } from '../cli/logger';
import { confirm } from './utils';
export async function create(keystore, command, options) {
export async function create(keystore, options) {
const logger = new Logger(options);
if (keystore.exists()) {

View file

@ -8,7 +8,7 @@
import { Logger } from '../cli/logger';
export function list(keystore, command, options = {}) {
export function list(keystore, options = {}) {
const logger = new Logger(options);
if (!keystore.exists()) {

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
export async function remove(keystore, key) {
export function remove(keystore, key) {
keystore.remove(key);
keystore.save();
}

View file

@ -0,0 +1,101 @@
/*
* 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.
*/
/**
* This data blob has 3 key/values set:
* - foo: "turbo2000"
* - bar: {"sub": 0}
* - num: 12345
*/
const mockKeystoreData =
'1:ae/OomiywlzhXnR8DnGLHheyAklj4WcvDUOzeIyeQIHEmrY' +
'MIYOYHvduos7NDOgw3TFAuh7xs6z9i0juEo1zFeJeIr8yoyIxdGi1J8GUCO0/' +
'OeaKxvLjTjczwoxiy34kM6CzlnJhjwnALAMiBvbehMUaCVzxf3Fu/3Gk2qeux0OPhidJ4Pn/RPjdMA==';
jest.mock('fs', () => ({
readFileSync: jest.fn().mockImplementation(() => JSON.stringify(mockKeystoreData)),
existsSync: jest.fn().mockImplementation((fileName) => {
if (fileName === 'non-existent-file.txt') {
return false;
} else {
return true;
}
}),
writeFileSync: jest.fn(),
}));
jest.mock('../cli/logger');
import { Logger } from '../cli/logger';
const mockLogFn = jest.fn();
Logger.prototype.log = mockLogFn;
const mockErrFn = jest.fn();
Logger.prototype.error = mockErrFn;
import { Keystore } from '../cli/keystore';
import { show } from './show';
describe('Kibana keystore: show', () => {
const keystore = new Keystore('mock-path', '');
it('reads stored strings', () => {
const exitCode = show(keystore, 'foo', {});
expect(exitCode).toBe(0);
expect(mockLogFn).toHaveBeenCalledWith('turbo2000');
expect(mockErrFn).not.toHaveBeenCalled();
});
it('reads stored numbers', () => {
const exitCode = show(keystore, 'num', {});
expect(exitCode).toBe(0);
expect(mockLogFn).toHaveBeenCalledWith('12345');
expect(mockErrFn).not.toHaveBeenCalled();
});
it('reads stored objecs', () => {
const exitCode = show(keystore, 'bar', {});
expect(exitCode).toBe(0);
expect(mockLogFn).toHaveBeenCalledWith(JSON.stringify({ sub: 0 }));
expect(mockErrFn).not.toHaveBeenCalled();
});
it('outputs to a file when the arg is passed', () => {
const exitCode = show(keystore, 'foo', { output: 'non-existent-file.txt' });
expect(exitCode).toBe(0);
expect(mockLogFn).toHaveBeenCalledWith('Writing output to file: non-existent-file.txt');
expect(mockErrFn).not.toHaveBeenCalled();
});
it('logs and terminates with an error when output file exists', () => {
const exitCode = show(keystore, 'foo', { output: 'existing-file.txt' });
expect(exitCode).toBe(-1);
expect(mockErrFn).toHaveBeenCalledWith(
'ERROR: Output file already exists. Remove it before retrying.'
);
expect(mockLogFn).not.toHaveBeenCalled();
});
it("logs and terminates with an error when the store doesn't have the key", () => {
const exitCode = show(keystore, 'no-key');
expect(exitCode).toBe(-1);
expect(mockErrFn).toHaveBeenCalledWith("ERROR: Kibana keystore doesn't have requested key.");
expect(mockLogFn).not.toHaveBeenCalled();
});
afterEach(() => {
mockLogFn.mockReset();
mockErrFn.mockReset();
jest.restoreAllMocks();
});
});

62
src/cli_keystore/show.ts Normal file
View file

@ -0,0 +1,62 @@
/*
* 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 { writeFileSync, existsSync } from 'fs';
import { Keystore } from '../cli/keystore';
import { Logger } from '../cli/logger';
interface ShowOptions {
silent?: boolean;
output?: string;
}
export function show(keystore: Keystore, key: string, options: ShowOptions = {}): number | void {
const { silent, output } = options;
const logger = new Logger({ silent });
if (!keystore.exists()) {
logger.error("ERROR: Kibana keystore not found. Use 'create' command to create one.");
return -1;
}
if (!keystore.has(key)) {
logger.error("ERROR: Kibana keystore doesn't have requested key.");
return -1;
}
const value = keystore.data[key];
const valueAsString = typeof value === 'string' ? value : JSON.stringify(value);
if (output) {
if (existsSync(output)) {
logger.error('ERROR: Output file already exists. Remove it before retrying.');
return -1;
} else {
writeFileSync(output, valueAsString);
logger.log('Writing output to file: ' + output);
}
} else {
logger.log(valueAsString);
}
return 0;
}
export function showCli(program: any, keystore: Keystore) {
program
.command('show <key>')
.description(
'Displays the value of a single setting in the keystore. Pass the -o (or --output) parameter to write the setting to a file.'
)
.option('-s, --silent', 'prevent all logging')
.option('-o, --output <file>', 'output value to a file')
.action((key: string, options: ShowOptions) => {
process.exitCode = show(keystore, key, options) || 0;
});
}

View file

@ -7,6 +7,7 @@
"keystore/**/*",
"utils/**/*",
"*.js",
"*.ts",
],
"kbn_references": [
{ "path": "../setup_node_env/tsconfig.json" },