mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
Merge remote-tracking branch 'remotes/from/ce/main'
This commit is contained in:
commit
0791b4cbf3
22 changed files with 771 additions and 199 deletions
3
changelog/_12343.txt
Normal file
3
changelog/_12343.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
**UI Secret engines intro**: Onboarding intro which provides feature context to users.
|
||||
```
|
||||
7
ui/.gitignore
vendored
7
ui/.gitignore
vendored
|
|
@ -43,4 +43,9 @@ package-lock.json
|
|||
vendor/jsondiffpatch.umd.js
|
||||
vendor/htmlformatter.umd.js
|
||||
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/.auth/
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
@level="base"
|
||||
@background="neutral-secondary"
|
||||
@hasBorder={{false}}
|
||||
@ariaLabel="{{@type.displayName}} - disabled engine type"
|
||||
aria-label="{{@type.displayName}} - disabled engine type"
|
||||
tabindex="0"
|
||||
{{on "click" (fn @handleDisabledPluginClick @type)}}
|
||||
{{on "keydown" (fn @handleDisabledPluginKeyDown @type)}}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
@level={{if isDisabled "base" "mid"}}
|
||||
@background={{if isDisabled "neutral-secondary" "neutral-primary"}}
|
||||
@hasBorder={{true}}
|
||||
@ariaLabel="{{@type.displayName}} - {{if isDisabled 'disabled' 'enabled'}} engine type"
|
||||
aria-label="{{@type.displayName}} - {{if isDisabled 'disabled' 'enabled'}} engine type"
|
||||
tabindex="0"
|
||||
{{on "click" this.handleSelection}}
|
||||
{{on "keydown" this.handleKeyDown}}
|
||||
|
|
|
|||
|
|
@ -12,189 +12,212 @@
|
|||
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||
</:breadcrumbs>
|
||||
<:actions>
|
||||
<Hds::Button @text="Enable new engine" @icon="plus" @route="vault.cluster.secrets.enable" data-test-enable-engine />
|
||||
{{#unless this.showWizard}}
|
||||
{{#if this.hasOnlyDefaultEngines}}
|
||||
<Hds::Button
|
||||
@color="secondary"
|
||||
@icon="bulb"
|
||||
@text="New to Secret engines?"
|
||||
{{on "click" this.showIntroPage}}
|
||||
data-test-button="intro"
|
||||
/>
|
||||
{{/if}}
|
||||
<Hds::Button @text="Enable new engine" @icon="plus" @route="vault.cluster.secrets.enable" data-test-enable-engine />
|
||||
{{/unless}}
|
||||
</:actions>
|
||||
</Page::Header>
|
||||
|
||||
{{! Filters section }}
|
||||
<Hds::SegmentedGroup class="has-top-margin-m" as |SG|>
|
||||
<SG.TextInput
|
||||
@width="300px"
|
||||
@type="search"
|
||||
placeholder="Search by path"
|
||||
aria-label="Search"
|
||||
data-test-input-search="secret-engine-path"
|
||||
{{on "input" (fn this.setSearchText "path")}}
|
||||
{{#if this.showWizard}}
|
||||
<Wizard::SecretEngines::SecretEnginesWizard
|
||||
@isIntroModal={{this.shouldRenderIntroModal}}
|
||||
@onRefresh={{this.refreshSecretEngineList}}
|
||||
/>
|
||||
<SG.Dropdown @width="175px" as |D|>
|
||||
<D.Header @hasDivider={{true}}>
|
||||
<SG.TextInput @type="search" placeholder="Search" aria-label="Search" {{on "input" (fn this.setSearchText "type")}} />
|
||||
</D.Header>
|
||||
<D.ToggleButton @color="secondary" @text="Engine type" data-test-toggle-input="filter-by-engine-type" />
|
||||
{{#each this.secretEngineArrayByType as |type|}}
|
||||
<D.Checkbox
|
||||
value={{type.name}}
|
||||
checked={{includes type.name this.engineTypeFilters}}
|
||||
{{on "click" (fn this.filterByEngineType type.name)}}
|
||||
data-test-checkbox={{type.name}}
|
||||
><Hds::Icon @name={{type.icon}} @isInline={{true}} /> {{type.name}}</D.Checkbox>
|
||||
{{/each}}
|
||||
</SG.Dropdown>
|
||||
<SG.Dropdown @width="250px" as |D|>
|
||||
<D.ToggleButton @color="secondary" @text="Version" data-test-toggle-input="filter-by-engine-version" />
|
||||
{{#if this.engineTypeFilters.length}}
|
||||
{{else}}
|
||||
{{! Filters section }}
|
||||
<Hds::SegmentedGroup class="has-top-margin-m" as |SG|>
|
||||
<SG.TextInput
|
||||
@width="300px"
|
||||
@type="search"
|
||||
placeholder="Search by path"
|
||||
aria-label="Search"
|
||||
data-test-input-search="secret-engine-path"
|
||||
{{on "input" (fn this.setSearchText "path")}}
|
||||
/>
|
||||
<SG.Dropdown @width="175px" as |D|>
|
||||
<D.Header @hasDivider={{true}}>
|
||||
<SG.TextInput
|
||||
@type="search"
|
||||
placeholder="Search"
|
||||
aria-label="Search"
|
||||
{{on "input" (fn this.setSearchText "version")}}
|
||||
{{on "input" (fn this.setSearchText "type")}}
|
||||
/>
|
||||
</D.Header>
|
||||
{{#each this.secretEngineArrayByVersions as |backend|}}
|
||||
<D.ToggleButton @color="secondary" @text="Engine type" data-test-toggle-input="filter-by-engine-type" />
|
||||
{{#each this.secretEngineArrayByType as |type|}}
|
||||
<D.Checkbox
|
||||
value={{backend.version}}
|
||||
checked={{includes backend.version this.engineVersionFilters}}
|
||||
{{on "click" (fn this.filterByEngineVersion backend.version)}}
|
||||
data-test-checkbox={{backend.version}}
|
||||
>
|
||||
{{backend.version}}
|
||||
</D.Checkbox>
|
||||
value={{type.name}}
|
||||
checked={{includes type.name this.engineTypeFilters}}
|
||||
{{on "click" (fn this.filterByEngineType type.name)}}
|
||||
data-test-checkbox={{type.name}}
|
||||
><Hds::Icon @name={{type.icon}} @isInline={{true}} /> {{type.name}}</D.Checkbox>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<D.Description class="has-top-padding-s" @text="Select an engine type first to filter by versions." />
|
||||
{{/if}}
|
||||
</SG.Dropdown>
|
||||
</Hds::SegmentedGroup>
|
||||
</SG.Dropdown>
|
||||
<SG.Dropdown @width="250px" as |D|>
|
||||
<D.ToggleButton @color="secondary" @text="Version" data-test-toggle-input="filter-by-engine-version" />
|
||||
{{#if this.engineTypeFilters.length}}
|
||||
<D.Header @hasDivider={{true}}>
|
||||
<SG.TextInput
|
||||
@type="search"
|
||||
placeholder="Search"
|
||||
aria-label="Search"
|
||||
{{on "input" (fn this.setSearchText "version")}}
|
||||
/>
|
||||
</D.Header>
|
||||
{{#each this.secretEngineArrayByVersions as |backend|}}
|
||||
<D.Checkbox
|
||||
value={{backend.version}}
|
||||
checked={{includes backend.version this.engineVersionFilters}}
|
||||
{{on "click" (fn this.filterByEngineVersion backend.version)}}
|
||||
data-test-checkbox={{backend.version}}
|
||||
>
|
||||
{{backend.version}}
|
||||
</D.Checkbox>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<D.Description class="has-top-padding-s" @text="Select an engine type first to filter by versions." />
|
||||
{{/if}}
|
||||
</SG.Dropdown>
|
||||
</Hds::SegmentedGroup>
|
||||
|
||||
<Hds::Layout::Flex @gap="8" class="has-top-margin-xs has-bottom-margin-xs" @align="center">
|
||||
{{#if (and (not this.engineTypeFilters) (not this.engineVersionFilters))}}
|
||||
<Hds::Text::Body class="has-top-padding-xs">No filters applied.</Hds::Text::Body>
|
||||
<Hds::Layout::Flex @gap="8" class="has-top-margin-xs has-bottom-margin-xs" @align="center">
|
||||
{{#if (and (not this.engineTypeFilters) (not this.engineVersionFilters))}}
|
||||
<Hds::Text::Body class="has-top-padding-xs">No filters applied.</Hds::Text::Body>
|
||||
{{else}}
|
||||
<Hds::Text::Body>Filters applied:</Hds::Text::Body>
|
||||
{{#each this.engineTypeFilters as |type|}}
|
||||
<Hds::Tag @text={{type}} @onDismiss={{fn this.filterByEngineType type}} data-test-button={{type}} />
|
||||
{{/each}}
|
||||
{{#each this.engineVersionFilters as |version|}}
|
||||
<Hds::Tag @text={{version}} @onDismiss={{fn this.filterByEngineVersion version}} data-test-button={{version}} />
|
||||
{{/each}}
|
||||
<Hds::Button
|
||||
@text="Clear all"
|
||||
@color="tertiary"
|
||||
@icon="x"
|
||||
@size="small"
|
||||
data-test-button="Clear all"
|
||||
{{on "click" this.clearAllFilters}}
|
||||
/>
|
||||
{{/if}}
|
||||
</Hds::Layout::Flex>
|
||||
{{! End Filters Section }}
|
||||
|
||||
{{! Table Section }}
|
||||
{{#if this.sortedDisplayableBackends}}
|
||||
<ListTable
|
||||
class="has-top-margin-xs"
|
||||
@columns={{this.tableColumns}}
|
||||
@selectionKey="path"
|
||||
@data={{this.sortedDisplayableBackends}}
|
||||
@onSelectionChange={{this.updateSelectedItems}}
|
||||
>
|
||||
<:selectedItems>
|
||||
{{#if this.selectedItems}}
|
||||
<Hds::Layout::Flex
|
||||
@gap="8"
|
||||
@direction="row"
|
||||
@justify="end"
|
||||
@align="center"
|
||||
class="has-bottom-margin-s has-top-margin-negative-xxl"
|
||||
@isInline="true"
|
||||
>
|
||||
<Hds::Text::Body role="status" @tag="p" @size="200" @color="foreground-primary">
|
||||
{{this.selectedItems.length}}
|
||||
selected out of
|
||||
{{this.sortedDisplayableBackends.length}}
|
||||
</Hds::Text::Body>
|
||||
<Hds::Button
|
||||
@text="Disable engines"
|
||||
@color="critical"
|
||||
@icon="trash"
|
||||
{{on "click" (fn (mut this.enginesToDisable) this.selectedItems)}}
|
||||
/>
|
||||
</Hds::Layout::Flex>
|
||||
{{/if}}
|
||||
</:selectedItems>
|
||||
<:customTableItem as |itemData|>
|
||||
{{#let (this.getEngineResourceData itemData.path) as |backendData|}}
|
||||
<Hds::TooltipButton
|
||||
aria-label="Type of backend"
|
||||
@text={{this.generateToolTipText backendData}}
|
||||
data-test-tooltip="Backend type"
|
||||
isInline={{true}}
|
||||
class="is-v-centered"
|
||||
>
|
||||
<Hds::Icon @name={{if backendData.icon backendData.icon "lock"}} />
|
||||
</Hds::TooltipButton>
|
||||
{{#if backendData.isSupportedBackend}}
|
||||
<Hds::Link::Inline
|
||||
@route={{backendData.backendLink}}
|
||||
@model={{backendData.id}}
|
||||
@color="secondary"
|
||||
class="has-text-weight-semibold"
|
||||
>{{backendData.path}}</Hds::Link::Inline>
|
||||
{{else}}
|
||||
{{backendData.path}}
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</:customTableItem>
|
||||
|
||||
<:popupMenu as |rowData|>
|
||||
{{#let (this.getEngineResourceData rowData.path) as |backendData|}}
|
||||
<Hds::Dropdown @isInline={{true}} as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="{{if backendData.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
<dd.Interactive
|
||||
@route={{backendData.backendConfigurationLink}}
|
||||
@model={{backendData.id}}
|
||||
data-test-popup-menu="View configuration"
|
||||
@icon="settings"
|
||||
>View configuration</dd.Interactive>
|
||||
{{#if (not-eq backendData.type "cubbyhole")}}
|
||||
<dd.Interactive
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.engineToDisable) backendData)}}
|
||||
data-test-popup-menu="Delete"
|
||||
@icon="trash"
|
||||
>Delete</dd.Interactive>
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
{{/let}}
|
||||
</:popupMenu>
|
||||
</ListTable>
|
||||
{{else}}
|
||||
<Hds::Text::Body>Filters applied:</Hds::Text::Body>
|
||||
{{#each this.engineTypeFilters as |type|}}
|
||||
<Hds::Tag @text={{type}} @onDismiss={{fn this.filterByEngineType type}} data-test-button={{type}} />
|
||||
{{/each}}
|
||||
{{#each this.engineVersionFilters as |version|}}
|
||||
<Hds::Tag @text={{version}} @onDismiss={{fn this.filterByEngineVersion version}} data-test-button={{version}} />
|
||||
{{/each}}
|
||||
<Hds::Button
|
||||
@text="Clear all"
|
||||
@color="tertiary"
|
||||
@icon="x"
|
||||
@size="small"
|
||||
data-test-button="Clear all"
|
||||
{{on "click" this.clearAllFilters}}
|
||||
<EmptyState @title="No Secrets engines found" />
|
||||
{{/if}}
|
||||
{{! End Table Section }}
|
||||
|
||||
{{#if this.engineToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in this engine will be permanently deleted."
|
||||
@confirmTitle="Disable engine?"
|
||||
@onClose={{fn (mut this.engineToDisable) null}}
|
||||
@onConfirm={{perform this.disableEngine this.engineToDisable}}
|
||||
/>
|
||||
{{/if}}
|
||||
</Hds::Layout::Flex>
|
||||
{{! End Filters Section }}
|
||||
|
||||
{{! Table Section }}
|
||||
{{#if this.sortedDisplayableBackends}}
|
||||
<ListTable
|
||||
class="has-top-margin-xs"
|
||||
@columns={{this.tableColumns}}
|
||||
@selectionKey="path"
|
||||
@data={{this.sortedDisplayableBackends}}
|
||||
@onSelectionChange={{this.updateSelectedItems}}
|
||||
>
|
||||
<:selectedItems>
|
||||
{{#if this.selectedItems}}
|
||||
<Hds::Layout::Flex
|
||||
@gap="8"
|
||||
@direction="row"
|
||||
@justify="end"
|
||||
@align="center"
|
||||
class="has-bottom-margin-s has-top-margin-negative-xxl"
|
||||
@isInline="true"
|
||||
>
|
||||
<Hds::Text::Body role="status" @tag="p" @size="200" @color="foreground-primary">
|
||||
{{this.selectedItems.length}}
|
||||
selected out of
|
||||
{{this.sortedDisplayableBackends.length}}
|
||||
</Hds::Text::Body>
|
||||
<Hds::Button
|
||||
@text="Disable engines"
|
||||
@color="critical"
|
||||
@icon="trash"
|
||||
{{on "click" (fn (mut this.enginesToDisable) this.selectedItems)}}
|
||||
/>
|
||||
</Hds::Layout::Flex>
|
||||
{{/if}}
|
||||
</:selectedItems>
|
||||
<:customTableItem as |itemData|>
|
||||
{{#let (this.getEngineResourceData itemData.path) as |backendData|}}
|
||||
<Hds::TooltipButton
|
||||
aria-label="Type of backend"
|
||||
@text={{this.generateToolTipText backendData}}
|
||||
data-test-tooltip="Backend type"
|
||||
isInline={{true}}
|
||||
class="is-v-centered"
|
||||
>
|
||||
<Hds::Icon @name={{if backendData.icon backendData.icon "lock"}} />
|
||||
</Hds::TooltipButton>
|
||||
{{#if backendData.isSupportedBackend}}
|
||||
<Hds::Link::Inline
|
||||
@route={{backendData.backendLink}}
|
||||
@model={{backendData.id}}
|
||||
@color="secondary"
|
||||
class="has-text-weight-semibold"
|
||||
>{{backendData.path}}</Hds::Link::Inline>
|
||||
{{else}}
|
||||
{{backendData.path}}
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</:customTableItem>
|
||||
|
||||
<:popupMenu as |rowData|>
|
||||
{{#let (this.getEngineResourceData rowData.path) as |backendData|}}
|
||||
<Hds::Dropdown @isInline={{true}} as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="{{if backendData.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
<dd.Interactive
|
||||
@route={{backendData.backendConfigurationLink}}
|
||||
@model={{backendData.id}}
|
||||
data-test-popup-menu="View configuration"
|
||||
@icon="settings"
|
||||
>View configuration</dd.Interactive>
|
||||
{{#if (not-eq backendData.type "cubbyhole")}}
|
||||
<dd.Interactive
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.engineToDisable) backendData)}}
|
||||
data-test-popup-menu="Delete"
|
||||
@icon="trash"
|
||||
>Delete</dd.Interactive>
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
{{/let}}
|
||||
</:popupMenu>
|
||||
</ListTable>
|
||||
{{else}}
|
||||
<EmptyState @title="No Secrets engines found" />
|
||||
{{/if}}
|
||||
{{! End Table Section }}
|
||||
|
||||
{{#if this.engineToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in this engine will be permanently deleted."
|
||||
@confirmTitle="Disable engine?"
|
||||
@onClose={{fn (mut this.engineToDisable) null}}
|
||||
@onConfirm={{perform this.disableEngine this.engineToDisable}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.enginesToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in these engines will be permanently deleted."
|
||||
@confirmTitle="Disable engines?"
|
||||
@onClose={{fn (mut this.enginesToDisable) null}}
|
||||
@onConfirm={{perform this.disableMultipleEngines this.enginesToDisable}}
|
||||
/>
|
||||
{{#if this.enginesToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in these engines will be permanently deleted."
|
||||
@confirmTitle="Disable engines?"
|
||||
@onClose={{fn (mut this.enginesToDisable) null}}
|
||||
@onConfirm={{perform this.disableMultipleEngines this.enginesToDisable}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
|
@ -18,6 +18,8 @@ import type NamespaceService from 'vault/services/namespace';
|
|||
import type RouterService from '@ember/routing/router-service';
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import type VersionService from 'vault/services/version';
|
||||
import type WizardService from 'vault/services/wizard';
|
||||
import { WIZARD_ID } from '../wizard/secret-engines/secret-engines-wizard';
|
||||
|
||||
/**
|
||||
* @module SecretEngineList handles the display of the list of secret engines, including the filtering.
|
||||
|
|
@ -35,11 +37,12 @@ interface Args {
|
|||
}
|
||||
|
||||
export default class SecretEngineList extends Component<Args> {
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly namespace: NamespaceService;
|
||||
@service declare readonly router: RouterService;
|
||||
@service declare readonly version: VersionService;
|
||||
@service declare readonly namespace: NamespaceService;
|
||||
@service declare readonly wizard: WizardService;
|
||||
|
||||
@tracked secretEngineOptions: Array<string> | [] = [];
|
||||
@tracked engineToDisable: SecretsEngineResource | undefined = undefined;
|
||||
|
|
@ -55,6 +58,9 @@ export default class SecretEngineList extends Component<Args> {
|
|||
|
||||
@tracked selectedItems = Array<string>();
|
||||
|
||||
@tracked shouldRenderIntroModal = false;
|
||||
wizardId = WIZARD_ID;
|
||||
|
||||
tableColumns = [
|
||||
{
|
||||
key: 'path',
|
||||
|
|
@ -189,6 +195,32 @@ export default class SecretEngineList extends Component<Args> {
|
|||
}));
|
||||
}
|
||||
|
||||
// The backend does not directly indicate which engines were mounted by default and which have been mounted by the user
|
||||
// Currently the cubbyhole/, sys/, identity/ engines are mounted by default. (secret/ is mounted in dev mode as well)
|
||||
// The sys/ and identity/ engines are non-displayable engines.
|
||||
// While not ideal, we can check whether there are other engines than the default cubbyhole/ engine
|
||||
// to determine whether we should show the intro page
|
||||
get hasOnlyDefaultEngines() {
|
||||
const listedEngines = this.sortedDisplayableBackends;
|
||||
return !listedEngines.length || (listedEngines.length === 1 && listedEngines[0]?.path === 'cubbyhole/');
|
||||
}
|
||||
|
||||
get showWizard() {
|
||||
return !this.wizard.isDismissed(this.wizardId) && this.hasOnlyDefaultEngines;
|
||||
}
|
||||
|
||||
@action
|
||||
showIntroPage() {
|
||||
// Reset the wizard dismissal state to allow re-entering the wizard
|
||||
this.wizard.reset(this.wizardId);
|
||||
this.shouldRenderIntroModal = true;
|
||||
}
|
||||
|
||||
@action
|
||||
refreshSecretEngineList() {
|
||||
this.router.refresh('vault.cluster.secrets.backends');
|
||||
}
|
||||
|
||||
// Returns engine resource data for a given engine path, needed to get icon and other metadata from SecretEnginesResource
|
||||
getEngineResourceData = (enginePath: string) => {
|
||||
return this.displayableBackends.find((backend) => backend.path === enginePath);
|
||||
|
|
|
|||
30
ui/app/components/wizard/secret-engines/intro.hbs
Normal file
30
ui/app/components/wizard/secret-engines/intro.hbs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
<Wizard::IntroContent
|
||||
@setupTime="15 min"
|
||||
@description="Secrets engines in Vault securely manage, store, and dynamically generate secrets, certificates, and encryption keys."
|
||||
@imageAlt="Flow of secret engine access control by policy"
|
||||
@imageCaption="Access to secrets within an engine is governed by policies. Users must be granted explicit access to the engine's unique path."
|
||||
@imageSrc={{img-path "~/secret-engines-intro.png"}}
|
||||
>
|
||||
<:features>
|
||||
<Wizard::IntroContent::Feature @icon="service">
|
||||
Store
|
||||
<strong>static</strong>
|
||||
application passwords (KV), generate dynamic, time-limited credentials for databases and cloud platforms.
|
||||
</Wizard::IntroContent::Feature>
|
||||
<Wizard::IntroContent::Feature @icon="replication-direct">
|
||||
One cluster can support multiple instances of the same engine type, with a
|
||||
<strong>unique path</strong>
|
||||
and granular access control.
|
||||
</Wizard::IntroContent::Feature>
|
||||
<Wizard::IntroContent::Feature @icon="lock">
|
||||
You’ll need a
|
||||
<strong>privileged account</strong>
|
||||
on the target system (root DB user or AWS Admin) that Vault can use to generate dynamic credentials, and appropriate
|
||||
ACL policies to control which users can interact with those secrets.
|
||||
</Wizard::IntroContent::Feature>
|
||||
</:features>
|
||||
</Wizard::IntroContent>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Wizard @wizardId={{this.wizardId}} @isModal={{@isIntroModal}} @title="Secrets engines" @onDismiss={{this.onDismiss}}>
|
||||
<:intro>
|
||||
<Wizard::SecretEngines::Intro />
|
||||
</:intro>
|
||||
<:introActions>
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button
|
||||
@icon="plus"
|
||||
@text="Enable a Secret engine"
|
||||
{{on "click" (fn this.onIntroChange false)}}
|
||||
data-test-button="intro"
|
||||
/>
|
||||
<Hds::Button
|
||||
@color="secondary"
|
||||
@text={{if @isIntroModal "Close" "Skip"}}
|
||||
{{on "click" this.onDismiss}}
|
||||
data-test-button="Skip"
|
||||
/>
|
||||
<Hds::Link::Standalone
|
||||
@icon="docs-link"
|
||||
@iconPosition="trailing"
|
||||
@text="View documentation"
|
||||
@href={{doc-link "/vault/tutorials/get-started/understand-static-dynamic-secrets"}}
|
||||
class="has-left-margin-m"
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
</:introActions>
|
||||
</Wizard>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type WizardService from 'vault/services/wizard';
|
||||
|
||||
interface Args {
|
||||
isIntroModal: boolean;
|
||||
onRefresh: CallableFunction;
|
||||
}
|
||||
|
||||
export const WIZARD_ID = 'secret-engines';
|
||||
|
||||
export default class WizardSecretEnginesWizardComponent extends Component<Args> {
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly router: RouterService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly wizard: WizardService;
|
||||
|
||||
wizardId = WIZARD_ID;
|
||||
|
||||
@action
|
||||
onDismiss() {
|
||||
this.wizard.dismiss(this.wizardId);
|
||||
this.args.onRefresh();
|
||||
}
|
||||
|
||||
@action
|
||||
onIntroChange(visible: boolean) {
|
||||
this.wizard.setIntroVisible(this.wizardId, visible);
|
||||
}
|
||||
}
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
</label>
|
||||
<div class="control">
|
||||
<Input
|
||||
aria-label="Key shares"
|
||||
id="key-shares"
|
||||
data-test-key-shares="true"
|
||||
class="input"
|
||||
autocomplete="off"
|
||||
|
|
@ -155,7 +155,7 @@
|
|||
</label>
|
||||
<div class="control">
|
||||
<Input
|
||||
aria-label="Key threshold"
|
||||
id="key-threshold"
|
||||
data-test-key-threshold="true"
|
||||
class="input"
|
||||
autocomplete="off"
|
||||
|
|
|
|||
2
ui/e2e/.gitignore
vendored
Normal file
2
ui/e2e/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
!*.hcl
|
||||
/tmp
|
||||
69
ui/e2e/init.setup.ts
Normal file
69
ui/e2e/init.setup.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { test as base } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { USER_POLICY_MAP } from './policies';
|
||||
|
||||
export type UserSetupOptions = {
|
||||
userType: string;
|
||||
};
|
||||
|
||||
// use superuser as the default policy if not provided in the config for a project
|
||||
export const setup = base.extend<UserSetupOptions>({
|
||||
userType: 'superuser',
|
||||
});
|
||||
|
||||
// setup will run once before all tests
|
||||
setup('initialize vault and setup user for testing', async ({ page, userType }) => {
|
||||
// on fresh app load navigating to the root will land us on the initialize page
|
||||
await page.goto('./');
|
||||
// initialize vault
|
||||
await page.getByRole('spinbutton', { name: 'Key shares' }).fill('1');
|
||||
await page.getByRole('spinbutton', { name: 'Key threshold' }).fill('1');
|
||||
await page.getByRole('button', { name: 'Initialize' }).click();
|
||||
// listen for download event so we can get the unseal key and root token
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByRole('button', { name: 'Download keys' }).click();
|
||||
const download = await downloadPromise;
|
||||
const keysPath = path.join(__dirname, `/tmp/${userType}-keys.json`);
|
||||
await download.saveAs(keysPath);
|
||||
const { keys, root_token } = JSON.parse(fs.readFileSync(keysPath, 'utf-8'));
|
||||
// unseal vault
|
||||
await page.getByRole('link', { name: 'Continue to Unseal' }).click();
|
||||
await page.getByRole('textbox', { name: 'Unseal Key Portion' }).fill(keys[0]);
|
||||
await page.getByRole('button', { name: 'Unseal' }).click();
|
||||
// use the root token to login
|
||||
await page.getByRole('textbox', { name: 'Token' }).fill(root_token);
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
// create a policy for a specific user persona
|
||||
// defaults to superuser but should be passed in via the project config in playwright.config.ts
|
||||
await page.getByRole('link', { name: 'Access', exact: true }).click();
|
||||
await page.getByRole('link', { name: 'Create ACL policy' }).click();
|
||||
await page.getByRole('textbox', { name: 'Policy name' }).fill(userType);
|
||||
await page.getByRole('radio', { name: 'Code editor' }).check();
|
||||
await page.getByRole('textbox', { name: 'Policy editor' }).fill(USER_POLICY_MAP[userType]);
|
||||
await page.getByRole('button', { name: 'Create policy' }).click();
|
||||
// there is no UI workflow for creating tokens with specific policies
|
||||
// generate a token using the web REPL and assign the new policy to it
|
||||
await page.getByRole('button', { name: 'Console toggle' }).click();
|
||||
await page
|
||||
.getByRole('textbox', { name: 'web R.E.P.L.' })
|
||||
.fill(`write -field=client_token auth/token/create policies=${userType} ttl=1d`);
|
||||
await page.getByRole('textbox', { name: 'web R.E.P.L.' }).press('Enter');
|
||||
const newToken = await page.locator('.console-ui-output pre').innerText();
|
||||
await page.getByRole('button', { name: 'Console toggle' }).click();
|
||||
// log out with the root token and log in with the new token/policy
|
||||
await page.getByRole('button', { name: 'User menu' }).click();
|
||||
await page.getByRole('link', { name: 'Log out' }).click();
|
||||
await page.getByRole('textbox', { name: 'Token' }).fill(newToken);
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
// wait for the dashboard to load to ensure login was successful
|
||||
await page.waitForURL('**/dashboard');
|
||||
// save the authenticated state to file
|
||||
// subsequent tests can then reuse this session data
|
||||
await page.context().storageState({ path: path.join(__dirname, `/tmp/${userType}-session.json`) });
|
||||
});
|
||||
15
ui/e2e/policies/index.ts
Normal file
15
ui/e2e/policies/index.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const readFile = (filePath: string) => {
|
||||
return fs.readFileSync(path.join(__dirname, filePath), 'utf-8');
|
||||
};
|
||||
|
||||
export const USER_POLICY_MAP = {
|
||||
superuser: readFile('./superuser.hcl'),
|
||||
};
|
||||
6
ui/e2e/policies/superuser.hcl
Normal file
6
ui/e2e/policies/superuser.hcl
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Copyright IBM Corp. 2016, 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
path "*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
|
||||
}
|
||||
120
ui/e2e/tests/superuser/kv.spec.ts
Normal file
120
ui/e2e/tests/superuser/kv.spec.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('kvv2 workflow', async ({ page }) => {
|
||||
await page.goto('dashboard');
|
||||
// enable kv secrets engine
|
||||
await page.getByRole('link', { name: 'Secrets Engines' }).click();
|
||||
await page.getByRole('link', { name: 'Enable new engine' }).click();
|
||||
await page.locator('div').filter({ hasText: 'KV' }).nth(4).click();
|
||||
await page.getByRole('textbox', { name: 'Path' }).click();
|
||||
await page.getByRole('textbox', { name: 'Path' }).fill('kv-test');
|
||||
await page.getByRole('button', { name: 'Enable engine' }).click();
|
||||
// once enabled it should navigate to the secrets engine overview page
|
||||
await expect(page.locator('section')).toContainText('kv-test version 2');
|
||||
await expect(page.locator('section')).toContainText(
|
||||
'No secrets yet When created, secrets will be listed here. Create a secret to get started.'
|
||||
);
|
||||
// verify that the kv engine appears in the list view
|
||||
await page.getByRole('link', { name: 'Secrets Engines' }).click();
|
||||
await page.getByRole('link', { name: 'kv-test/' }).click();
|
||||
// create a secret
|
||||
await page.getByRole('link', { name: 'Create secret' }).click();
|
||||
await page.getByRole('textbox', { name: 'Path for this secret' }).fill('foo');
|
||||
await page.getByRole('textbox', { name: 'key' }).fill('bar');
|
||||
await page.getByRole('textbox', { name: 'bar' }).fill('baz');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
// it should navigate to the overview page for the new secret
|
||||
await expect(page.locator('section')).toContainText('foo');
|
||||
await expect(page.locator('section')).toContainText(
|
||||
'Current version Create new The current version of this secret. 1'
|
||||
);
|
||||
// verify secret details
|
||||
await page.getByRole('link', { name: 'Secret', exact: true }).click();
|
||||
await expect(page.locator('section')).toContainText('bar');
|
||||
await page.getByRole('button', { name: 'show value' }).click();
|
||||
await expect(page.locator('pre')).toContainText('baz');
|
||||
await page.locator('label').click();
|
||||
await expect(page.getByRole('code')).toContainText('{ "bar": "baz" }');
|
||||
// create metadata for the secret
|
||||
await page.getByRole('link', { name: 'Metadata', exact: true }).click();
|
||||
await expect(page.locator('#app-main-content')).toContainText(
|
||||
'No custom metadata This data is version-agnostic and is usually used to describe the secret being stored. Add metadata'
|
||||
);
|
||||
await page.getByRole('link', { name: 'Edit metadata' }).click();
|
||||
await page.getByRole('textbox', { name: 'key' }).fill('meta');
|
||||
await page.getByRole('textbox', { name: 'value' }).fill('data');
|
||||
await page.getByRole('button', { name: 'Update' }).click();
|
||||
await expect(page.locator('#app-main-content')).toContainText('meta data');
|
||||
// create new version
|
||||
await page.getByRole('link', { name: 'Version History' }).click();
|
||||
await expect(page.locator('section')).toContainText('Version 1');
|
||||
await expect(page.locator('section')).toContainText('Current');
|
||||
await page.getByRole('button', { name: 'Manage version' }).click();
|
||||
await page.getByRole('link', { name: 'Create new version from 1', exact: true }).click();
|
||||
await page.getByRole('textbox', { name: 'key' }).first().fill('bar-v2');
|
||||
await page.getByRole('textbox', { name: 'bar-v2' }).fill('baz-v2');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(page.locator('section')).toContainText(
|
||||
'Current version Create new The current version of this secret. 2'
|
||||
);
|
||||
await page.getByRole('link', { name: 'Version History' }).click();
|
||||
await expect(page.locator('section')).toContainText('Version 2');
|
||||
await expect(page.locator('section')).toContainText('Current');
|
||||
await page.getByRole('link', { name: 'Version diff' }).click();
|
||||
await expect(page.locator('section')).toContainText('bar"baz"bar-v2"baz-v2"');
|
||||
// delete version 2
|
||||
await page.goto('secrets-engines/kv-test/kv/foo');
|
||||
await page.getByRole('link', { name: 'Secret', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
await page.getByRole('radio', { name: 'Delete this version This' }).check();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
await expect(page.locator('section')).toContainText(
|
||||
'Current version Deleted Create new The current version of this secret was deleted'
|
||||
);
|
||||
await page.getByRole('link', { name: 'Secret', exact: true }).click();
|
||||
await expect(page.locator('section')).toContainText(
|
||||
'Version 2 of this secret has been deleted This version has been deleted but can be undeleted. View other versions of this secret by clicking the Version History tab above. KV v2 API docs'
|
||||
);
|
||||
// undelete version
|
||||
await page.getByRole('button', { name: 'Undelete' }).click();
|
||||
await expect(page.locator('section')).toContainText(
|
||||
'Current version Create new The current version of this secret. 2'
|
||||
);
|
||||
// delete latest version
|
||||
await page.getByRole('link', { name: 'Secret', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Version' }).click();
|
||||
await page.getByRole('link', { name: 'Version 1' }).click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
await page.getByRole('radio', { name: 'Delete latest version This' }).check();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
await expect(page.locator('section')).toContainText(
|
||||
'Current version Deleted Create new The current version of this secret was deleted'
|
||||
);
|
||||
// destroy version 2
|
||||
await page.getByRole('link', { name: 'Secret', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Destroy' }).click();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
await expect(page.locator('section')).toContainText(
|
||||
'Current version Destroyed Create new The current version of this secret has been permanently deleted and cannot be restored. 2'
|
||||
);
|
||||
await page.getByRole('link', { name: 'Secret', exact: true }).click();
|
||||
await expect(page.locator('section')).toContainText(
|
||||
'Version 2 of this secret has been permanently destroyed A version that has been permanently deleted cannot be restored. You can view other versions of this secret in the Version History tab above. KV v2 API docs'
|
||||
);
|
||||
// destroy version 1
|
||||
await page.getByRole('button', { name: 'Version' }).click();
|
||||
await page.getByRole('link', { name: 'Version 1' }).click();
|
||||
await page.getByRole('button', { name: 'Destroy' }).click();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
await page.getByRole('link', { name: 'Secret', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Version' }).click();
|
||||
await page.getByRole('link', { name: 'Version 1' }).click();
|
||||
await expect(page.locator('section')).toContainText(
|
||||
'Version 1 of this secret has been permanently destroyed A version that has been permanently deleted cannot be restored. You can view other versions of this secret in the Version History tab above. KV v2 API docs'
|
||||
);
|
||||
});
|
||||
15
ui/e2e/vault-config.json
Normal file
15
ui/e2e/vault-config.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"ui": true,
|
||||
"disable_mlock": true,
|
||||
|
||||
"storage": {
|
||||
"inmem": {}
|
||||
},
|
||||
|
||||
"listener": {
|
||||
"tcp": {
|
||||
"address": "127.0.0.1:8204",
|
||||
"tls_disable": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,8 @@
|
|||
"test:server": "node scripts/start-vault.js --server",
|
||||
"test:dev": "node scripts/start-vault.js",
|
||||
"vault": "VAULT_REDIRECT_ADDR=http://127.0.0.1:8200 vault server -log-level=error -dev -dev-root-token-id=root -dev-ha -dev-transactional",
|
||||
"vault:cluster": "VAULT_REDIRECT_ADDR=http://127.0.0.1:8202 vault server -log-level=error -dev -dev-root-token-id=root -dev-listen-address=127.0.0.1:8202 -dev-ha -dev-transactional"
|
||||
"vault:cluster": "VAULT_REDIRECT_ADDR=http://127.0.0.1:8202 vault server -log-level=error -dev -dev-root-token-id=root -dev-listen-address=127.0.0.1:8202 -dev-ha -dev-transactional",
|
||||
"vault:e2e": "vault server"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "~7.27.0",
|
||||
|
|
@ -67,9 +68,11 @@
|
|||
"@glint/template": "^1.7.3",
|
||||
"@icholy/duration": "~5.1.0",
|
||||
"@lineal-viz/lineal": "~0.5.1",
|
||||
"@playwright/test": "^1.58.0",
|
||||
"@tsconfig/ember": "~2.0.0",
|
||||
"@types/d3-array": "~3.2.1",
|
||||
"@types/ember-data": "~4.4.16",
|
||||
"@types/node": "^25.1.0",
|
||||
"@types/qunit": "~2.19.12",
|
||||
"@types/rsvp": "~4.0.9",
|
||||
"@types/shell-quote": "~1.7.5",
|
||||
|
|
|
|||
93
ui/playwright.config.ts
Normal file
93
ui/playwright.config.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { USER_POLICY_MAP } from './e2e/policies';
|
||||
|
||||
import type { UserSetupOptions } from './e2e/init.setup';
|
||||
|
||||
const userTypes = Object.keys(USER_POLICY_MAP);
|
||||
|
||||
// start at port 8204 and increment for each project to allow them to run concurrently
|
||||
const getURL = (increment: number, server = false) => {
|
||||
const port = `820${4 + increment}`;
|
||||
return server ? `127.0.0.1:${port}` : `http://localhost:${port}/ui/vault/`;
|
||||
};
|
||||
|
||||
// create tmp dir if it doesn't exist for storing session, keys and vault config files
|
||||
const tmpDir = path.join(__dirname, '/e2e/tmp');
|
||||
fs.mkdirSync(tmpDir, { recursive: true });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig<UserSetupOptions>({
|
||||
testDir: './e2e',
|
||||
// opt out of parallel execution with a test file - by default tests will run in the order they are defined
|
||||
fullyParallel: false,
|
||||
// fail the build on CI if you accidentally left test.only in the source code.
|
||||
forbidOnly: !!process.env.CI,
|
||||
// retry on CI only
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
// use a worker for each project so they run concurrently
|
||||
workers: userTypes.length,
|
||||
// reporter to use. See https://playwright.dev/docs/test-reporters
|
||||
reporter: 'html',
|
||||
// shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions.
|
||||
use: {
|
||||
// collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
projects: [
|
||||
// create setup project for each user type
|
||||
...userTypes.map((userType, index) => ({
|
||||
name: `setup:${userType}`,
|
||||
testMatch: /init\.setup\.ts/,
|
||||
use: {
|
||||
userType,
|
||||
baseURL: getURL(index),
|
||||
},
|
||||
})),
|
||||
// create browser projects for each user type
|
||||
...userTypes.map((userType, index) => {
|
||||
const sessionFile = path.join(tmpDir, `${userType}-session.json`);
|
||||
return {
|
||||
name: `chrome:${userType}`,
|
||||
dependencies: [`setup:${userType}`],
|
||||
workers: 1,
|
||||
// only run tests for this user type
|
||||
testDir: `./e2e/tests/${userType}`,
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
// only use if file has already been created by the setup project
|
||||
storageState: fs.existsSync(sessionFile) ? sessionFile : undefined,
|
||||
// start at port 8204 and increment for each project to allow them to run concurrently without conflicts
|
||||
baseURL: getURL(index),
|
||||
},
|
||||
};
|
||||
}),
|
||||
],
|
||||
webServer: [
|
||||
// start a vault server for each project on a different port to allow them to run concurrently
|
||||
...userTypes.map((userType, index) => {
|
||||
// read base config file
|
||||
const config = JSON.parse(fs.readFileSync(path.join(__dirname, '/e2e/vault-config.json'), 'utf-8'));
|
||||
// set the listener address with correct port for this project
|
||||
config.listener.tcp.address = getURL(index, true);
|
||||
// write the config to a new file for this project
|
||||
const configPath = path.join(tmpDir, `${userType}-vault-config.json`);
|
||||
fs.writeFileSync(configPath, JSON.stringify(config));
|
||||
|
||||
return {
|
||||
// start vault server (not dev) with inmem storage
|
||||
command: `pnpm run vault:e2e -config=${configPath}`,
|
||||
url: getURL(index),
|
||||
reuseExistingServer: false,
|
||||
};
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
@ -126,6 +126,9 @@ importers:
|
|||
'@lineal-viz/lineal':
|
||||
specifier: ~0.5.1
|
||||
version: 0.5.1(@babel/core@7.26.10)(@glint/template@1.7.3)(ember-source@5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0))
|
||||
'@playwright/test':
|
||||
specifier: ^1.58.0
|
||||
version: 1.58.0
|
||||
'@tsconfig/ember':
|
||||
specifier: ~2.0.0
|
||||
version: 2.0.0
|
||||
|
|
@ -135,6 +138,9 @@ importers:
|
|||
'@types/ember-data':
|
||||
specifier: ~4.4.16
|
||||
version: 4.4.16(@babel/core@7.26.10)
|
||||
'@types/node':
|
||||
specifier: ^25.1.0
|
||||
version: 25.1.0
|
||||
'@types/qunit':
|
||||
specifier: ~2.19.12
|
||||
version: 2.19.12
|
||||
|
|
@ -1745,6 +1751,11 @@ packages:
|
|||
resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
||||
'@playwright/test@1.58.0':
|
||||
resolution: {integrity: sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@pnpm/constants@7.1.1':
|
||||
resolution: {integrity: sha512-31pZqMtjwV+Vaq7MaPrT1EoDFSYwye3dp6BiHIGRJmVThCQwySRKM7hCvqqI94epNkqFAAYoWrNynWoRYosGdw==}
|
||||
engines: {node: '>=16.14'}
|
||||
|
|
@ -2045,8 +2056,8 @@ packages:
|
|||
'@types/minimatch@5.1.2':
|
||||
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
|
||||
|
||||
'@types/node@22.15.21':
|
||||
resolution: {integrity: sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==}
|
||||
'@types/node@25.1.0':
|
||||
resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==}
|
||||
|
||||
'@types/prettier@2.7.3':
|
||||
resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==}
|
||||
|
|
@ -4891,6 +4902,11 @@ packages:
|
|||
fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
|
|
@ -6583,6 +6599,16 @@ packages:
|
|||
resolution: {integrity: sha512-cjJP/mYuGyMrjJ49jI04khId5Oufd3nFTUYBzQTIIVNI7/oAWdwXEfpwTF8HELFV/gz+WGYUBHCe3KHWD8rYvg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
playwright-core@1.58.0:
|
||||
resolution: {integrity: sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.58.0:
|
||||
resolution: {integrity: sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
portfinder@1.0.37:
|
||||
resolution: {integrity: sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==}
|
||||
engines: {node: '>= 10.12'}
|
||||
|
|
@ -7825,8 +7851,8 @@ packages:
|
|||
underscore@1.13.7:
|
||||
resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==}
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
undici-types@7.16.0:
|
||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.1:
|
||||
resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
|
||||
|
|
@ -10811,6 +10837,10 @@ snapshots:
|
|||
|
||||
'@pkgr/core@0.2.4': {}
|
||||
|
||||
'@playwright/test@1.58.0':
|
||||
dependencies:
|
||||
playwright: 1.58.0
|
||||
|
||||
'@pnpm/constants@7.1.1': {}
|
||||
|
||||
'@pnpm/error@5.0.3':
|
||||
|
|
@ -10892,7 +10922,7 @@ snapshots:
|
|||
'@types/body-parser@1.19.5':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
|
||||
'@types/chai-as-promised@7.1.8':
|
||||
dependencies:
|
||||
|
|
@ -10906,11 +10936,11 @@ snapshots:
|
|||
|
||||
'@types/connect@3.4.38':
|
||||
dependencies:
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
|
||||
'@types/cors@2.8.18':
|
||||
dependencies:
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
|
||||
'@types/d3-array@3.2.1': {}
|
||||
|
||||
|
|
@ -11184,7 +11214,7 @@ snapshots:
|
|||
|
||||
'@types/express-serve-static-core@4.19.6':
|
||||
dependencies:
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
'@types/qs': 6.14.0
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 0.17.4
|
||||
|
|
@ -11198,27 +11228,27 @@ snapshots:
|
|||
|
||||
'@types/fs-extra@5.1.0':
|
||||
dependencies:
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
|
||||
'@types/fs-extra@8.1.5':
|
||||
dependencies:
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
|
||||
'@types/fs-extra@9.0.13':
|
||||
dependencies:
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
|
||||
'@types/geojson@7946.0.16': {}
|
||||
|
||||
'@types/glob@7.2.0':
|
||||
dependencies:
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
|
||||
'@types/glob@8.1.0':
|
||||
dependencies:
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
|
||||
'@types/http-errors@2.0.4': {}
|
||||
|
||||
|
|
@ -11247,9 +11277,9 @@ snapshots:
|
|||
|
||||
'@types/minimatch@5.1.2': {}
|
||||
|
||||
'@types/node@22.15.21':
|
||||
'@types/node@25.1.0':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
undici-types: 7.16.0
|
||||
|
||||
'@types/prettier@2.7.3': {}
|
||||
|
||||
|
|
@ -11262,7 +11292,7 @@ snapshots:
|
|||
'@types/rimraf@2.0.5':
|
||||
dependencies:
|
||||
'@types/glob': 8.1.0
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
|
||||
'@types/rsvp@4.0.9': {}
|
||||
|
||||
|
|
@ -11271,12 +11301,12 @@ snapshots:
|
|||
'@types/send@0.17.4':
|
||||
dependencies:
|
||||
'@types/mime': 1.3.5
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
|
||||
'@types/serve-static@1.15.7':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.4
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
'@types/send': 0.17.4
|
||||
|
||||
'@types/shell-quote@1.7.5': {}
|
||||
|
|
@ -12380,7 +12410,7 @@ snapshots:
|
|||
|
||||
broccoli-rollup@4.0.0:
|
||||
dependencies:
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
broccoli-plugin: 2.1.0
|
||||
fs-tree-diff: 2.0.1
|
||||
heimdalljs: 0.2.6
|
||||
|
|
@ -14624,7 +14654,7 @@ snapshots:
|
|||
engine.io@6.6.4:
|
||||
dependencies:
|
||||
'@types/cors': 2.8.18
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
accepts: 1.3.8
|
||||
base64id: 2.0.0
|
||||
cookie: 0.7.2
|
||||
|
|
@ -15414,6 +15444,9 @@ snapshots:
|
|||
|
||||
fs.realpath@1.0.0: {}
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
|
|
@ -16150,7 +16183,7 @@ snapshots:
|
|||
|
||||
jest-worker@27.5.1:
|
||||
dependencies:
|
||||
'@types/node': 22.15.21
|
||||
'@types/node': 25.1.0
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
||||
|
|
@ -17211,6 +17244,14 @@ snapshots:
|
|||
bytestreamjs: 1.1.3
|
||||
pvutils: 1.1.3
|
||||
|
||||
playwright-core@1.58.0: {}
|
||||
|
||||
playwright@1.58.0:
|
||||
dependencies:
|
||||
playwright-core: 1.58.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
portfinder@1.0.37:
|
||||
dependencies:
|
||||
async: 2.6.4
|
||||
|
|
@ -18714,7 +18755,7 @@ snapshots:
|
|||
|
||||
underscore@1.13.7: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
undici-types@7.16.0: {}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
||||
|
||||
|
|
|
|||
BIN
ui/public/images/secret-engines-intro.png
Normal file
BIN
ui/public/images/secret-engines-intro.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from 'vault/tests/helpers/commands';
|
||||
import { login, loginNs } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import page from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
import localStorage from 'vault/lib/local-storage';
|
||||
|
||||
module('Acceptance | secret-engine list view', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
|
@ -32,9 +33,11 @@ module('Acceptance | secret-engine list view', function (hooks) {
|
|||
await click(SES.crumb(enginePath));
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
hooks.beforeEach(async function () {
|
||||
this.uid = uuidv4();
|
||||
return login();
|
||||
await login();
|
||||
// dismiss wizard
|
||||
localStorage.setItem('dismissed-wizards', ['secret-engines']);
|
||||
});
|
||||
|
||||
// the new API service camelizes response keys, so this tests is to assert that does NOT happen when we re-implement it
|
||||
|
|
@ -146,6 +149,7 @@ module('Acceptance | secret-engine list view', function (hooks) {
|
|||
|
||||
await runCmd([`write sys/namespaces/${this.namespace} -force`]);
|
||||
await loginNs(this.namespace);
|
||||
localStorage.setItem('dismissed-wizards', ['secret-engines']);
|
||||
await visit(`/vault/secrets-engines?namespace=${this.namespace}`);
|
||||
await click(`${GENERAL.tableData('cubbyhole/', 'path')} a`);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ module('Integration | Component | secret-engine/list', function (hooks) {
|
|||
this.version = this.owner.lookup('service:version');
|
||||
this.router = this.owner.lookup('service:router');
|
||||
this.router.transitionTo = sinon.stub();
|
||||
this.router.refresh = sinon.stub();
|
||||
this.flashMessages = this.owner.lookup('service:flash-messages');
|
||||
this.flashMessages.registerTypes(['success', 'danger']);
|
||||
this.flashSuccessSpy = sinon.spy(this.flashMessages, 'success');
|
||||
|
|
@ -44,6 +45,11 @@ module('Integration | Component | secret-engine/list', function (hooks) {
|
|||
];
|
||||
});
|
||||
|
||||
hooks.afterEach(async function () {
|
||||
// ensure clean state
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
test('it allows you to disable an engine', async function (assert) {
|
||||
const enginePath = 'kv-test';
|
||||
this.server.delete(`sys/mounts/${enginePath}`, () => {
|
||||
|
|
@ -172,4 +178,36 @@ module('Integration | Component | secret-engine/list', function (hooks) {
|
|||
.dom(GENERAL.tableData('aws-1/', 'path'))
|
||||
.hasClass('text-overflow-ellipsis', 'secret engine name has text overflow class ');
|
||||
});
|
||||
|
||||
test('it shows the intro page when only default engines are enabled', async function (assert) {
|
||||
// Only cubbyhole engine exists (default engine)
|
||||
const defaultEngines = [createSecretsEngine(undefined, 'cubbyhole', 'cubbyhole')];
|
||||
this.secretEngineModels = defaultEngines;
|
||||
|
||||
await render(hbs`<SecretEngine::List @secretEngines={{this.secretEngineModels}} />`);
|
||||
|
||||
assert.dom('[data-test-intro]').exists('Intro page is shown');
|
||||
assert.dom(GENERAL.button('intro')).exists('Shows intro button');
|
||||
assert.dom(GENERAL.button('Skip')).exists('Shows skip button');
|
||||
});
|
||||
|
||||
test('it does not show the intro page when other engines exist', async function (assert) {
|
||||
// Has engines beyond the default cubbyhole
|
||||
await render(hbs`<SecretEngine::List @secretEngines={{this.secretEngineModels}} />`);
|
||||
|
||||
assert.dom('[data-test-intro]').doesNotExist('Intro modal is not shown when engines exist');
|
||||
assert.dom(GENERAL.button('intro')).doesNotExist('Intro button is not shown');
|
||||
});
|
||||
|
||||
test('it can show the intro modal after dismissal', async function (assert) {
|
||||
const defaultEngines = [createSecretsEngine(undefined, 'cubbyhole', 'cubbyhole')];
|
||||
this.secretEngineModels = defaultEngines;
|
||||
|
||||
await render(hbs`<SecretEngine::List @secretEngines={{this.secretEngineModels}} />`);
|
||||
await click(GENERAL.button('Skip'));
|
||||
assert.dom('[data-test-intro]').doesNotExist('Intro is dismissed');
|
||||
|
||||
await click(GENERAL.button('intro'));
|
||||
assert.dom('[data-test-intro]').exists('Intro can be shown again after reset');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue