mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +00:00 
			
		
		
		
	UI/console update (#20590)
This commit is contained in:
		
							
								
								
									
										3
									
								
								changelog/20590.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/20590.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | ```release-note:improvement | ||||||
|  | ui: Update Web CLI with examples and a new `kv-get` command for reading kv v2 data and metadata | ||||||
|  | ``` | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| /** |  | ||||||
|  * Copyright (c) HashiCorp, Inc. |  | ||||||
|  * SPDX-License-Identifier: MPL-2.0 |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| import Component from '@ember/component'; |  | ||||||
|  |  | ||||||
| export default Component.extend({}); |  | ||||||
| @@ -8,15 +8,17 @@ import { alias, or } from '@ember/object/computed'; | |||||||
| import Component from '@ember/component'; | import Component from '@ember/component'; | ||||||
| import { getOwner } from '@ember/application'; | import { getOwner } from '@ember/application'; | ||||||
| import { schedule } from '@ember/runloop'; | import { schedule } from '@ember/runloop'; | ||||||
|  | import { camelize } from '@ember/string'; | ||||||
| import { task } from 'ember-concurrency'; | import { task } from 'ember-concurrency'; | ||||||
| import ControlGroupError from 'vault/lib/control-group-error'; | import ControlGroupError from 'vault/lib/control-group-error'; | ||||||
| import { | import { | ||||||
|   parseCommand, |   parseCommand, | ||||||
|   extractDataAndFlags, |  | ||||||
|   logFromResponse, |   logFromResponse, | ||||||
|   logFromError, |   logFromError, | ||||||
|   logErrorFromInput, |   formattedErrorFromInput, | ||||||
|   executeUICommand, |   executeUICommand, | ||||||
|  |   extractFlagsFromStrings, | ||||||
|  |   extractDataFromStrings, | ||||||
| } from 'vault/lib/console-helpers'; | } from 'vault/lib/console-helpers'; | ||||||
|  |  | ||||||
| export default Component.extend({ | export default Component.extend({ | ||||||
| @@ -64,29 +66,25 @@ export default Component.extend({ | |||||||
|  |  | ||||||
|     // parse to verify it's valid |     // parse to verify it's valid | ||||||
|     try { |     try { | ||||||
|       serviceArgs = parseCommand(command, shouldThrow); |       serviceArgs = parseCommand(command); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       this.logAndOutput(command, { type: 'help' }); |       if (shouldThrow) { | ||||||
|       return; |         this.logAndOutput(command, { type: 'help' }); | ||||||
|     } |       } | ||||||
|     // we have a invalid command but don't want to throw |  | ||||||
|     if (serviceArgs === false) { |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const [method, flagArray, path, dataArray] = serviceArgs; |     const { method, flagArray, path, dataArray } = serviceArgs; | ||||||
|  |     const flags = extractFlagsFromStrings(flagArray, method); | ||||||
|  |     const data = extractDataFromStrings(dataArray); | ||||||
|  |  | ||||||
|     if (dataArray || flagArray) { |     const inputError = formattedErrorFromInput(path, method, flags, dataArray); | ||||||
|       var { data, flags } = extractDataAndFlags(method, dataArray, flagArray); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const inputError = logErrorFromInput(path, method, flags, dataArray); |  | ||||||
|     if (inputError) { |     if (inputError) { | ||||||
|       this.logAndOutput(command, inputError); |       this.logAndOutput(command, inputError); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     try { |     try { | ||||||
|       const resp = yield service[method].call(service, path, data, flags.wrapTTL); |       const resp = yield service[camelize(method)].call(service, path, data, flags); | ||||||
|       this.logAndOutput(command, logFromResponse(resp, path, method, flags)); |       this.logAndOutput(command, logFromResponse(resp, path, method, flags)); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       if (error instanceof ControlGroupError) { |       if (error instanceof ControlGroupError) { | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ | |||||||
|       </:footer> |       </:footer> | ||||||
|     </Hds::SideNav> |     </Hds::SideNav> | ||||||
|   </Frame.Sidebar> |   </Frame.Sidebar> | ||||||
|   <Frame.Main id="app-main-content"> |   <Frame.Main id="app-main-content" class={{if this.console.isOpen "main--console-open"}}> | ||||||
|     {{! outlet for app content }} |     {{! outlet for app content }} | ||||||
|     <div id="modal-wormhole"></div> |     <div id="modal-wormhole"></div> | ||||||
|     <LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} /> |     <LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} /> | ||||||
|   | |||||||
| @@ -4,66 +4,107 @@ | |||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| import keys from 'vault/lib/keycodes'; | import keys from 'vault/lib/keycodes'; | ||||||
| import argTokenizer from './arg-tokenizer'; | import AdapterError from '@ember-data/adapter/error'; | ||||||
| import { parse } from 'shell-quote'; | import { parse } from 'shell-quote'; | ||||||
| 
 | 
 | ||||||
| const supportedCommands = ['read', 'write', 'list', 'delete']; | 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']; | const uiCommands = ['api', 'clearall', 'clear', 'fullscreen', 'refresh']; | ||||||
| 
 | 
 | ||||||
| export function extractDataAndFlags(method, data, flags) { | interface DataObj { | ||||||
|   return data.concat(flags).reduce( |   [key: string]: string | string[]; | ||||||
|     (accumulator, val) => { |  | ||||||
|       // will be "key=value" or "-flag=value" or "foo=bar=baz"
 |  | ||||||
|       // split on the first =
 |  | ||||||
|       // default to value of empty string
 |  | ||||||
|       const [item, value = ''] = val.split(/=(.+)?/); |  | ||||||
|       if (item.startsWith('-')) { |  | ||||||
|         let flagName = item.replace(/^-/, ''); |  | ||||||
|         if (flagName === 'wrap-ttl') { |  | ||||||
|           flagName = 'wrapTTL'; |  | ||||||
|         } else if (method === 'write') { |  | ||||||
|           if (flagName === 'f' || flagName === '-force') { |  | ||||||
|             flagName = 'force'; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         accumulator.flags[flagName] = value || true; |  | ||||||
|         return accumulator; |  | ||||||
|       } |  | ||||||
|       // if it exists in data already, then we have multiple
 |  | ||||||
|       // foo=bar in the list and need to make it an array
 |  | ||||||
|       if (accumulator.data[item]) { |  | ||||||
|         accumulator.data[item] = [].concat(accumulator.data[item], value); |  | ||||||
|         return accumulator; |  | ||||||
|       } |  | ||||||
|       accumulator.data[item] = value; |  | ||||||
|       return accumulator; |  | ||||||
|     }, |  | ||||||
|     { data: {}, flags: {} } |  | ||||||
|   ); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function executeUICommand(command, logAndOutput, commandFns) { | 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; | ||||||
|  |     } | ||||||
|  |     accumulator[item] = value; | ||||||
|  |     return accumulator; | ||||||
|  |   }, {}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Flags { | ||||||
|  |   field?: string; | ||||||
|  |   format?: string; | ||||||
|  |   force?: boolean; | ||||||
|  |   wrapTTL?: boolean; | ||||||
|  |   [key: string]: string | boolean | undefined; | ||||||
|  | } | ||||||
|  | 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; | ||||||
|  | 
 | ||||||
|  |     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 cmd = command.startsWith('api') ? 'api' : command; | ||||||
|   const isUICommand = uiCommands.includes(cmd); |   const isUICommand = uiCommands.includes(cmd); | ||||||
|   if (isUICommand) { |   if (isUICommand) { | ||||||
|     logAndOutput(command); |     logAndOutput(command); | ||||||
|   } |   } | ||||||
|   if (typeof commandFns[cmd] === 'function') { |   const execCommand = commandFns[cmd]; | ||||||
|     commandFns[cmd](); |   if (execCommand && typeof execCommand === 'function') { | ||||||
|  |     execCommand(); | ||||||
|   } |   } | ||||||
|   return isUICommand; |   return isUICommand; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function parseCommand(command, shouldThrow) { | interface ParsedCommand { | ||||||
|   const args = argTokenizer(parse(command)); |   method: string; | ||||||
|  |   path: string; | ||||||
|  |   flagArray: string[]; | ||||||
|  |   dataArray: string[]; | ||||||
|  | } | ||||||
|  | export function parseCommand(command: string): ParsedCommand { | ||||||
|  |   const args: string[] = argTokenizer(parse(command)); | ||||||
|   if (args[0] === 'vault') { |   if (args[0] === 'vault') { | ||||||
|     args.shift(); |     args.shift(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const [method, ...rest] = args; |   const [method = '', ...rest] = args; | ||||||
|   let path; |   let path = ''; | ||||||
|   const flags = []; |   const flags: string[] = []; | ||||||
|   const data = []; |   const data: string[] = []; | ||||||
| 
 | 
 | ||||||
|   rest.forEach((arg) => { |   rest.forEach((arg) => { | ||||||
|     if (arg.startsWith('-')) { |     if (arg.startsWith('-')) { | ||||||
| @@ -86,24 +127,28 @@ export function parseCommand(command, shouldThrow) { | |||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   if (!supportedCommands.includes(method)) { |   if (!supportedCommands.includes(method)) { | ||||||
|     if (shouldThrow) { |     throw new Error('invalid command'); | ||||||
|       throw new Error('invalid command'); |  | ||||||
|     } |  | ||||||
|     return false; |  | ||||||
|   } |   } | ||||||
|   return [method, flags, path, data]; |   return { method, flagArray: flags, path, dataArray: data }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function logFromResponse(response, path, method, flags) { | 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; |   const { format, field } = flags; | ||||||
|   let secret = response && (response.auth || response.data || response.wrap_info); |   const respData: StringMap | undefined = response && (response.auth || response.data || response.wrap_info); | ||||||
|   if (!secret) { |   const secret: StringMap | LogResponse = respData || response; | ||||||
|  | 
 | ||||||
|  |   if (!respData) { | ||||||
|     if (method === 'write') { |     if (method === 'write') { | ||||||
|       return { type: 'success', content: `Success! Data written to: ${path}` }; |       return { type: 'success', content: `Success! Data written to: ${path}` }; | ||||||
|     } else if (method === 'delete') { |     } else if (method === 'delete') { | ||||||
|       return { type: 'success', content: `Success! Data deleted (if it existed) at: ${path}` }; |       return { type: 'success', content: `Success! Data deleted (if it existed) at: ${path}` }; | ||||||
|     } else { |  | ||||||
|       secret = response; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @@ -143,11 +188,17 @@ export function logFromResponse(response, path, method, flags) { | |||||||
|   return { type: 'object', content: secret }; |   return { type: 'object', content: secret }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function logFromError(error, vaultPath, method) { | interface CustomError extends AdapterError { | ||||||
|  |   httpStatus: number; | ||||||
|  |   path: string; | ||||||
|  |   errors: string[]; | ||||||
|  | } | ||||||
|  | export function logFromError(error: CustomError, vaultPath: string, method: string) { | ||||||
|   let content; |   let content; | ||||||
|   const { httpStatus, path } = error; |   const { httpStatus, path } = error; | ||||||
|   const verbClause = { |   const verbClause = { | ||||||
|     read: 'reading from', |     read: 'reading from', | ||||||
|  |     'kv-get': 'reading secret', | ||||||
|     write: 'writing to', |     write: 'writing to', | ||||||
|     list: 'listing', |     list: 'listing', | ||||||
|     delete: 'deleting at', |     delete: 'deleting at', | ||||||
| @@ -162,7 +213,11 @@ export function logFromError(error, vaultPath, method) { | |||||||
|   return { type: 'error', content }; |   return { type: 'error', content }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function shiftCommandIndex(keyCode, history, index) { | interface CommandLog { | ||||||
|  |   type: string; | ||||||
|  |   content?: string; | ||||||
|  | } | ||||||
|  | export function shiftCommandIndex(keyCode: number, history: CommandLog[], index: number) { | ||||||
|   let newInputValue; |   let newInputValue; | ||||||
|   const commandHistoryLength = history.length; |   const commandHistoryLength = history.length; | ||||||
| 
 | 
 | ||||||
| @@ -186,17 +241,18 @@ export function shiftCommandIndex(keyCode, history, index) { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (newInputValue !== '') { |   if (newInputValue !== '') { | ||||||
|     newInputValue = history.objectAt(index).content; |     newInputValue = history.objectAt(index)?.content; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return [index, newInputValue]; |   return [index, newInputValue]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function logErrorFromInput(path, method, flags, dataArray) { | export function formattedErrorFromInput(path: string, method: string, flags: Flags, dataArray: string[]) { | ||||||
|   if (path === undefined) { |   if (path === undefined) { | ||||||
|     return { type: 'error', content: 'A path is required to make a request.' }; |     return { type: 'error', content: 'A path is required to make a request.' }; | ||||||
|   } |   } | ||||||
|   if (method === 'write' && !flags.force && dataArray.length === 0) { |   if (method === 'write' && !flags.force && dataArray.length === 0) { | ||||||
|     return { type: 'error', content: 'Must supply data or use -force' }; |     return { type: 'error', content: 'Must supply data or use -force' }; | ||||||
|   } |   } | ||||||
|  |   return; | ||||||
| } | } | ||||||
| @@ -84,11 +84,22 @@ export default Service.extend({ | |||||||
|     }); |     }); | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   read(path, data, wrapTTL) { |   kvGet(path, data, flags = {}) { | ||||||
|  |     const { wrapTTL, metadata } = flags; | ||||||
|  |     // Split on first / to find backend and secret path | ||||||
|  |     const pathSegment = metadata ? 'metadata' : 'data'; | ||||||
|  |     const [backend, secretPath] = path.split(/\/(.+)?/); | ||||||
|  |     const kvPath = `${backend}/${pathSegment}/${secretPath}`; | ||||||
|  |     return this.ajax('read', sanitizePath(kvPath), { wrapTTL }); | ||||||
|  |   }, | ||||||
|  |  | ||||||
|  |   read(path, data, flags) { | ||||||
|  |     const wrapTTL = flags?.wrapTTL; | ||||||
|     return this.ajax('read', sanitizePath(path), { wrapTTL }); |     return this.ajax('read', sanitizePath(path), { wrapTTL }); | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   write(path, data, wrapTTL) { |   write(path, data, flags) { | ||||||
|  |     const wrapTTL = flags?.wrapTTL; | ||||||
|     return this.ajax('write', sanitizePath(path), { data, wrapTTL }); |     return this.ajax('write', sanitizePath(path), { data, wrapTTL }); | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
| @@ -96,7 +107,8 @@ export default Service.extend({ | |||||||
|     return this.ajax('delete', sanitizePath(path)); |     return this.ajax('delete', sanitizePath(path)); | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   list(path, data, wrapTTL) { |   list(path, data, flags) { | ||||||
|  |     const wrapTTL = flags?.wrapTTL; | ||||||
|     const listPath = ensureTrailingSlash(sanitizePath(path)); |     const listPath = ensureTrailingSlash(sanitizePath(path)); | ||||||
|     return this.ajax('list', listPath, { |     return this.ajax('list', listPath, { | ||||||
|       data: { |       data: { | ||||||
|   | |||||||
| @@ -125,6 +125,10 @@ $console-close-height: 35px; | |||||||
|   min-height: 400px; |   min-height: 400px; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .main--console-open { | ||||||
|  |   padding-bottom: 400px; | ||||||
|  | } | ||||||
|  |  | ||||||
| .panel-open .console-ui-panel.fullscreen { | .panel-open .console-ui-panel.fullscreen { | ||||||
|   bottom: 0; |   bottom: 0; | ||||||
|   right: 0; |   right: 0; | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ | |||||||
|  |  | ||||||
| Commands: | Commands: | ||||||
|   read        Read data and retrieves secrets |   read        Read data and retrieves secrets | ||||||
|  |   kv-get      Read data for kv v2 secret engines. Use -metadata flag to read metadata | ||||||
|   write       Write data, configuration, and secrets |   write       Write data, configuration, and secrets | ||||||
|   delete      Delete secrets and configuration |   delete      Delete secrets and configuration | ||||||
|   list        List data or secrets |   list        List data or secrets | ||||||
|   | |||||||
| @@ -5,10 +5,18 @@ | |||||||
| </div> | </div> | ||||||
| <div class="console-ui-panel-content"> | <div class="console-ui-panel-content"> | ||||||
|   <div class="content has-bottom-margin-l"> |   <div class="content has-bottom-margin-l"> | ||||||
|     <p class="has-text-grey is-font-mono"> |     <p class="has-text-grey is-font-mono has-bottom-margin-s"> | ||||||
|       The Vault Browser CLI provides an easy way to execute the most common CLI commands, such as write, read, delete, and |       The Vault Browser CLI provides an easy way to execute common Vault CLI commands, such as write, read, delete, and list. | ||||||
|       list. |       It does not include kv v2 write or put commands. For guidance, type `help`. | ||||||
|     </p> |     </p> | ||||||
|  |     <p class="has-text-grey is-font-mono has-bottom-margin-s">Examples:</p> | ||||||
|  |     <p class="has-text-grey is-font-mono">→ Write secrets to kv v1: write <mount>/my-secret foo=bar</p> | ||||||
|  |     <p class="has-text-grey is-font-mono">→ List kv v1 secret keys: list <mount>/</p> | ||||||
|  |     <p class="has-text-grey is-font-mono">→ Read a kv v1 secret: read <mount>/my-secret</p> | ||||||
|  |     <p class="has-text-grey is-font-mono">→ Mount a kv v2 secret engine: write sys/mounts/<mount> type=kv | ||||||
|  |       options=version=2</p> | ||||||
|  |     <p class="has-text-grey is-font-mono">→ Read a kv v2 secret: kv-get <mount>/secret-path</p> | ||||||
|  |     <p class="has-text-grey is-font-mono">→ Read a kv v2 secret's metadata: kv-get <mount>/secret-path -metadata</p> | ||||||
|   </div> |   </div> | ||||||
|   <Console::OutputLog @outputLog={{this.cliLog}} /> |   <Console::OutputLog @outputLog={{this.cliLog}} /> | ||||||
|   <Console::CommandInput |   <Console::CommandInput | ||||||
|   | |||||||
| @@ -10,8 +10,11 @@ | |||||||
|   <ToolbarFilters> |   <ToolbarFilters> | ||||||
|     <div class="field is-marginless"> |     <div class="field is-marginless"> | ||||||
|       <p class="control has-icons-left"> |       <p class="control has-icons-left"> | ||||||
|  |         <label for="swagger-result-filter" class="sr-only">Filter operations by path</label> | ||||||
|         <input |         <input | ||||||
|           oninput={{queue (action "updateFilter") (action "proxyEvent")}} |           id="swagger-result-filter" | ||||||
|  |           {{on "input" (action "proxyEvent")}} | ||||||
|  |           {{on "change" (action "updateFilter")}} | ||||||
|           value={{@initialFilter}} |           value={{@initialFilter}} | ||||||
|           disabled={{this.swaggerLoading}} |           disabled={{this.swaggerLoading}} | ||||||
|           class="filter input" |           class="filter input" | ||||||
|   | |||||||
| @@ -92,6 +92,7 @@ | |||||||
|     "@types/ember__utils": "^4.0.2", |     "@types/ember__utils": "^4.0.2", | ||||||
|     "@types/qunit": "^2.19.3", |     "@types/qunit": "^2.19.3", | ||||||
|     "@types/rsvp": "^4.0.4", |     "@types/rsvp": "^4.0.4", | ||||||
|  |     "@types/shell-quote": "^1.7.1", | ||||||
|     "@typescript-eslint/eslint-plugin": "^5.19.0", |     "@typescript-eslint/eslint-plugin": "^5.19.0", | ||||||
|     "@typescript-eslint/parser": "^5.19.0", |     "@typescript-eslint/parser": "^5.19.0", | ||||||
|     "asn1js": "^2.2.0", |     "asn1js": "^2.2.0", | ||||||
|   | |||||||
| @@ -6,10 +6,11 @@ | |||||||
| import { module, test } from 'qunit'; | import { module, test } from 'qunit'; | ||||||
| import { | import { | ||||||
|   parseCommand, |   parseCommand, | ||||||
|   extractDataAndFlags, |  | ||||||
|   logFromResponse, |   logFromResponse, | ||||||
|   logFromError, |   logFromError, | ||||||
|   logErrorFromInput, |   formattedErrorFromInput, | ||||||
|  |   extractFlagsFromStrings, | ||||||
|  |   extractDataFromStrings, | ||||||
| } from 'vault/lib/console-helpers'; | } from 'vault/lib/console-helpers'; | ||||||
|  |  | ||||||
| module('Unit | Lib | console helpers', function () { | module('Unit | Lib | console helpers', function () { | ||||||
| @@ -20,16 +21,16 @@ module('Unit | Lib | console helpers', function () { | |||||||
|       access_key=AKIAJWVN5Z4FOFT7NLNA \ |       access_key=AKIAJWVN5Z4FOFT7NLNA \ | ||||||
|       secret_key=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 \ |       secret_key=R4nm063hgMVo4BTT5xOs5nHLeLXA6lar7ZJ3Nt0i \ | ||||||
|       region=us-east-1`, |       region=us-east-1`, | ||||||
|       expected: [ |       expected: { | ||||||
|         'write', |         method: 'write', | ||||||
|         [], |         flagArray: [], | ||||||
|         'aws/config/root', |         path: 'aws/config/root', | ||||||
|         [ |         dataArray: [ | ||||||
|           'access_key=AKIAJWVN5Z4FOFT7NLNA', |           'access_key=AKIAJWVN5Z4FOFT7NLNA', | ||||||
|           'secret_key=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', |           'secret_key=R4nm063hgMVo4BTT5xOs5nHLeLXA6lar7ZJ3Nt0i', | ||||||
|           'region=us-east-1', |           'region=us-east-1', | ||||||
|         ], |         ], | ||||||
|       ], |       }, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       name: 'write with space in a value', |       name: 'write with space in a value', | ||||||
| @@ -43,11 +44,11 @@ module('Unit | Lib | console helpers', function () { | |||||||
|       insecure_tls=true \ |       insecure_tls=true \ | ||||||
|       starttls=false |       starttls=false | ||||||
|       `, |       `, | ||||||
|       expected: [ |       expected: { | ||||||
|         'write', |         method: 'write', | ||||||
|         [], |         flagArray: [], | ||||||
|         'auth/ldap/config', |         path: 'auth/ldap/config', | ||||||
|         [ |         dataArray: [ | ||||||
|           'url=ldap://ldap.example.com:3268', |           'url=ldap://ldap.example.com:3268', | ||||||
|           'binddn=CN=ServiceViewDev,OU=Service Accounts,DC=example,DC=com', |           'binddn=CN=ServiceViewDev,OU=Service Accounts,DC=example,DC=com', | ||||||
|           'bindpass=xxxxxxxxxxxxxxxxxxxxxxxxxx', |           'bindpass=xxxxxxxxxxxxxxxxxxxxxxxxxx', | ||||||
| @@ -56,7 +57,7 @@ module('Unit | Lib | console helpers', function () { | |||||||
|           'insecure_tls=true', |           'insecure_tls=true', | ||||||
|           'starttls=false', |           'starttls=false', | ||||||
|         ], |         ], | ||||||
|       ], |       }, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       name: 'write with double quotes', |       name: 'write with double quotes', | ||||||
| @@ -64,7 +65,7 @@ module('Unit | Lib | console helpers', function () { | |||||||
|       auth/token/create \ |       auth/token/create \ | ||||||
|       policies="foo" |       policies="foo" | ||||||
|       `, |       `, | ||||||
|       expected: ['write', [], 'auth/token/create', ['policies=foo']], |       expected: { method: 'write', flagArray: [], path: 'auth/token/create', dataArray: ['policies=foo'] }, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       name: 'write with single quotes', |       name: 'write with single quotes', | ||||||
| @@ -72,7 +73,7 @@ module('Unit | Lib | console helpers', function () { | |||||||
|       auth/token/create \ |       auth/token/create \ | ||||||
|       policies='foo' |       policies='foo' | ||||||
|       `, |       `, | ||||||
|       expected: ['write', [], 'auth/token/create', ['policies=foo']], |       expected: { method: 'write', flagArray: [], path: 'auth/token/create', dataArray: ['policies=foo'] }, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       name: 'write with unmatched quotes', |       name: 'write with unmatched quotes', | ||||||
| @@ -80,30 +81,35 @@ module('Unit | Lib | console helpers', function () { | |||||||
|       auth/token/create \ |       auth/token/create \ | ||||||
|       policies="'foo" |       policies="'foo" | ||||||
|       `, |       `, | ||||||
|       expected: ['write', [], 'auth/token/create', ["policies='foo"]], |       expected: { method: 'write', flagArray: [], path: 'auth/token/create', dataArray: ["policies='foo"] }, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       name: 'write with shell characters', |       name: 'write with shell characters', | ||||||
|       /* eslint-disable no-useless-escape */ |       /* eslint-disable no-useless-escape */ | ||||||
|       command: `vault write  database/roles/api-prod db_name=apiprod creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" default_ttl=1h max_ttl=24h |       command: `vault write  database/roles/api-prod db_name=apiprod creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" default_ttl=1h max_ttl=24h | ||||||
|       `, |       `, | ||||||
|       expected: [ |       expected: { | ||||||
|         'write', |         method: 'write', | ||||||
|         [], |         flagArray: [], | ||||||
|         'database/roles/api-prod', |         path: 'database/roles/api-prod', | ||||||
|         [ |         dataArray: [ | ||||||
|           'db_name=apiprod', |           'db_name=apiprod', | ||||||
|           `creation_statements=CREATE ROLE {{name}} WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO {{name}};`, |           `creation_statements=CREATE ROLE {{name}} WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO {{name}};`, | ||||||
|           'default_ttl=1h', |           'default_ttl=1h', | ||||||
|           'max_ttl=24h', |           'max_ttl=24h', | ||||||
|         ], |         ], | ||||||
|       ], |       }, | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     { |     { | ||||||
|       name: 'read with field', |       name: 'read with field', | ||||||
|       command: `vault read -field=access_key aws/creds/my-role`, |       command: `vault read -field=access_key aws/creds/my-role`, | ||||||
|       expected: ['read', ['-field=access_key'], 'aws/creds/my-role', []], |       expected: { | ||||||
|  |         method: 'read', | ||||||
|  |         flagArray: ['-field=access_key'], | ||||||
|  |         path: 'aws/creds/my-role', | ||||||
|  |         dataArray: [], | ||||||
|  |       }, | ||||||
|     }, |     }, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
| @@ -115,16 +121,14 @@ module('Unit | Lib | console helpers', function () { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   test('#parseCommand: invalid commands', function (assert) { |   test('#parseCommand: invalid commands', function (assert) { | ||||||
|  |     assert.expect(1); | ||||||
|     const command = 'vault kv get foo'; |     const command = 'vault kv get foo'; | ||||||
|     const result = parseCommand(command); |  | ||||||
|     assert.false(result, 'parseCommand returns false by default'); |  | ||||||
|  |  | ||||||
|     assert.throws( |     assert.throws( | ||||||
|       () => { |       () => { | ||||||
|         parseCommand(command, true); |         parseCommand(command); | ||||||
|       }, |       }, | ||||||
|       /invalid command/, |       /invalid command/, | ||||||
|       'throws on invalid command when `shouldThrow` is true' |       'throws on invalid command' | ||||||
|     ); |     ); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
| @@ -132,14 +136,12 @@ module('Unit | Lib | console helpers', function () { | |||||||
|     { |     { | ||||||
|       method: 'read', |       method: 'read', | ||||||
|       name: 'data fields', |       name: 'data fields', | ||||||
|       input: [ |       dataInput: [ | ||||||
|         [ |         'access_key=AKIAJWVN5Z4FOFT7NLNA', | ||||||
|           'access_key=AKIAJWVN5Z4FOFT7NLNA', |         'secret_key=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', | ||||||
|           'secret_key=R4nm063hgMVo4BTT5xOs5nHLeLXA6lar7ZJ3Nt0i', |         'region=us-east-1', | ||||||
|           'region=us-east-1', |  | ||||||
|         ], |  | ||||||
|         [], |  | ||||||
|       ], |       ], | ||||||
|  |       flagInput: [], | ||||||
|       expected: { |       expected: { | ||||||
|         data: { |         data: { | ||||||
|           access_key: 'AKIAJWVN5Z4FOFT7NLNA', |           access_key: 'AKIAJWVN5Z4FOFT7NLNA', | ||||||
| @@ -152,7 +154,8 @@ module('Unit | Lib | console helpers', function () { | |||||||
|     { |     { | ||||||
|       method: 'read', |       method: 'read', | ||||||
|       name: 'repeated data and a flag', |       name: 'repeated data and a flag', | ||||||
|       input: [['allowed_domains=example.com', 'allowed_domains=foo.example.com'], ['-wrap-ttl=2h']], |       dataInput: ['allowed_domains=example.com', 'allowed_domains=foo.example.com'], | ||||||
|  |       flagInput: ['-wrap-ttl=2h'], | ||||||
|       expected: { |       expected: { | ||||||
|         data: { |         data: { | ||||||
|           allowed_domains: ['example.com', 'foo.example.com'], |           allowed_domains: ['example.com', 'foo.example.com'], | ||||||
| @@ -162,10 +165,27 @@ module('Unit | Lib | console helpers', function () { | |||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|  |     { | ||||||
|  |       method: 'read', | ||||||
|  |       name: 'triple data', | ||||||
|  |       dataInput: [ | ||||||
|  |         'allowed_domains=example.com', | ||||||
|  |         'allowed_domains=foo.example.com', | ||||||
|  |         'allowed_domains=dev.example.com', | ||||||
|  |       ], | ||||||
|  |       flagInput: [], | ||||||
|  |       expected: { | ||||||
|  |         data: { | ||||||
|  |           allowed_domains: ['example.com', 'foo.example.com', 'dev.example.com'], | ||||||
|  |         }, | ||||||
|  |         flags: {}, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|       method: 'read', |       method: 'read', | ||||||
|       name: 'data with more than one equals sign', |       name: 'data with more than one equals sign', | ||||||
|       input: [['foo=bar=baz', 'foo=baz=bop', 'some=value=val'], []], |       dataInput: ['foo=bar=baz', 'foo=baz=bop', 'some=value=val'], | ||||||
|  |       flagInput: [], | ||||||
|       expected: { |       expected: { | ||||||
|         data: { |         data: { | ||||||
|           foo: ['bar=baz', 'baz=bop'], |           foo: ['bar=baz', 'baz=bop'], | ||||||
| @@ -177,7 +197,8 @@ module('Unit | Lib | console helpers', function () { | |||||||
|     { |     { | ||||||
|       method: 'read', |       method: 'read', | ||||||
|       name: 'data with empty values', |       name: 'data with empty values', | ||||||
|       input: [[`foo=`, 'some=thing'], []], |       dataInput: [`foo=`, 'some=thing'], | ||||||
|  |       flagInput: [], | ||||||
|       expected: { |       expected: { | ||||||
|         data: { |         data: { | ||||||
|           foo: '', |           foo: '', | ||||||
| @@ -189,7 +210,8 @@ module('Unit | Lib | console helpers', function () { | |||||||
|     { |     { | ||||||
|       method: 'write', |       method: 'write', | ||||||
|       name: 'write with force flag', |       name: 'write with force flag', | ||||||
|       input: [[], ['-force']], |       dataInput: [], | ||||||
|  |       flagInput: ['-force'], | ||||||
|       expected: { |       expected: { | ||||||
|         data: {}, |         data: {}, | ||||||
|         flags: { |         flags: { | ||||||
| @@ -200,7 +222,8 @@ module('Unit | Lib | console helpers', function () { | |||||||
|     { |     { | ||||||
|       method: 'write', |       method: 'write', | ||||||
|       name: 'write with force short flag', |       name: 'write with force short flag', | ||||||
|       input: [[], ['-f']], |       dataInput: [], | ||||||
|  |       flagInput: ['-f'], | ||||||
|       expected: { |       expected: { | ||||||
|         data: {}, |         data: {}, | ||||||
|         flags: { |         flags: { | ||||||
| @@ -211,7 +234,8 @@ module('Unit | Lib | console helpers', function () { | |||||||
|     { |     { | ||||||
|       method: 'write', |       method: 'write', | ||||||
|       name: 'write with GNU style force flag', |       name: 'write with GNU style force flag', | ||||||
|       input: [[], ['--force']], |       dataInput: [], | ||||||
|  |       flagInput: ['--force'], | ||||||
|       expected: { |       expected: { | ||||||
|         data: {}, |         data: {}, | ||||||
|         flags: { |         flags: { | ||||||
| @@ -222,9 +246,12 @@ module('Unit | Lib | console helpers', function () { | |||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   testExtractCases.forEach(function (testCase) { |   testExtractCases.forEach(function (testCase) { | ||||||
|     test(`#extractDataAndFlags: ${testCase.name}`, function (assert) { |     test(`#extractDataFromStrings: ${testCase.name}`, function (assert) { | ||||||
|       const { data, flags } = extractDataAndFlags(testCase.method, ...testCase.input); |       const data = extractDataFromStrings(testCase.dataInput); | ||||||
|       assert.deepEqual(data, testCase.expected.data, 'has expected data'); |       assert.deepEqual(data, testCase.expected.data, 'has expected data'); | ||||||
|  |     }); | ||||||
|  |     test(`#extractFlagsFromStrings: ${testCase.name}`, function (assert) { | ||||||
|  |       const flags = extractFlagsFromStrings(testCase.flagInput, testCase.method); | ||||||
|       assert.deepEqual(flags, testCase.expected.flags, 'has expected flags'); |       assert.deepEqual(flags, testCase.expected.flags, 'has expected flags'); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| @@ -469,8 +496,8 @@ module('Unit | Lib | console helpers', function () { | |||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   testCommandCases.forEach(function (testCase) { |   testCommandCases.forEach(function (testCase) { | ||||||
|     test(`#logErrorFromInput: ${testCase.name}`, function (assert) { |     test(`#formattedErrorFromInput: ${testCase.name}`, function (assert) { | ||||||
|       const data = logErrorFromInput(...testCase.args); |       const data = formattedErrorFromInput(...testCase.args); | ||||||
|  |  | ||||||
|       assert.deepEqual( |       assert.deepEqual( | ||||||
|         data, |         data, | ||||||
|   | |||||||
| @@ -38,7 +38,7 @@ module('Unit | Service | console', function (hooks) { | |||||||
|  |  | ||||||
|     { |     { | ||||||
|       method: 'read', |       method: 'read', | ||||||
|       args: ['/secrets/foo/bar', {}, '30m'], |       args: ['/secrets/foo/bar', {}, { wrapTTL: '30m' }], | ||||||
|       expectedURL: 'secrets/foo/bar', |       expectedURL: 'secrets/foo/bar', | ||||||
|       expectedVerb: 'GET', |       expectedVerb: 'GET', | ||||||
|       expectedOptions: { data: undefined, wrapTTL: '30m' }, |       expectedOptions: { data: undefined, wrapTTL: '30m' }, | ||||||
| @@ -65,7 +65,7 @@ module('Unit | Service | console', function (hooks) { | |||||||
|  |  | ||||||
|     { |     { | ||||||
|       method: 'list', |       method: 'list', | ||||||
|       args: ['secret/mounts', {}, '1h'], |       args: ['secret/mounts', {}, { wrapTTL: '1h' }], | ||||||
|       expectedURL: 'secret/mounts/', |       expectedURL: 'secret/mounts/', | ||||||
|       expectedVerb: 'GET', |       expectedVerb: 'GET', | ||||||
|       expectedOptions: { data: { list: true }, wrapTTL: '1h' }, |       expectedOptions: { data: { list: true }, wrapTTL: '1h' }, | ||||||
| @@ -102,4 +102,58 @@ module('Unit | Service | console', function (hooks) { | |||||||
|       assert.deepEqual(options, testCase.expectedOptions, `${testCase.method}: uses the correct options`); |       assert.deepEqual(options, testCase.expectedOptions, `${testCase.method}: uses the correct options`); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   const kvTestCases = [ | ||||||
|  |     { | ||||||
|  |       method: 'kvGet', | ||||||
|  |       args: ['kv/foo'], | ||||||
|  |       expectedURL: 'kv/data/foo', | ||||||
|  |       expectedVerb: 'GET', | ||||||
|  |       expectedOptions: { data: undefined, wrapTTL: undefined }, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       method: 'kvGet', | ||||||
|  |       args: ['kv/foo', {}, { metadata: true }], | ||||||
|  |       expectedURL: 'kv/metadata/foo', | ||||||
|  |       expectedVerb: 'GET', | ||||||
|  |       expectedOptions: { data: undefined, wrapTTL: undefined }, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       method: 'kvGet', | ||||||
|  |       args: ['kv/foo', {}, { wrapTTL: '10m' }], | ||||||
|  |       expectedURL: 'kv/data/foo', | ||||||
|  |       expectedVerb: 'GET', | ||||||
|  |       expectedOptions: { data: undefined, wrapTTL: '10m' }, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       method: 'kvGet', | ||||||
|  |       args: ['kv/foo', {}, { metadata: true, wrapTTL: '10m' }], | ||||||
|  |       expectedURL: 'kv/metadata/foo', | ||||||
|  |       expectedVerb: 'GET', | ||||||
|  |       expectedOptions: { data: undefined, wrapTTL: '10m' }, | ||||||
|  |     }, | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   test('it reads kv secret and metadata', function (assert) { | ||||||
|  |     assert.expect(12); | ||||||
|  |     const ajax = sinon.stub(); | ||||||
|  |     const uiConsole = this.owner.factoryFor('service:console').create({ | ||||||
|  |       adapter() { | ||||||
|  |         return { | ||||||
|  |           buildURL(url) { | ||||||
|  |             return url; | ||||||
|  |           }, | ||||||
|  |           ajax, | ||||||
|  |         }; | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     kvTestCases.forEach((testCase) => { | ||||||
|  |       uiConsole[testCase.method](...testCase.args); | ||||||
|  |       const [url, verb, options] = ajax.lastCall.args; | ||||||
|  |       assert.strictEqual(url, testCase.expectedURL, `${testCase.method}: uses correct url`); | ||||||
|  |       assert.strictEqual(verb, testCase.expectedVerb, `${testCase.method}: uses the correct verb`); | ||||||
|  |       assert.deepEqual(options, testCase.expectedOptions, `${testCase.method}: uses the correct options`); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -5648,6 +5648,13 @@ __metadata: | |||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   linkType: hard | ||||||
|  |  | ||||||
|  | "@types/shell-quote@npm:^1.7.1": | ||||||
|  |   version: 1.7.1 | ||||||
|  |   resolution: "@types/shell-quote@npm:1.7.1" | ||||||
|  |   checksum: 51e58326b8c6dcb72846b94cebe3dc4c84f3514469a8e52bd29c52c601e784b427b851d7477acbeef47bfcccf25d2a5768684d27e7fc95fdd003393c1bbb7bc3 | ||||||
|  |   languageName: node | ||||||
|  |   linkType: hard | ||||||
|  |  | ||||||
| "@types/symlink-or-copy@npm:^1.2.0": | "@types/symlink-or-copy@npm:^1.2.0": | ||||||
|   version: 1.2.0 |   version: 1.2.0 | ||||||
|   resolution: "@types/symlink-or-copy@npm:1.2.0" |   resolution: "@types/symlink-or-copy@npm:1.2.0" | ||||||
| @@ -24224,6 +24231,7 @@ __metadata: | |||||||
|     "@types/ember__utils": ^4.0.2 |     "@types/ember__utils": ^4.0.2 | ||||||
|     "@types/qunit": ^2.19.3 |     "@types/qunit": ^2.19.3 | ||||||
|     "@types/rsvp": ^4.0.4 |     "@types/rsvp": ^4.0.4 | ||||||
|  |     "@types/shell-quote": ^1.7.1 | ||||||
|     "@typescript-eslint/eslint-plugin": ^5.19.0 |     "@typescript-eslint/eslint-plugin": ^5.19.0 | ||||||
|     "@typescript-eslint/parser": ^5.19.0 |     "@typescript-eslint/parser": ^5.19.0 | ||||||
|     asn1js: ^2.2.0 |     asn1js: ^2.2.0 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Chelsea Shaw
					Chelsea Shaw