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 { getOwner } from '@ember/application'; | ||||
| import { schedule } from '@ember/runloop'; | ||||
| import { camelize } from '@ember/string'; | ||||
| import { task } from 'ember-concurrency'; | ||||
| import ControlGroupError from 'vault/lib/control-group-error'; | ||||
| import { | ||||
|   parseCommand, | ||||
|   extractDataAndFlags, | ||||
|   logFromResponse, | ||||
|   logFromError, | ||||
|   logErrorFromInput, | ||||
|   formattedErrorFromInput, | ||||
|   executeUICommand, | ||||
|   extractFlagsFromStrings, | ||||
|   extractDataFromStrings, | ||||
| } from 'vault/lib/console-helpers'; | ||||
|  | ||||
| export default Component.extend({ | ||||
| @@ -64,29 +66,25 @@ export default Component.extend({ | ||||
|  | ||||
|     // parse to verify it's valid | ||||
|     try { | ||||
|       serviceArgs = parseCommand(command, shouldThrow); | ||||
|       serviceArgs = parseCommand(command); | ||||
|     } catch (e) { | ||||
|       this.logAndOutput(command, { type: 'help' }); | ||||
|       return; | ||||
|     } | ||||
|     // we have a invalid command but don't want to throw | ||||
|     if (serviceArgs === false) { | ||||
|       if (shouldThrow) { | ||||
|         this.logAndOutput(command, { type: 'help' }); | ||||
|       } | ||||
|       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) { | ||||
|       var { data, flags } = extractDataAndFlags(method, dataArray, flagArray); | ||||
|     } | ||||
|  | ||||
|     const inputError = logErrorFromInput(path, method, flags, dataArray); | ||||
|     const inputError = formattedErrorFromInput(path, method, flags, dataArray); | ||||
|     if (inputError) { | ||||
|       this.logAndOutput(command, inputError); | ||||
|       return; | ||||
|     } | ||||
|     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)); | ||||
|     } catch (error) { | ||||
|       if (error instanceof ControlGroupError) { | ||||
|   | ||||
| @@ -39,7 +39,7 @@ | ||||
|       </:footer> | ||||
|     </Hds::SideNav> | ||||
|   </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 }} | ||||
|     <div id="modal-wormhole"></div> | ||||
|     <LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} /> | ||||
|   | ||||
| @@ -4,66 +4,107 @@ | ||||
|  */ | ||||
| 
 | ||||
| import keys from 'vault/lib/keycodes'; | ||||
| import argTokenizer from './arg-tokenizer'; | ||||
| import AdapterError from '@ember-data/adapter/error'; | ||||
| 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']; | ||||
| 
 | ||||
| export function extractDataAndFlags(method, data, flags) { | ||||
|   return data.concat(flags).reduce( | ||||
|     (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: {} } | ||||
|   ); | ||||
| interface DataObj { | ||||
|   [key: string]: string | string[]; | ||||
| } | ||||
| 
 | ||||
| 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 isUICommand = uiCommands.includes(cmd); | ||||
|   if (isUICommand) { | ||||
|     logAndOutput(command); | ||||
|   } | ||||
|   if (typeof commandFns[cmd] === 'function') { | ||||
|     commandFns[cmd](); | ||||
|   const execCommand = commandFns[cmd]; | ||||
|   if (execCommand && typeof execCommand === 'function') { | ||||
|     execCommand(); | ||||
|   } | ||||
|   return isUICommand; | ||||
| } | ||||
| 
 | ||||
| export function parseCommand(command, shouldThrow) { | ||||
|   const args = argTokenizer(parse(command)); | ||||
| 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(); | ||||
|   } | ||||
| 
 | ||||
|   const [method, ...rest] = args; | ||||
|   let path; | ||||
|   const flags = []; | ||||
|   const data = []; | ||||
|   const [method = '', ...rest] = args; | ||||
|   let path = ''; | ||||
|   const flags: string[] = []; | ||||
|   const data: string[] = []; | ||||
| 
 | ||||
|   rest.forEach((arg) => { | ||||
|     if (arg.startsWith('-')) { | ||||
| @@ -86,24 +127,28 @@ export function parseCommand(command, shouldThrow) { | ||||
|   }); | ||||
| 
 | ||||
|   if (!supportedCommands.includes(method)) { | ||||
|     if (shouldThrow) { | ||||
|       throw new Error('invalid command'); | ||||
|     } | ||||
|     return false; | ||||
|     throw new Error('invalid command'); | ||||
|   } | ||||
|   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; | ||||
|   let secret = response && (response.auth || response.data || response.wrap_info); | ||||
|   if (!secret) { | ||||
|   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}` }; | ||||
|     } else { | ||||
|       secret = response; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @@ -143,11 +188,17 @@ export function logFromResponse(response, path, method, flags) { | ||||
|   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; | ||||
|   const { httpStatus, path } = error; | ||||
|   const verbClause = { | ||||
|     read: 'reading from', | ||||
|     'kv-get': 'reading secret', | ||||
|     write: 'writing to', | ||||
|     list: 'listing', | ||||
|     delete: 'deleting at', | ||||
| @@ -162,7 +213,11 @@ export function logFromError(error, vaultPath, method) { | ||||
|   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; | ||||
|   const commandHistoryLength = history.length; | ||||
| 
 | ||||
| @@ -186,17 +241,18 @@ export function shiftCommandIndex(keyCode, history, index) { | ||||
|   } | ||||
| 
 | ||||
|   if (newInputValue !== '') { | ||||
|     newInputValue = history.objectAt(index).content; | ||||
|     newInputValue = history.objectAt(index)?.content; | ||||
|   } | ||||
| 
 | ||||
|   return [index, newInputValue]; | ||||
| } | ||||
| 
 | ||||
| export function logErrorFromInput(path, method, flags, dataArray) { | ||||
| 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' }; | ||||
|   } | ||||
|   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 }); | ||||
|   }, | ||||
|  | ||||
|   write(path, data, wrapTTL) { | ||||
|   write(path, data, flags) { | ||||
|     const wrapTTL = flags?.wrapTTL; | ||||
|     return this.ajax('write', sanitizePath(path), { data, wrapTTL }); | ||||
|   }, | ||||
|  | ||||
| @@ -96,7 +107,8 @@ export default Service.extend({ | ||||
|     return this.ajax('delete', sanitizePath(path)); | ||||
|   }, | ||||
|  | ||||
|   list(path, data, wrapTTL) { | ||||
|   list(path, data, flags) { | ||||
|     const wrapTTL = flags?.wrapTTL; | ||||
|     const listPath = ensureTrailingSlash(sanitizePath(path)); | ||||
|     return this.ajax('list', listPath, { | ||||
|       data: { | ||||
|   | ||||
| @@ -125,6 +125,10 @@ $console-close-height: 35px; | ||||
|   min-height: 400px; | ||||
| } | ||||
|  | ||||
| .main--console-open { | ||||
|   padding-bottom: 400px; | ||||
| } | ||||
|  | ||||
| .panel-open .console-ui-panel.fullscreen { | ||||
|   bottom: 0; | ||||
|   right: 0; | ||||
|   | ||||
| @@ -6,6 +6,7 @@ | ||||
|  | ||||
| Commands: | ||||
|   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 | ||||
|   delete      Delete secrets and configuration | ||||
|   list        List data or secrets | ||||
|   | ||||
| @@ -5,10 +5,18 @@ | ||||
| </div> | ||||
| <div class="console-ui-panel-content"> | ||||
|   <div class="content has-bottom-margin-l"> | ||||
|     <p class="has-text-grey is-font-mono"> | ||||
|       The Vault Browser CLI provides an easy way to execute the most common CLI commands, such as write, read, delete, and | ||||
|       list. | ||||
|     <p class="has-text-grey is-font-mono has-bottom-margin-s"> | ||||
|       The Vault Browser CLI provides an easy way to execute common Vault CLI commands, such as write, read, delete, and list. | ||||
|       It does not include kv v2 write or put commands. For guidance, type `help`. | ||||
|     </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> | ||||
|   <Console::OutputLog @outputLog={{this.cliLog}} /> | ||||
|   <Console::CommandInput | ||||
|   | ||||
| @@ -10,8 +10,11 @@ | ||||
|   <ToolbarFilters> | ||||
|     <div class="field is-marginless"> | ||||
|       <p class="control has-icons-left"> | ||||
|         <label for="swagger-result-filter" class="sr-only">Filter operations by path</label> | ||||
|         <input | ||||
|           oninput={{queue (action "updateFilter") (action "proxyEvent")}} | ||||
|           id="swagger-result-filter" | ||||
|           {{on "input" (action "proxyEvent")}} | ||||
|           {{on "change" (action "updateFilter")}} | ||||
|           value={{@initialFilter}} | ||||
|           disabled={{this.swaggerLoading}} | ||||
|           class="filter input" | ||||
|   | ||||
| @@ -92,6 +92,7 @@ | ||||
|     "@types/ember__utils": "^4.0.2", | ||||
|     "@types/qunit": "^2.19.3", | ||||
|     "@types/rsvp": "^4.0.4", | ||||
|     "@types/shell-quote": "^1.7.1", | ||||
|     "@typescript-eslint/eslint-plugin": "^5.19.0", | ||||
|     "@typescript-eslint/parser": "^5.19.0", | ||||
|     "asn1js": "^2.2.0", | ||||
|   | ||||
| @@ -6,10 +6,11 @@ | ||||
| import { module, test } from 'qunit'; | ||||
| import { | ||||
|   parseCommand, | ||||
|   extractDataAndFlags, | ||||
|   logFromResponse, | ||||
|   logFromError, | ||||
|   logErrorFromInput, | ||||
|   formattedErrorFromInput, | ||||
|   extractFlagsFromStrings, | ||||
|   extractDataFromStrings, | ||||
| } from 'vault/lib/console-helpers'; | ||||
|  | ||||
| module('Unit | Lib | console helpers', function () { | ||||
| @@ -20,16 +21,16 @@ module('Unit | Lib | console helpers', function () { | ||||
|       access_key=AKIAJWVN5Z4FOFT7NLNA \ | ||||
|       secret_key=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 \ | ||||
|       region=us-east-1`, | ||||
|       expected: [ | ||||
|         'write', | ||||
|         [], | ||||
|         'aws/config/root', | ||||
|         [ | ||||
|       expected: { | ||||
|         method: 'write', | ||||
|         flagArray: [], | ||||
|         path: 'aws/config/root', | ||||
|         dataArray: [ | ||||
|           'access_key=AKIAJWVN5Z4FOFT7NLNA', | ||||
|           'secret_key=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', | ||||
|           'region=us-east-1', | ||||
|         ], | ||||
|       ], | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       name: 'write with space in a value', | ||||
| @@ -43,11 +44,11 @@ module('Unit | Lib | console helpers', function () { | ||||
|       insecure_tls=true \ | ||||
|       starttls=false | ||||
|       `, | ||||
|       expected: [ | ||||
|         'write', | ||||
|         [], | ||||
|         'auth/ldap/config', | ||||
|         [ | ||||
|       expected: { | ||||
|         method: 'write', | ||||
|         flagArray: [], | ||||
|         path: 'auth/ldap/config', | ||||
|         dataArray: [ | ||||
|           'url=ldap://ldap.example.com:3268', | ||||
|           'binddn=CN=ServiceViewDev,OU=Service Accounts,DC=example,DC=com', | ||||
|           'bindpass=xxxxxxxxxxxxxxxxxxxxxxxxxx', | ||||
| @@ -56,7 +57,7 @@ module('Unit | Lib | console helpers', function () { | ||||
|           'insecure_tls=true', | ||||
|           'starttls=false', | ||||
|         ], | ||||
|       ], | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       name: 'write with double quotes', | ||||
| @@ -64,7 +65,7 @@ module('Unit | Lib | console helpers', function () { | ||||
|       auth/token/create \ | ||||
|       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', | ||||
| @@ -72,7 +73,7 @@ module('Unit | Lib | console helpers', function () { | ||||
|       auth/token/create \ | ||||
|       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', | ||||
| @@ -80,30 +81,35 @@ module('Unit | Lib | console helpers', function () { | ||||
|       auth/token/create \ | ||||
|       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', | ||||
|       /* 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 | ||||
|       `, | ||||
|       expected: [ | ||||
|         'write', | ||||
|         [], | ||||
|         'database/roles/api-prod', | ||||
|         [ | ||||
|       expected: { | ||||
|         method: 'write', | ||||
|         flagArray: [], | ||||
|         path: 'database/roles/api-prod', | ||||
|         dataArray: [ | ||||
|           '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', | ||||
|         ], | ||||
|       ], | ||||
|       }, | ||||
|     }, | ||||
|  | ||||
|     { | ||||
|       name: 'read with field', | ||||
|       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) { | ||||
|     assert.expect(1); | ||||
|     const command = 'vault kv get foo'; | ||||
|     const result = parseCommand(command); | ||||
|     assert.false(result, 'parseCommand returns false by default'); | ||||
|  | ||||
|     assert.throws( | ||||
|       () => { | ||||
|         parseCommand(command, true); | ||||
|         parseCommand(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', | ||||
|       name: 'data fields', | ||||
|       input: [ | ||||
|         [ | ||||
|           'access_key=AKIAJWVN5Z4FOFT7NLNA', | ||||
|           'secret_key=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', | ||||
|           'region=us-east-1', | ||||
|         ], | ||||
|         [], | ||||
|       dataInput: [ | ||||
|         'access_key=AKIAJWVN5Z4FOFT7NLNA', | ||||
|         'secret_key=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', | ||||
|         'region=us-east-1', | ||||
|       ], | ||||
|       flagInput: [], | ||||
|       expected: { | ||||
|         data: { | ||||
|           access_key: 'AKIAJWVN5Z4FOFT7NLNA', | ||||
| @@ -152,7 +154,8 @@ module('Unit | Lib | console helpers', function () { | ||||
|     { | ||||
|       method: 'read', | ||||
|       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: { | ||||
|         data: { | ||||
|           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', | ||||
|       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: { | ||||
|         data: { | ||||
|           foo: ['bar=baz', 'baz=bop'], | ||||
| @@ -177,7 +197,8 @@ module('Unit | Lib | console helpers', function () { | ||||
|     { | ||||
|       method: 'read', | ||||
|       name: 'data with empty values', | ||||
|       input: [[`foo=`, 'some=thing'], []], | ||||
|       dataInput: [`foo=`, 'some=thing'], | ||||
|       flagInput: [], | ||||
|       expected: { | ||||
|         data: { | ||||
|           foo: '', | ||||
| @@ -189,7 +210,8 @@ module('Unit | Lib | console helpers', function () { | ||||
|     { | ||||
|       method: 'write', | ||||
|       name: 'write with force flag', | ||||
|       input: [[], ['-force']], | ||||
|       dataInput: [], | ||||
|       flagInput: ['-force'], | ||||
|       expected: { | ||||
|         data: {}, | ||||
|         flags: { | ||||
| @@ -200,7 +222,8 @@ module('Unit | Lib | console helpers', function () { | ||||
|     { | ||||
|       method: 'write', | ||||
|       name: 'write with force short flag', | ||||
|       input: [[], ['-f']], | ||||
|       dataInput: [], | ||||
|       flagInput: ['-f'], | ||||
|       expected: { | ||||
|         data: {}, | ||||
|         flags: { | ||||
| @@ -211,7 +234,8 @@ module('Unit | Lib | console helpers', function () { | ||||
|     { | ||||
|       method: 'write', | ||||
|       name: 'write with GNU style force flag', | ||||
|       input: [[], ['--force']], | ||||
|       dataInput: [], | ||||
|       flagInput: ['--force'], | ||||
|       expected: { | ||||
|         data: {}, | ||||
|         flags: { | ||||
| @@ -222,9 +246,12 @@ module('Unit | Lib | console helpers', function () { | ||||
|   ]; | ||||
|  | ||||
|   testExtractCases.forEach(function (testCase) { | ||||
|     test(`#extractDataAndFlags: ${testCase.name}`, function (assert) { | ||||
|       const { data, flags } = extractDataAndFlags(testCase.method, ...testCase.input); | ||||
|     test(`#extractDataFromStrings: ${testCase.name}`, function (assert) { | ||||
|       const data = extractDataFromStrings(testCase.dataInput); | ||||
|       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'); | ||||
|     }); | ||||
|   }); | ||||
| @@ -469,8 +496,8 @@ module('Unit | Lib | console helpers', function () { | ||||
|   ]; | ||||
|  | ||||
|   testCommandCases.forEach(function (testCase) { | ||||
|     test(`#logErrorFromInput: ${testCase.name}`, function (assert) { | ||||
|       const data = logErrorFromInput(...testCase.args); | ||||
|     test(`#formattedErrorFromInput: ${testCase.name}`, function (assert) { | ||||
|       const data = formattedErrorFromInput(...testCase.args); | ||||
|  | ||||
|       assert.deepEqual( | ||||
|         data, | ||||
|   | ||||
| @@ -38,7 +38,7 @@ module('Unit | Service | console', function (hooks) { | ||||
|  | ||||
|     { | ||||
|       method: 'read', | ||||
|       args: ['/secrets/foo/bar', {}, '30m'], | ||||
|       args: ['/secrets/foo/bar', {}, { wrapTTL: '30m' }], | ||||
|       expectedURL: 'secrets/foo/bar', | ||||
|       expectedVerb: 'GET', | ||||
|       expectedOptions: { data: undefined, wrapTTL: '30m' }, | ||||
| @@ -65,7 +65,7 @@ module('Unit | Service | console', function (hooks) { | ||||
|  | ||||
|     { | ||||
|       method: 'list', | ||||
|       args: ['secret/mounts', {}, '1h'], | ||||
|       args: ['secret/mounts', {}, { wrapTTL: '1h' }], | ||||
|       expectedURL: 'secret/mounts/', | ||||
|       expectedVerb: 'GET', | ||||
|       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`); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   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 | ||||
|   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": | ||||
|   version: 1.2.0 | ||||
|   resolution: "@types/symlink-or-copy@npm:1.2.0" | ||||
| @@ -24224,6 +24231,7 @@ __metadata: | ||||
|     "@types/ember__utils": ^4.0.2 | ||||
|     "@types/qunit": ^2.19.3 | ||||
|     "@types/rsvp": ^4.0.4 | ||||
|     "@types/shell-quote": ^1.7.1 | ||||
|     "@typescript-eslint/eslint-plugin": ^5.19.0 | ||||
|     "@typescript-eslint/parser": ^5.19.0 | ||||
|     asn1js: ^2.2.0 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Chelsea Shaw
					Chelsea Shaw