open-vault/ui/app/lib/console-helpers.ts

259 lines
7.2 KiB
TypeScript
Raw Normal View History

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import keys from 'vault/lib/keycodes';
2023-05-17 16:41:02 +00:00
import AdapterError from '@ember-data/adapter/error';
import { parse } from 'shell-quote';
2023-05-17 16:41:02 +00:00
import argTokenizer from './arg-tokenizer';
import { StringMap } from 'vault/vault/app-types';
// Add new commands to `log-help` component for visibility
const supportedCommands = ['read', 'write', 'list', 'delete', 'kv-get'];
const uiCommands = ['api', 'clearall', 'clear', 'fullscreen', 'refresh'];
2023-05-17 16:41:02 +00:00
interface DataObj {
[key: string]: string | string[];
}
export function extractDataFromStrings(dataArray: string[]): DataObj {
if (!dataArray) return {};
return dataArray.reduce((accumulator: DataObj, val: string) => {
// will be "key=value" or "foo=bar=baz"
// split on the first =
// default to value of empty string
const [item = '', value = ''] = val.split(/=(.+)?/);
if (!item) return accumulator;
// if it exists in data already, then we have multiple
// foo=bar in the list and need to make it an array
const existingValue = accumulator[item];
if (existingValue) {
accumulator[item] = Array.isArray(existingValue) ? [...existingValue, value] : [existingValue, value];
return accumulator;
2023-05-17 16:41:02 +00:00
}
accumulator[item] = value;
return accumulator;
}, {});
}
interface Flags {
field?: string;
format?: string;
force?: boolean;
wrapTTL?: boolean;
[key: string]: string | boolean | undefined;
}
2023-05-17 16:41:02 +00:00
export function extractFlagsFromStrings(flagArray: string[], method: string): Flags {
if (!flagArray) return {};
return flagArray.reduce((accumulator: Flags, val: string) => {
// val will be "-flag=value" or "--force"
// split on the first =
// default to value or true
const [item, value] = val.split(/=(.+)?/);
if (!item) return accumulator;
2023-05-17 16:41:02 +00:00
let flagName = item.replace(/^-/, '');
if (flagName === 'wrap-ttl') {
flagName = 'wrapTTL';
} else if (method === 'write') {
if (flagName === 'f' || flagName === '-force') {
flagName = 'force';
}
}
accumulator[flagName] = value || true;
return accumulator;
}, {});
}
interface CommandFns {
[key: string]: CallableFunction;
}
export function executeUICommand(
command: string,
logAndOutput: CallableFunction,
commandFns: CommandFns
): boolean {
const cmd = command.startsWith('api') ? 'api' : command;
const isUICommand = uiCommands.includes(cmd);
if (isUICommand) {
logAndOutput(command);
}
2023-05-17 16:41:02 +00:00
const execCommand = commandFns[cmd];
if (execCommand && typeof execCommand === 'function') {
execCommand();
}
return isUICommand;
}
2023-05-17 16:41:02 +00:00
interface ParsedCommand {
method: string;
path: string;
flagArray: string[];
dataArray: string[];
}
export function parseCommand(command: string): ParsedCommand {
const args: string[] = argTokenizer(parse(command));
if (args[0] === 'vault') {
args.shift();
}
2023-05-17 16:41:02 +00:00
const [method = '', ...rest] = args;
let path = '';
const flags: string[] = [];
const data: string[] = [];
Ember Upgrade to 3.24 (#13443) * Update browserslist * Add browserslistrc * ember-cli-update --to 3.26, fix conflicts * Run codemodes that start with ember-* * More codemods - before cp* * More codemods (curly data-test-*) * WIP ember-basic-dropdown template errors * updates ember-basic-dropdown and related deps to fix build issues * updates basic dropdown instances to new version API * updates more deps -- ember-template-lint is working again * runs no-implicit-this codemod * creates and runs no-quoteless-attributes codemod * runs angle brackets codemod * updates lint:hbs globs to only touch hbs files * removes yield only templates * creates and runs deprecated args transform * supresses lint error for invokeAction on LinkTo component * resolves remaining ambiguous path lint errors * resolves simple-unless lint errors * adds warnings for deprecated tagName arg on LinkTo components * adds warnings for remaining curly component invocation * updates global template lint rules * resolves remaining template lint errors * disables some ember specfic lint rules that target pre octane patterns * js lint fix run * resolves remaining js lint errors * fixes test run * adds npm-run-all dep * fixes test attribute issues * fixes console acceptance tests * fixes tests * adds yield only wizard/tutorial-active template * fixes more tests * attempts to fix more flaky tests * removes commented out settled in transit test * updates deprecations workflow and adds initializer to filter by version * updates flaky policies acl old test * updates to flaky transit test * bumps ember deps down to LTS version * runs linters after main merge * fixes client count tests after bad merge conflict fixes * fixes client count history test * more updates to lint config * another round of hbs lint fixes after extending stylistic rule * updates lint-staged commands * removes indent eslint rule since it seems to break things * fixes bad attribute in transform-edit-form template * test fixes * fixes enterprise tests * adds changelog * removes deprecated ember-concurrency-test-waiters dep and adds @ember/test-waiters * flaky test fix Co-authored-by: hashishaw <cshaw@hashicorp.com>
2021-12-17 03:44:29 +00:00
rest.forEach((arg) => {
if (arg.startsWith('-')) {
flags.push(arg);
} else {
if (path) {
const strippedArg = arg
// we'll have arg=something or arg="lol I need spaces", so need to split on the first =
.split(/=(.+)/)
// if there were quotes, there's an empty string as the last member in the array that we don't want,
// so filter it out
Ember Upgrade to 3.24 (#13443) * Update browserslist * Add browserslistrc * ember-cli-update --to 3.26, fix conflicts * Run codemodes that start with ember-* * More codemods - before cp* * More codemods (curly data-test-*) * WIP ember-basic-dropdown template errors * updates ember-basic-dropdown and related deps to fix build issues * updates basic dropdown instances to new version API * updates more deps -- ember-template-lint is working again * runs no-implicit-this codemod * creates and runs no-quoteless-attributes codemod * runs angle brackets codemod * updates lint:hbs globs to only touch hbs files * removes yield only templates * creates and runs deprecated args transform * supresses lint error for invokeAction on LinkTo component * resolves remaining ambiguous path lint errors * resolves simple-unless lint errors * adds warnings for deprecated tagName arg on LinkTo components * adds warnings for remaining curly component invocation * updates global template lint rules * resolves remaining template lint errors * disables some ember specfic lint rules that target pre octane patterns * js lint fix run * resolves remaining js lint errors * fixes test run * adds npm-run-all dep * fixes test attribute issues * fixes console acceptance tests * fixes tests * adds yield only wizard/tutorial-active template * fixes more tests * attempts to fix more flaky tests * removes commented out settled in transit test * updates deprecations workflow and adds initializer to filter by version * updates flaky policies acl old test * updates to flaky transit test * bumps ember deps down to LTS version * runs linters after main merge * fixes client count tests after bad merge conflict fixes * fixes client count history test * more updates to lint config * another round of hbs lint fixes after extending stylistic rule * updates lint-staged commands * removes indent eslint rule since it seems to break things * fixes bad attribute in transform-edit-form template * test fixes * fixes enterprise tests * adds changelog * removes deprecated ember-concurrency-test-waiters dep and adds @ember/test-waiters * flaky test fix Co-authored-by: hashishaw <cshaw@hashicorp.com>
2021-12-17 03:44:29 +00:00
.filter((str) => str !== '')
// glue the data back together
.join('=');
data.push(strippedArg);
} else {
path = arg;
}
}
});
if (!supportedCommands.includes(method)) {
2023-05-17 16:41:02 +00:00
throw new Error('invalid command');
}
2023-05-17 16:41:02 +00:00
return { method, flagArray: flags, path, dataArray: data };
}
2023-05-17 16:41:02 +00:00
interface LogResponse {
auth?: StringMap;
data?: StringMap;
wrap_info?: StringMap;
[key: string]: unknown;
}
export function logFromResponse(response: LogResponse, path: string, method: string, flags: Flags) {
const { format, field } = flags;
2023-05-17 16:41:02 +00:00
const respData: StringMap | undefined = response && (response.auth || response.data || response.wrap_info);
const secret: StringMap | LogResponse = respData || response;
if (!respData) {
if (method === 'write') {
return { type: 'success', content: `Success! Data written to: ${path}` };
} else if (method === 'delete') {
return { type: 'success', content: `Success! Data deleted (if it existed) at: ${path}` };
}
}
if (field) {
const fieldValue = secret[field];
let response;
if (fieldValue) {
if (format && format === 'json') {
return { type: 'json', content: fieldValue };
}
if (typeof fieldValue == 'string') {
response = { type: 'text', content: fieldValue };
} else if (typeof fieldValue == 'number') {
response = { type: 'text', content: JSON.stringify(fieldValue) };
} else if (typeof fieldValue == 'boolean') {
response = { type: 'text', content: JSON.stringify(fieldValue) };
} else if (Array.isArray(fieldValue)) {
response = { type: 'text', content: JSON.stringify(fieldValue) };
} else {
response = { type: 'object', content: fieldValue };
}
} else {
response = { type: 'error', content: `Field "${field}" not present in secret` };
}
return response;
}
if (format && format === 'json') {
// just print whole response
return { type: 'json', content: response };
}
if (method === 'list') {
return { type: 'list', content: secret };
}
return { type: 'object', content: secret };
}
2023-05-17 16:41:02 +00:00
interface CustomError extends AdapterError {
httpStatus: number;
path: string;
errors: string[];
}
export function logFromError(error: CustomError, vaultPath: string, method: string) {
let content;
const { httpStatus, path } = error;
const verbClause = {
read: 'reading from',
2023-05-17 16:41:02 +00:00
'kv-get': 'reading secret',
write: 'writing to',
list: 'listing',
delete: 'deleting at',
}[method];
content = `Error ${verbClause}: ${vaultPath}.\nURL: ${path}\nCode: ${httpStatus}`;
if (typeof error.errors[0] === 'string') {
content = `${content}\nErrors:\n ${error.errors.join('\n ')}`;
}
return { type: 'error', content };
}
2023-05-17 16:41:02 +00:00
interface CommandLog {
type: string;
content?: string;
}
export function shiftCommandIndex(keyCode: number, history: CommandLog[], index: number) {
let newInputValue;
const commandHistoryLength = history.length;
if (!commandHistoryLength) {
return [];
}
if (keyCode === keys.UP) {
index -= 1;
if (index < 0) {
index = commandHistoryLength - 1;
}
} else {
index += 1;
if (index === commandHistoryLength) {
newInputValue = '';
}
if (index > commandHistoryLength) {
index -= 1;
}
}
if (newInputValue !== '') {
2023-05-17 16:41:02 +00:00
newInputValue = history.objectAt(index)?.content;
}
return [index, newInputValue];
}
2023-05-17 16:41:02 +00:00
export function formattedErrorFromInput(path: string, method: string, flags: Flags, dataArray: string[]) {
if (path === undefined) {
return { type: 'error', content: 'A path is required to make a request.' };
}
if (method === 'write' && !flags.force && dataArray.length === 0) {
return { type: 'error', content: 'Must supply data or use -force' };
}
2023-05-17 16:41:02 +00:00
return;
}