-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
UI: add copyable paths for CLI and API commands to kv v2 (#22551)
* add paths route * WIP copy secret path component * wip component * ad v1 * use each-in to iterate over info table row * update copy * add commands to kv paths page * add comments * WIP tests * finish tests * remove version, address comments and use path arg directly remove secret * update copy * fix typo for perms * remove destructuring, that was confusing * add changelog * add secure protocal
- Loading branch information
1 parent
0f5a39c
commit 42a3374
Showing
13 changed files
with
323 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:feature | ||
**Copyable KV v2 paths in UI**: KV v2 secret paths are copyable for use in CLI commands or API calls | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle={{@path}}> | ||
<:tabLinks> | ||
<LinkTo @route="secret.details" data-test-secrets-tab="Secret">Secret</LinkTo> | ||
<LinkTo @route="secret.metadata.index" data-test-secrets-tab="Metadata">Metadata</LinkTo> | ||
<LinkTo @route="secret.paths" data-test-secrets-tab="Paths">Paths</LinkTo> | ||
{{#if @canReadMetadata}} | ||
<LinkTo @route="secret.metadata.versions" data-test-secrets-tab="Version History">Version History</LinkTo> | ||
{{/if}} | ||
</:tabLinks> | ||
</KvPageHeader> | ||
|
||
<h2 class="title is-5 has-top-margin-xl"> | ||
Paths | ||
</h2> | ||
|
||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless"> | ||
{{#each this.paths as |path|}} | ||
<InfoTableRow @label={{path.label}} @labelWidth="is-one-third" @helperText={{path.text}}> | ||
{{! replace with Hds::Copy::Snippet }} | ||
<CopyButton | ||
class="button is-compact is-transparent level-right" | ||
@clipboardText={{path.snippet}} | ||
@buttonType="button" | ||
@success={{fn (set-flash-message (concat path.label " copied!"))}} | ||
> | ||
<Icon @name="clipboard-copy" aria-label="Copy" /> | ||
</CopyButton> | ||
<code class="has-left-margin-s level-left"> | ||
{{path.snippet}} | ||
</code> | ||
</InfoTableRow> | ||
{{/each}} | ||
</div> | ||
|
||
<h2 class="title is-5 has-top-margin-xl"> | ||
Commands | ||
</h2> | ||
|
||
<div class="box is-fullwidth is-sideless"> | ||
<h3 class="is-label"> | ||
CLI | ||
<Hds::Badge @text="kv get" @color="neutral" /> | ||
</h3> | ||
<p class="helper-text has-text-grey has-bottom-padding-s"> | ||
This command retrieves the value from KV secrets engine at the given key name. For other CLI commands, | ||
<DocLink @path="/vault/docs/commands/kv"> | ||
learn more. | ||
</DocLink> | ||
</p> | ||
<CodeSnippet data-test-commands="cli" @codeBlock={{this.commands.cli}} /> | ||
|
||
<h3 class="has-top-margin-l is-label"> | ||
API read secret version | ||
</h3> | ||
<p class="helper-text has-text-grey has-bottom-padding-s"> | ||
This command obtains data and metadata for the latest version of this secret. In this example, Vault is located at | ||
https://127.0.0.1:8200. For other API commands, | ||
<DocLink @path="/vault/api-docs/secret/kv/kv-v2"> | ||
learn more. | ||
</DocLink> | ||
</p> | ||
<CodeSnippet data-test-commands="api" @clipboardCode={{this.commands.apiCopy}} @codeBlock={{this.commands.apiDisplay}} /> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/** | ||
* Copyright (c) HashiCorp, Inc. | ||
* SPDX-License-Identifier: MPL-2.0 | ||
*/ | ||
|
||
import Component from '@glimmer/component'; | ||
import { inject as service } from '@ember/service'; | ||
import { kvMetadataPath, kvDataPath } from 'vault/utils/kv-path'; | ||
|
||
/** | ||
* @module KvSecretPaths is used to display copyable secret paths for KV v2 for CLI and API use. | ||
* This view is permission agnostic because args come from the views mount path and url params. | ||
* | ||
* <Page::Secret::Paths | ||
* @path={{this.model.path}} | ||
* @backend={{this.model.backend}} | ||
* @breadcrumbs={{this.breadcrumbs}} | ||
* @canReadMetadata={{this.model.secret.canReadMetadata}} | ||
* /> | ||
* | ||
* @param {string} path - kv secret path for building the CLI and API paths | ||
* @param {string} backend - the secret engine mount path, comes from the secretMountPath service defined in the route | ||
* @param {array} breadcrumbs - Array to generate breadcrumbs, passed to the page header component | ||
* @param {boolean} [canReadMetadata=true] - if true, displays tab for Version History | ||
*/ | ||
|
||
export default class KvSecretPaths extends Component { | ||
@service namespace; | ||
|
||
get paths() { | ||
const { backend, path } = this.args; | ||
const namespace = this.namespace.path; | ||
const cli = `-mount="${backend}" "${path}"`; | ||
const data = kvDataPath(backend, path); | ||
const metadata = kvMetadataPath(backend, path); | ||
|
||
return [ | ||
{ | ||
label: 'API path', | ||
snippet: namespace ? `/v1/${namespace}/${data}` : `/v1/${data}`, | ||
text: 'Use this path when referring to this secret in the API.', | ||
}, | ||
{ | ||
label: 'CLI path', | ||
snippet: namespace ? `-namespace=${namespace} ${cli}` : cli, | ||
text: 'Use this path when referring to this secret in the CLI.', | ||
}, | ||
{ | ||
label: 'API path for metadata', | ||
snippet: namespace ? `/v1/${namespace}/${metadata}` : `/v1/${metadata}`, | ||
text: `Use this path when referring to this secret's metadata in the API and permanent secret deletion.`, | ||
}, | ||
]; | ||
} | ||
|
||
get commands() { | ||
const cliPath = this.paths.findBy('label', 'CLI path').snippet; | ||
const apiPath = this.paths.findBy('label', 'API path').snippet; | ||
// as a future improvement, it might be nice to use window.location.protocol here: | ||
const url = `https://127.0.0.1:8200${apiPath}`; | ||
|
||
return { | ||
cli: `vault kv get ${cliPath}`, | ||
/* eslint-disable-next-line no-useless-escape */ | ||
apiCopy: `curl \ --header "X-Vault-Token: ..." \ --request GET \ ${url}`, | ||
apiDisplay: `curl \\ | ||
--header "X-Vault-Token: ..." \\ | ||
--request GET \\ | ||
${url}`, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/** | ||
* Copyright (c) HashiCorp, Inc. | ||
* SPDX-License-Identifier: MPL-2.0 | ||
*/ | ||
|
||
import Route from '@ember/routing/route'; | ||
import { breadcrumbsForSecret } from 'kv/utils/kv-breadcrumbs'; | ||
|
||
export default class KvSecretPathsRoute extends Route { | ||
setupController(controller, resolvedModel) { | ||
super.setupController(controller, resolvedModel); | ||
|
||
controller.breadcrumbs = [ | ||
{ label: 'secrets', route: 'secrets', linkExternal: true }, | ||
{ label: resolvedModel.backend, route: 'list' }, | ||
...breadcrumbsForSecret(resolvedModel.path), | ||
{ label: 'paths' }, | ||
]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<Page::Secret::Paths | ||
@path={{this.model.path}} | ||
@backend={{this.model.backend}} | ||
@breadcrumbs={{this.breadcrumbs}} | ||
@canReadMetadata={{this.model.secret.canReadMetadata}} | ||
/> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
148 changes: 148 additions & 0 deletions
148
ui/tests/integration/components/kv/page/kv-page-secret-paths-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
/** | ||
* Copyright (c) HashiCorp, Inc. | ||
* SPDX-License-Identifier: MPL-2.0 | ||
*/ | ||
|
||
import { module, test } from 'qunit'; | ||
import { setupRenderingTest } from 'ember-qunit'; | ||
import { setupEngine } from 'ember-engines/test-support'; | ||
import { render } from '@ember/test-helpers'; | ||
import { hbs } from 'ember-cli-htmlbars'; | ||
import { PAGE } from 'vault/tests/helpers/kv/kv-selectors'; | ||
/* eslint-disable no-useless-escape */ | ||
|
||
module('Integration | Component | kv-v2 | Page::Secret::Paths', function (hooks) { | ||
setupRenderingTest(hooks); | ||
setupEngine(hooks, 'kv'); | ||
|
||
hooks.beforeEach(async function () { | ||
this.backend = 'kv-engine'; | ||
this.path = 'my-secret'; | ||
this.breadcrumbs = [ | ||
{ label: 'secrets', route: 'secrets', linkExternal: true }, | ||
{ label: this.backend, route: 'list' }, | ||
{ label: this.path }, | ||
]; | ||
|
||
this.assertClipboard = (assert, element, expected) => { | ||
assert.dom(element).hasAttribute('data-clipboard-text', expected); | ||
}; | ||
}); | ||
|
||
test('it renders copyable paths', async function (assert) { | ||
assert.expect(6); | ||
|
||
const paths = [ | ||
{ label: 'API path', expected: `/v1/${this.backend}/data/${this.path}` }, | ||
{ label: 'CLI path', expected: `-mount="${this.backend}" "${this.path}"` }, | ||
{ label: 'API path for metadata', expected: `/v1/${this.backend}/metadata/${this.path}` }, | ||
]; | ||
|
||
await render( | ||
hbs` | ||
<Page::Secret::Paths | ||
@path={{this.path}} | ||
@backend={{this.backend}} | ||
@breadcrumbs={{this.breadcrumbs}} | ||
/> | ||
`, | ||
{ owner: this.engine } | ||
); | ||
|
||
for (const path of paths) { | ||
assert.dom(PAGE.infoRowValue(path.label)).hasText(path.expected); | ||
this.assertClipboard(assert, PAGE.paths.copyButton(path.label), path.expected); | ||
} | ||
}); | ||
|
||
test('it renders copyable encoded mount and secret paths', async function (assert) { | ||
assert.expect(6); | ||
this.path = `my spacey!"secret`; | ||
this.backend = `my fancy!"backend`; | ||
const backend = encodeURIComponent(this.backend); | ||
const path = encodeURIComponent(this.path); | ||
const paths = [ | ||
{ | ||
label: 'API path', | ||
expected: `/v1/${backend}/data/${path}`, | ||
}, | ||
{ label: 'CLI path', expected: `-mount="${this.backend}" "${this.path}"` }, | ||
{ | ||
label: 'API path for metadata', | ||
expected: `/v1/${backend}/metadata/${path}`, | ||
}, | ||
]; | ||
|
||
await render( | ||
hbs` | ||
<Page::Secret::Paths | ||
@path={{this.path}} | ||
@backend={{this.backend}} | ||
@breadcrumbs={{this.breadcrumbs}} | ||
/> | ||
`, | ||
{ owner: this.engine } | ||
); | ||
|
||
for (const path of paths) { | ||
assert.dom(PAGE.infoRowValue(path.label)).hasText(path.expected); | ||
this.assertClipboard(assert, PAGE.paths.copyButton(path.label), path.expected); | ||
} | ||
}); | ||
|
||
test('it renders copyable commands', async function (assert) { | ||
assert.expect(4); | ||
const url = `https://127.0.0.1:8200/v1/${this.backend}/data/${this.path}`; | ||
const expected = { | ||
cli: `vault kv get -mount="${this.backend}" "${this.path}"`, | ||
apiDisplay: `curl \\ --header \"X-Vault-Token: ...\" \\ --request GET \\ ${url}`, | ||
apiCopy: `curl --header \"X-Vault-Token: ...\" --request GET \ ${url}`, | ||
}; | ||
await render( | ||
hbs` | ||
<Page::Secret::Paths | ||
@path={{this.path}} | ||
@backend={{this.backend}} | ||
@breadcrumbs={{this.breadcrumbs}} | ||
/> | ||
`, | ||
{ owner: this.engine } | ||
); | ||
|
||
assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli); | ||
assert.dom(PAGE.paths.snippetCopy('cli')).hasAttribute('data-clipboard-text', expected.cli); | ||
assert.dom(PAGE.paths.codeSnippet('api')).hasText(expected.apiDisplay); | ||
assert.dom(PAGE.paths.snippetCopy('api')).hasAttribute('data-clipboard-text', expected.apiCopy); | ||
}); | ||
|
||
test('it renders copyable encoded mount and path commands', async function (assert) { | ||
assert.expect(4); | ||
this.path = `my spacey!"secret`; | ||
this.backend = `my fancy!"backend`; | ||
|
||
const backend = encodeURIComponent(this.backend); | ||
const path = encodeURIComponent(this.path); | ||
const url = `https://127.0.0.1:8200/v1/${backend}/data/${path}`; | ||
|
||
const expected = { | ||
cli: `vault kv get -mount="${this.backend}" "${this.path}"`, | ||
apiDisplay: `curl \\ --header \"X-Vault-Token: ...\" \\ --request GET \\ ${url}`, | ||
apiCopy: `curl --header \"X-Vault-Token: ...\" --request GET \ ${url}`, | ||
}; | ||
await render( | ||
hbs` | ||
<Page::Secret::Paths | ||
@path={{this.path}} | ||
@backend={{this.backend}} | ||
@breadcrumbs={{this.breadcrumbs}} | ||
/> | ||
`, | ||
{ owner: this.engine } | ||
); | ||
|
||
assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli); | ||
assert.dom(PAGE.paths.snippetCopy('cli')).hasAttribute('data-clipboard-text', expected.cli); | ||
assert.dom(PAGE.paths.codeSnippet('api')).hasText(expected.apiDisplay); | ||
assert.dom(PAGE.paths.snippetCopy('api')).hasAttribute('data-clipboard-text', expected.apiCopy); | ||
}); | ||
}); |