From 62e0e627428b80eb9dc4fce9b034d446e42b5ae3 Mon Sep 17 00:00:00 2001 From: akshya96 <87045294+akshya96@users.noreply.github.com> Date: Fri, 16 Aug 2024 11:46:08 -0700 Subject: [PATCH 01/12] updating remaining occurances of upload-artifact (#28108) --- .github/actions/build-vault/action.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/build-vault/action.yml b/.github/actions/build-vault/action.yml index 8fc228415a..5e26413442 100644 --- a/.github/actions/build-vault/action.yml +++ b/.github/actions/build-vault/action.yml @@ -146,7 +146,7 @@ runs: BUNDLE_PATH: out/${{ steps.metadata.outputs.artifact-basename }}.zip shell: bash run: make ci-bundle - - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: ${{ steps.metadata.outputs.artifact-basename }}.zip path: out/${{ steps.metadata.outputs.artifact-basename }}.zip @@ -178,13 +178,13 @@ runs: echo "deb-files=$(basename out/*.deb)" } | tee -a "$GITHUB_OUTPUT" - if: inputs.create-packages == 'true' - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: ${{ steps.package-files.outputs.rpm-files }} path: out/${{ steps.package-files.outputs.rpm-files }} if-no-files-found: error - if: inputs.create-packages == 'true' - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: ${{ steps.package-files.outputs.deb-files }} path: out/${{ steps.package-files.outputs.deb-files }} From d5c67768c58b8ab82d70866004a44613f6de09d1 Mon Sep 17 00:00:00 2001 From: Ryan Cragun Date: Fri, 16 Aug 2024 13:49:05 -0600 Subject: [PATCH 02/12] scan: skip running if the PR head is a fork (#28107) Signed-off-by: Ryan Cragun --- .github/workflows/security-scan.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 2ca6fcf599..a396ed9314 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -16,10 +16,10 @@ on: jobs: scan: runs-on: ${{ github.repository == 'hashicorp/vault' && 'ubuntu-latest' || fromJSON('["self-hosted","ondemand","os=linux","type=c6a.4xlarge"]') }} - # The first check ensures this doesn't run on community-contributed PRs, who - # won't have the permissions to run this job. + # The first check ensures this doesn't run on community-contributed PRs, who won't have the + # permissions to run this job. if: | - (startsWith(github.repository, 'hashicorp/vault') || (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) && + ! github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && github.actor != 'hc-github-team-secure-vault-core' steps: From ae6854e9f230aeacb6d65f9c83c6ea02e0d369cf Mon Sep 17 00:00:00 2001 From: akshya96 <87045294+akshya96@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:00:48 -0700 Subject: [PATCH 03/12] updating remaining occurances of setup-go (#28110) --- .github/actions/set-up-go/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/set-up-go/action.yml b/.github/actions/set-up-go/action.yml index 9a80bf32f4..548555d266 100644 --- a/.github/actions/set-up-go/action.yml +++ b/.github/actions/set-up-go/action.yml @@ -40,7 +40,7 @@ runs: else echo "go-version=${{ inputs.go-version }}" | tee -a "$GITHUB_OUTPUT" fi - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version: ${{ steps.go-version.outputs.go-version }} cache: false # We use our own caching strategy From 255db7aab15d6a8b45564a18fb2bfa990da02ead Mon Sep 17 00:00:00 2001 From: gkoutsou Date: Fri, 16 Aug 2024 22:18:47 +0200 Subject: [PATCH 04/12] Add ENVs using NewTestDockerCluster (#27457) * Add ENVs using NewTestDockerCluster Currently NewTestDockerCluster had no means for setting any environment variables. This makes it tricky to create test for functionality that require thems, like having to set AWS environment variables. DockerClusterOptions now exposes an option to pass extra enviroment variables to the containers, which are appended to the existing ones. * adding changelog * added test case for setting env variables to containers * fix changelog typo; env name * Update changelog/27457.txt Co-authored-by: akshya96 <87045294+akshya96@users.noreply.github.com> * adding the missing copyright --------- Co-authored-by: akshya96 <87045294+akshya96@users.noreply.github.com> --- changelog/27457.txt | 3 ++ sdk/helper/testcluster/docker/environment.go | 20 ++++++---- .../testcluster/docker/environment_test.go | 38 +++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 changelog/27457.txt create mode 100644 sdk/helper/testcluster/docker/environment_test.go diff --git a/changelog/27457.txt b/changelog/27457.txt new file mode 100644 index 0000000000..e3cf89a765 --- /dev/null +++ b/changelog/27457.txt @@ -0,0 +1,3 @@ +```release-note:improvement +sdk/helper: Allow setting environment variables when using NewTestDockerCluster +``` diff --git a/sdk/helper/testcluster/docker/environment.go b/sdk/helper/testcluster/docker/environment.go index fd1c11ffee..8dd40904f7 100644 --- a/sdk/helper/testcluster/docker/environment.go +++ b/sdk/helper/testcluster/docker/environment.go @@ -805,20 +805,23 @@ func (n *DockerClusterNode) Start(ctx context.Context, opts *DockerClusterOption } } + envs := []string{ + // For now we're using disable_mlock, because this is for testing + // anyway, and because it prevents us using external plugins. + "SKIP_SETCAP=true", + "VAULT_LOG_FORMAT=json", + "VAULT_LICENSE=" + opts.VaultLicense, + } + envs = append(envs, opts.Envs...) + r, err := dockhelper.NewServiceRunner(dockhelper.RunOptions{ ImageRepo: n.ImageRepo, ImageTag: n.ImageTag, // We don't need to run update-ca-certificates in the container, because // we're providing the CA in the raft join call, and otherwise Vault // servers don't talk to one another on the API port. - Cmd: append([]string{"server"}, opts.Args...), - Env: []string{ - // For now we're using disable_mlock, because this is for testing - // anyway, and because it prevents us using external plugins. - "SKIP_SETCAP=true", - "VAULT_LOG_FORMAT=json", - "VAULT_LICENSE=" + opts.VaultLicense, - }, + Cmd: append([]string{"server"}, opts.Args...), + Env: envs, Ports: ports, ContainerName: n.Name(), NetworkName: opts.NetworkName, @@ -1089,6 +1092,7 @@ type DockerClusterOptions struct { CA *testcluster.CA VaultBinary string Args []string + Envs []string StartProbe func(*api.Client) error Storage testcluster.ClusterStorage DisableTLS bool diff --git a/sdk/helper/testcluster/docker/environment_test.go b/sdk/helper/testcluster/docker/environment_test.go new file mode 100644 index 0000000000..bb23764052 --- /dev/null +++ b/sdk/helper/testcluster/docker/environment_test.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package docker + +import ( + "testing" +) + +func TestSettingEnvsToContainer(t *testing.T) { + expectedEnv := "TEST_ENV=value1" + expectedEnv2 := "TEST_ENV2=value2" + opts := &DockerClusterOptions{ + ImageRepo: "hashicorp/vault", + ImageTag: "latest", + Envs: []string{expectedEnv, expectedEnv2}, + } + cluster := NewTestDockerCluster(t, opts) + defer cluster.Cleanup() + + envs := cluster.GetActiveClusterNode().Container.Config.Env + + if !findEnv(envs, expectedEnv) { + t.Errorf("Missing ENV variable: %s", expectedEnv) + } + if !findEnv(envs, expectedEnv2) { + t.Errorf("Missing ENV variable: %s", expectedEnv2) + } +} + +func findEnv(envs []string, env string) bool { + for _, e := range envs { + if e == env { + return true + } + } + return false +} From 30da9aef468becd11def0f05ada327e464574f3d Mon Sep 17 00:00:00 2001 From: claire bontempo <68122737+hellobontempo@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:40:23 -0700 Subject: [PATCH 05/12] UI: Build KV v2 overview page (#28106) * move date-from-now helper to addon * make overview cards consistent across engines * make kv-paths-card component * remove overview margin all together * small styling changes for paths card * small selector additions * add overview card test * add overview page and test * add default timestamp format * cleanup paths test * fix dateFromNow import * fix selectors, cleanup pki selectors * and more selector cleanup * make deactivated state single arg * fix template and remove @isDeleted and @isDestroyed * add test and hide badge unless deactivated * address failings from changing selectors * oops, not ready to show overview tab just yet! * add deletionTime to currentSecret metadata getter --- ui/app/models/kv/metadata.js | 6 +- ui/lib/core/addon/components/json-editor.js | 1 + .../core/addon/components/overview-card.hbs | 12 +- ui/lib/core/addon/helpers/date-format.js | 2 +- .../core/addon}/helpers/date-from-now.js | 0 ui/lib/core/app/helpers/date-from-now.js | 6 + ui/lib/kv/addon/components/kv-paths-card.hbs | 82 +++++ .../secret/paths.js => kv-paths-card.js} | 28 +- ui/lib/kv/addon/components/kv-subkeys.hbs | 4 +- .../addon/components/page/secret/overview.hbs | 105 ++++++ .../addon/components/page/secret/overview.js | 51 +++ .../kv/addon/components/page/secret/paths.hbs | 64 +--- .../addon/components/page/pki-overview.hbs | 4 +- ui/tests/acceptance/pki/pki-overview-test.js | 20 +- .../acceptance/sync/secrets/overview-test.js | 2 +- ui/tests/helpers/general-selectors.ts | 5 +- ui/tests/helpers/kv/kv-run-commands.js | 30 ++ ui/tests/helpers/pki/pki-selectors.ts | 11 - .../components/kv/kv-paths-card-test.js | 137 +++++++ .../kv/page/kv-page-overview-test.js | 341 ++++++++++++++++++ .../kv/page/kv-page-secret-paths-test.js | 136 ++----- .../components/overview-card-test.js | 14 +- .../components/pki/page/pki-overview-test.js | 28 +- .../sync/secrets/page/overview-test.js | 4 +- .../integration/helpers/date-format-test.js | 9 + .../integration/helpers/date-from-now-test.js | 2 +- 26 files changed, 875 insertions(+), 229 deletions(-) rename ui/{app => lib/core/addon}/helpers/date-from-now.js (100%) create mode 100644 ui/lib/core/app/helpers/date-from-now.js create mode 100644 ui/lib/kv/addon/components/kv-paths-card.hbs rename ui/lib/kv/addon/components/{page/secret/paths.js => kv-paths-card.js} (67%) create mode 100644 ui/lib/kv/addon/components/page/secret/overview.hbs create mode 100644 ui/lib/kv/addon/components/page/secret/overview.js create mode 100644 ui/tests/integration/components/kv/kv-paths-card-test.js create mode 100644 ui/tests/integration/components/kv/page/kv-page-overview-test.js diff --git a/ui/app/models/kv/metadata.js b/ui/app/models/kv/metadata.js index 66d1d1b8c0..c598651821 100644 --- a/ui/app/models/kv/metadata.js +++ b/ui/app/models/kv/metadata.js @@ -69,11 +69,6 @@ export default class KvSecretMetadataModel extends Model { return keyIsFolder(this.path); } - // cannot use isDeleted due to ember property conflict - get isSecretDeleted() { - return isDeleted(this.deletionTime); - } - // turns version object into an array for version dropdown menu get sortedVersions() { const array = []; @@ -93,6 +88,7 @@ export default class KvSecretMetadataModel extends Model { return { state, isDeactivated: state !== 'created', + deletionTime: data.deletion_time, }; } diff --git a/ui/lib/core/addon/components/json-editor.js b/ui/lib/core/addon/components/json-editor.js index 749e76ea7e..14624f8124 100644 --- a/ui/lib/core/addon/components/json-editor.js +++ b/ui/lib/core/addon/components/json-editor.js @@ -16,6 +16,7 @@ import { obfuscateData } from 'core/utils/advanced-secret'; * * * @param {string} [title] - Name above codemirror view + * @param {boolean} [showToolbar=true] - If false, toolbar and title are hidden * @param {string} value - a specific string the comes from codemirror. It's the value inside the codemirror display * @param {Function} [valueUpdated] - action to preform when you edit the codemirror value. * @param {Function} [onFocusOut] - action to preform when you focus out of codemirror. diff --git a/ui/lib/core/addon/components/overview-card.hbs b/ui/lib/core/addon/components/overview-card.hbs index 7bf6d32d17..d497a218cb 100644 --- a/ui/lib/core/addon/components/overview-card.hbs +++ b/ui/lib/core/addon/components/overview-card.hbs @@ -10,10 +10,14 @@ data-test-overview-card-container={{@cardTitle}} ...attributes > -
- - {{@cardTitle}} - +
+ {{#if (has-block "customTitle")}} + {{yield to="customTitle"}} + {{else}} + + {{@cardTitle}} + + {{/if}} {{#if (has-block "action")}} {{yield to="action"}} diff --git a/ui/lib/core/addon/helpers/date-format.js b/ui/lib/core/addon/helpers/date-format.js index 1084c2af99..c541a58fb8 100644 --- a/ui/lib/core/addon/helpers/date-format.js +++ b/ui/lib/core/addon/helpers/date-format.js @@ -43,7 +43,7 @@ function dateFromString(str) { return null; } -export function dateFormat([value, style], { withTimeZone = false }) { +export function dateFormat([value, style = 'MMM d yyyy, h:mm:ss aa'], { withTimeZone = false }) { // see format breaking in upgrade to date-fns 2.x https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md#changed-5 let date; switch (checkType(value)) { diff --git a/ui/app/helpers/date-from-now.js b/ui/lib/core/addon/helpers/date-from-now.js similarity index 100% rename from ui/app/helpers/date-from-now.js rename to ui/lib/core/addon/helpers/date-from-now.js diff --git a/ui/lib/core/app/helpers/date-from-now.js b/ui/lib/core/app/helpers/date-from-now.js new file mode 100644 index 0000000000..64cb815924 --- /dev/null +++ b/ui/lib/core/app/helpers/date-from-now.js @@ -0,0 +1,6 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +export { default } from 'core/helpers/date-from-now'; diff --git a/ui/lib/kv/addon/components/kv-paths-card.hbs b/ui/lib/kv/addon/components/kv-paths-card.hbs new file mode 100644 index 0000000000..d7976d92f2 --- /dev/null +++ b/ui/lib/kv/addon/components/kv-paths-card.hbs @@ -0,0 +1,82 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + +
+ + Paths + + {{#if @isCondensed}} + + The paths to use when referring to this secret in API or CLI. + + {{/if}} + +
+ {{#each this.paths as |path|}} + + + + {{path.snippet}} + + + {{/each}} +
+ + {{#unless @isCondensed}} + + Commands + + +
+

+ CLI + +

+

+ This command retrieves the value from KV secrets engine at the given key name. See our + + documentation + for other CLI commands. +

+ + +

+ API read secret version +

+

+ 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, + + learn more. + +

+ +
+ {{/unless}} +
\ No newline at end of file diff --git a/ui/lib/kv/addon/components/page/secret/paths.js b/ui/lib/kv/addon/components/kv-paths-card.js similarity index 67% rename from ui/lib/kv/addon/components/page/secret/paths.js rename to ui/lib/kv/addon/components/kv-paths-card.js index 716c76c398..008efa8f2b 100644 --- a/ui/lib/kv/addon/components/page/secret/paths.js +++ b/ui/lib/kv/addon/components/kv-paths-card.js @@ -9,23 +9,21 @@ import { kvMetadataPath, kvDataPath } from 'vault/utils/kv-path'; import { encodePath } from 'vault/utils/path-encoding-helpers'; /** - * @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. + * @module KvPathsCard is used to display copyable secret paths for KV v2 for CLI and API use. + * This component is permission agnostic because args come from the views mount path and url params. * - * * * @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 + * @param {boolean} isCondensed - if true a smaller version displays with no commands section or extra explanatory text */ -export default class KvSecretPaths extends Component { +export default class KvPathsCard extends Component { @service namespace; get paths() { @@ -46,11 +44,15 @@ export default class KvSecretPaths extends Component { 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/${encodePath(namespace)}/${metadata}` : `/v1/${metadata}`, - text: `Use this path when referring to this secret's metadata in the API and permanent secret deletion.`, - }, + ...(this.args.isCondensed + ? [] + : [ + { + label: 'API path for metadata', + snippet: namespace ? `/v1/${encodePath(namespace)}/${metadata}` : `/v1/${metadata}`, + text: `Use this path when referring to this secret's metadata in the API and permanent secret deletion.`, + }, + ]), ]; } diff --git a/ui/lib/kv/addon/components/kv-subkeys.hbs b/ui/lib/kv/addon/components/kv-subkeys.hbs index a36f18be69..62451b7368 100644 --- a/ui/lib/kv/addon/components/kv-subkeys.hbs +++ b/ui/lib/kv/addon/components/kv-subkeys.hbs @@ -3,9 +3,9 @@ SPDX-License-Identifier: BUSL-1.1 ~}} - + <:customSubtext> - + {{#if this.showJson}} These are the subkeys within this secret. All underlying values of leaf keys are not retrieved and are replaced with null diff --git a/ui/lib/kv/addon/components/page/secret/overview.hbs b/ui/lib/kv/addon/components/page/secret/overview.hbs new file mode 100644 index 0000000000..43da56c1aa --- /dev/null +++ b/ui/lib/kv/addon/components/page/secret/overview.hbs @@ -0,0 +1,105 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + + + <:tabLinks> +
  • + Overview +
  • +
  • + Secret +
  • +
  • + Metadata +
  • +
  • + Paths +
  • + {{#if @canReadMetadata}} +
  • + Version History +
  • + {{/if}} + +
    + +{{#if (or @metadata @subkeys)}} +
    + + <:customTitle> + + Current version + {{#unless this.isActive}} + + {{/unless}} + + + <:action> + {{#if @canUpdateSecret}} + + {{/if}} + + <:content> + + {{or @metadata.currentVersion @subkeys.metadata.version}} + + + + + {{#if this.isActive}} + {{#let (or @metadata.createdTime @subkeys.metadata.created_time) as |timestamp|}} + + <:action> + {{#if @canReadMetadata}} + + {{/if}} + + <:content> + + {{date-from-now timestamp}} + + + + {{/let}} + {{/if}} +
    +{{/if}} + + + + + +{{#if @subkeys.subkeys}} + +{{/if}} \ No newline at end of file diff --git a/ui/lib/kv/addon/components/page/secret/overview.js b/ui/lib/kv/addon/components/page/secret/overview.js new file mode 100644 index 0000000000..0c41df40dc --- /dev/null +++ b/ui/lib/kv/addon/components/page/secret/overview.js @@ -0,0 +1,51 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Component from '@glimmer/component'; +import { dateFormat } from 'core/helpers/date-format'; + +/** + * @module KvSecretOverview + * + * + * @param {string} backend - kv secret mount to make network request + * @param {array} breadcrumbs - Array to generate breadcrumbs, passed to the page header component + * @param {boolean} canReadMetadata - permissions to read metadata + * @param {boolean} canUpdateSecret - permissions to create a new version of a secret + * @param {model} metadata - Ember data model: 'kv/metadata' + * @param {string} path - path to request secret data for selected version + * @param {string} secretState - if a secret has been "destroyed", "deleted" or "created" (still active) + * @param {object} subkeys - API response from subkeys endpoint, object with "subkeys" and "metadata" keys + */ + +export default class KvSecretOverview extends Component { + get isActive() { + const state = this.args.secretState; + return state !== 'destroyed' && state !== 'deleted'; + } + + get versionSubtext() { + const state = this.args.secretState; + if (state === 'destroyed') { + return 'The current version of this secret has been permanently deleted and cannot be restored.'; + } + if (state === 'deleted') { + const time = + this.args.metadata?.currentSecret.deletionTime || this.args.subkeys?.metadata.deletion_time; + const date = dateFormat([time], {}); + return `The current version of this secret was deleted ${date}.`; + } + return 'The current version of this secret.'; + } +} diff --git a/ui/lib/kv/addon/components/page/secret/paths.hbs b/ui/lib/kv/addon/components/page/secret/paths.hbs index 0472d30bc2..f7e7e0c15d 100644 --- a/ui/lib/kv/addon/components/page/secret/paths.hbs +++ b/ui/lib/kv/addon/components/page/secret/paths.hbs @@ -36,66 +36,4 @@ -

    - Paths -

    - -
    - {{#each this.paths as |path|}} - - - - {{path.snippet}} - - - {{/each}} -
    - -

    - Commands -

    - -
    -

    - CLI - -

    -

    - This command retrieves the value from KV secrets engine at the given key name. For other CLI commands, - - learn more. - -

    - - -

    - API read secret version -

    -

    - 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, - - learn more. - -

    - -
    \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/pki/addon/components/page/pki-overview.hbs b/ui/lib/pki/addon/components/page/pki-overview.hbs index 6372e98444..21b58e47f7 100644 --- a/ui/lib/pki/addon/components/page/pki-overview.hbs +++ b/ui/lib/pki/addon/components/page/pki-overview.hbs @@ -18,7 +18,7 @@ /> <:content> - + {{format-number (if (eq @issuers 404) 0 @issuers.length)}} @@ -38,7 +38,7 @@ /> <:content> - + {{format-number (if (eq @roles 404) 0 @roles.length)}} diff --git a/ui/tests/acceptance/pki/pki-overview-test.js b/ui/tests/acceptance/pki/pki-overview-test.js index 4281e1a1e9..cae35d6be4 100644 --- a/ui/tests/acceptance/pki/pki-overview-test.js +++ b/ui/tests/acceptance/pki/pki-overview-test.js @@ -14,6 +14,8 @@ import { click, currentURL, currentRouteName, visit } from '@ember/test-helpers' import { runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands'; import { clearRecords } from 'vault/tests/helpers/pki/pki-helpers'; import { PKI_OVERVIEW } from 'vault/tests/helpers/pki/pki-selectors'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; +const { overviewCard } = GENERAL; module('Acceptance | pki overview', function (hooks) { setupApplicationTest(hooks); @@ -59,9 +61,9 @@ module('Acceptance | pki overview', function (hooks) { test('navigates to view issuers when link is clicked on issuer card', async function (assert) { await authPage.login(this.pkiAdminToken); await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.dom(PKI_OVERVIEW.issuersCardTitle).hasText('Issuers'); - assert.dom(PKI_OVERVIEW.issuersCardOverviewNum).hasText('1'); - await click(PKI_OVERVIEW.issuersCardLink); + assert.dom(overviewCard.title('Issuers')).hasText('Issuers'); + assert.dom(`${overviewCard.container('Issuers')} p`).hasText('1'); + await click(overviewCard.actionLink('Issuers')); assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers`); await visit(`/vault/secrets/${this.mountPath}/pki/overview`); }); @@ -69,9 +71,9 @@ module('Acceptance | pki overview', function (hooks) { test('navigates to view roles when link is clicked on roles card', async function (assert) { await authPage.login(this.pkiAdminToken); await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.dom(PKI_OVERVIEW.rolesCardTitle).hasText('Roles'); - assert.dom(PKI_OVERVIEW.rolesCardOverviewNum).hasText('0'); - await click(PKI_OVERVIEW.rolesCardLink); + assert.dom(overviewCard.title('Roles')).hasText('Roles'); + assert.dom(`${overviewCard.container('Roles')} p`).hasText('0'); + await click(overviewCard.actionLink('Roles')); assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`); await runCmd([ `write ${this.mountPath}/roles/some-role \ @@ -81,14 +83,14 @@ module('Acceptance | pki overview', function (hooks) { max_ttl="720h"`, ]); await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.dom(PKI_OVERVIEW.rolesCardOverviewNum).hasText('1'); + assert.dom(`${overviewCard.container('Roles')} p`).hasText('1'); }); test('hides roles card if user does not have permissions', async function (assert) { await authPage.login(this.pkiIssuersList); await visit(`/vault/secrets/${this.mountPath}/pki/overview`); - assert.dom(PKI_OVERVIEW.rolesCardTitle).doesNotExist('Roles card does not exist'); - assert.dom(PKI_OVERVIEW.issuersCardTitle).exists('Issuers card exists'); + assert.dom(overviewCard.title('Roles')).doesNotExist('Roles card does not exist'); + assert.dom(overviewCard.title('Issuers')).hasText('Issuers'); }); test('navigates to generate certificate page for Issue Certificates card', async function (assert) { diff --git a/ui/tests/acceptance/sync/secrets/overview-test.js b/ui/tests/acceptance/sync/secrets/overview-test.js index c7070e7c6e..b36d112a7d 100644 --- a/ui/tests/acceptance/sync/secrets/overview-test.js +++ b/ui/tests/acceptance/sync/secrets/overview-test.js @@ -64,7 +64,7 @@ module('Acceptance | sync | overview', function (hooks) { await click(ts.navLink('Secrets Sync')); await click(ts.destinations.list.create); await click(ts.createCancel); - await click(ts.overviewCard.actionLink('Create new')); + await click(ts.overviewCard.actionText('Create new')); await click(ts.createCancel); await waitFor(ts.overview.table.actionToggle(0)); await click(ts.overview.table.actionToggle(0)); diff --git a/ui/tests/helpers/general-selectors.ts b/ui/tests/helpers/general-selectors.ts index dec99d1297..af135def16 100644 --- a/ui/tests/helpers/general-selectors.ts +++ b/ui/tests/helpers/general-selectors.ts @@ -74,11 +74,12 @@ export const GENERAL = { removeSelected: '[data-test-selected-list-button="delete"]', }, overviewCard: { + container: (title: string) => `[data-test-overview-card-container="${title}"]`, title: (title: string) => `[data-test-overview-card-title="${title}"]`, description: (title: string) => `[data-test-overview-card-subtitle="${title}"]`, content: (title: string) => `[data-test-overview-card-content="${title}"]`, - action: (title: string) => `[data-test-overview-card-container="${title}"] [data-test-action-text]`, - actionLink: (label: string) => `[data-test-action-text="${label}"]`, + actionText: (text: string) => `[data-test-action-text="${text}"]`, + actionLink: (label: string) => `[data-test-overview-card="${label}"] a`, }, pagination: { next: '.hds-pagination-nav__arrow--direction-next', diff --git a/ui/tests/helpers/kv/kv-run-commands.js b/ui/tests/helpers/kv/kv-run-commands.js index 9b4e6a0571..a25886189b 100644 --- a/ui/tests/helpers/kv/kv-run-commands.js +++ b/ui/tests/helpers/kv/kv-run-commands.js @@ -7,6 +7,9 @@ import { click, fillIn, visit, settled } from '@ember/test-helpers'; import { FORM } from './kv-selectors'; import { encodePath } from 'vault/utils/path-encoding-helpers'; +import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs'; +import { assert } from '@ember/debug'; +import { kvMetadataPath } from 'vault/utils/kv-path'; // CUSTOM ACTIONS RELEVANT TO KV-V2 @@ -66,3 +69,30 @@ export function clearRecords(store) { store.unloadAll('kv/metatata'); store.unloadAll('capabilities'); } + +// TEST SETUP HELPERS + +// sets basic path, backend, and metadata +export const baseSetup = (context) => { + assert( + `'baseSetup()' requires mirage: import { setupMirage } from 'ember-cli-mirage/test-support'`, + context.server + ); + context.store = context.owner.lookup('service:store'); + context.server.post('/sys/capabilities-self', allowAllCapabilitiesStub()); + context.backend = 'kv-engine'; + context.path = 'my-secret'; + context.metadata = metadataModel(context, { withCustom: false }); +}; + +export const metadataModel = (context, { withCustom = false }) => { + const metadata = withCustom + ? context.server.create('kv-metadatum', 'withCustomMetadata') + : context.server.create('kv-metadatum'); + metadata.id = kvMetadataPath(context.backend, context.path); + context.store.pushPayload('kv/metadata', { + modelName: 'kv/metadata', + ...metadata, + }); + return context.store.peekRecord('kv/metadata', metadata.id); +}; diff --git a/ui/tests/helpers/pki/pki-selectors.ts b/ui/tests/helpers/pki/pki-selectors.ts index 09c0e2039e..2bd7bd320e 100644 --- a/ui/tests/helpers/pki/pki-selectors.ts +++ b/ui/tests/helpers/pki/pki-selectors.ts @@ -4,23 +4,12 @@ */ export const PKI_OVERVIEW = { - issuersCardTitle: '[data-test-overview-card-title="Issuers"]', - issuersCardSubtitle: '[data-test-overview-card-subtitle="Issuers"]', - issuersCardLink: '[data-test-overview-card-container="Issuers"] a', - issuersCardOverviewNum: '[data-test-overview-card-container="Issuers"] h2', - rolesCardTitle: '[data-test-overview-card-title="Roles"]', - rolesCardSubtitle: '[data-test-overview-card-subtitle="Roles"]', - rolesCardLink: '[data-test-overview-card-container="Roles"] a', - rolesCardOverviewNum: '[data-test-overview-card-container="Roles"] h2', - issueCertificate: '[data-test-overview-card-title="Issue certificate"]', issueCertificateInput: '[data-test-issue-certificate-input]', issueCertificatePowerSearch: '[data-test-issue-certificate-input] span', issueCertificateButton: '[data-test-issue-certificate-button]', - viewCertificate: '[data-test-overview-card-title="View certificate"]', viewCertificateInput: '[data-test-view-certificate-input]', viewCertificatePowerSearch: '[data-test-view-certificate-input] span', viewCertificateButton: '[data-test-view-certificate-button]', - viewIssuerInput: '[data-test-issue-issuer-input]', viewIssuerPowerSearch: '[data-test-issue-issuer-input] span', viewIssuerButton: '[data-test-view-issuer-button]', firstPowerSelectOption: '[data-option-index="0"]', diff --git a/ui/tests/integration/components/kv/kv-paths-card-test.js b/ui/tests/integration/components/kv/kv-paths-card-test.js new file mode 100644 index 0000000000..b29a0ee8bc --- /dev/null +++ b/ui/tests/integration/components/kv/kv-paths-card-test.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +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 | KvPathsCard', function (hooks) { + setupRenderingTest(hooks); + setupEngine(hooks, 'kv'); + + hooks.beforeEach(async function () { + this.backend = 'kv-engine'; + this.path = 'my-secret'; + this.isCondensed = false; + + this.assertClipboard = (assert, element, expected) => { + assert.dom(element).hasAttribute('data-test-copy-button', expected); + }; + + this.renderComponent = async () => { + return render( + hbs``, + { + owner: this.engine, + } + ); + }; + }); + + test('it renders condensed version', async function (assert) { + this.isCondensed = true; + + await this.renderComponent(); + + assert.dom('[data-test-component="info-table-row"] .helper-text').doesNotExist('subtext does not render'); + assert.dom('[data-test-label-div]').hasClass('is-one-quarter'); + assert.dom(PAGE.infoRowValue('API path for metadata')).doesNotExist(); + assert.dom(PAGE.paths.codeSnippet('cli')).doesNotExist(); + assert.dom(PAGE.paths.codeSnippet('api')).doesNotExist(); + + const paths = [ + { label: 'API path', expected: `/v1/${this.backend}/data/${this.path}` }, + { label: 'CLI path', expected: `-mount="${this.backend}" "${this.path}"` }, + ]; + 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 uncondensed version', async function (assert) { + await this.renderComponent(); + + assert.dom('[data-test-component="info-table-row"] .helper-text').exists('subtext renders'); + assert.dom('[data-test-label-div]').hasClass('is-one-third'); + assert.dom(PAGE.infoRowValue('API path for metadata')).exists(); + assert.dom(PAGE.paths.codeSnippet('cli')).exists(); + assert.dom(PAGE.paths.codeSnippet('api')).exists(); + }); + + test('it renders copyable paths', async function (assert) { + 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 this.renderComponent(); + + 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) { + 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 this.renderComponent(); + + 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) { + 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}"`, + api: `curl \\ --header \"X-Vault-Token: ...\" \\ --request GET \\ ${url}`, + }; + await this.renderComponent(); + + assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli); + assert.dom(PAGE.paths.codeSnippet('api')).hasText(expected.api); + }); + + test('it renders copyable encoded mount and path commands', async function (assert) { + 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}"`, + api: `curl \\ --header \"X-Vault-Token: ...\" \\ --request GET \\ ${url}`, + }; + await this.renderComponent(); + + assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli); + assert.dom(PAGE.paths.codeSnippet('api')).hasText(expected.api); + }); +}); diff --git a/ui/tests/integration/components/kv/page/kv-page-overview-test.js b/ui/tests/integration/components/kv/page/kv-page-overview-test.js new file mode 100644 index 0000000000..944c19f8f9 --- /dev/null +++ b/ui/tests/integration/components/kv/page/kv-page-overview-test.js @@ -0,0 +1,341 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { setupEngine } from 'ember-engines/test-support'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { PAGE } from 'vault/tests/helpers/kv/kv-selectors'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import { dateFormat } from 'core/helpers/date-format'; +import { dateFromNow } from 'core/helpers/date-from-now'; +import { baseSetup } from 'vault/tests/helpers/kv/kv-run-commands'; + +const { overviewCard } = GENERAL; +module('Integration | Component | kv-v2 | Page::Secret::Overview', function (hooks) { + setupRenderingTest(hooks); + setupEngine(hooks, 'kv'); + setupMirage(hooks); + + hooks.beforeEach(async function () { + baseSetup(this); + this.breadcrumbs = [ + { label: 'Secrets', route: 'secrets', linkExternal: true }, + { label: this.backend, route: 'list' }, + { label: this.path }, + ]; + this.subkeys = { + subkeys: { + foo: null, + bar: { + baz: null, + }, + quux: null, + }, + metadata: { + created_time: '2021-12-14T20:28:00.773477Z', + custom_metadata: null, + deletion_time: '', + destroyed: false, + version: 1, + }, + }; + this.canReadMetadata = true; + this.canUpdateSecret = true; + this.secretState = 'created'; + + this.format = (time) => dateFormat([time, 'MMM d yyyy, h:mm:ss aa'], {}); + this.renderComponent = async () => { + return render( + hbs` + `, + { owner: this.engine } + ); + }; + }); + + module('it renders when version is not deleted nor destroyed', function () { + test('it renders tabs', async function (assert) { + await this.renderComponent(); + const tabs = ['Overview', 'Secret', 'Metadata', 'Paths', 'Version History']; + for (const tab of tabs) { + assert.dom(PAGE.secretTab(tab)).hasText(tab); + } + }); + + test('it renders header', async function (assert) { + await this.renderComponent(); + assert.dom(PAGE.breadcrumbs).hasText(`Secrets ${this.backend} ${this.path}`); + assert.dom(PAGE.title).hasText(this.path); + }); + + test('it renders with full permissions', async function (assert) { + await this.renderComponent(); + const fromNow = dateFromNow([this.metadata.createdTime]); // uses date-fns so can't stub timestamp util + assert.dom(`${overviewCard.container('Current version')} .hds-badge`).doesNotExist(); + assert + .dom(overviewCard.container('Current version')) + .hasText( + `Current version Create new The current version of this secret. ${this.metadata.currentVersion}` + ); + assert + .dom(overviewCard.container('Secret age')) + .hasText( + `Secret age View metadata Time since last update at ${this.format( + this.metadata.createdTime + )}. ${fromNow}` + ); + assert + .dom(overviewCard.container('Paths')) + .hasText( + `Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"` + ); + assert + .dom(overviewCard.container('Subkeys')) + .hasText( + `Subkeys JSON The table is displaying the top level subkeys. Toggle on the JSON view to see the full depth. Keys ${Object.keys( + this.subkeys.subkeys + ).join(' ')}` + ); + }); + + test('it hides link when no secret update permissions', async function (assert) { + // creating a new version of a secret is updating a secret + // the overview only exists after an initial version is created + // which is why we just check for update and not also create + this.canUpdateSecret = false; + await this.renderComponent(); + assert + .dom(`${overviewCard.container('Current version')} a`) + .doesNotExist('create link does not render'); + assert + .dom(overviewCard.container('Current version')) + .hasText(`Current version The current version of this secret. ${this.metadata.currentVersion}`); + }); + + test('it renders with no metadata permissions', async function (assert) { + this.metadata = null; + this.canReadMetadata = false; + // all secret metadata instead comes from subkeys endpoint + const subkeyMeta = this.subkeys.metadata; + await this.renderComponent(); + const fromNow = dateFromNow([subkeyMeta.created_time]); // uses date-fns so can't stub timestamp util + assert + .dom(overviewCard.container('Current version')) + .hasText(`Current version Create new The current version of this secret. ${subkeyMeta.version}`); + assert + .dom(overviewCard.container('Secret age')) + .hasText(`Secret age Time since last update at ${this.format(subkeyMeta.created_time)}. ${fromNow}`); + assert.dom(`${overviewCard.container('Secret age')} a`).doesNotExist('metadata link does not render'); + assert + .dom(overviewCard.container('Paths')) + .hasText( + `Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"` + ); + assert + .dom(overviewCard.container('Subkeys')) + .hasText( + `Subkeys JSON The table is displaying the top level subkeys. Toggle on the JSON view to see the full depth. Keys ${Object.keys( + this.subkeys.subkeys + ).join(' ')}` + ); + }); + + test('it renders with no subkeys permissions', async function (assert) { + this.subkeys = null; + await this.renderComponent(); + const fromNow = dateFromNow([this.metadata.createdTime]); // uses date-fns so can't stub timestamp util + const expectedTime = this.format(this.metadata.createdTime); + assert + .dom(overviewCard.container('Current version')) + .hasText( + `Current version Create new The current version of this secret. ${this.metadata.currentVersion}` + ); + assert + .dom(overviewCard.container('Secret age')) + .hasText(`Secret age View metadata Time since last update at ${expectedTime}. ${fromNow}`); + assert + .dom(overviewCard.container('Paths')) + .hasText( + `Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"` + ); + assert.dom(overviewCard.container('Subkeys')).doesNotExist(); + }); + + test('it renders with no subkey or metadata permissions', async function (assert) { + this.subkeys = null; + this.metadata = null; + await this.renderComponent(); + assert.dom(overviewCard.container('Current version')).doesNotExist(); + assert.dom(overviewCard.container('Secret age')).doesNotExist(); + assert.dom(overviewCard.container('Subkeys')).doesNotExist(); + assert + .dom(overviewCard.container('Paths')) + .hasText( + `Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"` + ); + }); + }); + + module('it renders when version is deleted', function (hooks) { + hooks.beforeEach(async function () { + this.secretState = 'deleted'; + // subkeys is null but metadata still has data + this.subkeys = { + subkeys: null, + metadata: { + created_time: '2021-12-14T20:28:00.773477Z', + custom_metadata: null, + deletion_time: '2022-02-14T20:28:00.773477Z', + destroyed: false, + version: 1, + }, + }; + this.metadata.versions[4].deletion_time = '2024-08-15T23:01:08.312332Z'; + this.assertBadge = (assert) => { + assert + .dom(`${overviewCard.container('Current version')} .hds-badge`) + .hasClass('hds-badge--color-neutral'); + assert + .dom(`${overviewCard.container('Current version')} .hds-badge`) + .hasClass('hds-badge--type-inverted'); + assert.dom(`${overviewCard.container('Current version')} .hds-badge`).hasText('Deleted'); + }; + }); + + test('with full permissions', async function (assert) { + const expectedTime = this.format(this.metadata.versions[4].deletion_time); + await this.renderComponent(); + this.assertBadge(assert); + assert + .dom(overviewCard.container('Current version')) + .hasText( + `Current version Deleted Create new The current version of this secret was deleted ${expectedTime}. ${this.metadata.currentVersion}` + ); + assert.dom(overviewCard.container('Secret age')).doesNotExist(); + assert.dom(overviewCard.container('Subkeys')).doesNotExist(); + assert + .dom(overviewCard.container('Paths')) + .hasText( + `Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"` + ); + }); + + test('with no metadata permissions', async function (assert) { + this.metadata = null; + const expectedTime = this.format(this.subkeys.metadata.deletion_time); + await this.renderComponent(); + this.assertBadge(assert); + assert + .dom(overviewCard.container('Current version')) + .hasText( + `Current version Deleted Create new The current version of this secret was deleted ${expectedTime}. ${this.subkeys.metadata.version}` + ); + }); + + test('with no subkey permissions', async function (assert) { + this.subkeys = null; + const expectedTime = this.format(this.metadata.versions[4].deletion_time); + await this.renderComponent(); + this.assertBadge(assert); + assert + .dom(overviewCard.container('Current version')) + .hasText( + `Current version Deleted Create new The current version of this secret was deleted ${expectedTime}. ${this.metadata.currentVersion}` + ); + }); + + test('with no permissions', async function (assert) { + this.subkeys = null; + this.metadata = null; + await this.renderComponent(); + assert.dom(overviewCard.container('Current version')).doesNotExist(); + }); + }); + + module('it renders when version is destroyed', function (hooks) { + hooks.beforeEach(async function () { + this.secretState = 'destroyed'; + // subkeys is null but metadata still has data + this.subkeys = { + subkeys: null, + metadata: { + created_time: '2024-08-15T01:24:43.658478Z', + custom_metadata: null, + deletion_time: '', + destroyed: true, + version: 1, + }, + }; + this.metadata.versions[4].destroyed = true; + this.assertBadge = (assert) => { + assert + .dom(`${overviewCard.container('Current version')} .hds-badge`) + .hasClass('hds-badge--color-critical'); + assert + .dom(`${overviewCard.container('Current version')} .hds-badge`) + .hasClass('hds-badge--type-outlined'); + assert.dom(`${overviewCard.container('Current version')} .hds-badge`).hasText('Destroyed'); + }; + }); + + test('with full permissions', async function (assert) { + await this.renderComponent(); + this.assertBadge(assert); + assert + .dom(overviewCard.container('Current version')) + .hasText( + `Current version Destroyed Create new The current version of this secret has been permanently deleted and cannot be restored. ${this.metadata.currentVersion}` + ); + assert.dom(overviewCard.container('Secret age')).doesNotExist(); + assert.dom(overviewCard.container('Subkeys')).doesNotExist(); + assert + .dom(overviewCard.container('Paths')) + .hasText( + `Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"` + ); + }); + + test('with no metadata permissions', async function (assert) { + this.metadata = null; + await this.renderComponent(); + this.assertBadge(assert); + assert + .dom(overviewCard.container('Current version')) + .hasText( + `Current version Destroyed Create new The current version of this secret has been permanently deleted and cannot be restored. ${this.subkeys.metadata.version}` + ); + }); + + test('with no subkeys permissions', async function (assert) { + this.subkeys = null; + await this.renderComponent(); + this.assertBadge(assert); + assert + .dom(overviewCard.container('Current version')) + .hasText( + `Current version Destroyed Create new The current version of this secret has been permanently deleted and cannot be restored. ${this.metadata.currentVersion}` + ); + }); + + test('with no permissions', async function (assert) { + this.subkeys = null; + this.metadata = null; + await this.renderComponent(); + assert.dom(overviewCard.container('Current version')).doesNotExist(); + }); + }); +}); diff --git a/ui/tests/integration/components/kv/page/kv-page-secret-paths-test.js b/ui/tests/integration/components/kv/page/kv-page-secret-paths-test.js index fae78bb41b..8b1766e803 100644 --- a/ui/tests/integration/components/kv/page/kv-page-secret-paths-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-secret-paths-test.js @@ -9,8 +9,6 @@ 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'); @@ -18,123 +16,55 @@ module('Integration | Component | kv-v2 | Page::Secret::Paths', function (hooks) hooks.beforeEach(async function () { this.backend = 'kv-engine'; this.path = 'my-secret'; + this.canReadMetadata = true; 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-test-copy-button', expected); + this.renderComponent = async () => { + await render( + hbs` + + `, + { owner: this.engine } + ); }; }); - 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` - - `, - { 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 tabs', async function (assert) { + await this.renderComponent(); + const tabs = ['Secret', 'Metadata', 'Paths', 'Version History']; + for (const tab of tabs) { + assert.dom(PAGE.secretTab(tab)).hasText(tab); } }); - 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` - - `, - { 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 hides version history when cannot READ metadata', async function (assert) { + this.canReadMetadata = false; + await this.renderComponent(); + const tabs = ['Secret', 'Metadata', 'Paths']; + for (const tab of tabs) { + assert.dom(PAGE.secretTab(tab)).hasText(tab); } + assert.dom(PAGE.secretTab('Version History')).doesNotExist(); }); - test('it renders copyable commands', async function (assert) { - 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}"`, - api: `curl \\ --header \"X-Vault-Token: ...\" \\ --request GET \\ ${url}`, - }; - await render( - hbs` - - `, - { owner: this.engine } - ); - - assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli); - assert.dom(PAGE.paths.codeSnippet('api')).hasText(expected.api); + test('it renders header', async function (assert) { + await this.renderComponent(); + assert.dom(PAGE.breadcrumbs).hasText(`Secrets ${this.backend} ${this.path}`); + assert.dom(PAGE.title).hasText(this.path); }); - test('it renders copyable encoded mount and path commands', async function (assert) { - 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}"`, - api: `curl \\ --header \"X-Vault-Token: ...\" \\ --request GET \\ ${url}`, - }; - await render( - hbs` - - `, - { owner: this.engine } - ); - - assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli); - assert.dom(PAGE.paths.codeSnippet('api')).hasText(expected.api); + test('it renders commands which is the uncondensed version of KvPathsCard', async function (assert) { + await this.renderComponent(); + assert.dom(PAGE.paths.codeSnippet('cli')).exists(); + assert.dom(PAGE.paths.codeSnippet('api')).exists(); }); }); diff --git a/ui/tests/integration/components/overview-card-test.js b/ui/tests/integration/components/overview-card-test.js index 38383808ce..356b018dbc 100644 --- a/ui/tests/integration/components/overview-card-test.js +++ b/ui/tests/integration/components/overview-card-test.js @@ -13,6 +13,7 @@ const ACTION_TEXT = 'View card'; const SUBTEXT = 'This is subtext for card'; const SELECTORS = { + container: '[data-test-overview-card-container]', title: '[data-test-overview-card-title]', subtitle: '[data-test-overview-card-subtitle]', action: '[data-test-action-text]', @@ -28,10 +29,21 @@ module('Integration | Component | overview-card', function (hooks) { this.set('subText', SUBTEXT); }); - test('it returns card title, ', async function (assert) { + test('it returns card title', async function (assert) { await render(hbs``); assert.dom(SELECTORS.title).hasText('Card title'); }); + test('it returns custom title if both exist', async function (assert) { + await render(hbs` + + <:customTitle> + Fancy custom title + + + `); + assert.dom(SELECTORS.container).hasText('Fancy custom title'); + assert.dom(SELECTORS.container).doesNotIncludeText(this.cardTitle); + }); test('it renders card @subText arg, ', async function (assert) { await render(hbs``); assert.dom(SELECTORS.subtitle).hasText('This is subtext for card'); diff --git a/ui/tests/integration/components/pki/page/pki-overview-test.js b/ui/tests/integration/components/pki/page/pki-overview-test.js index 6043e0181d..5b15d4943f 100644 --- a/ui/tests/integration/components/pki/page/pki-overview-test.js +++ b/ui/tests/integration/components/pki/page/pki-overview-test.js @@ -10,7 +10,9 @@ import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { PKI_OVERVIEW } from 'vault/tests/helpers/pki/pki-selectors'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; +const { overviewCard } = GENERAL; module('Integration | Component | Page::PkiOverview', function (hooks) { setupRenderingTest(hooks); setupEngine(hooks, 'pki'); @@ -39,9 +41,11 @@ module('Integration | Component | Page::PkiOverview', function (hooks) { hbs`,`, { owner: this.engine } ); - assert.dom(PKI_OVERVIEW.issuersCardTitle).hasText('Issuers'); - assert.dom(PKI_OVERVIEW.issuersCardOverviewNum).hasText('2'); - assert.dom(PKI_OVERVIEW.issuersCardLink).hasText('View issuers'); + assert + .dom(overviewCard.container('Issuers')) + .hasText( + 'Issuers View issuers The total number of issuers in this PKI mount. Includes both root and intermediate certificates. 2' + ); }); test('shows the correct information on roles card', async function (assert) { @@ -49,15 +53,21 @@ module('Integration | Component | Page::PkiOverview', function (hooks) { hbs`,`, { owner: this.engine } ); - assert.dom(PKI_OVERVIEW.rolesCardTitle).hasText('Roles'); - assert.dom(PKI_OVERVIEW.rolesCardOverviewNum).hasText('3'); - assert.dom(PKI_OVERVIEW.rolesCardLink).hasText('View roles'); + assert + .dom(overviewCard.container('Roles')) + .hasText( + 'Roles View roles The total number of roles in this PKI mount that have been created to generate certificates. 3' + ); this.roles = 404; await render( hbs`,`, { owner: this.engine } ); - assert.dom(PKI_OVERVIEW.rolesCardOverviewNum).hasText('0'); + assert + .dom(overviewCard.container('Roles')) + .hasText( + 'Roles View roles The total number of roles in this PKI mount that have been created to generate certificates. 0' + ); }); test('shows the input search fields for View Certificates card', async function (assert) { @@ -65,7 +75,7 @@ module('Integration | Component | Page::PkiOverview', function (hooks) { hbs`,`, { owner: this.engine } ); - assert.dom(PKI_OVERVIEW.issueCertificate).hasText('Issue certificate'); + assert.dom(overviewCard.title('Issue certificate')).hasText('Issue certificate'); assert.dom(PKI_OVERVIEW.issueCertificateInput).exists(); assert.dom(PKI_OVERVIEW.issueCertificateButton).hasText('Issue'); }); @@ -75,7 +85,7 @@ module('Integration | Component | Page::PkiOverview', function (hooks) { hbs`,`, { owner: this.engine } ); - assert.dom(PKI_OVERVIEW.viewCertificate).hasText('View certificate'); + assert.dom(overviewCard.title('View certificate')).hasText('View certificate'); assert.dom(PKI_OVERVIEW.viewCertificateInput).exists(); assert.dom(PKI_OVERVIEW.viewCertificateButton).hasText('View'); }); diff --git a/ui/tests/integration/components/sync/secrets/page/overview-test.js b/ui/tests/integration/components/sync/secrets/page/overview-test.js index 5e587fc359..bdc00663bc 100644 --- a/ui/tests/integration/components/sync/secrets/page/overview-test.js +++ b/ui/tests/integration/components/sync/secrets/page/overview-test.js @@ -358,7 +358,7 @@ module('Integration | Component | sync | Page::Overview', function (hooks) { test('it should show the Totals cards', async function (assert) { await this.renderComponent(); - const { title, description, action, content } = overviewCard; + const { title, description, actionLink, content } = overviewCard; const cardData = [ { cardTitle: 'Total destinations', @@ -379,7 +379,7 @@ module('Integration | Component | sync | Page::Overview', function (hooks) { assert.dom(title(cardTitle)).hasText(cardTitle, `${cardTitle} card title renders`); assert.dom(description(cardTitle)).hasText(subText, ` ${cardTitle} card description renders`); assert.dom(content(cardTitle)).hasText(count, 'Total count renders'); - assert.dom(action(cardTitle)).hasText(actionText, 'Card action renders'); + assert.dom(actionLink(cardTitle)).hasText(actionText, 'Card action renders'); }); }); }); diff --git a/ui/tests/integration/helpers/date-format-test.js b/ui/tests/integration/helpers/date-format-test.js index c5407fa9b2..502adb2a2f 100644 --- a/ui/tests/integration/helpers/date-format-test.js +++ b/ui/tests/integration/helpers/date-format-test.js @@ -8,6 +8,7 @@ import { setupRenderingTest } from 'ember-qunit'; import { find, render, settled } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { formatTimeZone } from 'core/helpers/date-format'; +import { isMatch } from 'date-fns'; const TEST_DATE = new Date('2018-04-03T14:15:30'); @@ -56,6 +57,14 @@ module('Integration | Helper | date-format', function (hooks) { assert.strictEqual(resultLengthWithTimezone - 4, 4, 'Adds 4 characters for timezone'); }); + test('it renders default format', async function (assert) { + this.set('timestampDate', TEST_DATE); + await render(hbs`{{date-format this.timestampDate}}`); + const value = find('[data-test-formatted]').innerText; + const format = 'MMM d yyyy, h:mm:ss aa'; + assert.true(isMatch(value, format), `${value} is formatted as ${format}`); + }); + test('fails gracefully if given a non-date value', async function (assert) { this.set('value', 'not a date'); diff --git a/ui/tests/integration/helpers/date-from-now-test.js b/ui/tests/integration/helpers/date-from-now-test.js index a42392ce7e..0d64b67661 100644 --- a/ui/tests/integration/helpers/date-from-now-test.js +++ b/ui/tests/integration/helpers/date-from-now-test.js @@ -6,7 +6,7 @@ import { module, test } from 'qunit'; import { subMinutes } from 'date-fns'; import { setupRenderingTest } from 'ember-qunit'; -import { dateFromNow } from '../../../helpers/date-from-now'; +import { dateFromNow } from 'core/helpers/date-from-now'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; From f3e9f854d6e4e8ddcc797ad94c78aa66df1acef4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:48:16 -0700 Subject: [PATCH 06/12] Bump actions/download-artifact from 4.1.7 to 4.1.8 (#27704) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.1.7 to 4.1.8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/65a9edc5881444af0b9093a5e628f2fe47ea3b2e...fa0a91b85d4f404e444e00e005971372dc801d16) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: akshya96 <87045294+akshya96@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/enos-run-k8s.yml | 2 +- .github/workflows/test-go.yml | 4 ++-- .github/workflows/test-run-enos-scenario-matrix.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81654f48be..37ee8c48a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -335,7 +335,7 @@ jobs: # to secrets. - if: ${{ needs.setup.outputs.is-fork == 'false' }} name: Download failure summaries - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: pattern: failure-summary-*.md path: failure-summaries diff --git a/.github/workflows/enos-run-k8s.yml b/.github/workflows/enos-run-k8s.yml index fc5fd51f45..c629e44b47 100644 --- a/.github/workflows/enos-run-k8s.yml +++ b/.github/workflows/enos-run-k8s.yml @@ -44,7 +44,7 @@ jobs: github-token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} - name: Download Docker Image id: download - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: ${{ inputs.artifact-name }} path: ./enos/support/downloads diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml index 67be67784f..6a835ae634 100644 --- a/.github/workflows/test-go.yml +++ b/.github/workflows/test-go.yml @@ -617,7 +617,7 @@ jobs: data-race-output: ${{ steps.status.outputs.data-race-output }} data-race-result: ${{ steps.status.outputs.data-race-result }} steps: - - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: pattern: ${{ needs.test-go.outputs.data-race-log-download-pattern }} path: data-race-logs @@ -666,7 +666,7 @@ jobs: restore-keys: | ${{ inputs.test-timing-cache-key }}- - if: ${{ ! cancelled() && needs.test-go.result == 'success' && inputs.test-timing-cache-enabled }} - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: path: ${{ needs.test-matrix.outputs.go-test-dir }} pattern: ${{ needs.test-go.outputs.go-test-results-download-pattern }} diff --git a/.github/workflows/test-run-enos-scenario-matrix.yml b/.github/workflows/test-run-enos-scenario-matrix.yml index 7b24738629..6ab0d4176d 100644 --- a/.github/workflows/test-run-enos-scenario-matrix.yml +++ b/.github/workflows/test-run-enos-scenario-matrix.yml @@ -132,7 +132,7 @@ jobs: chmod 600 "./enos/support/private_key.pem" echo "debug_data_artifact_name=enos-debug-data_$(echo "${{ matrix.scenario }}" | sed -e 's/ /_/g' | sed -e 's/:/=/g')" >> "$GITHUB_OUTPUT" - if: contains(inputs.sample-name, 'build') - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: ${{ inputs.build-artifact-name }} path: ./enos/support/downloads From 754e97018fa1d3d2dd4f3ad867a255f8fdd4b826 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Aug 2024 16:15:59 -0700 Subject: [PATCH 07/12] Bump actions/setup-node from 4.0.2 to 4.0.3 (#27738) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.2 to 4.0.3. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/60edb5dd545a775178f52524783378180af0d1f8...1e60f620b9541d16bece96c5465dc8ee9832be0b) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: akshya96 <87045294+akshya96@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/test-enos-scenario-ui.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8a0e2bb055..1ab18b15eb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -177,7 +177,7 @@ jobs: key: ui-${{ steps.ui-hash.outputs.ui-hash }} - if: steps.cache-ui-assets.outputs.cache-hit != 'true' name: Set up node and yarn - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version-file: ui/package.json cache: yarn diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37ee8c48a9..9ed68cbea1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,12 +154,12 @@ jobs: with: github-token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} # Setup node.js without caching to allow running npm install -g yarn (next step) - - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version-file: './ui/package.json' - run: npm install -g yarn # Setup node.js with caching using the yarn.lock file - - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version-file: './ui/package.json' cache: yarn diff --git a/.github/workflows/test-enos-scenario-ui.yml b/.github/workflows/test-enos-scenario-ui.yml index 7a8e9ce5f6..3aa49eb27a 100644 --- a/.github/workflows/test-enos-scenario-ui.yml +++ b/.github/workflows/test-enos-scenario-ui.yml @@ -82,7 +82,7 @@ jobs: - name: Set Up Git run: git config --global url."https://${{ secrets.elevated_github_token }}:@github.com".insteadOf "https://github.com" - name: Set Up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version-file: './ui/package.json' - name: Set Up Terraform From c99e4f1a3f9a89c808fb77059c6a6dab65936fc6 Mon Sep 17 00:00:00 2001 From: Jonathan Frappier <92055993+jonathanfrappier@users.noreply.github.com> Date: Fri, 16 Aug 2024 19:54:28 -0400 Subject: [PATCH 08/12] Add valid IP callout (#28112) Co-authored-by: Yoko Hyakuna --- website/content/docs/configuration/index.mdx | 26 +++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/website/content/docs/configuration/index.mdx b/website/content/docs/configuration/index.mdx index 9fc995fa18..b1dbdea459 100644 --- a/website/content/docs/configuration/index.mdx +++ b/website/content/docs/configuration/index.mdx @@ -13,6 +13,14 @@ The format of this file is [HCL](https://github.com/hashicorp/hcl) or JSON. An example configuration is shown below: + + +For multi-node clusters, replace the loopback address with a valid, routable IP address for each Vault node in your network. + +Refer to the [Vault HA clustering with integrated storage tutorial](/vault/tutorials/raft/raft-storage) for a complete scenario. + + + ```hcl ui = true cluster_addr = "https://127.0.0.1:8201" @@ -116,10 +124,14 @@ to specify where the configuration is. sudo setcap cap_ipc_lock=+ep $(readlink -f $(which vault)) ``` - ~> Note: Since each plugin runs as a separate process, you need to do the same + + + Since each plugin runs as a separate process, you need to do the same for each plugin in your [plugins directory](/vault/docs/plugins/plugin-architecture#plugin-directory). + + If you use a Linux distribution with a modern version of systemd, you can add the following directive to the "[Service]" configuration section: @@ -211,12 +223,20 @@ can have a negative effect on performance due to the tracking of each lock attem Supported values (in order of descending detail) are `trace`, `debug`, `info`, `warn`, and `error`. This can also be specified via the `VAULT_LOG_LEVEL` environment variable. - ~> Note: On SIGHUP (`sudo kill -s HUP` _pid of vault_), if a valid value is specified, Vault will update the existing log level, + + + On SIGHUP (`sudo kill -s HUP` _pid of vault_), if a valid value is specified, Vault will update the existing log level, overriding (even if specified) both the CLI flag and environment variable. - ~> Note: Not all parts of Vault's logging can have its log level be changed dynamically this way; in particular, + + + + + Not all parts of Vault's logging can have its log level be changed dynamically this way; in particular, secrets/auth plugins are currently not updated dynamically. + + - `log_format` - Equivalent to the [`-log-format` command-line flag](/vault/docs/commands/server#_log_format). - `log_file` - Equivalent to the [`-log-file` command-line flag](/vault/docs/commands/server#_log_file). From ec95f85dc875f7920e8aa0f14b33e1d84e105f2c Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Mon, 19 Aug 2024 15:58:37 -0600 Subject: [PATCH 09/12] Refactor SSH Configuration workflow (#28122) * initial copy from other #28004 * pr feedback * grr --- ui/app/adapters/secret-engine.js | 7 - ui/app/adapters/ssh/ca-config.js | 30 ++- .../secret-engine/configuration-details.hbs | 5 +- .../secret-engine/configure-ssh.hbs | 138 +++++++------- .../components/secret-engine/configure-ssh.js | 38 ---- .../components/secret-engine/configure-ssh.ts | 118 ++++++++++++ .../secrets/backend/configuration/edit.js | 25 --- ui/app/models/secret-engine.js | 26 --- ui/app/models/ssh/ca-config.js | 38 +++- .../secrets/backend/configuration/edit.js | 70 ------- .../secrets/backend/configuration/edit.ts | 109 +++++++++++ .../secrets/backend/configuration/edit.hbs | 11 +- .../secrets/backend/ssh/configuration-test.js | 36 ++-- .../secrets/backend/ssh/roles-test.js | 16 +- .../secret-engine/secret-engine-helpers.js | 2 + .../secret-engine/secret-engine-selectors.ts | 3 +- .../configuration-details-test.js | 8 +- .../secret-engine/configure-ssh-test.js | 143 +++++++++----- ui/tests/unit/adapters/secret-engine-test.js | 178 +++++++++++++----- ui/tests/unit/models/secret-engine-test.js | 74 -------- 20 files changed, 631 insertions(+), 444 deletions(-) delete mode 100644 ui/app/components/secret-engine/configure-ssh.js create mode 100644 ui/app/components/secret-engine/configure-ssh.ts delete mode 100644 ui/app/routes/vault/cluster/secrets/backend/configuration/edit.js create mode 100644 ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts diff --git a/ui/app/adapters/secret-engine.js b/ui/app/adapters/secret-engine.js index c19b68b3fb..685707453f 100644 --- a/ui/app/adapters/secret-engine.js +++ b/ui/app/adapters/secret-engine.js @@ -87,13 +87,6 @@ export default ApplicationAdapter.extend({ } }, - findRecord(store, type, path, snapshot) { - if (snapshot.attr('type') === 'ssh') { - return this.ajax(`/v1/${encodePath(path)}/config/ca`, 'GET'); - } - return { data: {} }; - }, - queryRecord(store, type, query) { if (query.type === 'aws') { return this.ajax(`/v1/${encodePath(query.backend)}/config/lease`, 'GET').then((resp) => { diff --git a/ui/app/adapters/ssh/ca-config.js b/ui/app/adapters/ssh/ca-config.js index 1f89badf51..6f745a70a9 100644 --- a/ui/app/adapters/ssh/ca-config.js +++ b/ui/app/adapters/ssh/ca-config.js @@ -8,13 +8,39 @@ import { encodePath } from 'vault/utils/path-encoding-helpers'; export default class SshCaConfig extends ApplicationAdapter { namespace = 'v1'; - // For now this is only being used on the vault.cluster.secrets.backend.configuration route. This is a read-only route. - // Eventually, this will be used to create the ca config for the SSH secret backend, replacing the requests located on the secret-engine adapter. + queryRecord(store, type, query) { const { backend } = query; return this.ajax(`${this.buildURL()}/${encodePath(backend)}/config/ca`, 'GET').then((resp) => { resp.id = backend; + resp.backend = backend; return resp; }); } + + createOrUpdate(store, type, snapshot) { + const serializer = store.serializerFor(type.modelName); + const data = serializer.serialize(snapshot); + const backend = snapshot.record.backend; + return this.ajax(`${this.buildURL()}/${backend}/config/ca`, 'POST', { data }).then((resp) => { + // ember data requires an id on the response + return { + ...resp, + id: backend, + }; + }); + } + + createRecord() { + return this.createOrUpdate(...arguments); + } + + updateRecord() { + return this.createOrUpdate(...arguments); + } + + deleteRecord(store, type, snapshot) { + const backend = snapshot.record.backend; + return this.ajax(`${this.buildURL()}/${backend}/config/ca`, 'DELETE'); + } } diff --git a/ui/app/components/secret-engine/configuration-details.hbs b/ui/app/components/secret-engine/configuration-details.hbs index 9ed62214dd..a65b7cb39b 100644 --- a/ui/app/components/secret-engine/configuration-details.hbs +++ b/ui/app/components/secret-engine/configuration-details.hbs @@ -6,13 +6,14 @@ {{#if @configModels.length}} {{#each @configModels as |configModel|}} {{#each configModel.attrs as |attr|}} - {{#if attr.options.sensitive}} + {{! public key while not sensitive when editing/creating, should be hidden by default on viewing }} + {{#if (or attr.options.sensitive (eq attr.name "publicKey"))}} - {{#if attr.options.sensitive}} + {{#if (or attr.options.sensitive (eq attr.name "publicKey"))}} -
    - +
    +
    + + + {{#unless @model.isNew}} +

    + NOTE: You must delete your existing certificate and key before saving new values. +

    + {{/unless}} +
    + {{#if @model.isNew}} +
    + {{#each @model.formFields as |attr|}} + + {{/each}} +
    +
    - +
    + {{#if this.invalidFormAlert}} + + {{/if}}
    -
    -
    - - - - -
    -{{else}} - -
    - -
    - -
    - -
    -
    + {{else}} + {{! Model is not new and keys have already been created. Require user deletes the keys before creating new ones }} +
    -