mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-12 16:16:47 +02:00
UI: improve control group UX (#28232)
* wip control group fix? * dont rely on models for capabilities; * Revert "wip control group fix?" This reverts commit cf3e896ba05d2fdfe1f6287bba5c862df4e5d553. * make explicit request for data * remove dangerous triple curlies * cleanup template logic and reuse each-in * remove capability checks from model * update tests to reflect new behavior * add test coverage * fix mirage factory, update details tests * test control groups VAULT-29471 * finish patch test * alphabetize! * does await help? * fix factory * add conditionals for control group error
This commit is contained in:
parent
ff7309573f
commit
3a9db72792
@ -49,6 +49,8 @@ export default class KvSecretMetadataModel extends Model {
|
||||
})
|
||||
deleteVersionAfter;
|
||||
|
||||
// the API returns custom_metadata: null if empty but because the attr is an 'object' ember data transforms it to an empty object.
|
||||
// this is important because we rely on the empty object as a truthy value in template conditionals
|
||||
@attr('object', {
|
||||
editType: 'kv',
|
||||
isSectionHeader: true,
|
||||
|
||||
@ -135,6 +135,10 @@ export default Service.extend({
|
||||
return {
|
||||
type: 'error-with-html',
|
||||
content: lines.join('\n'),
|
||||
href,
|
||||
token,
|
||||
accessor,
|
||||
creation_path,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
35
ui/lib/core/addon/components/control-group-inline-error.hbs
Normal file
35
ui/lib/core/addon/components/control-group-inline-error.hbs
Normal file
@ -0,0 +1,35 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
{{! This display only template is for rendering control group errors when the error is thrown from a component }}
|
||||
|
||||
<Hds::Alert data-test-message-error @type="inline" @color="warning" ...attributes as |A|>
|
||||
<A.Title>Control Group Error</A.Title>
|
||||
{{#if (and @error.creation_path @error.token @error.accessor)}}
|
||||
<A.Description>
|
||||
A Control Group was encountered at
|
||||
<code>{{@error.creation_path}}</code>. The Control Group Token is
|
||||
<code>{{@error.token}}</code>. The Accessor is
|
||||
<code>{{@error.accessor}}</code>.
|
||||
</A.Description>
|
||||
{{/if}}
|
||||
{{#if (has-block "customMessage")}}
|
||||
<A.Description>
|
||||
{{yield to="customMessage"}}
|
||||
</A.Description>
|
||||
{{/if}}
|
||||
{{#if @error.href}}
|
||||
<A.Description>
|
||||
Visit
|
||||
<Hds::Link::Inline
|
||||
@color="primary"
|
||||
@href={{@error.href}}
|
||||
@isHrefExternal={{false}}
|
||||
data-test-control-error="href"
|
||||
>{{@error.href}}</Hds::Link::Inline>
|
||||
for more details.
|
||||
</A.Description>
|
||||
{{/if}}
|
||||
</Hds::Alert>
|
||||
6
ui/lib/core/app/components/control-group-inline-error.js
Normal file
6
ui/lib/core/app/components/control-group-inline-error.js
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'core/components/control-group-inline-error';
|
||||
@ -64,11 +64,11 @@
|
||||
<li>
|
||||
<LinkTo @route="secret.paths" @models={{array @secret.backend @path}} data-test-secrets-tab="Paths">Paths</LinkTo>
|
||||
</li>
|
||||
{{#if @secret.canReadMetadata}}
|
||||
{{#if @canReadMetadata}}
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="secret.metadata.versions"
|
||||
@models={{array @secret.backend @path}}
|
||||
@models={{array @backend @path}}
|
||||
data-test-secrets-tab="Version History"
|
||||
>Version History</LinkTo>
|
||||
</li>
|
||||
@ -104,10 +104,10 @@
|
||||
{{#if this.showDestroy}}
|
||||
<KvDeleteModal @mode="destroy" @secret={{@secret}} @onDelete={{this.handleDestruction}} @version={{this.version}} />
|
||||
{{/if}}
|
||||
{{#if (or @secret.canReadData @secret.canReadMetadata @secret.canEditData)}}
|
||||
{{#if (or @canReadData @canReadMetadata @canUpdateData)}}
|
||||
<div class="toolbar-separator"></div>
|
||||
{{/if}}
|
||||
{{#if (and @secret.canReadData (eq @secret.state "created"))}}
|
||||
{{#if (and @canReadData (eq @secret.state "created"))}}
|
||||
<CopySecretDropdown
|
||||
@clipboardText={{stringify @secret.secretData}}
|
||||
@onWrap={{perform this.wrapSecret}}
|
||||
@ -116,7 +116,7 @@
|
||||
@onClose={{this.clearWrappedData}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if @secret.canReadMetadata}}
|
||||
{{#if @canReadMetadata}}
|
||||
<KvVersionDropdown @displayVersion={{this.version}} @metadata={{@metadata}} @onClose={{this.closeVersionMenu}} />
|
||||
{{/if}}
|
||||
{{! @isPatchAllowed is true if the version is enterprise AND a user has "patch" secret + "read" subkeys capabilities }}
|
||||
@ -125,7 +125,7 @@
|
||||
Patch latest version
|
||||
</ToolbarLink>
|
||||
{{/if}}
|
||||
{{#if @secret.canEditData}}
|
||||
{{#if @canUpdateData}}
|
||||
<ToolbarLink
|
||||
data-test-create-new-version
|
||||
@route="secret.details.edit"
|
||||
|
||||
@ -17,16 +17,26 @@ import { isAdvancedSecret } from 'core/utils/advanced-secret';
|
||||
* @module KvSecretDetails renders the key/value data of a KV secret.
|
||||
* It also renders a dropdown to display different versions of the secret.
|
||||
* <Page::Secret::Details
|
||||
* @path={{this.model.path}}
|
||||
* @secret={{this.model.secret}}
|
||||
* @metadata={{this.model.metadata}}
|
||||
* @breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
* @backend={{this.model.backend}}
|
||||
* @breadcrumbs={{this.breadcrumbs}}
|
||||
* @canReadData={{this.model.canReadData}}
|
||||
* @canReadMetadata={{this.model.canReadMetadata}}
|
||||
* @canUpdateData={{this.model.canUpdateData}}
|
||||
* @isPatchAllowed={{this.model.isPatchAllowed}}
|
||||
* @metadata={{this.model.metadata}}
|
||||
* @path={{this.model.path}}
|
||||
* @secret={{this.model.secret}}
|
||||
* />
|
||||
*
|
||||
* @param {string} backend - path where kv engine is mounted
|
||||
* @param {array} breadcrumbs - Array to generate breadcrumbs, passed to the page header component
|
||||
* @param {boolean} canReadData - if true and the secret is not destroyed/deleted the copy secret dropdown renders
|
||||
* @param {boolean} canReadMetadata - if true it renders the kv select version dropdown in the toolbar and "Version History" tab
|
||||
* @param {boolean} canUpdateData - if true it renders "Create new version" toolbar action
|
||||
* @param {boolean} isPatchAllowed - if true it renders "Patch latest version" toolbar action
|
||||
* @param {model} metadata - Ember data model: 'kv/metadata'
|
||||
* @param {string} path - path of kv secret 'my/secret' used as the title for the KV page header
|
||||
* @param {model} secret - Ember data model: 'kv/data'
|
||||
* @param {model} metadata - Ember data model: 'kv/metadata'
|
||||
* @param {array} breadcrumbs - Array to generate breadcrumbs, passed to the page header component
|
||||
*/
|
||||
|
||||
export default class KvSecretDetails extends Component {
|
||||
@ -188,7 +198,7 @@ export default class KvSecretDetails extends Component {
|
||||
}
|
||||
|
||||
get emptyState() {
|
||||
if (!this.args.secret.canReadData) {
|
||||
if (!this.args.canReadData) {
|
||||
return {
|
||||
title: 'You do not have permission to read this secret',
|
||||
message:
|
||||
@ -201,7 +211,7 @@ export default class KvSecretDetails extends Component {
|
||||
return {
|
||||
title: `Version ${version} of this secret has been permanently destroyed`,
|
||||
message: `A version that has been permanently deleted cannot be restored. ${
|
||||
this.args.secret.canReadMetadata
|
||||
this.args.canReadMetadata
|
||||
? ' You can view other versions of this secret in the Version History tab above.'
|
||||
: ''
|
||||
}`,
|
||||
@ -212,7 +222,7 @@ export default class KvSecretDetails extends Component {
|
||||
return {
|
||||
title: `Version ${version} of this secret has been deleted`,
|
||||
message: `This version has been deleted but can be undeleted. ${
|
||||
this.args.secret.canReadMetadata
|
||||
this.args.canReadMetadata
|
||||
? 'View other versions of this secret by clicking the Version History tab above.'
|
||||
: ''
|
||||
}`,
|
||||
|
||||
@ -48,9 +48,9 @@
|
||||
Custom metadata
|
||||
</h2>
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless" data-test-kv-custom-metadata-section>
|
||||
{{! if the user had read permissions and there is no custom_metadata @customMetadata is an empty object, without read capabilities it's undefined }}
|
||||
{{#if @customMetadata}}
|
||||
{{#each-in @customMetadata as |key value|}}
|
||||
{{! if the user had read permissions and there is no custom_metadata this is an empty object, without read capabilities it's falsy }}
|
||||
{{#if this.customMetadata}}
|
||||
{{#each-in this.customMetadata as |key value|}}
|
||||
<InfoTableRow @alwaysRender={{false}} @label={{key}} @value={{value}} />
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@ -69,6 +69,35 @@
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/each-in}}
|
||||
{{else if @canReadData}}
|
||||
{{! Offer opportunity to manually request /data/ for custom_metadata }}
|
||||
{{#if this.error.isControlGroup}}
|
||||
<ControlGroupInlineError @error={{this.error}} class="has-top-margin-s has-bottom-margin-s" />
|
||||
{{else if this.error}}
|
||||
<MessageError @errorMessage={{this.error}} />
|
||||
{{/if}}
|
||||
<EmptyState
|
||||
@title="Request custom metadata?"
|
||||
@bottomBorder={{true}}
|
||||
@message="You do not have access to the metadata endpoint but you can retrieve custom metadata from the secret data endpoint."
|
||||
>
|
||||
<div class="is-block">
|
||||
<Hds::Alert @type="compact" @color="critical" class="has-top-margin-xs" as |A|>
|
||||
<A.Description>
|
||||
Sensitive secret data will be retrieved.
|
||||
</A.Description>
|
||||
</Hds::Alert>
|
||||
<Hds::Button
|
||||
class="has-top-margin-xs"
|
||||
@text="Request data"
|
||||
@icon="reload"
|
||||
@iconPosition="trailing"
|
||||
@isFullWidth={{true}}
|
||||
data-test-request-data
|
||||
{{on "click" this.requestData}}
|
||||
/>
|
||||
</div>
|
||||
</EmptyState>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="You do not have access to read custom metadata"
|
||||
@ -80,7 +109,7 @@
|
||||
<h2 class="title is-5 has-bottom-padding-s has-top-margin-l">
|
||||
Secret metadata
|
||||
</h2>
|
||||
{{#if @canReadMetadata}}
|
||||
{{#if @metadata}}
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless" data-test-kv-metadata-section>
|
||||
<InfoTableRow @alwaysRender={{true}} @label="Last updated">
|
||||
<KvTooltipTimestamp @timestamp={{@metadata.updatedTime}} />
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { service } from '@ember/service';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
@ -13,10 +14,10 @@ import errorMessage from 'vault/utils/error-message';
|
||||
* <Page::Secret::Metadata::Details
|
||||
* @backend={{this.model.backend}}
|
||||
* @breadcrumbs={{this.breadcrumbs}}
|
||||
* @canDeleteMetadata={{this.model.permissions.metadata.canDelete}}
|
||||
* @canReadMetadata={{this.model.permissions.metadata.canRead}}
|
||||
* @canUpdateMetadata={{this.model.permissions.metadata.canUpdate}}
|
||||
* @customMetadata={{or this.model.metadata.customMetadata this.model.secret.customMetadata}}
|
||||
* @canDeleteMetadata={{this.model.canDeleteMetadata}}
|
||||
* @canReadData={{this.model.canReadData}}
|
||||
* @canReadMetadata={{this.model.canReadMetadata}}
|
||||
* @canUpdateMetadata={{this.model.canUpdateMetadata}}
|
||||
* @metadata={{this.model.metadata}}
|
||||
* @path={{this.model.path}}
|
||||
* />
|
||||
@ -24,9 +25,9 @@ import errorMessage from 'vault/utils/error-message';
|
||||
* @param {string} backend - The name of the kv secret engine.
|
||||
* @param {array} breadcrumbs - Array to generate breadcrumbs, passed to the page header component
|
||||
* @param {boolean} canDeleteMetadata - if true, "Permanently delete" action renders in the toolbar
|
||||
* @param {boolean} canReadData - if true, user can make a request for custom_metadata if they don't have "read" permissions for metadata
|
||||
* @param {boolean} canReadMetadata - if true, secret metadata renders below custom_metadata
|
||||
* @param {boolean} canUpdateMetadata - if true, "Edit" action renders in the toolbar
|
||||
* @param {object} customMetadata - comes from secret metadata or data endpoint. if undefined, user does not have "read" access, if an empty object then there is none
|
||||
* @param {model} metadata - Ember data model: 'kv/metadata'
|
||||
* @param {string} path - path of kv secret 'my/secret' used as the title for the KV page header
|
||||
*
|
||||
@ -34,10 +35,18 @@ import errorMessage from 'vault/utils/error-message';
|
||||
*/
|
||||
|
||||
export default class KvSecretMetadataDetails extends Component {
|
||||
@service controlGroup;
|
||||
@service flashMessages;
|
||||
@service router;
|
||||
@service store;
|
||||
|
||||
@tracked error = null;
|
||||
@tracked customMetadataFromData = null;
|
||||
|
||||
get customMetadata() {
|
||||
return this.args.metadata?.customMetadata || this.customMetadataFromData;
|
||||
}
|
||||
|
||||
@action
|
||||
async onDelete() {
|
||||
// The only delete option from this view is delete metadata and all versions
|
||||
@ -54,4 +63,22 @@ export default class KvSecretMetadataDetails extends Component {
|
||||
this.flashMessages.danger(`There was an issue deleting ${path} metadata. \n ${errorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async requestData() {
|
||||
const { backend, path } = this.args;
|
||||
try {
|
||||
const secretData = await this.store.queryRecord('kv/data', { backend, path });
|
||||
this.customMetadataFromData = secretData.customMetadata;
|
||||
} catch (error) {
|
||||
if (error.message === 'Control Group encountered') {
|
||||
this.controlGroup.saveTokenFromError(error);
|
||||
this.error = this.controlGroup.logFromError(error);
|
||||
this.error.isControlGroup = true;
|
||||
return;
|
||||
}
|
||||
this.error.isControlGroup = false;
|
||||
this.error = errorMessage(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
</Hds::Text::Display>
|
||||
</:customTitle>
|
||||
<:action>
|
||||
{{#if @canUpdateSecret}}
|
||||
{{#if @canUpdateData}}
|
||||
<Hds::Link::Standalone
|
||||
@text="Create new"
|
||||
@route="secret.details.edit"
|
||||
|
||||
@ -13,7 +13,7 @@ import { isDeleted } from 'kv/utils/kv-deleted';
|
||||
* @backend={{this.model.backend}}
|
||||
* @breadcrumbs={{this.breadcrumbs}}
|
||||
* @canReadMetadata={{true}}
|
||||
* @canUpdateSecret={{true}}
|
||||
* @canUpdateData={{true}}
|
||||
* @metadata={{this.model.metadata}}
|
||||
* @path={{this.model.path}}
|
||||
* @subkeys={{this.model.subkeys}}
|
||||
@ -22,7 +22,7 @@ import { isDeleted } from 'kv/utils/kv-deleted';
|
||||
* @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 {boolean} canUpdateData - 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 {object} subkeys - API response from subkeys endpoint, object with "subkeys" and "metadata" keys. This arg is null for community edition
|
||||
|
||||
@ -4,7 +4,15 @@
|
||||
~}}
|
||||
|
||||
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Patch Secret to New Version" />
|
||||
|
||||
{{#if this.controlGroupError}}
|
||||
<ControlGroupInlineError @error={{this.controlGroupError}} class="has-top-margin-s has-bottom-margin-s">
|
||||
<:customMessage>
|
||||
<strong>
|
||||
You can re-submit the form once access is granted. Ask your authorizer when to attempt saving again.
|
||||
</strong>
|
||||
</:customMessage>
|
||||
</ControlGroupInlineError>
|
||||
{{/if}}
|
||||
<MessageError @errorMessage={{this.errorMessage}} />
|
||||
|
||||
<div class="box is-sideless is-fullwidth is-bottomless">
|
||||
|
||||
@ -40,6 +40,7 @@ export default class KvSecretPatch extends Component {
|
||||
@service router;
|
||||
@service store;
|
||||
|
||||
@tracked controlGroupError;
|
||||
@tracked errorMessage;
|
||||
@tracked invalidFormAlert;
|
||||
@tracked patchMethod = 'UI';
|
||||
@ -60,21 +61,19 @@ export default class KvSecretPatch extends Component {
|
||||
|
||||
const { backend, path, metadata, subkeysMeta } = this.args;
|
||||
// if no metadata permission, use subkey metadata as backup
|
||||
const version = metadata.currentVersion || subkeysMeta.version;
|
||||
const version = metadata?.currentVersion || subkeysMeta?.version;
|
||||
const adapter = this.store.adapterFor('kv/data');
|
||||
try {
|
||||
yield adapter.patchSecret(backend, path, patchData, version);
|
||||
this.flashMessages.success(`Successfully patched new version of ${path}.`);
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.kv.secret.index');
|
||||
} catch (error) {
|
||||
// TODO test...this is copy pasta'd from the edit page
|
||||
let message = errorMessage(error);
|
||||
if (error.message === 'Control Group encountered') {
|
||||
this.controlGroup.saveTokenFromError(error);
|
||||
const err = this.controlGroup.logFromError(error);
|
||||
message = err.content;
|
||||
this.controlGroupError = this.controlGroup.logFromError(error);
|
||||
return;
|
||||
}
|
||||
this.errorMessage = message;
|
||||
this.errorMessage = errorMessage(error);
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,33 +35,38 @@ export default class KvSecretRoute extends Route {
|
||||
return null;
|
||||
}
|
||||
|
||||
isPatchAllowed(backend, path) {
|
||||
isPatchAllowed({ subkeys, data }) {
|
||||
if (!this.version.isEnterprise) return false;
|
||||
const capabilities = {
|
||||
canPatch: this.capabilities.canPatch(`${backend}/data/${path}`),
|
||||
canReadSubkeys: this.capabilities.canRead(`${backend}/subkeys/${path}`),
|
||||
};
|
||||
return hash(capabilities).then(
|
||||
({ canPatch, canReadSubkeys }) => canPatch && canReadSubkeys,
|
||||
// this callback fires if either promise is rejected
|
||||
// since this feature is only client-side gated we return false (instead of default to true)
|
||||
// for debugging you can pass an arg to log the failure reason
|
||||
() => false
|
||||
);
|
||||
return subkeys.canRead && data.canPatch;
|
||||
}
|
||||
|
||||
model() {
|
||||
async fetchCapabilities(backend, path) {
|
||||
const metadataPath = `${backend}/metadata/${path}`;
|
||||
const dataPath = `${backend}/data/${path}`;
|
||||
const subkeysPath = `${backend}/subkeys/${path}`;
|
||||
const perms = await this.capabilities.fetchMultiplePaths([metadataPath, dataPath, subkeysPath]);
|
||||
return {
|
||||
metadata: perms[metadataPath],
|
||||
data: perms[dataPath],
|
||||
subkeys: perms[subkeysPath],
|
||||
};
|
||||
}
|
||||
|
||||
async model() {
|
||||
const backend = this.secretMountPath.currentPath;
|
||||
const { name: path } = this.paramsFor('secret');
|
||||
|
||||
const capabilities = await this.fetchCapabilities(backend, path);
|
||||
return hash({
|
||||
path,
|
||||
backend,
|
||||
subkeys: this.fetchSubkeys(backend, path),
|
||||
metadata: this.fetchSecretMetadata(backend, path),
|
||||
isPatchAllowed: this.isPatchAllowed(backend, path),
|
||||
// for creating a new secret version
|
||||
canUpdateSecret: this.capabilities.canUpdate(`${backend}/data/${path}`),
|
||||
isPatchAllowed: this.isPatchAllowed(capabilities),
|
||||
canUpdateData: capabilities.data.canUpdate,
|
||||
canReadData: capabilities.data.canRead,
|
||||
canReadMetadata: capabilities.metadata.canRead,
|
||||
canDeleteMetadata: capabilities.metadata.canDelete,
|
||||
canUpdateMetadata: capabilities.metadata.canUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -20,44 +20,22 @@ export default class KvSecretMetadataRoute extends Route {
|
||||
});
|
||||
}
|
||||
|
||||
async fetchCapabilities(backend, path) {
|
||||
const metadataPath = `${backend}/metadata/${path}`;
|
||||
const dataPath = `${backend}/data/${path}`;
|
||||
const capabilities = await this.capabilities.fetchMultiplePaths([metadataPath, dataPath]);
|
||||
return {
|
||||
metadata: capabilities[metadataPath],
|
||||
data: capabilities[dataPath],
|
||||
};
|
||||
}
|
||||
|
||||
async model() {
|
||||
const parentModel = this.modelFor('secret');
|
||||
const { backend, path } = parentModel;
|
||||
const permissions = await this.fetchCapabilities(backend, path);
|
||||
const model = {
|
||||
...parentModel,
|
||||
permissions,
|
||||
};
|
||||
if (!parentModel.metadata) {
|
||||
// metadata read on the secret root fails silently
|
||||
// if there's no metadata, try again in case it's a control group
|
||||
const metadata = await this.fetchMetadata(backend, path);
|
||||
if (metadata) {
|
||||
return {
|
||||
...model,
|
||||
...parentModel,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
// only fetch secret data if metadata is unavailable and user can read endpoint
|
||||
if (permissions.data.canRead) {
|
||||
// fail silently because this request is just for custom_metadata
|
||||
const secret = await this.store.queryRecord('kv/data', { backend, path }).catch(() => {});
|
||||
return {
|
||||
...model,
|
||||
secret,
|
||||
};
|
||||
}
|
||||
}
|
||||
return model;
|
||||
// if users can read secret data they can make an explicit request
|
||||
// to retrieve secret data in the component
|
||||
return parentModel;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,9 +4,13 @@
|
||||
~}}
|
||||
|
||||
<Page::Secret::Details
|
||||
@backend={{this.model.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
@canReadData={{this.model.canReadData}}
|
||||
@canReadMetadata={{this.model.canReadMetadata}}
|
||||
@canUpdateData={{this.model.canUpdateData}}
|
||||
@isPatchAllowed={{this.model.isPatchAllowed}}
|
||||
@metadata={{this.model.metadata}}
|
||||
@path={{this.model.path}}
|
||||
@secret={{this.model.secret}}
|
||||
@metadata={{this.model.metadata}}
|
||||
/>
|
||||
@ -7,8 +7,8 @@
|
||||
@backend={{this.model.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
@canReadMetadata={{this.model.metadata.canReadMetadata}}
|
||||
@canUpdateData={{this.model.canUpdateData}}
|
||||
@isPatchAllowed={{this.model.isPatchAllowed}}
|
||||
@canUpdateSecret={{this.model.canUpdateSecret}}
|
||||
@metadata={{this.model.metadata}}
|
||||
@path={{this.model.path}}
|
||||
@subkeys={{this.model.subkeys}}
|
||||
|
||||
@ -6,10 +6,10 @@
|
||||
<Page::Secret::Metadata::Details
|
||||
@backend={{this.model.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
@canDeleteMetadata={{this.model.permissions.metadata.canDelete}}
|
||||
@canReadMetadata={{this.model.permissions.metadata.canRead}}
|
||||
@canUpdateMetadata={{this.model.permissions.metadata.canUpdate}}
|
||||
@customMetadata={{or this.model.metadata.customMetadata this.model.secret.customMetadata}}
|
||||
@canDeleteMetadata={{this.model.canDeleteMetadata}}
|
||||
@canReadData={{this.model.canReadData}}
|
||||
@canReadMetadata={{this.model.canReadMetadata}}
|
||||
@canUpdateMetadata={{this.model.canUpdateMetadata}}
|
||||
@metadata={{this.model.metadata}}
|
||||
@path={{this.model.path}}
|
||||
/>
|
||||
@ -17,6 +17,9 @@ const data = {
|
||||
max_versions: 15,
|
||||
oldest_version: 0,
|
||||
updated_time: '2023-07-21T03:11:58.095971Z',
|
||||
// the API returns custom_metadata: null if empty but because the attr is an 'object' ember data transforms it to an empty object.
|
||||
// this is important because we rely on the empty object as a truthy value in template conditionals
|
||||
custom_metadata: null,
|
||||
versions: {
|
||||
1: {
|
||||
created_time: '2018-03-22T02:24:06.945319214Z',
|
||||
@ -45,10 +48,13 @@ export default Factory.extend({
|
||||
data,
|
||||
|
||||
withCustomMetadata: trait({
|
||||
custom_metadata: {
|
||||
foo: 'abc',
|
||||
bar: '123',
|
||||
baz: '5c07d823-3810-48f6-a147-4c06b5219e84',
|
||||
data: {
|
||||
...data,
|
||||
custom_metadata: {
|
||||
foo: 'abc',
|
||||
bar: '123',
|
||||
baz: '5c07d823-3810-48f6-a147-4c06b5219e84',
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
|
||||
@ -403,6 +403,10 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
|
||||
|
||||
// Metadata page
|
||||
await click(PAGE.secretTab('Metadata'));
|
||||
assert
|
||||
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
||||
.hasText('Request custom metadata?');
|
||||
await click(PAGE.metadata.requestData);
|
||||
assert
|
||||
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
||||
.hasText('No custom metadata', 'No custom metadata empty state');
|
||||
@ -548,6 +552,10 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
|
||||
|
||||
// Metadata page
|
||||
await click(PAGE.secretTab('Metadata'));
|
||||
assert
|
||||
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
||||
.hasText('Request custom metadata?');
|
||||
await click(PAGE.metadata.requestData);
|
||||
assert
|
||||
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
||||
.hasText('No custom metadata', 'No custom metadata empty state');
|
||||
|
||||
@ -122,7 +122,7 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) {
|
||||
await click(PAGE.list.item(secret));
|
||||
assert
|
||||
.dom(GENERAL.overviewCard.container('Current version'))
|
||||
.hasText(`Current version Create new The current version of this secret. 1`);
|
||||
.hasText(`Current version The current version of this secret. 1`);
|
||||
// Secret details visible
|
||||
await click(PAGE.secretTab('Secret'));
|
||||
assert.dom(PAGE.title).hasText(this.fullSecretPath);
|
||||
|
||||
@ -5,7 +5,17 @@
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { click, currentRouteName, currentURL, findAll, typeIn, visit, waitUntil } from '@ember/test-helpers';
|
||||
import {
|
||||
click,
|
||||
currentRouteName,
|
||||
currentURL,
|
||||
find,
|
||||
findAll,
|
||||
fillIn,
|
||||
typeIn,
|
||||
visit,
|
||||
waitUntil,
|
||||
} from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'vault/tests/helpers';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import {
|
||||
@ -571,7 +581,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
|
||||
assert.true(currentURL().startsWith(`/vault/secrets/${backend}/kv/list`), 'links back to list root');
|
||||
});
|
||||
test('versioned secret nav, tabs, breadcrumbs (dr)', async function (assert) {
|
||||
assert.expect(31);
|
||||
assert.expect(32);
|
||||
const backend = this.backend;
|
||||
await navToBackend(backend);
|
||||
|
||||
@ -614,6 +624,10 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
|
||||
assertCorrectBreadcrumbs(assert, ['Secrets', backend, secretPath, 'Metadata']);
|
||||
assert.dom(PAGE.title).hasText(secretPath);
|
||||
assert.dom(PAGE.toolbarAction).doesNotExist('no toolbar actions available on metadata');
|
||||
assert
|
||||
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
||||
.hasText('Request custom metadata?');
|
||||
await click(PAGE.metadata.requestData);
|
||||
assert
|
||||
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
||||
.hasText('No custom metadata');
|
||||
@ -764,7 +778,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
|
||||
assert.true(currentURL().startsWith(`/vault/secrets/${backend}/kv/list`), 'links back to list root');
|
||||
});
|
||||
test('versioned secret nav, tabs, breadcrumbs (dlr)', async function (assert) {
|
||||
assert.expect(31);
|
||||
assert.expect(32);
|
||||
const backend = this.backend;
|
||||
await navToBackend(backend);
|
||||
await click(PAGE.list.item(secretPath));
|
||||
@ -804,6 +818,10 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
|
||||
);
|
||||
assertCorrectBreadcrumbs(assert, ['Secrets', backend, secretPath, 'Metadata']);
|
||||
assert.dom(PAGE.title).hasText(secretPath);
|
||||
assert
|
||||
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
||||
.hasText('Request custom metadata?');
|
||||
await click(PAGE.metadata.requestData);
|
||||
assert
|
||||
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
||||
.hasText('No custom metadata');
|
||||
@ -1291,11 +1309,11 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
|
||||
// Set up control group scenario
|
||||
const userPolicy = `
|
||||
path "${this.backend}/data/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list"]
|
||||
capabilities = ["create", "read", "update", "delete", "list", "patch"]
|
||||
control_group = {
|
||||
max_ttl = "24h"
|
||||
factor "ops_manager" {
|
||||
controlled_capabilities = ["read"]
|
||||
controlled_capabilities = ["read", "patch"]
|
||||
identity {
|
||||
group_names = ["managers"]
|
||||
approvals = 1
|
||||
@ -1307,6 +1325,10 @@ path "${this.backend}/data/*" {
|
||||
path "${this.backend}/*" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
|
||||
path "${this.backend}/subkeys/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
`;
|
||||
const { userToken } = await setupControlGroup({ userPolicy, backend: this.backend });
|
||||
this.userToken = userToken;
|
||||
@ -1315,7 +1337,7 @@ path "${this.backend}/*" {
|
||||
return;
|
||||
});
|
||||
test('can access nested secret (cg)', async function (assert) {
|
||||
assert.expect(43);
|
||||
assert.expect(44);
|
||||
const backend = this.backend;
|
||||
await navToBackend(backend);
|
||||
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'title text correct');
|
||||
@ -1379,7 +1401,7 @@ path "${this.backend}/*" {
|
||||
);
|
||||
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'app', 'nested', 'secret']);
|
||||
assert.dom(PAGE.title).hasText('app/nested/secret', 'title is full secret path');
|
||||
assertDetailsToolbar(assert, ['delete', 'copy', 'createNewVersion']);
|
||||
assertDetailsToolbar(assert, ['delete', 'copy', 'createNewVersion', 'patchLatest']);
|
||||
|
||||
await click(PAGE.breadcrumbAtIdx(3));
|
||||
assert.true(
|
||||
@ -1397,7 +1419,7 @@ path "${this.backend}/*" {
|
||||
assert.true(currentURL().startsWith(`/vault/secrets/${backend}/kv/list`), 'links back to list root');
|
||||
});
|
||||
test('breadcrumbs & page titles are correct (cg)', async function (assert) {
|
||||
assert.expect(43);
|
||||
assert.expect(42);
|
||||
const backend = this.backend;
|
||||
await navToBackend(backend);
|
||||
await click(PAGE.secretTab('Configuration'));
|
||||
@ -1446,17 +1468,6 @@ path "${this.backend}/*" {
|
||||
|
||||
assert.dom(PAGE.secretTab('Version History')).doesNotExist('Version History tab not shown');
|
||||
|
||||
await click(PAGE.secretTab('Secret'));
|
||||
assert.true(
|
||||
await waitUntil(() => currentRouteName() === 'vault.cluster.access.control-group-accessor'),
|
||||
'redirects to access control group route'
|
||||
);
|
||||
await grantAccess({
|
||||
apiPath: `${backend}/data/${encodeURIComponent(secretPath)}`,
|
||||
originUrl: `/vault/secrets/${backend}/kv/${secretPathUrlEncoded}/paths`,
|
||||
userToken: this.userToken,
|
||||
backend: this.backend,
|
||||
});
|
||||
await click(PAGE.secretTab('Secret'));
|
||||
assertCorrectBreadcrumbs(assert, ['Secrets', backend, secretPath]);
|
||||
assert.dom(PAGE.title).hasText(secretPath, 'correct page title for secret details');
|
||||
@ -1464,6 +1475,102 @@ path "${this.backend}/*" {
|
||||
assertCorrectBreadcrumbs(assert, ['Secrets', backend, secretPath, 'Edit']);
|
||||
assert.dom(PAGE.title).hasText('Create New Version', 'correct page title for secret edit');
|
||||
});
|
||||
test('can request custom_metadata from data endpoint (cg)', async function (assert) {
|
||||
// custom metadata is empty
|
||||
assert.expect(3);
|
||||
const backend = this.backend;
|
||||
await visit(`/vault/secrets/${backend}/kv/${secretPathUrlEncoded}`);
|
||||
await click(PAGE.secretTab('Metadata'));
|
||||
assert
|
||||
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
||||
.hasText('Request custom metadata?');
|
||||
await click(PAGE.metadata.requestData);
|
||||
assert
|
||||
.dom(GENERAL.messageError)
|
||||
.hasTextContaining(
|
||||
`Control Group Error A Control Group was encountered at ${backend}/data/${secretPath}.`
|
||||
);
|
||||
const url = find('[data-test-control-error="href"]').innerText;
|
||||
await visit(url);
|
||||
await grantAccess({
|
||||
apiPath: `${backend}/data/${encodeURIComponent(secretPath)}`,
|
||||
originUrl: `/vault/secrets/${backend}/kv/${secretPathUrlEncoded}/metadata`,
|
||||
userToken: this.userToken,
|
||||
backend: this.backend,
|
||||
});
|
||||
await click(PAGE.metadata.requestData);
|
||||
assert
|
||||
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
||||
.hasText('No custom metadata', 'empty state updates when access is granted');
|
||||
});
|
||||
test('can patch a secret (cg)', async function (assert) {
|
||||
assert.expect(3);
|
||||
const backend = this.backend;
|
||||
await visit(`/vault/secrets/${backend}/kv/${secretPathUrlEncoded}`);
|
||||
await click(GENERAL.overviewCard.actionText('Patch secret'));
|
||||
await fillIn(FORM.keyInput('new'), 'newkey');
|
||||
await fillIn(FORM.valueInput('new'), 'newvalue');
|
||||
await click(FORM.saveBtn);
|
||||
assert
|
||||
.dom(GENERAL.messageError)
|
||||
.hasTextContaining(
|
||||
`Control Group Error A Control Group was encountered at ${backend}/data/${secretPath}.`
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.messageError)
|
||||
.hasTextContaining(
|
||||
'You can re-submit the form once access is granted. Ask your authorizer when to attempt saving again.'
|
||||
);
|
||||
const url = find('[data-test-control-error="href"]').innerText;
|
||||
await visit(url);
|
||||
await grantAccess({
|
||||
apiPath: `${backend}/data/${encodeURIComponent(secretPath)}`,
|
||||
originUrl: `/vault/secrets/${backend}/kv/${secretPathUrlEncoded}/patch`,
|
||||
userToken: this.userToken,
|
||||
backend: this.backend,
|
||||
});
|
||||
// we have to refill the data because granting access reloads the form
|
||||
// however in the real world it's likely access is authorized in a separate browser
|
||||
// once granted, the user can click "submit" the form will save successfully.
|
||||
await fillIn(FORM.keyInput('new'), 'newkey');
|
||||
await fillIn(FORM.valueInput('new'), 'newvalue');
|
||||
await click(FORM.saveBtn);
|
||||
assert.dom(GENERAL.overviewCard.container('Subkeys')).hasTextContaining('Keys foo newkey');
|
||||
});
|
||||
test('can read custom_metadata from data endpoint (cg)', async function (assert) {
|
||||
assert.expect(3);
|
||||
// login is root user and make custom metadata since console can't be used to pass an object
|
||||
await authPage.login();
|
||||
await visit(`/vault/secrets/${this.backend}/kv/${secretPathUrlEncoded}/metadata/edit`);
|
||||
await fillIn(FORM.keyInput(), 'special');
|
||||
await fillIn(FORM.valueInput(), 'secret');
|
||||
await click(FORM.saveBtn);
|
||||
await authPage.login(this.userToken);
|
||||
|
||||
const backend = this.backend;
|
||||
await visit(`/vault/secrets/${backend}/kv/${secretPathUrlEncoded}`);
|
||||
|
||||
await click(PAGE.secretTab('Metadata'));
|
||||
assert
|
||||
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
||||
.hasText('Request custom metadata?');
|
||||
await click(PAGE.metadata.requestData);
|
||||
assert
|
||||
.dom(GENERAL.messageError)
|
||||
.hasTextContaining(
|
||||
`Control Group Error A Control Group was encountered at ${backend}/data/${secretPath}.`
|
||||
);
|
||||
const url = find('[data-test-control-error="href"]').innerText;
|
||||
await visit(url);
|
||||
await grantAccess({
|
||||
apiPath: `${backend}/data/${encodeURIComponent(secretPath)}`,
|
||||
originUrl: `/vault/secrets/${backend}/kv/${secretPathUrlEncoded}/metadata`,
|
||||
userToken: this.userToken,
|
||||
backend: this.backend,
|
||||
});
|
||||
await click(PAGE.metadata.requestData);
|
||||
assert.dom(PAGE.infoRowValue('special')).hasText('secret', 'it renders custom metadata');
|
||||
});
|
||||
});
|
||||
|
||||
// patch is technically enterprise only but stubbing the version so they can run on both CE and enterprise
|
||||
|
||||
@ -29,6 +29,7 @@ export const PAGE = {
|
||||
link: (backend) => `[data-test-secrets-backend-link="${backend}"]`,
|
||||
},
|
||||
metadata: {
|
||||
requestData: '[data-test-request-data]',
|
||||
editBtn: '[data-test-edit-metadata]',
|
||||
addCustomMetadataBtn: '[data-test-add-custom-metadata]',
|
||||
customMetadataSection: '[data-test-kv-custom-metadata-section]',
|
||||
|
||||
@ -221,7 +221,7 @@ module('Integration | Component | edit form kmip role', function (hooks) {
|
||||
);
|
||||
}
|
||||
|
||||
click('[data-test-edit-form-submit]');
|
||||
await click('[data-test-edit-form-submit]');
|
||||
|
||||
later(() => cancelTimers(), 50);
|
||||
await settled();
|
||||
|
||||
@ -9,7 +9,6 @@ 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 { kvDataPath } from 'vault/utils/kv-path';
|
||||
import { PAGE } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
import { baseSetup, metadataModel } from 'vault/tests/helpers/kv/kv-run-commands';
|
||||
import { dateFormat } from 'core/helpers/date-format';
|
||||
@ -20,15 +19,8 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', func
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
// this.metadata is setup by baseSetup
|
||||
baseSetup(this);
|
||||
this.dataId = kvDataPath(this.backend, this.path);
|
||||
// empty secret model always exists for permissions
|
||||
this.store.pushPayload('kv/data', {
|
||||
modelName: 'kv/data',
|
||||
id: this.dataId,
|
||||
custom_metadata: null,
|
||||
});
|
||||
this.secret = this.store.peekRecord('kv/data', this.dataId);
|
||||
|
||||
// this is the route model, not an ember data model
|
||||
this.model = {
|
||||
@ -36,26 +28,28 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', func
|
||||
path: this.path,
|
||||
secret: this.secret,
|
||||
metadata: this.metadata,
|
||||
canDeleteMetadata: true,
|
||||
canReadData: true,
|
||||
canReadCustomMetadata: true,
|
||||
canReadMetadata: true,
|
||||
canUpdateMetadata: true,
|
||||
};
|
||||
this.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.model.backend, route: 'list' },
|
||||
{ label: this.model.path },
|
||||
];
|
||||
this.canDeleteMetadata = true;
|
||||
this.canReadCustomMetadata = true;
|
||||
this.canReadMetadata = true;
|
||||
this.canUpdateMetadata = true;
|
||||
|
||||
this.renderComponent = () => {
|
||||
return render(
|
||||
hbs`
|
||||
<Page::Secret::Metadata::Details
|
||||
@backend={{this.model.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
@canDeleteMetadata={{this.canDeleteMetadata}}
|
||||
@canReadMetadata={{this.canReadMetadata}}
|
||||
@canUpdateMetadata={{this.canReadMetadata}}
|
||||
@customMetadata={{or this.model.metadata.customMetadata this.model.secret.customMetadata}}
|
||||
@canDeleteMetadata={{this.model.canDeleteMetadata}}
|
||||
@canReadData={{this.model.canReadData}}
|
||||
@canReadMetadata={{this.model.canReadMetadata}}
|
||||
@canUpdateMetadata={{this.model.canUpdateMetadata}}
|
||||
@metadata={{this.model.metadata}}
|
||||
@path={{this.model.path}}
|
||||
/>
|
||||
@ -86,13 +80,12 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', func
|
||||
.hasText('3 hours 25 minutes 19 seconds', 'correctly shows and formats the timestamp.');
|
||||
});
|
||||
|
||||
test('it renders custom metadata from secret model', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.secret.customMetadata = { hi: 'there' };
|
||||
test('it renders empty state if cannot read metadata but can read data', async function (assert) {
|
||||
this.model.metadata = null;
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(PAGE.emptyStateTitle).doesNotExist();
|
||||
assert.dom(PAGE.infoRowValue('hi')).hasText('there', 'renders custom metadata from secret');
|
||||
assert
|
||||
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
||||
.hasText('Request custom metadata?');
|
||||
});
|
||||
|
||||
test('it renders custom metadata from metadata model', async function (assert) {
|
||||
@ -107,26 +100,13 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', func
|
||||
assert.dom(PAGE.infoRowValue('baz')).hasText('5c07d823-3810-48f6-a147-4c06b5219e84');
|
||||
});
|
||||
|
||||
test('it renders custom metadata from metadata if secret data exists', async function (assert) {
|
||||
assert.expect(4);
|
||||
this.secret.customMetadata = { hi: 'there' };
|
||||
this.model.metadata = metadataModel(this, { withCustom: true });
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(PAGE.emptyStateTitle).doesNotExist();
|
||||
// Metadata details
|
||||
assert.dom(PAGE.infoRowValue('foo')).hasText('abc');
|
||||
assert.dom(PAGE.infoRowValue('bar')).hasText('123');
|
||||
assert.dom(PAGE.infoRowValue('baz')).hasText('5c07d823-3810-48f6-a147-4c06b5219e84');
|
||||
});
|
||||
|
||||
test('it hides delete modal when no permissions', async function (assert) {
|
||||
this.canDeleteMetadata = false;
|
||||
this.model.canDeleteMetadata = false;
|
||||
assert.dom(PAGE.metadata.deleteMetadata).doesNotExist();
|
||||
});
|
||||
|
||||
test('it hides edit action when no permissions', async function (assert) {
|
||||
this.canUpdateMetadata = false;
|
||||
this.model.canUpdateMetadata = false;
|
||||
assert.dom(PAGE.metadata.editBtn).doesNotExist();
|
||||
});
|
||||
});
|
||||
|
||||
@ -47,7 +47,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Overview', function (hoo
|
||||
},
|
||||
};
|
||||
this.canReadMetadata = true;
|
||||
this.canUpdateSecret = true;
|
||||
this.canUpdateData = true;
|
||||
|
||||
this.format = (time) => dateFormat([time, 'MMM d yyyy, h:mm:ss aa'], {});
|
||||
this.renderComponent = async () => {
|
||||
@ -57,7 +57,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Overview', function (hoo
|
||||
@backend={{this.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
@canReadMetadata={{this.canReadMetadata}}
|
||||
@canUpdateSecret={{this.canUpdateSecret}}
|
||||
@canUpdateData={{this.canUpdateData}}
|
||||
@metadata={{this.metadata}}
|
||||
@path={{this.path}}
|
||||
@subkeys={{this.subkeys}}
|
||||
@ -116,7 +116,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Overview', function (hoo
|
||||
// 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;
|
||||
this.canUpdateData = false;
|
||||
await this.renderComponent();
|
||||
assert
|
||||
.dom(`${overviewCard.container('Current version')} a`)
|
||||
|
||||
@ -38,7 +38,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Patch', function (hooks)
|
||||
},
|
||||
quux: null,
|
||||
};
|
||||
this.subkeyMeta = {
|
||||
this.subkeysMeta = {
|
||||
created_time: '2021-12-14T20:28:00.773477Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '',
|
||||
@ -244,6 +244,38 @@ module('Integration | Component | kv-v2 | Page::Secret::Patch', function (hooks)
|
||||
await codemirror().setValue('{ "foo": "" }');
|
||||
await click(FORM.saveBtn);
|
||||
});
|
||||
|
||||
test('patch data without metadata permissions', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.metadata = null;
|
||||
this.server.patch(this.endpoint, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
const expected = {
|
||||
data: { aKey: '1' },
|
||||
options: {
|
||||
cas: this.subkeysMeta.version,
|
||||
},
|
||||
};
|
||||
assert.true(true, `PATCH request made to ${this.endpoint}`);
|
||||
assert.propEqual(
|
||||
payload,
|
||||
expected,
|
||||
`payload: ${JSON.stringify(payload)} matches expected: ${JSON.stringify(payload)}`
|
||||
);
|
||||
return EXAMPLE_KV_DATA_CREATE_RESPONSE;
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
await fillIn(FORM.keyInput('new'), 'aKey');
|
||||
await fillIn(FORM.valueInput('new'), '1');
|
||||
await click(FORM.saveBtn);
|
||||
const [route] = this.transitionStub.lastCall.args;
|
||||
assert.strictEqual(
|
||||
route,
|
||||
'vault.cluster.secrets.backend.kv.secret.index',
|
||||
`it transitions on save to: ${route}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('it does not submit', function (hooks) {
|
||||
|
||||
@ -26,7 +26,6 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
||||
this.version = 2;
|
||||
this.dataId = kvDataPath(this.backend, this.path);
|
||||
this.dataIdComplex = kvDataPath(this.backend, this.pathComplex);
|
||||
|
||||
this.secretData = { foo: 'bar' };
|
||||
this.store.pushPayload('kv/data', {
|
||||
modelName: 'kv/data',
|
||||
@ -58,13 +57,17 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
||||
});
|
||||
this.secret = this.store.peekRecord('kv/data', this.dataId);
|
||||
this.secretComplex = this.store.peekRecord('kv/data', this.dataIdComplex);
|
||||
|
||||
// this is the route model, not an ember data model
|
||||
this.model = {
|
||||
backend: this.backend,
|
||||
// permissions are tested in navigation acceptance test, so just stub as all true here
|
||||
canReadData: true,
|
||||
canReadMetadata: true,
|
||||
canUpdateData: true,
|
||||
isPatchAllowed: true,
|
||||
metadata: this.metadata,
|
||||
path: this.path,
|
||||
secret: this.secret,
|
||||
metadata: this.metadata,
|
||||
};
|
||||
this.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
@ -77,6 +80,25 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
||||
secret: this.secretComplex,
|
||||
metadata: this.metadata,
|
||||
};
|
||||
this.renderComponent = (model) => {
|
||||
this.model = model ? { ...this.model, ...model } : this.model;
|
||||
return render(
|
||||
hbs`
|
||||
<Page::Secret::Details
|
||||
@backend={{this.model.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
@canReadData={{this.model.canReadData}}
|
||||
@canReadMetadata={{this.model.canReadMetadata}}
|
||||
@canUpdateData={{this.model.canUpdateData}}
|
||||
@isPatchAllowed={{this.model.isPatchAllowed}}
|
||||
@metadata={{this.model.metadata}}
|
||||
@path={{this.model.path}}
|
||||
@secret={{this.model.secret}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
test('it renders secret details and toggles json view', async function (assert) {
|
||||
@ -94,19 +116,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
||||
// no records so response returns 404
|
||||
return syncStatusResponse(schema, req);
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Details
|
||||
@path={{this.model.path}}
|
||||
@secret={{this.model.secret}}
|
||||
@metadata={{this.model.metadata}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
await this.renderComponent();
|
||||
assert
|
||||
.dom(PAGE.detail.syncAlert())
|
||||
.doesNotExist('sync page alert banner does not render when sync status errors');
|
||||
@ -125,16 +135,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
||||
|
||||
test('it renders json view when secret is complex', async function (assert) {
|
||||
assert.expect(4);
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Details
|
||||
@path={{this.modelComplex.path}}
|
||||
@secret={{this.modelComplex.secret}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
await this.renderComponent(this.modelComplex);
|
||||
assert.dom(PAGE.infoRowValue('foo')).doesNotExist('does not render rows of secret data');
|
||||
assert.dom(FORM.toggleJson).isChecked();
|
||||
assert.dom(FORM.toggleJson).isNotDisabled();
|
||||
@ -144,17 +145,8 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
||||
test('it renders deleted empty state', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.secret.deletionTime = '2023-07-23T02:12:17.379762Z';
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Details
|
||||
@path={{this.model.path}}
|
||||
@secret={{this.model.secret}}
|
||||
@metadata={{this.model.metadata}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(PAGE.emptyStateTitle).hasText('Version 2 of this secret has been deleted');
|
||||
assert
|
||||
.dom(PAGE.emptyStateMessage)
|
||||
@ -169,17 +161,8 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
||||
test('it renders destroyed empty state', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.secret.destroyed = true;
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Details
|
||||
@path={{this.model.path}}
|
||||
@secret={{this.model.secret}}
|
||||
@metadata={{this.model.metadata}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(PAGE.emptyStateTitle).hasText('Version 2 of this secret has been permanently destroyed');
|
||||
assert
|
||||
.dom(PAGE.emptyStateMessage)
|
||||
@ -190,18 +173,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
||||
|
||||
test('it renders secret version dropdown', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Details
|
||||
@path={{this.model.path}}
|
||||
@secret={{this.model.secret}}
|
||||
@metadata={{this.model.metadata}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(PAGE.detail.versionTimestamp).includesText(this.version, 'renders version');
|
||||
assert.dom(PAGE.detail.versionDropdown).hasText(`Version ${this.secret.version}`);
|
||||
@ -246,18 +218,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
||||
return syncStatusResponse(schema, req);
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Details
|
||||
@path={{this.model.path}}
|
||||
@secret={{this.model.secret}}
|
||||
@metadata={{this.model.metadata}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
await this.renderComponent();
|
||||
assert
|
||||
.dom(PAGE.detail.syncAlert(destinationName))
|
||||
.hasTextContaining(
|
||||
@ -290,18 +251,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
||||
creation_path: `${this.backend}/data/${this.path}}`,
|
||||
};
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Details
|
||||
@path={{this.model.path}}
|
||||
@secret={{this.model.secret}}
|
||||
@metadata={{this.model.metadata}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
await this.renderComponent();
|
||||
|
||||
await click(PAGE.detail.copy);
|
||||
await click(PAGE.detail.wrap);
|
||||
@ -325,17 +275,8 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
||||
return syncStatusResponse(schema, req);
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Details
|
||||
@path={{this.model.path}}
|
||||
@secret={{this.model.secret}}
|
||||
@metadata={{this.model.metadata}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
await this.renderComponent();
|
||||
|
||||
assert
|
||||
.dom(PAGE.detail.syncAlert('aws-dest'))
|
||||
.hasTextContaining('Synced aws-dest - last updated September', 'renders status for aws destination');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user