mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-28 04:10:44 -04:00
Merge branch 'main' into patch-1
This commit is contained in:
commit
ecb0df365b
62 changed files with 1612 additions and 713 deletions
6
.github/actions/build-vault/action.yml
vendored
6
.github/actions/build-vault/action.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
2
.github/actions/set-up-go/action.yml
vendored
2
.github/actions/set-up-go/action.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
|
|
@ -154,17 +154,17 @@ 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
|
||||
cache-dependency-path: ui/yarn.lock
|
||||
- uses: browser-actions/setup-chrome@db1b524c26f20a8d1a10f7fc385c92387e2d0477 # v1.7.1
|
||||
- uses: browser-actions/setup-chrome@facf10a55b9caf92e0cc749b4f82bf8220989148 # v1.7.2
|
||||
with:
|
||||
# Temporarily pin our Chrome version while we sort out a broken test on latest
|
||||
chrome-version: 1314712
|
||||
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/enos-run-k8s.yml
vendored
2
.github/workflows/enos-run-k8s.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
6
.github/workflows/security-scan.yml
vendored
6
.github/workflows/security-scan.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
4
.github/workflows/test-enos-scenario-ui.yml
vendored
4
.github/workflows/test-enos-scenario-ui.yml
vendored
|
|
@ -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
|
||||
|
|
@ -108,7 +108,7 @@ jobs:
|
|||
sudo apt install -y libnss3-dev libgdk-pixbuf2.0-dev libgtk-3-dev libxss-dev libasound2
|
||||
- name: Install Chrome
|
||||
if: steps.chrome-check.outputs.chrome-version == 'not-installed'
|
||||
uses: browser-actions/setup-chrome@db1b524c26f20a8d1a10f7fc385c92387e2d0477 # v1.7.1
|
||||
uses: browser-actions/setup-chrome@facf10a55b9caf92e0cc749b4f82bf8220989148 # v1.7.2
|
||||
- name: Installed Chrome Version
|
||||
run: |
|
||||
echo "Installed Chrome Version = [$(chrome --version 2> /dev/null || google-chrome --version 2> /dev/null || google-chrome-stable --version 2> /dev/null)]"
|
||||
|
|
|
|||
4
.github/workflows/test-go.yml
vendored
4
.github/workflows/test-go.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
3
changelog/27457.txt
Normal file
3
changelog/27457.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
sdk/helper: Allow setting environment variables when using NewTestDockerCluster
|
||||
```
|
||||
2
go.mod
2
go.mod
|
|
@ -146,7 +146,7 @@ require (
|
|||
github.com/hashicorp/vault-plugin-secrets-ad v0.18.0
|
||||
github.com/hashicorp/vault-plugin-secrets-alicloud v0.17.0
|
||||
github.com/hashicorp/vault-plugin-secrets-azure v0.19.2
|
||||
github.com/hashicorp/vault-plugin-secrets-gcp v0.19.0
|
||||
github.com/hashicorp/vault-plugin-secrets-gcp v0.19.1-0.20240725202923-b240a5ad808a
|
||||
github.com/hashicorp/vault-plugin-secrets-gcpkms v0.17.0
|
||||
github.com/hashicorp/vault-plugin-secrets-kubernetes v0.8.0
|
||||
github.com/hashicorp/vault-plugin-secrets-kv v0.19.0
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -1586,8 +1586,8 @@ github.com/hashicorp/vault-plugin-secrets-alicloud v0.17.0 h1:b8mTj3e/3JgkEpBFq3
|
|||
github.com/hashicorp/vault-plugin-secrets-alicloud v0.17.0/go.mod h1:9eT3ysHGq0jIarcJFcb9rZoaVbQoAsG3Iug8KM9H83w=
|
||||
github.com/hashicorp/vault-plugin-secrets-azure v0.19.2 h1:/9eARO5fffhRh7/oLVgdKlylTl9mQuk5oXUVksvUjH8=
|
||||
github.com/hashicorp/vault-plugin-secrets-azure v0.19.2/go.mod h1:R8CJArW9qdsp7ycyKndTaHo7Zj4vXjFMgMXchr/ct1s=
|
||||
github.com/hashicorp/vault-plugin-secrets-gcp v0.19.0 h1:5FRAVA3JYpn7zDMebQ3V622NxMKtY6tJg3YDuvTzS3A=
|
||||
github.com/hashicorp/vault-plugin-secrets-gcp v0.19.0/go.mod h1:/xdangTVszef3HQdZ0Ct2MmokRuYSpjx/SyX7aWxTbg=
|
||||
github.com/hashicorp/vault-plugin-secrets-gcp v0.19.1-0.20240725202923-b240a5ad808a h1:6v0mIf3QIbViuOBmTlG0Msnudp03WamA87sCR/AlCjw=
|
||||
github.com/hashicorp/vault-plugin-secrets-gcp v0.19.1-0.20240725202923-b240a5ad808a/go.mod h1:/xdangTVszef3HQdZ0Ct2MmokRuYSpjx/SyX7aWxTbg=
|
||||
github.com/hashicorp/vault-plugin-secrets-gcpkms v0.17.0 h1:GeARWON8OFWZFsulNOQqvDRG2H/38l9ye35VFk5MO1g=
|
||||
github.com/hashicorp/vault-plugin-secrets-gcpkms v0.17.0/go.mod h1:IQl89mmTl+GISbF16Rxr2d8YCfUfEdLeZJkV2rjuZCQ=
|
||||
github.com/hashicorp/vault-plugin-secrets-kubernetes v0.8.0 h1:PhLMvNO+VbTYzX04tZDkgQIrBxjDBYTbVFQsa7qbE84=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
38
sdk/helper/testcluster/docker/environment_test.go
Normal file
38
sdk/helper/testcluster/docker/environment_test.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))}}
|
||||
<InfoTableRow
|
||||
alwaysRender={{not (is-empty-value (get configModel attr.name))}}
|
||||
@label={{or attr.options.label (to-label attr.name)}}
|
||||
@value={{get configModel (or attr.options.fieldValue attr.name)}}
|
||||
>
|
||||
{{#if attr.options.sensitive}}
|
||||
{{#if (or attr.options.sensitive (eq attr.name "publicKey"))}}
|
||||
<MaskedInput
|
||||
@value={{get configModel attr.name}}
|
||||
@name={{attr.name}}
|
||||
|
|
|
|||
|
|
@ -3,88 +3,84 @@
|
|||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
{{#if @configured}}
|
||||
<div class="box is-fullwidth is-sideless is-marginless" data-test-edit-config-section>
|
||||
<div class="field">
|
||||
<label for="publicKey" class="is-label">
|
||||
Public key
|
||||
</label>
|
||||
<form {{on "submit" (perform this.save)}} aria-label="save ssh creds" data-test-configure-form>
|
||||
<div class="box is-fullwidth is-shadowless is-marginless">
|
||||
<NamespaceReminder @mode="save" @noun="configuration" />
|
||||
<MessageError @errorMessage={{this.errorMessage}} />
|
||||
{{#unless @model.isNew}}
|
||||
<p class="has-text-grey-dark">
|
||||
NOTE: You must delete your existing certificate and key before saving new values.
|
||||
</p>
|
||||
{{/unless}}
|
||||
</div>
|
||||
{{#if @model.isNew}}
|
||||
<div class="box is-fullwidth is-sideless">
|
||||
{{#each @model.formFields as |attr|}}
|
||||
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<MaskedInput
|
||||
@name="publickey"
|
||||
@id="publicKey"
|
||||
@value={{@model.publicKey}}
|
||||
@displayOnly={{true}}
|
||||
@allowCopy={{true}}
|
||||
data-test-input="public-key"
|
||||
<Hds::Button
|
||||
@text="Save"
|
||||
@icon={{if this.save.isRunning "loading"}}
|
||||
type="submit"
|
||||
disabled={{this.save.isRunning}}
|
||||
data-test-configure-save-button
|
||||
/>
|
||||
<Hds::Button
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
class="has-left-margin-s"
|
||||
disabled={{this.save.isRunning}}
|
||||
{{on "click" this.onCancel}}
|
||||
data-test-cancel-button
|
||||
/>
|
||||
</div>
|
||||
{{#if this.invalidFormAlert}}
|
||||
<AlertInline
|
||||
data-test-invalid-form-alert
|
||||
class="has-top-padding-s"
|
||||
@type="danger"
|
||||
@message={{this.invalidFormAlert}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Copy::Button
|
||||
@text="Copy"
|
||||
@textToCopy={{@model.publicKey}}
|
||||
@onError={{(fn (set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger"))}}
|
||||
class="primary"
|
||||
/>
|
||||
<ConfirmAction
|
||||
@buttonText="Delete"
|
||||
@buttonColor="secondary"
|
||||
@confirmMessage="This will remove the CA certificate information."
|
||||
@onConfirmAction={{this.delete}}
|
||||
data-test-delete-public-key
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
</div>
|
||||
{{else}}
|
||||
<form {{on "submit" this.saveConfig}} data-test-configure-form>
|
||||
<div class="box is-fullwidth is-sideless is-marginless">
|
||||
<NamespaceReminder @mode="save" @noun="configuration" />
|
||||
<div class="field">
|
||||
<label for="privateKey" class="is-label">
|
||||
Private key
|
||||
</label>
|
||||
<div class="control">
|
||||
<MaskedInput @name="privateKey" id="privateKey" @value={{@model.privateKey}} @onChange={{mut @model.privateKey}} />
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{! Model is not new and keys have already been created. Require user deletes the keys before creating new ones }}
|
||||
<div class="box is-fullwidth is-sideless is-marginless" data-test-edit-config-section>
|
||||
<div class="field">
|
||||
<label for="publicKey" class="is-label">
|
||||
Public key
|
||||
</label>
|
||||
<div class="control">
|
||||
<Textarea name="publicKey" id="publicKey" class="input" @value={{@model.publicKey}} data-test-input="publicKey" />
|
||||
<MaskedInput
|
||||
@name="publickey"
|
||||
@id="publicKey"
|
||||
@value={{@model.publicKey}}
|
||||
@displayOnly={{true}}
|
||||
@allowCopy={{true}}
|
||||
data-test-input="public-key"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="b-checkbox">
|
||||
<Input
|
||||
@type="checkbox"
|
||||
id="generateSigningKey"
|
||||
class="styled"
|
||||
@checked={{@model.generateSigningKey}}
|
||||
{{on "change" (fn (mut @model.generateSigningKey) (not @model.generateSigningKey))}}
|
||||
data-test-input="generate-signing-key-checkbox"
|
||||
/>
|
||||
<label for="generateSigningKey" class="is-label">
|
||||
Generate signing key
|
||||
<InfoTooltip>
|
||||
Specifies if Vault should generate the signing key pair internally
|
||||
</InfoTooltip>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field box is-fullwidth is-bottomless is-marginless">
|
||||
<div class="control">
|
||||
<Hds::Button
|
||||
@text="Save"
|
||||
@icon={{if @loading "loading"}}
|
||||
type="submit"
|
||||
disabled={{@loading}}
|
||||
data-test-configure-save-button
|
||||
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Copy::Button
|
||||
@text="Copy"
|
||||
@textToCopy={{@model.publicKey}}
|
||||
@onError={{fn (set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger")}}
|
||||
class="primary"
|
||||
/>
|
||||
</div>
|
||||
<ConfirmAction
|
||||
@buttonText="Delete"
|
||||
@buttonColor="secondary"
|
||||
@confirmMessage="Confirming will remove the CA certificate information."
|
||||
@onConfirmAction={{this.deleteCaConfig}}
|
||||
data-test-delete-public-key
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
</div>
|
||||
</form>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</form>
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
/**
|
||||
* @module ConfigureSshSComponent
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <SecretEngine::ConfigureSsh
|
||||
* @model={{this.model}}
|
||||
* @configured={{this.configured}}
|
||||
* @saveConfig={{action "saveConfig"}}
|
||||
* @loading={{this.loading}}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @param {string} model - ssh secret engine model
|
||||
* @param {Function} saveConfig - parent action which updates the configuration
|
||||
* @param {boolean} loading - property in parent that updates depending on status of parent's action
|
||||
*
|
||||
*/
|
||||
export default class ConfigureSshComponent extends Component {
|
||||
@action
|
||||
delete() {
|
||||
this.args.saveConfig({ delete: true });
|
||||
}
|
||||
|
||||
@action
|
||||
saveConfig(event) {
|
||||
event.preventDefault();
|
||||
this.args.saveConfig({ delete: false });
|
||||
}
|
||||
}
|
||||
118
ui/app/components/secret-engine/configure-ssh.ts
Normal file
118
ui/app/components/secret-engine/configure-ssh.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { ValidationMap } from 'vault/vault/app-types';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
import type CaConfigModel from 'vault/models/ssh/ca-config';
|
||||
import type Router from '@ember/routing/router';
|
||||
import type Store from '@ember-data/store';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
|
||||
/**
|
||||
* @module ConfigureSshComponent is used to configure the SSH secret engine.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <SecretEngine::ConfigureSsh
|
||||
* @model={{this.model.ssh-ca-config}}
|
||||
* @id={{this.model.id}}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @param {string} model - SSH ca-config model
|
||||
* @param {string} id - name of the SSH secret engine, ex: 'ssh-123'
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
model: CaConfigModel;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default class ConfigureSshComponent extends Component<Args> {
|
||||
@service declare readonly router: Router;
|
||||
@service declare readonly store: Store;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
@tracked errorMessage: string | null = null;
|
||||
@tracked invalidFormAlert: string | null = null;
|
||||
@tracked modelValidations: ValidationMap | null = null;
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*save(event: Event) {
|
||||
event.preventDefault();
|
||||
this.resetErrors();
|
||||
const { id, model } = this.args;
|
||||
const isValid = this.validate(model);
|
||||
|
||||
if (!isValid) return;
|
||||
// Check if any of the model's attributes have changed.
|
||||
// If no changes to the model, transition and notify user.
|
||||
// Otherwise, save the model.
|
||||
const attributesChanged = Object.keys(model.changedAttributes()).length > 0;
|
||||
if (!attributesChanged) {
|
||||
this.flashMessages.info('No changes detected.');
|
||||
this.transition();
|
||||
}
|
||||
|
||||
try {
|
||||
yield model.save();
|
||||
this.transition();
|
||||
this.flashMessages.success(`Successfully saved ${id}'s root configuration.`);
|
||||
} catch (error) {
|
||||
this.errorMessage = errorMessage(error);
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
}
|
||||
|
||||
validate(model: CaConfigModel) {
|
||||
const { isValid, state, invalidFormMessage } = model.validate();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
this.invalidFormAlert = isValid ? '' : invalidFormMessage;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
resetErrors() {
|
||||
this.flashMessages.clearMessages();
|
||||
this.errorMessage = null;
|
||||
this.invalidFormAlert = null;
|
||||
}
|
||||
|
||||
transition(isDelete = false) {
|
||||
// deleting a key is the only case in which we want to stay on the create/edit page.
|
||||
if (isDelete) {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.configuration.edit', this.args.id);
|
||||
} else {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.configuration', this.args.id);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onCancel() {
|
||||
// clear errors because they're canceling out of the workflow.
|
||||
this.resetErrors();
|
||||
this.transition();
|
||||
}
|
||||
|
||||
@action
|
||||
async deleteCaConfig() {
|
||||
const { model } = this.args;
|
||||
try {
|
||||
await model.destroyRecord();
|
||||
this.transition(true);
|
||||
this.flashMessages.success('CA information deleted successfully.');
|
||||
} catch (error) {
|
||||
model.rollbackAttributes();
|
||||
this.flashMessages.danger(errorMessage(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,31 +29,6 @@ export default Controller.extend(CONFIG_ATTRS, {
|
|||
this.setProperties(CONFIG_ATTRS);
|
||||
},
|
||||
actions: {
|
||||
saveConfig(options = { delete: false }) {
|
||||
const isDelete = options.delete;
|
||||
if (this.model.type === 'ssh') {
|
||||
this.set('loading', true);
|
||||
this.model
|
||||
.saveCA({ isDelete })
|
||||
.then(() => {
|
||||
this.send('refreshRoute');
|
||||
this.set('configured', !isDelete);
|
||||
if (isDelete) {
|
||||
this.flashMessages.success('SSH Certificate Authority Configuration deleted!');
|
||||
} else {
|
||||
this.flashMessages.success('SSH Certificate Authority Configuration saved!');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage = error.errors ? error.errors.join('. ') : error;
|
||||
this.flashMessages.danger(errorMessage);
|
||||
})
|
||||
.finally(() => {
|
||||
this.set('loading', false);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
save(method, data) {
|
||||
this.set('loading', true);
|
||||
const hasData = Object.keys(data).some((key) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,14 +68,6 @@ export default class SecretEngineModel extends Model {
|
|||
})
|
||||
version;
|
||||
|
||||
// SSH specific attributes
|
||||
@attr('string') privateKey;
|
||||
@attr('string') publicKey;
|
||||
@attr('boolean', {
|
||||
defaultValue: true,
|
||||
})
|
||||
generateSigningKey;
|
||||
|
||||
// AWS specific attributes
|
||||
@attr('string') lease;
|
||||
@attr('string') leaseMax;
|
||||
|
|
@ -257,24 +249,6 @@ export default class SecretEngineModel extends Model {
|
|||
}
|
||||
|
||||
/* ACTIONS */
|
||||
saveCA(options) {
|
||||
if (this.type !== 'ssh') {
|
||||
return;
|
||||
}
|
||||
if (options.isDelete) {
|
||||
this.privateKey = null;
|
||||
this.publicKey = null;
|
||||
this.generateSigningKey = false;
|
||||
}
|
||||
return this.save({
|
||||
adapterOptions: {
|
||||
options: options,
|
||||
apiPath: 'config/ca',
|
||||
attrsToSend: ['privateKey', 'publicKey', 'generateSigningKey'],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
saveZeroAddressConfig() {
|
||||
return this.save({
|
||||
adapterOptions: {
|
||||
|
|
|
|||
|
|
@ -5,16 +5,50 @@
|
|||
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
|
||||
const validations = {
|
||||
generateSigningKey: [
|
||||
{
|
||||
validator(model) {
|
||||
const { publicKey, privateKey, generateSigningKey } = model;
|
||||
// if generateSigningKey is false, both public and private keys are required
|
||||
if (!generateSigningKey && (!publicKey || !privateKey)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
message: 'Provide a Public and Private key or set "Generate Signing Key" to true.',
|
||||
},
|
||||
],
|
||||
publicKey: [
|
||||
{
|
||||
validator(model) {
|
||||
const { publicKey, privateKey } = model;
|
||||
// regardless of generateSigningKey, if one key is set they both need to be set.
|
||||
return publicKey || privateKey ? publicKey && privateKey : true;
|
||||
},
|
||||
message: 'You must provide a Public and Private keys or leave both unset.',
|
||||
},
|
||||
],
|
||||
};
|
||||
// there are more options available on the API, but the UI does not support them yet.
|
||||
@withModelValidations(validations)
|
||||
export default class SshCaConfig extends Model {
|
||||
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
|
||||
@attr('string', { sensitive: true }) privateKey; // obfuscated, never returned by API
|
||||
@attr('string', { sensitive: true }) publicKey;
|
||||
@attr('string') publicKey;
|
||||
@attr('boolean', { defaultValue: true })
|
||||
generateSigningKey;
|
||||
// there are more options available on the API, but the UI does not support them yet.
|
||||
|
||||
// do not return private key for configuration.index view
|
||||
get attrs() {
|
||||
const keys = ['publicKey', 'generateSigningKey'];
|
||||
return expandAttributeMeta(this, keys);
|
||||
}
|
||||
// return private key for edit/create view
|
||||
get formFields() {
|
||||
const keys = ['privateKey', 'publicKey', 'generateSigningKey'];
|
||||
return expandAttributeMeta(this, keys);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import AdapterError from '@ember-data/adapter/error';
|
||||
import { set } from '@ember/object';
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { CONFIGURABLE_SECRET_ENGINES } from 'vault/helpers/mountable-secret-engines';
|
||||
|
||||
export default Route.extend({
|
||||
store: service(),
|
||||
|
||||
model() {
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
return this.store.query('secret-engine', { path: backend }).then((modelList) => {
|
||||
const model = modelList && modelList[0];
|
||||
if (!model || !CONFIGURABLE_SECRET_ENGINES.includes(model.type)) {
|
||||
const error = new AdapterError();
|
||||
set(error, 'httpStatus', 404);
|
||||
throw error;
|
||||
}
|
||||
return this.store.findRecord('secret-engine', backend).then(
|
||||
() => {
|
||||
return model;
|
||||
},
|
||||
() => {
|
||||
return model;
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
afterModel(model) {
|
||||
const type = model.type;
|
||||
|
||||
if (type === 'aws') {
|
||||
return this.store
|
||||
.queryRecord('secret-engine', {
|
||||
backend: model.id,
|
||||
type,
|
||||
})
|
||||
.then(
|
||||
() => model,
|
||||
() => model
|
||||
);
|
||||
}
|
||||
return model;
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
if (model.publicKey) {
|
||||
controller.set('configured', true);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
resetController(controller, isExiting) {
|
||||
if (isExiting) {
|
||||
controller.reset();
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
refreshRoute() {
|
||||
this.refresh();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import AdapterError from '@ember-data/adapter/error';
|
||||
import { set } from '@ember/object';
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { CONFIGURABLE_SECRET_ENGINES } from 'vault/helpers/mountable-secret-engines';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
import type Store from '@ember-data/store';
|
||||
import type SecretEngineModel from 'vault/models/secret-engine';
|
||||
|
||||
// This route file is reused for all configurable secret engines.
|
||||
// It generates config models based on the engine type.
|
||||
// Saving and updating of those models are done within the engine specific components.
|
||||
|
||||
const CONFIG_ADAPTERS_PATHS: Record<string, string[]> = {
|
||||
// aws: ['aws/lease-config', 'aws/root-config'], TODO will be uncommented when AWS refactor occurs
|
||||
ssh: ['ssh/ca-config'],
|
||||
};
|
||||
|
||||
export default class SecretsBackendConfigurationEdit extends Route {
|
||||
@service declare readonly store: Store;
|
||||
|
||||
async model() {
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
const secretEngineRecord = this.modelFor('vault.cluster.secrets.backend') as { type: SecretEngineModel };
|
||||
const type = secretEngineRecord.type as string;
|
||||
|
||||
// if the engine type is not configurable, return a 404.
|
||||
if (!secretEngineRecord || !CONFIGURABLE_SECRET_ENGINES.includes(type)) {
|
||||
const error = new AdapterError();
|
||||
set(error, 'httpStatus', 404);
|
||||
throw error;
|
||||
}
|
||||
// TODO this conditional will be removed when we handle AWS
|
||||
if (type !== 'aws') {
|
||||
// generate the model based on the engine type.
|
||||
// and pre-set with the type and backend (e.g. type: ssh, id: ssh-123)
|
||||
const model: Record<string, unknown> = { type, id: backend };
|
||||
for (const adapterPath of CONFIG_ADAPTERS_PATHS[type] as string[]) {
|
||||
// convert the adapterPath with a name that can be passed to the components
|
||||
// ex: adapterPath = ssh/ca-config, convert to: ssh-ca-config so that you can pass to component @model={{this.model.ssh-ca-config}}
|
||||
const standardizedKey = adapterPath.replace(/\//g, '-');
|
||||
try {
|
||||
model[standardizedKey] = await this.store.queryRecord(adapterPath, {
|
||||
backend,
|
||||
type,
|
||||
});
|
||||
} catch (e: AdapterError) {
|
||||
// For most models if the adapter returns a 404, we want to create a new record.
|
||||
// The ssh secret engine however returns a 400 if the CA is not configured.
|
||||
// For ssh's 400 error, we want to create the CA config model.
|
||||
if (
|
||||
e.httpStatus === 404 ||
|
||||
(type === 'ssh' && e.httpStatus === 400 && errorMessage(e) === `keys haven't been configured yet`)
|
||||
) {
|
||||
model[standardizedKey] = await this.store.createRecord(adapterPath, {
|
||||
backend,
|
||||
type,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
return model;
|
||||
} else {
|
||||
// TODO for now AWS configs rely on the secret-engine model and adapter. This will be refactored.
|
||||
return await this.store.findRecord('secret-engine', backend);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO everything below line will be removed once we handle AWS. This is the old code wrapped in AWS conditionals when appropriate.
|
||||
afterModel(model: Record<string, unknown>) {
|
||||
const type = model.type;
|
||||
|
||||
if (type === 'aws') {
|
||||
return this.store
|
||||
.queryRecord('secret-engine', {
|
||||
backend: model.id,
|
||||
type,
|
||||
})
|
||||
.then(
|
||||
() => model,
|
||||
() => model
|
||||
);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
resetController(controller, isExiting) {
|
||||
if (controller.model.type === 'aws') {
|
||||
if (isExiting) {
|
||||
controller.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
willTransition() {
|
||||
// catch the transition and refresh model so the route shows the most recent model data.
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
|
@ -41,12 +41,5 @@
|
|||
@saveAWSLease={{action "save" "saveAWSLease"}}
|
||||
/>
|
||||
{{else if (eq this.model.type "ssh")}}
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.model}}
|
||||
@configured={{this.configured}}
|
||||
@saveConfig={{action "saveConfig"}}
|
||||
@loading={{this.loading}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{outlet}}
|
||||
<SecretEngine::ConfigureSsh @model={{this.model.ssh-ca-config}} @id={{this.model.id}} />
|
||||
{{/if}}
|
||||
|
|
@ -16,6 +16,7 @@ import { obfuscateData } from 'core/utils/advanced-secret';
|
|||
* <JsonEditor @title="Policy" @value={{hash foo="bar"}} @viewportMargin={{100}} />
|
||||
*
|
||||
* @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.
|
||||
|
|
|
|||
|
|
@ -10,10 +10,14 @@
|
|||
data-test-overview-card-container={{@cardTitle}}
|
||||
...attributes
|
||||
>
|
||||
<div class="flex row-wrap space-between has-bottom-margin-s" data-test-overview-card={{@cardTitle}}>
|
||||
<Hds::Text::Display @weight="bold" @size="300" data-test-overview-card-title={{@cardTitle}}>
|
||||
{{@cardTitle}}
|
||||
</Hds::Text::Display>
|
||||
<div class="flex row-wrap space-between" data-test-overview-card={{@cardTitle}}>
|
||||
{{#if (has-block "customTitle")}}
|
||||
{{yield to="customTitle"}}
|
||||
{{else}}
|
||||
<Hds::Text::Display @weight="semibold" @size="300" data-test-overview-card-title={{@cardTitle}}>
|
||||
{{@cardTitle}}
|
||||
</Hds::Text::Display>
|
||||
{{/if}}
|
||||
|
||||
{{#if (has-block "action")}}
|
||||
{{yield to="action"}}
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
6
ui/lib/core/app/helpers/date-from-now.js
Normal file
6
ui/lib/core/app/helpers/date-from-now.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'core/helpers/date-from-now';
|
||||
82
ui/lib/kv/addon/components/kv-paths-card.hbs
Normal file
82
ui/lib/kv/addon/components/kv-paths-card.hbs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<div data-test-overview-card-container="Paths" ...attributes>
|
||||
<Hds::Text::Display @weight="semibold" @size="300" @tag="h2">
|
||||
Paths
|
||||
</Hds::Text::Display>
|
||||
{{#if @isCondensed}}
|
||||
<Hds::Text::Body @tag="p" @color="faint">
|
||||
The paths to use when referring to this secret in API or CLI.
|
||||
</Hds::Text::Body>
|
||||
{{/if}}
|
||||
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless {{if @isCondensed 'is-shadowless'}} ">
|
||||
{{#each this.paths as |path|}}
|
||||
<InfoTableRow
|
||||
@label={{path.label}}
|
||||
@labelWidth={{if @isCondensed "is-one-quarter" "is-one-third"}}
|
||||
@helperText={{if @isCondensed "" path.text}}
|
||||
@truncateValue={{true}}
|
||||
>
|
||||
<Hds::Copy::Button
|
||||
@text="Copy"
|
||||
@isIconOnly={{true}}
|
||||
@textToCopy={{path.snippet}}
|
||||
@onError={{fn (set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger")}}
|
||||
data-test-copy-button={{path.snippet}}
|
||||
class="transparent"
|
||||
/>
|
||||
<code class="is-flex-1 text-overflow-ellipsis has-left-margin-s">
|
||||
{{path.snippet}}
|
||||
</code>
|
||||
</InfoTableRow>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{#unless @isCondensed}}
|
||||
<Hds::Text::Display @weight="semibold" @size="300" @tag="h3" class="has-top-margin-xl">
|
||||
Commands
|
||||
</Hds::Text::Display>
|
||||
|
||||
<div class="box is-fullwidth is-sideless">
|
||||
<h3 class="is-label">
|
||||
CLI
|
||||
<Hds::Badge @text="kv get" @color="neutral" />
|
||||
</h3>
|
||||
<p class="helper-text has-text-grey-light has-bottom-padding-s">
|
||||
This command retrieves the value from KV secrets engine at the given key name. See our
|
||||
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/docs/commands/kv"}}>
|
||||
documentation</Hds::Link::Inline>
|
||||
for other CLI commands.
|
||||
</p>
|
||||
<Hds::CodeBlock
|
||||
data-test-commands="cli"
|
||||
@language="bash"
|
||||
@hasLineNumbers={{false}}
|
||||
@hasCopyButton={{true}}
|
||||
@value={{this.commands.cli}}
|
||||
/>
|
||||
|
||||
<h3 class="has-top-margin-l is-label">
|
||||
API read secret version
|
||||
</h3>
|
||||
<p class="helper-text has-text-grey-light has-bottom-padding-s">
|
||||
This command obtains data and metadata for the latest version of this secret. In this example, Vault is located at
|
||||
https://127.0.0.1:8200. For other API commands,
|
||||
<DocLink @path="/vault/api-docs/secret/kv/kv-v2">
|
||||
learn more.
|
||||
</DocLink>
|
||||
</p>
|
||||
<Hds::CodeBlock
|
||||
data-test-commands="api"
|
||||
@language="bash"
|
||||
@hasLineNumbers={{false}}
|
||||
@hasCopyButton={{true}}
|
||||
@value={{this.commands.api}}
|
||||
/>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
|
@ -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.
|
||||
*
|
||||
* <Page::Secret::Paths
|
||||
* <KvPathsCard
|
||||
* @path={{this.model.path}}
|
||||
* @backend={{this.model.backend}}
|
||||
* @breadcrumbs={{this.breadcrumbs}}
|
||||
* @canReadMetadata={{this.model.secret.canReadMetadata}}
|
||||
* @isCondensed={{false}}
|
||||
* />
|
||||
*
|
||||
* @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.`,
|
||||
},
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -3,9 +3,9 @@
|
|||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<OverviewCard @cardTitle="Subkeys">
|
||||
<OverviewCard @cardTitle="Subkeys" class="has-top-margin-l">
|
||||
<:customSubtext>
|
||||
<Hds::Text::Body @color="faint" data-test-overview-card-subtitle="Subkeys">
|
||||
<Hds::Text::Body @tag="p" @color="faint" data-test-overview-card-subtitle="Subkeys">
|
||||
{{#if this.showJson}}
|
||||
These are the subkeys within this secret. All underlying values of leaf keys are not retrieved and are replaced with
|
||||
<code>null</code>
|
||||
|
|
|
|||
105
ui/lib/kv/addon/components/page/secret/overview.hbs
Normal file
105
ui/lib/kv/addon/components/page/secret/overview.hbs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle={{@path}}>
|
||||
<:tabLinks>
|
||||
<li>
|
||||
<LinkTo @route="secret.index" @models={{array @backend @path}} data-test-secrets-tab="Overview">Overview</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<LinkTo @route="secret.details" @models={{array @backend @path}} data-test-secrets-tab="Secret">Secret</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="secret.metadata.index"
|
||||
@models={{array @backend @path}}
|
||||
data-test-secrets-tab="Metadata"
|
||||
>Metadata</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<LinkTo @route="secret.paths" @models={{array @backend @path}} data-test-secrets-tab="Paths">Paths</LinkTo>
|
||||
</li>
|
||||
{{#if @canReadMetadata}}
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="secret.metadata.versions"
|
||||
@models={{array @backend @path}}
|
||||
data-test-secrets-tab="Version History"
|
||||
>Version History</LinkTo>
|
||||
</li>
|
||||
{{/if}}
|
||||
</:tabLinks>
|
||||
</KvPageHeader>
|
||||
|
||||
{{#if (or @metadata @subkeys)}}
|
||||
<div class="flex row-wrap gap-24 has-top-margin-l">
|
||||
<OverviewCard @cardTitle="Current version" @subText={{this.versionSubtext}} class="is-flex-1">
|
||||
<:customTitle>
|
||||
<Hds::Text::Display @weight="semibold" @size="300">
|
||||
Current version
|
||||
{{#unless this.isActive}}
|
||||
<Hds::Badge
|
||||
@text={{capitalize @secretState}}
|
||||
@type={{if (eq @secretState "destroyed") "outlined" "inverted"}}
|
||||
@color={{if (eq @secretState "destroyed") "critical"}}
|
||||
@icon="x"
|
||||
/>
|
||||
{{/unless}}
|
||||
</Hds::Text::Display>
|
||||
</:customTitle>
|
||||
<:action>
|
||||
{{#if @canUpdateSecret}}
|
||||
<Hds::Link::Standalone
|
||||
@text="Create new"
|
||||
@route="secret.details.edit"
|
||||
@models={{array @backend @path}}
|
||||
@icon="plus"
|
||||
@iconPosition="trailing"
|
||||
/>
|
||||
{{/if}}
|
||||
</:action>
|
||||
<:content>
|
||||
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
|
||||
{{or @metadata.currentVersion @subkeys.metadata.version}}
|
||||
</Hds::Text::Display>
|
||||
</:content>
|
||||
</OverviewCard>
|
||||
|
||||
{{#if this.isActive}}
|
||||
{{#let (or @metadata.createdTime @subkeys.metadata.created_time) as |timestamp|}}
|
||||
<OverviewCard
|
||||
@cardTitle="Secret age"
|
||||
@subText="Time since last update at {{date-format timestamp}}."
|
||||
class="is-flex-1"
|
||||
>
|
||||
<:action>
|
||||
{{#if @canReadMetadata}}
|
||||
<Hds::Link::Standalone
|
||||
@text="View metadata"
|
||||
@route="secret.metadata"
|
||||
@models={{array @backend @path}}
|
||||
@icon="arrow-right"
|
||||
@iconPosition="trailing"
|
||||
/>
|
||||
{{/if}}
|
||||
</:action>
|
||||
<:content>
|
||||
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
|
||||
{{date-from-now timestamp}}
|
||||
</Hds::Text::Display>
|
||||
</:content>
|
||||
</OverviewCard>
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-l has-top-margin-l">
|
||||
<KvPathsCard @backend={{@backend}} @path={{@path}} @isCondensed={{true}} />
|
||||
</Hds::Card::Container>
|
||||
|
||||
{{#if @subkeys.subkeys}}
|
||||
<KvSubkeys @subkeys={{@subkeys.subkeys}} />
|
||||
{{/if}}
|
||||
51
ui/lib/kv/addon/components/page/secret/overview.js
Normal file
51
ui/lib/kv/addon/components/page/secret/overview.js
Normal file
|
|
@ -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
|
||||
* <Page::Secret::Overview
|
||||
* @backend={{this.model.backend}}
|
||||
* @breadcrumbs={{this.breadcrumbs}}
|
||||
* @canReadMetadata={{true}}
|
||||
* @canUpdateSecret={{true}}
|
||||
* @metadata={{this.model.metadata}}
|
||||
* @path={{this.model.path}}
|
||||
* @secretState="created"
|
||||
* @subkeys={{this.model.subkeys}}
|
||||
* />
|
||||
*
|
||||
* @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.';
|
||||
}
|
||||
}
|
||||
|
|
@ -36,66 +36,4 @@
|
|||
</:tabLinks>
|
||||
</KvPageHeader>
|
||||
|
||||
<h2 class="title is-5 has-top-margin-xl">
|
||||
Paths
|
||||
</h2>
|
||||
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
{{#each this.paths as |path|}}
|
||||
<InfoTableRow @label={{path.label}} @labelWidth="is-one-third" @helperText={{path.text}} @truncateValue={{true}}>
|
||||
<Hds::Copy::Button
|
||||
@text="Copy"
|
||||
@isIconOnly={{true}}
|
||||
@textToCopy={{path.snippet}}
|
||||
@onError={{(fn (set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger"))}}
|
||||
data-test-copy-button={{path.snippet}}
|
||||
class="transparent"
|
||||
/>
|
||||
<code class="is-flex-1 text-overflow-ellipsis has-left-margin-s">
|
||||
{{path.snippet}}
|
||||
</code>
|
||||
</InfoTableRow>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<h2 class="title is-5 has-top-margin-xl">
|
||||
Commands
|
||||
</h2>
|
||||
|
||||
<div class="box is-fullwidth is-sideless">
|
||||
<h3 class="is-label">
|
||||
CLI
|
||||
<Hds::Badge @text="kv get" @color="neutral" />
|
||||
</h3>
|
||||
<p class="helper-text has-text-grey-light has-bottom-padding-s">
|
||||
This command retrieves the value from KV secrets engine at the given key name. For other CLI commands,
|
||||
<DocLink @path="/vault/docs/commands/kv">
|
||||
learn more.
|
||||
</DocLink>
|
||||
</p>
|
||||
<Hds::CodeBlock
|
||||
data-test-commands="cli"
|
||||
@language="bash"
|
||||
@hasLineNumbers={{false}}
|
||||
@hasCopyButton={{true}}
|
||||
@value={{this.commands.cli}}
|
||||
/>
|
||||
|
||||
<h3 class="has-top-margin-l is-label">
|
||||
API read secret version
|
||||
</h3>
|
||||
<p class="helper-text has-text-grey-light has-bottom-padding-s">
|
||||
This command obtains data and metadata for the latest version of this secret. In this example, Vault is located at
|
||||
https://127.0.0.1:8200. For other API commands,
|
||||
<DocLink @path="/vault/api-docs/secret/kv/kv-v2">
|
||||
learn more.
|
||||
</DocLink>
|
||||
</p>
|
||||
<Hds::CodeBlock
|
||||
data-test-commands="api"
|
||||
@language="bash"
|
||||
@hasLineNumbers={{false}}
|
||||
@hasCopyButton={{true}}
|
||||
@value={{this.commands.api}}
|
||||
/>
|
||||
</div>
|
||||
<KvPathsCard @backend={{@backend}} @path={{@path}} class="has-top-margin-xl" />
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
/>
|
||||
</:action>
|
||||
<:content>
|
||||
<Hds::Text::Display @tag="h2" @size="500">
|
||||
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
|
||||
{{format-number (if (eq @issuers 404) 0 @issuers.length)}}
|
||||
</Hds::Text::Display>
|
||||
</:content>
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
/>
|
||||
</:action>
|
||||
<:content>
|
||||
<Hds::Text::Display @tag="h2" @size="500">
|
||||
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
|
||||
{{format-number (if (eq @roles 404) 0 @roles.length)}}
|
||||
</Hds::Text::Display>
|
||||
</:content>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { click, fillIn, currentURL, waitFor, visit } from '@ember/test-helpers';
|
||||
import { click, fillIn, currentURL, visit, waitFor } from '@ember/test-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { spy } from 'sinon';
|
||||
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
|
|
@ -23,10 +22,7 @@ module('Acceptance | ssh | configuration', function (hooks) {
|
|||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
const flash = this.owner.lookup('service:flash-messages');
|
||||
this.flashDangerSpy = spy(flash, 'danger');
|
||||
this.store = this.owner.lookup('service:store');
|
||||
|
||||
this.uid = uuidv4();
|
||||
return authPage.login();
|
||||
});
|
||||
|
|
@ -70,18 +66,20 @@ module('Acceptance | ssh | configuration', function (hooks) {
|
|||
await click(SES.ssh.save);
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${sshPath}/configuration/edit`,
|
||||
'stays on configuration form page.'
|
||||
`/vault/secrets/${sshPath}/configuration`,
|
||||
'navigates to the details page.'
|
||||
);
|
||||
// There is a delay in the backend for the public key to be generated, wait for it to complete by checking that the public key is displayed
|
||||
await waitFor(GENERAL.inputByAttr('public-key'));
|
||||
assert.dom(GENERAL.inputByAttr('public-key')).hasText('***********', 'public key is masked');
|
||||
await waitFor(GENERAL.infoRowLabel('Public key'));
|
||||
assert.dom(GENERAL.infoRowLabel('Public key')).exists('public key shown on the details screen');
|
||||
|
||||
await click(SES.configure);
|
||||
assert
|
||||
.dom(SES.ssh.editConfigSection)
|
||||
.exists('renders the edit configuration section of the form and not the create part');
|
||||
// delete Public key
|
||||
await click(SES.ssh.deletePublicKey);
|
||||
assert.dom(GENERAL.confirmMessage).hasText('This will remove the CA certificate information.');
|
||||
await click(SES.ssh.delete);
|
||||
assert.dom(GENERAL.confirmMessage).hasText('Confirming will remove the CA certificate information.');
|
||||
await click(GENERAL.confirmButton);
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
|
|
@ -90,9 +88,7 @@ module('Acceptance | ssh | configuration', function (hooks) {
|
|||
);
|
||||
assert.dom(GENERAL.maskedInput('privateKey')).hasNoText('Private key is empty and reset');
|
||||
assert.dom(GENERAL.inputByAttr('publicKey')).hasNoText('Public key is empty and reset');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('generate-signing-key-checkbox'))
|
||||
.isNotChecked('Generate signing key is unchecked');
|
||||
assert.dom(GENERAL.inputByAttr('generateSigningKey')).isChecked('Generate signing key is checked');
|
||||
await click(SES.viewBackend);
|
||||
await click(SES.configTab);
|
||||
assert
|
||||
|
|
@ -107,15 +103,15 @@ module('Acceptance | ssh | configuration', function (hooks) {
|
|||
await enablePage.enable('ssh', path);
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('generate-signing-key-checkbox'))
|
||||
.isChecked('generate_signing_key defaults to true');
|
||||
await click(GENERAL.inputByAttr('generate-signing-key-checkbox'));
|
||||
assert.dom(GENERAL.inputByAttr('generateSigningKey')).isChecked('generate_signing_key defaults to true');
|
||||
await click(GENERAL.inputByAttr('generateSigningKey'));
|
||||
await click(SES.ssh.save);
|
||||
assert.true(this.flashDangerSpy.calledWith('missing public_key'), 'Danger flash message is displayed');
|
||||
assert
|
||||
.dom(GENERAL.inlineError)
|
||||
.hasText('Provide a Public and Private key or set "Generate Signing Key" to true.');
|
||||
// visit the details page and confirm the public key is not shown
|
||||
await visit(`/vault/secrets/${path}/configuration`);
|
||||
assert.dom(GENERAL.infoRowLabel('Public key')).doesNotExist('Public Key label does not exist');
|
||||
assert.dom(GENERAL.infoRowLabel('Public key')).doesNotExist('Public key label does not exist');
|
||||
assert.dom(GENERAL.emptyStateTitle).hasText('SSH not configured', 'SSH not configured');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,16 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { click, fillIn, currentURL, find, settled, waitUntil, currentRouteName } from '@ember/test-helpers';
|
||||
import {
|
||||
click,
|
||||
fillIn,
|
||||
currentURL,
|
||||
find,
|
||||
settled,
|
||||
waitUntil,
|
||||
currentRouteName,
|
||||
waitFor,
|
||||
} from '@ember/test-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
|
@ -102,8 +111,9 @@ module('Acceptance | ssh | roles', function (hooks) {
|
|||
await click(SES.configure);
|
||||
// default has generate CA checked so we just submit the form
|
||||
await click(SES.ssh.save);
|
||||
await click(SES.viewBackend);
|
||||
|
||||
// There is a delay in the backend for the public key to be generated, wait for it to complete by checking that the public key is displayed
|
||||
await waitFor(GENERAL.infoRowLabel('Public key'));
|
||||
await click(GENERAL.tab(sshPath));
|
||||
for (const role of ROLES) {
|
||||
// create a role
|
||||
await click(SES.createSecret);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"]',
|
||||
|
|
|
|||
|
|
@ -48,11 +48,13 @@ const createAwsLeaseConfig = (store, backend) => {
|
|||
};
|
||||
|
||||
const createSshCaConfig = (store, backend) => {
|
||||
// consider this model a placeholder for the actual ssh/ca-config model that has been generated with data. isNew is false.
|
||||
store.pushPayload('ssh/ca-config', {
|
||||
id: backend,
|
||||
modelName: 'ssh/ca-config',
|
||||
data: {
|
||||
backend,
|
||||
public_key: '123456',
|
||||
generate_signing_key: true,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,8 +32,9 @@ export const SECRET_ENGINE_SELECTORS = {
|
|||
ssh: {
|
||||
configureForm: '[data-test-configure-form]',
|
||||
editConfigSection: '[data-test-edit-config-section]',
|
||||
deletePublicKey: '[data-test-delete-public-key]',
|
||||
save: '[data-test-configure-save-button]',
|
||||
cancel: '[data-test-cancel-button]',
|
||||
delete: '[data-test-delete-public-key]',
|
||||
createRole: '[data-test-role-ssh-create]',
|
||||
deleteRole: '[data-test-ssh-role-delete]',
|
||||
},
|
||||
|
|
|
|||
137
ui/tests/integration/components/kv/kv-paths-card-test.js
Normal file
137
ui/tests/integration/components/kv/kv-paths-card-test.js
Normal file
|
|
@ -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`<KvPathsCard @backend={{this.backend}} @path={{this.path}} @isCondensed={{this.isCondensed}} />`,
|
||||
{
|
||||
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);
|
||||
});
|
||||
});
|
||||
341
ui/tests/integration/components/kv/page/kv-page-overview-test.js
Normal file
341
ui/tests/integration/components/kv/page/kv-page-overview-test.js
Normal file
|
|
@ -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`
|
||||
<Page::Secret::Overview
|
||||
@backend={{this.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
@canReadMetadata={{this.canReadMetadata}}
|
||||
@canUpdateSecret={{this.canUpdateSecret}}
|
||||
@metadata={{this.metadata}}
|
||||
@path={{this.path}}
|
||||
@secretState={{this.secretState}}
|
||||
@subkeys={{this.subkeys}}
|
||||
/>`,
|
||||
{ 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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`
|
||||
<Page::Secret::Paths
|
||||
@path={{this.path}}
|
||||
@backend={{this.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
@canReadMetadata={{this.canReadMetadata}}
|
||||
/>
|
||||
`,
|
||||
{ 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`
|
||||
<Page::Secret::Paths
|
||||
@path={{this.path}}
|
||||
@backend={{this.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
for (const path of paths) {
|
||||
assert.dom(PAGE.infoRowValue(path.label)).hasText(path.expected);
|
||||
this.assertClipboard(assert, PAGE.paths.copyButton(path.label), path.expected);
|
||||
test('it renders 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`
|
||||
<Page::Secret::Paths
|
||||
@path={{this.path}}
|
||||
@backend={{this.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
for (const path of paths) {
|
||||
assert.dom(PAGE.infoRowValue(path.label)).hasText(path.expected);
|
||||
this.assertClipboard(assert, PAGE.paths.copyButton(path.label), path.expected);
|
||||
test('it 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`
|
||||
<Page::Secret::Paths
|
||||
@path={{this.path}}
|
||||
@backend={{this.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli);
|
||||
assert.dom(PAGE.paths.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`
|
||||
<Page::Secret::Paths
|
||||
@path={{this.path}}
|
||||
@backend={{this.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli);
|
||||
assert.dom(PAGE.paths.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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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`<OverviewCard @cardTitle={{this.cardTitle}}/>`);
|
||||
assert.dom(SELECTORS.title).hasText('Card title');
|
||||
});
|
||||
test('it returns custom title if both exist', async function (assert) {
|
||||
await render(hbs`
|
||||
<OverviewCard @cardTitle={{this.cardTitle}}>
|
||||
<:customTitle>
|
||||
Fancy custom title
|
||||
</:customTitle>
|
||||
</OverviewCard>
|
||||
`);
|
||||
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`<OverviewCard @cardTitle={{this.cardTitle}} @subText={{this.subText}} />`);
|
||||
assert.dom(SELECTORS.subtitle).hasText('This is subtext for card');
|
||||
|
|
|
|||
|
|
@ -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`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
|
||||
{ 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`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
|
||||
{ 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`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
|
||||
{ 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`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
|
||||
{ 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`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
|
||||
{ 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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ module('Integration | Component | SecretEngine/configuration-details', function
|
|||
});
|
||||
|
||||
test('it shows config details if configModel(s) are passed in', async function (assert) {
|
||||
assert.expect(14);
|
||||
assert.expect(21);
|
||||
for (const type of CONFIGURABLE_SECRET_ENGINES) {
|
||||
const backend = `test-${type}`;
|
||||
this.configModels = createConfig(this.store, backend, type);
|
||||
|
|
@ -45,6 +45,12 @@ module('Integration | Component | SecretEngine/configuration-details', function
|
|||
assert
|
||||
.dom(GENERAL.infoRowValue(key))
|
||||
.hasText(responseKeyAndValue, `${key} value for the ${type} config details exists.`);
|
||||
// make sure the ones that should be masked are masked, and others are not.
|
||||
if (key === 'private_key' || key === 'public_key') {
|
||||
assert.dom(GENERAL.infoRowValue(key)).hasClass('masked-input', `${key} is masked`);
|
||||
} else {
|
||||
assert.dom(GENERAL.infoRowValue(key)).doesNotHaveClass('masked-input', `${key} is not masked`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,94 +5,145 @@
|
|||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { render, click } from '@ember/test-helpers';
|
||||
import { render, click, fillIn } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
import { createConfig } from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import sinon from 'sinon';
|
||||
|
||||
module('Integration | Component | SecretEngine/configure-ssh', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.model = createConfig(this.store, 'ssh-test', 'ssh');
|
||||
this.saveConfig = sinon.stub();
|
||||
const router = this.owner.lookup('service:router');
|
||||
this.id = 'ssh-test';
|
||||
this.model = this.store.createRecord('ssh/ca-config', { backend: this.id });
|
||||
this.transitionStub = sinon.stub(router, 'transitionTo');
|
||||
this.refreshStub = sinon.stub(router, 'refresh');
|
||||
});
|
||||
|
||||
test('it shows create fields if not configured', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.model}}
|
||||
@configured={{false}}
|
||||
@saveConfig={{this.saveConfig}}
|
||||
@loading={{false}}
|
||||
@id={{this.id}}
|
||||
/>
|
||||
`);
|
||||
assert.dom(GENERAL.maskedInput('privateKey')).hasNoText('Private key is empty and reset');
|
||||
assert.dom(GENERAL.inputByAttr('publicKey')).hasNoText('Public key is empty and reset');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('generate-signing-key-checkbox'))
|
||||
.dom(GENERAL.inputByAttr('generateSigningKey'))
|
||||
.isChecked('Generate signing key is checked by default');
|
||||
});
|
||||
|
||||
test('it calls save with correct arg', async function (assert) {
|
||||
test('it should go back to parent route on cancel', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.model}}
|
||||
@configured={{false}}
|
||||
@saveConfig={{this.saveConfig}}
|
||||
@loading={{false}}
|
||||
@id={{this.id}}
|
||||
/>
|
||||
`);
|
||||
|
||||
await click(SES.ssh.cancel);
|
||||
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', 'ssh-test'),
|
||||
'On cancel the router transitions to the parent configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should validate form fields', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.model}}
|
||||
@id={{this.id}}
|
||||
/>
|
||||
`);
|
||||
await fillIn(GENERAL.inputByAttr('publicKey'), 'hello');
|
||||
await click(SES.ssh.save);
|
||||
assert.ok(
|
||||
this.saveConfig.withArgs({ delete: false }).calledOnce,
|
||||
'calls the saveConfig action with args delete:false'
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.inlineError)
|
||||
.hasText(
|
||||
'You must provide a Public and Private keys or leave both unset.',
|
||||
'Public key validation error renders.'
|
||||
);
|
||||
|
||||
await click(GENERAL.inputByAttr('generateSigningKey'));
|
||||
await click(SES.ssh.save);
|
||||
assert
|
||||
.dom(GENERAL.inlineError)
|
||||
.hasText(
|
||||
'You must provide a Public and Private keys or leave both unset.',
|
||||
'Generate signing key validation message shows.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it shows masked key if model is not new', async function (assert) {
|
||||
// replace model with model that has public_key
|
||||
this.model = {
|
||||
publicKey:
|
||||
'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3lCZ7W2eJZ9W9qzv7K9GJ5qJYQ2cY6C+5Kv8Jtjz8h6wqZJ9U9K1lJ9Z6zq4sX0f7Q5X2l8L4gTt2+2ZKpVv6g1KQ6JG5H4QbVrQq2r4FzZQ2B0Y8q5c7q3Y5X6q4Q6',
|
||||
};
|
||||
test('it should generate signing key', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.post('/ssh-test/config/ca', (schema, req) => {
|
||||
const data = JSON.parse(req.requestBody);
|
||||
const expected = {
|
||||
backend: this.id,
|
||||
generate_signing_key: true,
|
||||
};
|
||||
assert.deepEqual(expected, data, 'POST request made to save ca-config with correct properties');
|
||||
});
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.model}}
|
||||
@configured={{true}}
|
||||
@saveConfig={{this.saveConfig}}
|
||||
@loading={{false}}
|
||||
@id={{this.id}}
|
||||
/>
|
||||
`);
|
||||
assert
|
||||
.dom(SES.ssh.editConfigSection)
|
||||
.exists('renders the edit configuration section of the form and not the create part');
|
||||
assert.dom(GENERAL.inputByAttr('public-key')).hasText('***********', 'public key is masked');
|
||||
await click('[data-test-button="toggle-masked"]');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('public-key'))
|
||||
.hasText(this.model.publicKey, 'public key is unmasked and shows the actual value');
|
||||
|
||||
await click(SES.ssh.save);
|
||||
assert.dom(SES.ssh.editConfigSection).exists('renders the edit configuration section of the form');
|
||||
});
|
||||
|
||||
test('it calls delete correctly', async function (assert) {
|
||||
await render(hbs`
|
||||
module('editing', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.editId = 'ssh-edit-me';
|
||||
this.editModel = createConfig(this.store, 'ssh-edit-me', 'ssh');
|
||||
});
|
||||
test('it populates fields when editing', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.model}}
|
||||
@configured={{true}}
|
||||
@saveConfig={{this.saveConfig}}
|
||||
@loading={{false}}
|
||||
@model={{this.editModel}}
|
||||
@id={{this.editId}}
|
||||
/>
|
||||
`);
|
||||
// delete Public key
|
||||
await click(SES.ssh.deletePublicKey);
|
||||
assert.dom(GENERAL.confirmMessage).hasText('This will remove the CA certificate information.');
|
||||
await click(GENERAL.confirmButton);
|
||||
assert.ok(
|
||||
this.saveConfig.withArgs({ delete: true }).calledOnce,
|
||||
'calls the saveConfig action with args delete:true'
|
||||
);
|
||||
assert
|
||||
.dom(SES.ssh.editConfigSection)
|
||||
.exists('renders the edit configuration section of the form and not the create part');
|
||||
assert.dom(GENERAL.inputByAttr('public-key')).hasText('***********', 'public key is masked');
|
||||
await click('[data-test-button="toggle-masked"]');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('public-key'))
|
||||
.hasText(this.editModel.publicKey, 'public key is unmasked and shows the actual value');
|
||||
});
|
||||
|
||||
test('it allows you to delete a public key', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.server.delete('/ssh-edit-me/config/ca', () => {
|
||||
assert.true(true, 'DELETE request made to ca-config with correct properties');
|
||||
});
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.editModel}}
|
||||
@id={{this.editId}}
|
||||
/>
|
||||
`);
|
||||
// delete Public key
|
||||
await click(SES.ssh.delete);
|
||||
assert.dom(GENERAL.confirmMessage).hasText('Confirming will remove the CA certificate information.');
|
||||
await click(GENERAL.confirmButton);
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration.edit', 'ssh-edit-me'),
|
||||
'On delete the router transitions to the current route.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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`<span data-test-formatted>{{date-format this.timestampDate}}</span>`);
|
||||
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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,62 +4,146 @@
|
|||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { render, click, fillIn } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
import { createConfig } from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import sinon from 'sinon';
|
||||
|
||||
module('Unit | Adapter | secret engine', function (hooks) {
|
||||
setupTest(hooks);
|
||||
module('Integration | Component | SecretEngine/configure-ssh', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
const storeStub = {
|
||||
serializerFor() {
|
||||
return {
|
||||
serializeIntoHash() {},
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
const router = this.owner.lookup('service:router');
|
||||
this.id = 'ssh-test';
|
||||
this.model = this.store.createRecord('ssh/ca-config', { backend: this.id });
|
||||
this.transitionStub = sinon.stub(router, 'transitionTo');
|
||||
this.refreshStub = sinon.stub(router, 'refresh');
|
||||
});
|
||||
|
||||
test('it shows create fields if not configured', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.model}}
|
||||
@id={{this.id}}
|
||||
/>
|
||||
`);
|
||||
assert.dom(GENERAL.maskedInput('privateKey')).hasNoText('Private key is empty and reset');
|
||||
assert.dom(GENERAL.inputByAttr('publicKey')).hasNoText('Public key is empty and reset');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('generateSigningKey'))
|
||||
.isChecked('Generate signing key is checked by default');
|
||||
});
|
||||
|
||||
test('it should go back to parent route on cancel', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.model}}
|
||||
@id={{this.id}}
|
||||
/>
|
||||
`);
|
||||
|
||||
await click(SES.ssh.cancel);
|
||||
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', 'ssh-test'),
|
||||
'On cancel the router transitions to the parent configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should validate form fields', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.model}}
|
||||
@id={{this.id}}
|
||||
/>
|
||||
`);
|
||||
await fillIn(GENERAL.inputByAttr('publicKey'), 'hello');
|
||||
await click(SES.ssh.save);
|
||||
assert
|
||||
.dom(GENERAL.inlineError)
|
||||
.hasText(
|
||||
'You must provide a Public and Private keys or leave both unset.',
|
||||
'Public key validation error renders.'
|
||||
);
|
||||
|
||||
await click(GENERAL.inputByAttr('generateSigningKey'));
|
||||
await click(SES.ssh.save);
|
||||
assert
|
||||
.dom(GENERAL.inlineError)
|
||||
.hasText(
|
||||
'You must provide a Public and Private keys or leave both unset.',
|
||||
'Generate signing key validation message shows.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should generate signing key', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.post('/ssh-test/config/ca', (schema, req) => {
|
||||
const data = JSON.parse(req.requestBody);
|
||||
const expected = {
|
||||
backend: this.id,
|
||||
generate_signing_key: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
const type = {
|
||||
modelName: 'secret-engine',
|
||||
};
|
||||
assert.deepEqual(expected, data, 'POST request made to save ca-config with correct properties');
|
||||
});
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.model}}
|
||||
@id={{this.id}}
|
||||
/>
|
||||
`);
|
||||
|
||||
test('Empty query', function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.get('/sys/internal/ui/mounts', () => {
|
||||
assert.ok('query calls the correct url');
|
||||
return {};
|
||||
});
|
||||
const adapter = this.owner.lookup('adapter:secret-engine');
|
||||
adapter['query'](storeStub, type, {});
|
||||
});
|
||||
test('Query with a path', function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.get('/sys/internal/ui/mounts/foo', () => {
|
||||
assert.ok('query calls the correct url');
|
||||
return {};
|
||||
});
|
||||
const adapter = this.owner.lookup('adapter:secret-engine');
|
||||
adapter['query'](storeStub, type, { path: 'foo' });
|
||||
await click(SES.ssh.save);
|
||||
assert.dom(SES.ssh.editConfigSection).exists('renders the edit configuration section of the form');
|
||||
});
|
||||
|
||||
test('Query with nested path', function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.get('/sys/internal/ui/mounts/foo/bar/baz', () => {
|
||||
assert.ok('query calls the correct url');
|
||||
return {};
|
||||
module('editing', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.editId = 'ssh-edit-me';
|
||||
this.editModel = createConfig(this.store, 'ssh-edit-me', 'ssh');
|
||||
});
|
||||
test('it populates fields when editing', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.editModel}}
|
||||
@id={{this.editId}}
|
||||
/>
|
||||
`);
|
||||
assert
|
||||
.dom(SES.ssh.editConfigSection)
|
||||
.exists('renders the edit configuration section of the form and not the create part');
|
||||
assert.dom(GENERAL.inputByAttr('public-key')).hasText('***********', 'public key is masked');
|
||||
await click('[data-test-button="toggle-masked"]');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('public-key'))
|
||||
.hasText(this.editModel.publicKey, 'public key is unmasked and shows the actual value');
|
||||
});
|
||||
const adapter = this.owner.lookup('adapter:secret-engine');
|
||||
adapter['query'](storeStub, type, { path: 'foo/bar/baz' });
|
||||
});
|
||||
|
||||
test('Fails gracefully finding records for non ssh engines', function (assert) {
|
||||
assert.expect(1);
|
||||
const snapshot = {
|
||||
attr() {
|
||||
return { type: 'aws', path: 'aws/' };
|
||||
},
|
||||
};
|
||||
const adapter = this.owner.lookup('adapter:secret-engine');
|
||||
const response = adapter.findRecord(storeStub, 'aws', { path: 'aws' }, snapshot);
|
||||
assert.propEqual(response, { data: {} }, 'returns empty data object');
|
||||
test('it allows you to delete a public key', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.server.delete('/ssh-edit-me/config/ca', () => {
|
||||
assert.true(true, 'DELETE request made to ca-config with correct properties');
|
||||
});
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.editModel}}
|
||||
@id={{this.editId}}
|
||||
/>
|
||||
`);
|
||||
// delete Public key
|
||||
await click(SES.ssh.delete);
|
||||
assert.dom(GENERAL.confirmMessage).hasText('Confirming will remove the CA certificate information.');
|
||||
await click(GENERAL.confirmButton);
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration.edit', 'ssh-edit-me'),
|
||||
'On delete the router transitions to the current route.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -346,80 +346,6 @@ module('Unit | Model | secret-engine', function (hooks) {
|
|||
});
|
||||
});
|
||||
|
||||
module('saveCA', function () {
|
||||
test('does not call endpoint if type != ssh', async function (assert) {
|
||||
assert.expect(1);
|
||||
const model = this.store.createRecord('secret-engine', {
|
||||
type: 'not-ssh',
|
||||
});
|
||||
const saveSpy = sinon.spy(model, 'save');
|
||||
await model.saveCA({});
|
||||
assert.ok(saveSpy.notCalled, 'save not called');
|
||||
});
|
||||
test('calls save with correct params', async function (assert) {
|
||||
assert.expect(4);
|
||||
const model = this.store.createRecord('secret-engine', {
|
||||
type: 'ssh',
|
||||
privateKey: 'private-key',
|
||||
publicKey: 'public-key',
|
||||
generateSigningKey: true,
|
||||
});
|
||||
const saveStub = sinon.stub(model, 'save').callsFake((params) => {
|
||||
assert.deepEqual(
|
||||
params,
|
||||
{
|
||||
adapterOptions: {
|
||||
options: {},
|
||||
apiPath: 'config/ca',
|
||||
attrsToSend: ['privateKey', 'publicKey', 'generateSigningKey'],
|
||||
},
|
||||
},
|
||||
'send correct params to save'
|
||||
);
|
||||
return;
|
||||
});
|
||||
|
||||
await model.saveCA({});
|
||||
assert.strictEqual(model.privateKey, 'private-key', 'value exists before save');
|
||||
assert.strictEqual(model.publicKey, 'public-key', 'value exists before save');
|
||||
assert.true(model.generateSigningKey, 'value true before save');
|
||||
|
||||
saveStub.restore();
|
||||
});
|
||||
test('sets properties when isDelete', async function (assert) {
|
||||
assert.expect(7);
|
||||
const model = this.store.createRecord('secret-engine', {
|
||||
type: 'ssh',
|
||||
privateKey: 'private-key',
|
||||
publicKey: 'public-key',
|
||||
generateSigningKey: true,
|
||||
});
|
||||
const saveStub = sinon.stub(model, 'save').callsFake((params) => {
|
||||
assert.deepEqual(
|
||||
params,
|
||||
{
|
||||
adapterOptions: {
|
||||
options: { isDelete: true },
|
||||
apiPath: 'config/ca',
|
||||
attrsToSend: ['privateKey', 'publicKey', 'generateSigningKey'],
|
||||
},
|
||||
},
|
||||
'send correct params to save'
|
||||
);
|
||||
return;
|
||||
});
|
||||
assert.strictEqual(model.privateKey, 'private-key', 'value exists before save');
|
||||
assert.strictEqual(model.publicKey, 'public-key', 'value exists before save');
|
||||
assert.true(model.generateSigningKey, 'value true before save');
|
||||
|
||||
await model.saveCA({ isDelete: true });
|
||||
assert.strictEqual(model.privateKey, null, 'value null after save');
|
||||
assert.strictEqual(model.publicKey, null, 'value null after save');
|
||||
assert.false(model.generateSigningKey, 'value false after save');
|
||||
saveStub.restore();
|
||||
});
|
||||
});
|
||||
|
||||
module('saveZeroAddressConfig', function () {
|
||||
test('calls save with correct params', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,14 @@ The format of this file is [HCL](https://github.com/hashicorp/hcl) or JSON.
|
|||
|
||||
An example configuration is shown below:
|
||||
|
||||
<Note>
|
||||
|
||||
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.
|
||||
|
||||
</Note>
|
||||
|
||||
```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
|
||||
<Note>
|
||||
|
||||
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).
|
||||
|
||||
</Note>
|
||||
|
||||
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,
|
||||
<Note>
|
||||
|
||||
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,
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
|
||||
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.
|
||||
|
||||
</Note>
|
||||
|
||||
- `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).
|
||||
|
|
|
|||
|
|
@ -83,14 +83,15 @@ management tool.
|
|||
If a trust relationship exists between Vault and Azure through WIF, the secrets
|
||||
engine can exchange the Vault identity token for a federated access token.
|
||||
|
||||
To configure a trusted relationship between Vault and Azure, :
|
||||
- You must configure the [identity token issuer backend](/vault/api-docs/secret/identity/tokens#configure-the-identity-tokens-backend)
|
||||
for Vault.
|
||||
- Azure must have a
|
||||
[federated identity credential](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-create-trust?pivots=identity-wif-apps-methods-azp#configure-a-federated-identity-credential-on-an-app)
|
||||
configured with information about the fully qualified and network-reachable
|
||||
issuer URL for the Vault plugin
|
||||
[identity token provider](/vault/api-docs/secret/identity/tokens#read-plugin-identity-well-known-configurations).
|
||||
To configure a trusted relationship between Vault and Azure:
|
||||
|
||||
- You must configure the [identity token issuer backend](/vault/api-docs/secret/identity/tokens#configure-the-identity-tokens-backend)
|
||||
for Vault.
|
||||
- Azure must have a
|
||||
[federated identity credential](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-create-trust?pivots=identity-wif-apps-methods-azp#configure-a-federated-identity-credential-on-an-app)
|
||||
configured with information about the fully qualified and network-reachable
|
||||
issuer URL for the Vault plugin
|
||||
[identity token provider](/vault/api-docs/secret/identity/tokens#read-plugin-identity-well-known-configurations).
|
||||
|
||||
Establishing a trusted relationship between Vault and Azure ensures that Azure
|
||||
can fetch JWKS
|
||||
|
|
|
|||
Loading…
Reference in a new issue