mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[keystore] Add password support (#180414)
This adds support a password protected keystore. The UX should match other stack products. Closes https://github.com/elastic/kibana/issues/21756. ``` [jon@mbpkbn1]/tmp/kibana-8.15.0-SNAPSHOT% bin/kibana-keystore create --password A Kibana keystore already exists. Overwrite? [y/N] y Enter new password for the kibana keystore (empty for no password): ******** Created Kibana keystore in /tmp/kibana-8.15.0-SNAPSHOT/config/kibana.keystore [jon@mbpkbn1]/tmp/kibana-8.15.0-SNAPSHOT% bin/kibana-keystore add elasticsearch.username Enter password for the kibana keystore: ******** Enter value for elasticsearch.username: ************* [jon@mbpkbn1]/tmp/kibana-8.15.0-SNAPSHOT% bin/kibana-keystore add elasticsearch.password Enter password for the kibana keystore: ******** Enter value for elasticsearch.password: ******** [jon@mbpkbn1]/tmp/kibana-8.15.0-SNAPSHOT% bin/kibana ... Enter password for the kibana keystore: ******** [2024-04-30T09:47:03.560-05:00][INFO ][root] Kibana is starting [jon@mbpkbn1]/tmp/kibana-8.15.0-SNAPSHOT% bin/kibana-keystore has-passwd Keystore is password-protected [jon@mbpkbn1]/tmp/kibana-8.15.0-SNAPSHOT% ./bin/kibana-keystore show elasticsearch.username Enter password for the kibana keystore: ******** kibana_system [jon@mbpkbn1]/tmp/kibana-8.15.0-SNAPSHOT% ./bin/kibana-keystore remove elasticsearch.username Enter password for the kibana keystore: ******** [jon@mbpkbn1]/tmp/kibana-8.15.0-SNAPSHOT% ./bin/kibana-keystore show elasticsearch.username Enter password for the kibana keystore: ******** ERROR: Kibana keystore doesn't have requested key. [jon@mbpkbn1]/tmp/kibana-8.15.0-SNAPSHOT% bin/kibana-keystore passwd Enter password for the kibana keystore: ******** Enter new password for the kibana keystore (empty for no password): [jon@mbpkbn1]/tmp/kibana-8.15.0-SNAPSHOT% ./bin/kibana-keystore has-passwd Error: Keystore is not password protected [jon@mbpkbn1]/tmp/kibana-8.15.0-SNAPSHOT% ./bin/kibana ... [2024-04-30T09:49:03.220-05:00][INFO ][root] Kibana is starting ``` ## Password input Environment variable usage is not consistent across stack products. I implemented `KBN_KEYSTORE_PASSWORD_FILE` and `KBN_KEYSTORE_PASSWORD` to be used to avoid prompts. @elastic/kibana-security do you have any thoughts? - `LOGSTASH_KEYSTORE_PASS` - https://www.elastic.co/guide/en/logstash/current/keystore.html#keystore-password - `KEYSTORE_PASSWORD` - https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#docker-keystore-bind-mount - `ES_KEYSTORE_PASSPHRASE_FILE` - https://www.elastic.co/guide/en/elasticsearch/reference/current/rpm.html#rpm-running-systemd - Beats discussion, unresolved: https://github.com/elastic/beats/issues/5737 ## Release note Adds password support to the Kibana keystore.
This commit is contained in:
parent
424411296f
commit
8b015ebedd
30 changed files with 442 additions and 108 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -1204,6 +1204,7 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql
|
|||
/src/setup_node_env/ @elastic/kibana-operations
|
||||
/src/cli/keystore/ @elastic/kibana-operations
|
||||
/src/cli/serve/ @elastic/kibana-operations
|
||||
/src/cli_keystore/ @elastic/kibana-operations
|
||||
/.github/workflows/ @elastic/kibana-operations
|
||||
/vars/ @elastic/kibana-operations
|
||||
/.bazelignore @elastic/kibana-operations
|
||||
|
|
|
@ -26,6 +26,8 @@ bin/kibana-keystore create
|
|||
The file `kibana.keystore` will be created in the `config` directory defined by the
|
||||
environment variable `KBN_PATH_CONF`.
|
||||
|
||||
To create a password protected keystore use the `--password` flag.
|
||||
|
||||
[float]
|
||||
[[list-settings]]
|
||||
=== List settings in the keystore
|
||||
|
@ -92,3 +94,27 @@ To display the configured setting values, use the `show` command:
|
|||
----------------------------------------------------------------
|
||||
bin/kibana-keystore show setting.key
|
||||
----------------------------------------------------------------
|
||||
|
||||
[float]
|
||||
[[change-password]]
|
||||
=== Change password
|
||||
|
||||
To change the password of the keystore, use the `passwd` command:
|
||||
|
||||
[source, sh]
|
||||
----------------------------------------------------------------
|
||||
bin/kibana-keystore passwd
|
||||
----------------------------------------------------------------
|
||||
|
||||
[float]
|
||||
[[has-password]]
|
||||
=== Has password
|
||||
|
||||
To check if the keystore is password protected, use the `has-passwd` command.
|
||||
An exit code of 0 will be returned if the keystore is password protected,
|
||||
and the command will fail otherwise.
|
||||
|
||||
[source, sh]
|
||||
----------------------------------------------------------------
|
||||
bin/kibana-keystore has-passwd
|
||||
----------------------------------------------------------------
|
|
@ -2,7 +2,10 @@
|
|||
== Start and stop {kib}
|
||||
|
||||
The method for starting and stopping {kib} varies depending on how you installed
|
||||
it.
|
||||
it. If a password protected keystore is used, the environment variable
|
||||
`KBN_KEYSTORE_PASSPHRASE_FILE` can be used to point to a file containing the password,
|
||||
the environment variable `KEYSTORE_PASSWORD` can be defined, or you will be prompted
|
||||
to enter to enter the password on startup,
|
||||
|
||||
[float]
|
||||
[[start-start-targz]]
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
||||
import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from 'crypto';
|
||||
import { question } from './utils/prompt';
|
||||
import * as errors from './errors';
|
||||
|
||||
const VERSION = 1;
|
||||
|
@ -15,12 +16,16 @@ const ALGORITHM = 'aes-256-gcm';
|
|||
const ITERATIONS = 10000;
|
||||
|
||||
export class Keystore {
|
||||
static async initialize(path, password) {
|
||||
const keystore = new Keystore(path, password);
|
||||
await keystore.load();
|
||||
return keystore;
|
||||
}
|
||||
|
||||
constructor(path, password = '') {
|
||||
this.path = path;
|
||||
this.password = password;
|
||||
|
||||
this.reset();
|
||||
this.load();
|
||||
}
|
||||
|
||||
static errors = errors;
|
||||
|
@ -71,11 +76,23 @@ export class Keystore {
|
|||
writeFileSync(this.path, keystore);
|
||||
}
|
||||
|
||||
load() {
|
||||
async load() {
|
||||
try {
|
||||
if (this.hasPassword() && !this.password) {
|
||||
if (process.env.KBN_KEYSTORE_PASSPHRASE_FILE) {
|
||||
this.password = readFileSync(process.env.KBN_KEYSTORE_PASSPHRASE_FILE, {
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
} else if (process.env.KEYSTORE_PASSWORD) {
|
||||
this.password = process.env.KEYSTORE_PASSWORD;
|
||||
} else {
|
||||
this.password = await question('Enter password for the kibana keystore', {
|
||||
mask: '*',
|
||||
});
|
||||
}
|
||||
}
|
||||
const keystore = readFileSync(this.path);
|
||||
const [, data] = keystore.toString().split(':');
|
||||
|
||||
this.data = JSON.parse(Keystore.decrypt(data, this.password));
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
|
@ -109,4 +126,21 @@ export class Keystore {
|
|||
remove(key) {
|
||||
delete this.data[key];
|
||||
}
|
||||
|
||||
hasPassword() {
|
||||
try {
|
||||
const keystore = readFileSync(this.path);
|
||||
const [, data] = keystore.toString().split(':');
|
||||
Keystore.decrypt(data);
|
||||
} catch (e) {
|
||||
if (e instanceof errors.UnableToReadKeystore) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setPassword(password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,14 @@ jest.mock('fs', () => ({
|
|||
return JSON.stringify(mockProtectedKeystoreData);
|
||||
}
|
||||
|
||||
if (path.includes('keystore_correct_password_file')) {
|
||||
return 'changeme';
|
||||
}
|
||||
|
||||
if (path.includes('keystore_incorrect_password_file')) {
|
||||
return 'wrongpassword';
|
||||
}
|
||||
|
||||
if (path.includes('data/test') || path.includes('data/nonexistent')) {
|
||||
throw { code: 'ENOENT' };
|
||||
}
|
||||
|
@ -55,12 +63,12 @@ describe('Keystore', () => {
|
|||
});
|
||||
|
||||
describe('save', () => {
|
||||
it('thows permission denied', () => {
|
||||
it('thows permission denied', async () => {
|
||||
expect.assertions(1);
|
||||
const path = '/inaccessible/test.keystore';
|
||||
|
||||
try {
|
||||
const keystore = new Keystore(path);
|
||||
const keystore = await Keystore.initialize(path);
|
||||
keystore.save();
|
||||
} catch (e) {
|
||||
expect(e.code).toEqual('EACCES');
|
||||
|
@ -84,23 +92,66 @@ describe('Keystore', () => {
|
|||
});
|
||||
|
||||
describe('load', () => {
|
||||
it('is called on initialization', () => {
|
||||
const env = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
process.env = { ...env };
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = env;
|
||||
});
|
||||
|
||||
it('is called on initialization', async () => {
|
||||
const load = sandbox.spy(Keystore.prototype, 'load');
|
||||
|
||||
new Keystore('/data/protected.keystore', 'changeme');
|
||||
await Keystore.initialize('/data/protected.keystore', 'changeme');
|
||||
|
||||
expect(load.calledOnce).toBe(true);
|
||||
});
|
||||
|
||||
it('can load a password protected keystore', () => {
|
||||
const keystore = new Keystore('/data/protected.keystore', 'changeme');
|
||||
it('can load a password protected keystore', async () => {
|
||||
const keystore = await Keystore.initialize('/data/protected.keystore', 'changeme');
|
||||
expect(keystore.data).toEqual({ 'a1.b2.c3': 'foo', a2: 'bar' });
|
||||
});
|
||||
|
||||
it('throws unable to read keystore', () => {
|
||||
it('can load a valid password protected keystore from env KEYSTORE_PASSWORD', async () => {
|
||||
process.env.KEYSTORE_PASSWORD = 'changeme';
|
||||
const keystore = await Keystore.initialize('/data/protected.keystore');
|
||||
expect(keystore.data).toEqual({ 'a1.b2.c3': 'foo', a2: 'bar' });
|
||||
});
|
||||
|
||||
it('can not load a password protected keystore from env KEYSTORE_PASSWORD with the wrong password', async () => {
|
||||
process.env.KEYSTORE_PASSWORD = 'wrongpassword';
|
||||
expect.assertions(1);
|
||||
try {
|
||||
new Keystore('/data/protected.keystore', 'wrongpassword');
|
||||
await Keystore.initialize('/data/protected.keystore');
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(Keystore.errors.UnableToReadKeystore);
|
||||
}
|
||||
});
|
||||
|
||||
it('can load a password protected keystore from env KBN_KEYSTORE_PASSPHRASE_FILE', async () => {
|
||||
process.env.KBN_KEYSTORE_PASSPHRASE_FILE = 'keystore_correct_password_file';
|
||||
const keystore = await Keystore.initialize('/data/protected.keystore');
|
||||
expect(keystore.data).toEqual({ 'a1.b2.c3': 'foo', a2: 'bar' });
|
||||
});
|
||||
|
||||
it('can not load a password protected keystore from env KBN_KEYSTORE_PASSPHRASE_FILE with the wrong password', async () => {
|
||||
process.env.KBN_KEYSTORE_PASSPHRASE_FILE = 'keystore_incorrect_password_file';
|
||||
expect.assertions(1);
|
||||
try {
|
||||
await Keystore.initialize('/data/protected.keystore');
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(Keystore.errors.UnableToReadKeystore);
|
||||
}
|
||||
});
|
||||
|
||||
it('throws unable to read keystore', async () => {
|
||||
expect.assertions(1);
|
||||
try {
|
||||
await Keystore.initialize('/data/protected.keystore', 'wrongpassword');
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(Keystore.errors.UnableToReadKeystore);
|
||||
}
|
||||
|
@ -112,16 +163,16 @@ describe('Keystore', () => {
|
|||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('clears the data', () => {
|
||||
const keystore = new Keystore('/data/protected.keystore', 'changeme');
|
||||
it('clears the data', async () => {
|
||||
const keystore = await Keystore.initialize('/data/protected.keystore', 'changeme');
|
||||
keystore.reset();
|
||||
expect(keystore.data).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('keys', () => {
|
||||
it('lists object keys', () => {
|
||||
const keystore = new Keystore('/data/unprotected.keystore');
|
||||
it('lists object keys', async () => {
|
||||
const keystore = await Keystore.initialize('/data/unprotected.keystore');
|
||||
const keys = keystore.keys();
|
||||
|
||||
expect(keys).toEqual(['a1.b2.c3', 'a2']);
|
||||
|
@ -129,22 +180,22 @@ describe('Keystore', () => {
|
|||
});
|
||||
|
||||
describe('has', () => {
|
||||
it('returns true if key exists', () => {
|
||||
const keystore = new Keystore('/data/unprotected.keystore');
|
||||
it('returns true if key exists', async () => {
|
||||
const keystore = await Keystore.initialize('/data/unprotected.keystore');
|
||||
|
||||
expect(keystore.has('a2')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if key does not exist', () => {
|
||||
const keystore = new Keystore('/data/unprotected.keystore');
|
||||
it('returns false if key does not exist', async () => {
|
||||
const keystore = await Keystore.initialize('/data/unprotected.keystore');
|
||||
|
||||
expect(keystore.has('invalid')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add', () => {
|
||||
it('adds a key/value pair', () => {
|
||||
const keystore = new Keystore('/data/unprotected.keystore');
|
||||
it('adds a key/value pair', async () => {
|
||||
const keystore = await Keystore.initialize('/data/unprotected.keystore');
|
||||
keystore.add('a3', 'baz');
|
||||
keystore.add('a4', [1, 'a', 2, 'b']);
|
||||
|
||||
|
@ -158,8 +209,8 @@ describe('Keystore', () => {
|
|||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('removes a key/value pair', () => {
|
||||
const keystore = new Keystore('/data/unprotected.keystore');
|
||||
it('removes a key/value pair', async () => {
|
||||
const keystore = await Keystore.initialize('/data/unprotected.keystore');
|
||||
keystore.remove('a1.b2.c3');
|
||||
|
||||
expect(keystore.data).toEqual({
|
||||
|
|
|
@ -11,10 +11,8 @@ import { set } from '@kbn/safer-lodash-set';
|
|||
import { Keystore } from '.';
|
||||
import { getKeystore } from './get_keystore';
|
||||
|
||||
export function readKeystore(keystorePath = getKeystore()) {
|
||||
const keystore = new Keystore(keystorePath);
|
||||
keystore.load();
|
||||
|
||||
export async function readKeystore(keystorePath = getKeystore()) {
|
||||
const keystore = await Keystore.initialize(keystorePath);
|
||||
const keys = Object.keys(keystore.data);
|
||||
const data = {};
|
||||
|
||||
|
|
|
@ -14,14 +14,18 @@ import { Keystore } from '.';
|
|||
|
||||
describe('cli/serve/read_keystore', () => {
|
||||
beforeEach(() => {
|
||||
Keystore.initialize.mockResolvedValue(Promise.resolve(new Keystore()));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('returns structured keystore data', () => {
|
||||
it('returns structured keystore data', async () => {
|
||||
const keystoreData = { 'elasticsearch.password': 'changeme' };
|
||||
Keystore.prototype.data = keystoreData;
|
||||
|
||||
const data = readKeystore();
|
||||
const data = await readKeystore();
|
||||
expect(data).toEqual({
|
||||
elasticsearch: {
|
||||
password: 'changeme',
|
||||
|
@ -29,17 +33,17 @@ describe('cli/serve/read_keystore', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('uses data path if provided', () => {
|
||||
it('uses data path if provided', async () => {
|
||||
const keystorePath = path.join('/foo/', 'kibana.keystore');
|
||||
|
||||
readKeystore(keystorePath);
|
||||
expect(Keystore.mock.calls[0][0]).toContain(keystorePath);
|
||||
await readKeystore(keystorePath);
|
||||
expect(Keystore.initialize.mock.calls[0][0]).toContain(keystorePath);
|
||||
});
|
||||
|
||||
it('uses the getKeystore path if not', () => {
|
||||
readKeystore();
|
||||
it('uses the getKeystore path if not', async () => {
|
||||
await readKeystore();
|
||||
// we test exact path scenarios in get_keystore.test.js - we use both
|
||||
// deprecated and new to cover any older local environments
|
||||
expect(Keystore.mock.calls[0][0]).toMatch(/data|config/);
|
||||
expect(Keystore.initialize.mock.calls[0][0]).toMatch(/data|config/);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -96,7 +96,7 @@ function pathCollector() {
|
|||
const configPathCollector = pathCollector();
|
||||
const pluginPathCollector = pathCollector();
|
||||
|
||||
export function applyConfigOverrides(rawConfig, opts, extraCliOptions) {
|
||||
export function applyConfigOverrides(rawConfig, opts, extraCliOptions, keystoreConfig) {
|
||||
const set = _.partial(lodashSet, rawConfig);
|
||||
const get = _.partial(_.get, rawConfig);
|
||||
const has = _.partial(_.has, rawConfig);
|
||||
|
@ -209,7 +209,7 @@ export function applyConfigOverrides(rawConfig, opts, extraCliOptions) {
|
|||
set('plugins.paths', _.compact([].concat(get('plugins.paths'), opts.pluginPath)));
|
||||
|
||||
_.mergeWith(rawConfig, extraCliOptions, mergeAndReplaceArrays);
|
||||
_.merge(rawConfig, readKeystore());
|
||||
_.merge(rawConfig, keystoreConfig);
|
||||
|
||||
return rawConfig;
|
||||
}
|
||||
|
@ -324,11 +324,12 @@ export default function (program) {
|
|||
// Kibana server process, and will be using core's bootstrap script
|
||||
// to effectively start Kibana.
|
||||
const bootstrapScript = getBootstrapScript(cliArgs.dev);
|
||||
|
||||
const keystoreConfig = await readKeystore();
|
||||
await bootstrapScript({
|
||||
configs,
|
||||
cliArgs,
|
||||
applyConfigOverrides: (rawConfig) => applyConfigOverrides(rawConfig, opts, unknownOptions),
|
||||
applyConfigOverrides: (rawConfig) =>
|
||||
applyConfigOverrides(rawConfig, opts, unknownOptions, keystoreConfig),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { applyConfigOverrides } from './serve';
|
|||
|
||||
describe('applyConfigOverrides', () => {
|
||||
it('merges empty objects to an empty config', () => {
|
||||
const output = applyConfigOverrides({}, {}, {});
|
||||
const output = applyConfigOverrides({}, {}, {}, {});
|
||||
const defaultEmptyConfig = {
|
||||
plugins: {
|
||||
paths: [],
|
||||
|
@ -33,7 +33,8 @@ describe('applyConfigOverrides', () => {
|
|||
tomato: {
|
||||
weight: 100,
|
||||
},
|
||||
}
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
|
@ -63,7 +64,8 @@ describe('applyConfigOverrides', () => {
|
|||
weight: 100,
|
||||
arr: [4, 5],
|
||||
},
|
||||
}
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { confirm, question } from '../cli_keystore/utils';
|
||||
import { confirm, question } from '../cli/keystore/utils';
|
||||
import { getConfigDirectory } from '@kbn/utils';
|
||||
import { safeDump } from 'js-yaml';
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import { EncryptionConfig } from './encryption_config';
|
|||
import { generate } from './generate';
|
||||
|
||||
import { Logger } from '../cli/logger';
|
||||
import * as prompt from '../cli_keystore/utils/prompt';
|
||||
import * as prompt from '../cli/keystore/utils/prompt';
|
||||
import fs from 'fs';
|
||||
import crypto from 'crypto';
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { Logger } from '../cli/logger';
|
||||
import { confirm, question } from './utils';
|
||||
import { confirm, question } from '../cli/keystore/utils';
|
||||
// import from path since add.test.js mocks 'fs' required for @kbn/utils
|
||||
import { createPromiseFromStreams, createConcatStream } from '@kbn/utils/src/streams';
|
||||
|
||||
|
@ -24,6 +24,8 @@ export async function add(keystore, key, options = {}) {
|
|||
const logger = new Logger(options);
|
||||
let value;
|
||||
|
||||
await keystore.load();
|
||||
|
||||
if (!keystore.exists()) {
|
||||
return logger.error("ERROR: Kibana keystore not found. Use 'create' command to create one.");
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ import { PassThrough } from 'stream';
|
|||
import { Keystore } from '../cli/keystore';
|
||||
import { add } from './add';
|
||||
import { Logger } from '../cli/logger';
|
||||
import * as prompt from './utils/prompt';
|
||||
import * as prompt from '../cli/keystore/utils/prompt';
|
||||
|
||||
describe('Kibana keystore', () => {
|
||||
describe('add', () => {
|
||||
|
|
|
@ -18,39 +18,47 @@ import { listCli } from './list';
|
|||
import { addCli } from './add';
|
||||
import { removeCli } from './remove';
|
||||
import { showCli } from './show';
|
||||
import { passwdCli } from './passwd';
|
||||
import { hasPasswdCli } from './has_passwd';
|
||||
|
||||
const argv = process.argv.slice();
|
||||
const program = new Command('bin/kibana-keystore');
|
||||
|
||||
program
|
||||
.version(pkg.version)
|
||||
.description('A tool for managing settings stored in the Kibana keystore');
|
||||
async function initialize() {
|
||||
const program = new Command('bin/kibana-keystore');
|
||||
program
|
||||
.version(pkg.version)
|
||||
.description('A tool for managing settings stored in the Kibana keystore');
|
||||
|
||||
const keystore = new Keystore(getKeystore());
|
||||
const keystore = new Keystore(getKeystore());
|
||||
|
||||
createCli(program, keystore);
|
||||
listCli(program, keystore);
|
||||
addCli(program, keystore);
|
||||
removeCli(program, keystore);
|
||||
showCli(program, keystore);
|
||||
createCli(program, keystore);
|
||||
listCli(program, keystore);
|
||||
addCli(program, keystore);
|
||||
removeCli(program, keystore);
|
||||
showCli(program, keystore);
|
||||
passwdCli(program, keystore);
|
||||
hasPasswdCli(program, keystore);
|
||||
|
||||
program
|
||||
.command('help <command>')
|
||||
.description('get the help for a specific command')
|
||||
.action(function (cmdName) {
|
||||
const cmd = _.find(program.commands, { _name: cmdName });
|
||||
if (!cmd) return program.error(`unknown command ${cmdName}`);
|
||||
cmd.help();
|
||||
program
|
||||
.command('help <command>')
|
||||
.description('get the help for a specific command')
|
||||
.action(function (cmdName) {
|
||||
const cmd = _.find(program.commands, { _name: cmdName });
|
||||
if (!cmd) return program.error(`unknown command ${cmdName}`);
|
||||
cmd.help();
|
||||
});
|
||||
|
||||
program.command('*', null, { noHelp: true }).action(function (cmd) {
|
||||
program.error(`unknown command ${cmd}`);
|
||||
});
|
||||
|
||||
program.command('*', null, { noHelp: true }).action(function (cmd) {
|
||||
program.error(`unknown command ${cmd}`);
|
||||
});
|
||||
// check for no command name
|
||||
const subCommand = argv[2] && !String(argv[2][0]).match(/^-|^\.|\//);
|
||||
if (!subCommand) {
|
||||
program.defaultHelp();
|
||||
}
|
||||
|
||||
// check for no command name
|
||||
const subCommand = argv[2] && !String(argv[2][0]).match(/^-|^\.|\//);
|
||||
if (!subCommand) {
|
||||
program.defaultHelp();
|
||||
program.parse(process.argv);
|
||||
}
|
||||
|
||||
program.parse(process.argv);
|
||||
initialize().catch((e) => console.error(e));
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
*/
|
||||
|
||||
import { Logger } from '../cli/logger';
|
||||
import { confirm } from './utils';
|
||||
import { confirm, question } from '../cli/keystore/utils';
|
||||
|
||||
export async function create(keystore, options) {
|
||||
export async function create(keystore, options = {}) {
|
||||
const logger = new Logger(options);
|
||||
|
||||
if (keystore.exists()) {
|
||||
|
@ -21,6 +21,17 @@ export async function create(keystore, options) {
|
|||
}
|
||||
|
||||
keystore.reset();
|
||||
|
||||
if (options.password) {
|
||||
const password = await question(
|
||||
'Enter new password for the kibana keystore (empty for no password)',
|
||||
{
|
||||
mask: '*',
|
||||
}
|
||||
);
|
||||
if (password) keystore.setPassword(password);
|
||||
}
|
||||
|
||||
keystore.save();
|
||||
|
||||
logger.log(`Created Kibana keystore in ${keystore.path}`);
|
||||
|
@ -30,6 +41,7 @@ export function createCli(program, keystore) {
|
|||
program
|
||||
.command('create')
|
||||
.description('Creates a new Kibana keystore')
|
||||
.option('-s, --silent', 'prevent all logging')
|
||||
.option('-p, --password', 'Prompt for password to encrypt the keystore')
|
||||
.option('-s, --silent', 'Show minimal output')
|
||||
.action(create.bind(null, keystore));
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ import sinon from 'sinon';
|
|||
import { Keystore } from '../cli/keystore';
|
||||
import { create } from './create';
|
||||
import { Logger } from '../cli/logger';
|
||||
import * as prompt from './utils/prompt';
|
||||
import * as prompt from '../cli/keystore/utils/prompt';
|
||||
|
||||
describe('Kibana keystore', () => {
|
||||
describe('create', () => {
|
||||
|
@ -46,7 +46,7 @@ describe('Kibana keystore', () => {
|
|||
});
|
||||
|
||||
it('creates keystore file', async () => {
|
||||
const keystore = new Keystore('/data/foo.keystore');
|
||||
const keystore = await Keystore.initialize('/data/foo.keystore');
|
||||
sandbox.stub(keystore, 'save');
|
||||
|
||||
await create(keystore);
|
||||
|
@ -56,7 +56,7 @@ describe('Kibana keystore', () => {
|
|||
|
||||
it('logs successful keystore creating', async () => {
|
||||
const path = '/data/foo.keystore';
|
||||
const keystore = new Keystore(path);
|
||||
const keystore = await Keystore.initialize(path);
|
||||
|
||||
await create(keystore);
|
||||
|
||||
|
@ -67,7 +67,7 @@ describe('Kibana keystore', () => {
|
|||
it('prompts for overwrite', async () => {
|
||||
sandbox.stub(prompt, 'confirm').returns(Promise.resolve(true));
|
||||
|
||||
const keystore = new Keystore('/data/test.keystore');
|
||||
const keystore = await Keystore.initialize('/data/test.keystore');
|
||||
await create(keystore);
|
||||
|
||||
sinon.assert.calledOnce(prompt.confirm);
|
||||
|
@ -79,7 +79,7 @@ describe('Kibana keystore', () => {
|
|||
it('aborts if overwrite is denied', async () => {
|
||||
sandbox.stub(prompt, 'confirm').returns(Promise.resolve(false));
|
||||
|
||||
const keystore = new Keystore('/data/test.keystore');
|
||||
const keystore = await Keystore.initialize('/data/test.keystore');
|
||||
sandbox.stub(keystore, 'save');
|
||||
|
||||
await create(keystore);
|
||||
|
|
38
src/cli_keystore/has_passwd.js
Normal file
38
src/cli_keystore/has_passwd.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { Logger } from '../cli/logger';
|
||||
|
||||
export function hasPasswd(keystore, options = {}) {
|
||||
const logger = new Logger();
|
||||
|
||||
if (!keystore.exists()) {
|
||||
if (!options.silent) logger.error('Error: Keystore not found');
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
if (!keystore.hasPassword()) {
|
||||
if (!options.silent) logger.error('Error: Keystore is not password protected');
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
if (!options.silent) {
|
||||
logger.log('Keystore is password-protected');
|
||||
return process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
export function hasPasswdCli(program, keystore) {
|
||||
program
|
||||
.command('has-passwd')
|
||||
.description(
|
||||
'Succeeds if the keystore exists and is password-protected, fails with exit code 1 otherwise'
|
||||
)
|
||||
.option('-s, --silent', 'prevent all logging')
|
||||
.action(hasPasswd.bind(null, keystore));
|
||||
}
|
69
src/cli_keystore/has_passwd.test.js
Normal file
69
src/cli_keystore/has_passwd.test.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const mockKeystoreWithoutPassword =
|
||||
'1:20nsJf6P1Koi1x2kwrOhc4la7bqisOqJFlb5XpI95Qc/4sJjCHxoRzO1iGiBuoAtqolCHxRs976t59uFXQXtTv9zY5PoUvGyoPOxbA4q/H7n+EygneCbSc18MGHXA5K0NZm8RBhjWaKphe4=';
|
||||
const mockKeystoreWithPassword =
|
||||
'1:j/zZA0L6cPonF6zacVTOT0qwZeXgPJOZrLHhFYg+CzchCIcjjhH/70JyHj7gPCEa/ZrBm8gCAKbcXSo8eQsHP25Qf922f/tXI9m6IiXPf6G/v/KiO0rOSjobDNFYWCxCD7aIJmYnuoPMhqc=';
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
readFileSync: jest.fn().mockImplementation((path) => {
|
||||
if (path.includes('with_password.keystore')) {
|
||||
return JSON.stringify(mockKeystoreWithPassword);
|
||||
}
|
||||
if (path.includes('without_password.keystore')) {
|
||||
return JSON.stringify(mockKeystoreWithoutPassword);
|
||||
}
|
||||
|
||||
throw { code: 'ENOENT' };
|
||||
}),
|
||||
existsSync: jest.fn().mockImplementation(() => true),
|
||||
writeFileSync: jest.fn(),
|
||||
}));
|
||||
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { Keystore } from '../cli/keystore';
|
||||
import { hasPasswd } from './has_passwd';
|
||||
import { Logger } from '../cli/logger';
|
||||
|
||||
describe('Kibana keystore', () => {
|
||||
describe('has_passwd', () => {
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(Logger.prototype, 'log');
|
||||
sandbox.stub(Logger.prototype, 'error');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
it('exits 0 if password protected', async () => {
|
||||
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
||||
const keystore = new Keystore('with_password.keystore');
|
||||
hasPasswd(keystore);
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(0);
|
||||
sinon.assert.calledWith(Logger.prototype.log, 'Keystore is password-protected');
|
||||
});
|
||||
|
||||
it('exits 1 if not password protected', async () => {
|
||||
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
||||
const keystore = new Keystore('without_password.keystore');
|
||||
hasPasswd(keystore);
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
sinon.assert.calledWith(Logger.prototype.error, 'Error: Keystore is not password protected');
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
});
|
|
@ -8,8 +8,9 @@
|
|||
|
||||
import { Logger } from '../cli/logger';
|
||||
|
||||
export function list(keystore, options = {}) {
|
||||
export async function list(keystore, options = {}) {
|
||||
const logger = new Logger(options);
|
||||
await keystore.load();
|
||||
|
||||
if (!keystore.exists()) {
|
||||
return logger.error("ERROR: Kibana keystore not found. Use 'create' command to create one.");
|
||||
|
|
|
@ -42,17 +42,17 @@ describe('Kibana keystore', () => {
|
|||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('outputs keys', () => {
|
||||
it('outputs keys', async () => {
|
||||
const keystore = new Keystore('/data/test.keystore');
|
||||
list(keystore);
|
||||
await list(keystore);
|
||||
|
||||
sinon.assert.calledOnce(Logger.prototype.log);
|
||||
sinon.assert.calledWith(Logger.prototype.log, 'a1.b2.c3\na2');
|
||||
});
|
||||
|
||||
it('handles a nonexistent keystore', () => {
|
||||
it('handles a nonexistent keystore', async () => {
|
||||
const keystore = new Keystore('/data/nonexistent.keystore');
|
||||
list(keystore);
|
||||
await list(keystore);
|
||||
|
||||
sinon.assert.calledOnce(Logger.prototype.error);
|
||||
sinon.assert.calledWith(
|
||||
|
|
34
src/cli_keystore/passwd.js
Normal file
34
src/cli_keystore/passwd.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { Logger } from '../cli/logger';
|
||||
import { question } from '../cli/keystore/utils';
|
||||
|
||||
export async function passwd(keystore, options = {}) {
|
||||
const logger = new Logger(options);
|
||||
await keystore.load();
|
||||
|
||||
if (!keystore.exists()) {
|
||||
return logger.error("ERROR: Kibana keystore not found. Use 'create' command to create one.");
|
||||
}
|
||||
|
||||
const password =
|
||||
(await question('Enter new password for the kibana keystore (empty for no password)', {
|
||||
mask: '*',
|
||||
})) || '';
|
||||
keystore.setPassword(password);
|
||||
keystore.save();
|
||||
}
|
||||
|
||||
export function passwdCli(program, keystore) {
|
||||
program
|
||||
.command('passwd')
|
||||
.description('Changes the password of a keystore')
|
||||
.option('-s, --silent', 'prevent all logging')
|
||||
.action(passwd.bind(null, keystore));
|
||||
}
|
40
src/cli_keystore/passwd.test.js
Normal file
40
src/cli_keystore/passwd.test.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const mockKeystoreWithOldPassword =
|
||||
'1:9OsRzJI+gyDEH1ZjAHuKZFfYH7nEguzFRJwxWgj5WTJm5w+mzwKUzIdy65/lBW+XxY4wa1qYf0RSGJmfJKPz/er7pt58RJ8OgpicM2nCOMrqjPuovQr0QoMPbx736YlHEEIsuAaGAGItW7rlAQ==';
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
readFileSync: jest.fn().mockImplementation(() => JSON.stringify(mockKeystoreWithOldPassword)),
|
||||
existsSync: jest.fn().mockImplementation(() => true),
|
||||
writeFileSync: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as prompt from '../cli/keystore/utils/prompt';
|
||||
|
||||
import { Keystore } from '../cli/keystore';
|
||||
import { passwd } from './passwd';
|
||||
import fs from 'fs';
|
||||
|
||||
describe('Kibana keystore', () => {
|
||||
describe('has_passwd', () => {
|
||||
it('changes the password', async () => {
|
||||
const keystore = new Keystore('keystore', 'old_password');
|
||||
jest.spyOn(prompt, 'question').mockResolvedValue('new_password');
|
||||
await passwd(keystore);
|
||||
const newKeystoreData = fs.writeFileSync.mock.calls[0][1];
|
||||
jest.spyOn(fs, 'readFileSync').mockReturnValue(newKeystoreData);
|
||||
const newKeystore = await Keystore.initialize('keystore', 'new_password');
|
||||
expect(newKeystore.data).toEqual({ hello: 'world' });
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
});
|
|
@ -6,7 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export function remove(keystore, key) {
|
||||
export async function remove(keystore, key) {
|
||||
await keystore.load();
|
||||
keystore.remove(key);
|
||||
keystore.save();
|
||||
}
|
||||
|
|
|
@ -30,19 +30,19 @@ describe('Kibana keystore', () => {
|
|||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('removes key', () => {
|
||||
it('removes key', async () => {
|
||||
const keystore = new Keystore('/data/test.keystore');
|
||||
|
||||
remove(keystore, 'a2');
|
||||
await remove(keystore, 'a2');
|
||||
|
||||
expect(keystore.data).toEqual({ 'a1.b2.c3': 'foo' });
|
||||
});
|
||||
|
||||
it('persists the keystore', () => {
|
||||
it('persists the keystore', async () => {
|
||||
const keystore = new Keystore('/data/test.keystore');
|
||||
sandbox.stub(keystore, 'save');
|
||||
|
||||
remove(keystore, 'a2');
|
||||
await remove(keystore, 'a2');
|
||||
|
||||
sinon.assert.calledOnce(keystore.save);
|
||||
});
|
||||
|
|
|
@ -41,42 +41,46 @@ import { Keystore } from '../cli/keystore';
|
|||
import { show } from './show';
|
||||
|
||||
describe('Kibana keystore: show', () => {
|
||||
const keystore = new Keystore('mock-path', '');
|
||||
let keystore: Keystore;
|
||||
|
||||
it('reads stored strings', () => {
|
||||
const exitCode = show(keystore, 'foo', {});
|
||||
beforeAll(async () => {
|
||||
keystore = new Keystore('mock-path', '');
|
||||
});
|
||||
|
||||
it('reads stored strings', async () => {
|
||||
const exitCode = await show(keystore, 'foo', {});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(mockLogFn).toHaveBeenCalledWith('turbo2000');
|
||||
expect(mockErrFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reads stored numbers', () => {
|
||||
const exitCode = show(keystore, 'num', {});
|
||||
it('reads stored numbers', async () => {
|
||||
const exitCode = await show(keystore, 'num', {});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(mockLogFn).toHaveBeenCalledWith('12345');
|
||||
expect(mockErrFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reads stored objecs', () => {
|
||||
const exitCode = show(keystore, 'bar', {});
|
||||
it('reads stored objecs', async () => {
|
||||
const exitCode = await 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' });
|
||||
it('outputs to a file when the arg is passed', async () => {
|
||||
const exitCode = await 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' });
|
||||
it('logs and terminates with an error when output file exists', async () => {
|
||||
const exitCode = await show(keystore, 'foo', { output: 'existing-file.txt' });
|
||||
|
||||
expect(exitCode).toBe(-1);
|
||||
expect(mockErrFn).toHaveBeenCalledWith(
|
||||
|
@ -85,8 +89,8 @@ describe('Kibana keystore: show', () => {
|
|||
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');
|
||||
it("logs and terminates with an error when the store doesn't have the key", async () => {
|
||||
const exitCode = await show(keystore, 'no-key');
|
||||
|
||||
expect(exitCode).toBe(-1);
|
||||
expect(mockErrFn).toHaveBeenCalledWith("ERROR: Kibana keystore doesn't have requested key.");
|
||||
|
|
|
@ -16,10 +16,16 @@ interface ShowOptions {
|
|||
output?: string;
|
||||
}
|
||||
|
||||
export function show(keystore: Keystore, key: string, options: ShowOptions = {}): number | void {
|
||||
export async function show(
|
||||
keystore: Keystore,
|
||||
key: string,
|
||||
options: ShowOptions = {}
|
||||
): Promise<number | void> {
|
||||
const { silent, output } = options;
|
||||
const logger = new Logger({ silent });
|
||||
|
||||
await keystore.load();
|
||||
|
||||
if (!keystore.exists()) {
|
||||
logger.error("ERROR: Kibana keystore not found. Use 'create' command to create one.");
|
||||
return -1;
|
||||
|
@ -56,7 +62,7 @@ export function showCli(program: any, keystore: Keystore) {
|
|||
)
|
||||
.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;
|
||||
.action(async (key: string, options: ShowOptions) => {
|
||||
process.exitCode = (await show(keystore, key, options)) || 0;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
},
|
||||
"include": [
|
||||
"keystore/**/*",
|
||||
"utils/**/*",
|
||||
"*.js",
|
||||
"*.ts",
|
||||
],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue