diff --git a/.github/actions/build-vault/action.yml b/.github/actions/build-vault/action.yml
index 8fc228415a02..5e2641344208 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 }}
diff --git a/.github/actions/set-up-go/action.yml b/.github/actions/set-up-go/action.yml
index 9a80bf32f497..548555d26693 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
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a7edd5df97e4..1ab18b15ebe5 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -387,7 +387,7 @@ jobs:
with:
version: ${{ needs.setup.outputs.vault-version-metadata }}
product: ${{ needs.setup.outputs.vault-binary-name }}
- - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
+ - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
if: steps.generate-metadata-file.outcome == 'success' # upload our metadata if we created it
with:
name: metadata.json
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a958b2589760..e3859061aa6a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -207,7 +207,7 @@ jobs:
mkdir -p test-results/qunit
yarn ${{ needs.setup.outputs.is-enterprise == 'true' && 'test' || 'test:oss' }}
- if: always()
- uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
+ uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
with:
name: test-results-ui
path: ui/test-results
diff --git a/.github/workflows/plugin-update-check.yml b/.github/workflows/plugin-update-check.yml
index c7ef203efc07..36bc0d27e7df 100644
--- a/.github/workflows/plugin-update-check.yml
+++ b/.github/workflows/plugin-update-check.yml
@@ -29,7 +29,7 @@ jobs:
# https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow
token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
- - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
+ - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
cache: false # save cache space for vault builds: https://github.com/hashicorp/vault/pull/21764
go-version-file: .go-version
diff --git a/.github/workflows/plugin-update.yml b/.github/workflows/plugin-update.yml
index c63ec724e7de..55469104f01e 100644
--- a/.github/workflows/plugin-update.yml
+++ b/.github/workflows/plugin-update.yml
@@ -34,7 +34,7 @@ jobs:
# https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow
token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
- - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
+ - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
cache: false # save cache space for vault builds: https://github.com/hashicorp/vault/pull/21764
go-version-file: .go-version
diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml
index 078ddb9471cb..a396ed9314d2 100644
--- a/.github/workflows/security-scan.yml
+++ b/.github/workflows/security-scan.yml
@@ -16,17 +16,17 @@ 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:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Set up Go
- uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
+ uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
cache: false # save cache space for vault builds: https://github.com/hashicorp/vault/pull/21764
go-version-file: .go-version
diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml
index 8f04208f4ecf..67be67784f46 100644
--- a/.github/workflows/test-go.yml
+++ b/.github/workflows/test-go.yml
@@ -478,7 +478,7 @@ jobs:
run: |
tar -cvf '${{ steps.metadata.outputs.go-test-log-archive-name }}' -C "${{ steps.metadata.outputs.go-test-log-dir }}" .
- name: Upload test logs archives
- uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
+ uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
with:
name: ${{ steps.metadata.outputs.go-test-log-archive-name }}
path: ${{ steps.metadata.outputs.go-test-log-archive-name }}
@@ -486,7 +486,7 @@ jobs:
if: success() || failure()
- name: Upload test results
if: success() || failure()
- uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
+ uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
with:
name: ${{ steps.metadata.outputs.go-test-results-upload-key }}
path: |
@@ -526,7 +526,7 @@ jobs:
if: |
(success() || failure()) &&
steps.data-race-check.outputs.data-race-result == 'failure'
- uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
+ uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
with:
name: ${{ steps.metadata.outputs.data-race-log-upload-key }}
path: ${{ steps.metadata.outputs.go-test-dir }}/${{ steps.metadata.outputs.data-race-log-file }}
@@ -599,7 +599,7 @@ jobs:
'${{ steps.metadata.outputs.gotestsum-timing-events }}' \
>> '${{ steps.metadata.outputs.failure-summary-file-name }}'
- name: Upload failure summary
- uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
+ uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
if: success() || failure()
with:
name: ${{ steps.metadata.outputs.failure-summary-file-name }}
diff --git a/.github/workflows/test-run-acc-tests-for-path.yml b/.github/workflows/test-run-acc-tests-for-path.yml
index 1e2a3bfd27bf..7385905a2670 100644
--- a/.github/workflows/test-run-acc-tests-for-path.yml
+++ b/.github/workflows/test-run-acc-tests-for-path.yml
@@ -25,7 +25,7 @@ jobs:
with:
github-token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
- run: go test -v ./${{ inputs.path }}/... 2>&1 | tee ${{ inputs.name }}.txt
- - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
+ - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
with:
name: ${{ inputs.name }}-output
path: ${{ inputs.name }}.txt
diff --git a/.github/workflows/test-run-enos-scenario-matrix.yml b/.github/workflows/test-run-enos-scenario-matrix.yml
index acca9cbd5f4f..7b2473862908 100644
--- a/.github/workflows/test-run-enos-scenario-matrix.yml
+++ b/.github/workflows/test-run-enos-scenario-matrix.yml
@@ -155,7 +155,7 @@ jobs:
run: enos scenario launch --timeout 60m0s --chdir ./enos ${{ matrix.scenario.id.filter }}
- name: Upload Debug Data
if: failure()
- uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
+ uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
with:
# The name of the artifact is the same as the matrix scenario name with the spaces replaced with underscores and colons replaced by equals.
name: ${{ steps.prepare_scenario.outputs.debug_data_artifact_name }}
diff --git a/audit/broker.go b/audit/broker.go
index c6f9d0e3de22..601831eab067 100644
--- a/audit/broker.go
+++ b/audit/broker.go
@@ -305,7 +305,7 @@ func (b *Broker) LogRequest(ctx context.Context, in *logical.LogInput) (retErr e
if hasAuditPipelines(b.broker) {
status, err = b.broker.Send(auditContext, event.AuditType.AsEventType(), e)
if err != nil {
- return fmt.Errorf("%w: %w", err, errors.Join(status.Warnings...))
+ return errors.Join(append([]error{err}, status.Warnings...)...)
}
}
@@ -389,7 +389,7 @@ func (b *Broker) LogResponse(ctx context.Context, in *logical.LogInput) (retErr
if hasAuditPipelines(b.broker) {
status, err = b.broker.Send(auditContext, event.AuditType.AsEventType(), e)
if err != nil {
- return fmt.Errorf("%w: %w", err, errors.Join(status.Warnings...))
+ return errors.Join(append([]error{err}, status.Warnings...)...)
}
}
diff --git a/changelog/27457.txt b/changelog/27457.txt
new file mode 100644
index 000000000000..e3cf89a76524
--- /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 fd1c11ffee9b..8dd40904f7d9 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 000000000000..bb2376405281
--- /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
+}
diff --git a/ui/app/models/kv/metadata.js b/ui/app/models/kv/metadata.js
index 66d1d1b8c0df..c5986518219b 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 749e76ea7e31..14624f8124c3 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 7bf6d32d176c..d497a218cb68 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 1084c2af99ff..c541a58fb8bd 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 000000000000..64cb81592449
--- /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 000000000000..d7976d92f27c
--- /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 716c76c3982f..008efa8f2b01 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 a36f18be69fa..62451b7368e0 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 000000000000..43da56c1aaf9
--- /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 000000000000..0c41df40dc8e
--- /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 0472d30bc20d..f7e7e0c15dcc 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 6372e9844424..21b58e47f7a2 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 4281e1a1e9d1..cae35d6be479 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 c7070e7c6e1f..b36d112a7d19 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 dec99d129751..af135def1652 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 9b4e6a057120..a25886189b79 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 09c0e2039eb9..2bd7bd320e99 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 000000000000..b29a0ee8bc86
--- /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 000000000000..944c19f8f9be
--- /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 fae78bb41be7..8b1766e8036b 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 38383808cedd..356b018dbc39 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 6043e0181dee..5b15d4943f75 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 5e587fc3591c..bdc00663bc3b 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 c5407fa9b2fe..502adb2a2f68 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 a42392ce7e39..0d64b67661b0 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';