UI: Fix Showing KMIP Credentials (#30778)

* move role-form files

* Revert "move role-form files"

This reverts commit ad16dd059b.

* show credentials after generating

* add changelog

* periods
This commit is contained in:
claire bontempo 2025-05-28 13:20:42 -07:00 committed by GitHub
parent 71071183a6
commit 1b61e2e187
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 144 additions and 62 deletions

3
changelog/30778.txt Normal file
View file

@ -0,0 +1,3 @@
```release-note:bug
ui/kmip: Fixes KMIP credentials view and displays `private_key` after generating
```

View file

@ -0,0 +1,62 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Toolbar>
<ToolbarActions>
{{#if @credentials.deletePath.canDelete}}
<ConfirmAction
@buttonText="Revoke credentials"
class="toolbar-button"
@buttonColor="secondary"
@onConfirmAction={{@onRevokeCredentials}}
@confirmTitle="Revoke this?"
@confirmMessage="Any client using these credentials will no longer be able to."
/>
<div class="toolbar-separator"></div>
{{/if}}
<Hds::Copy::Button
@text="Copy certificate"
@textToCopy={{@credentials.certificate}}
@onError={{(fn (set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger"))}}
class="toolbar-button"
data-test-copy-button
/>
<div class="toolbar-separator"></div>
<ToolbarLink
@route="credentials.index"
@models={{array @credentials.scope @credentials.role}}
data-test-kmip-link-back-to-role
>
Back to role
</ToolbarLink>
</ToolbarActions>
</Toolbar>
<div class="box is-shadowless is-fullwidth is-sideless">
<InfoTableRow @label="Serial number" @value={{@credentials.id}}>
<MaskedInput @value={{@credentials.id}} @displayOnly={{true}} @allowCopy={{true}} />
</InfoTableRow>
{{#if @credentials.privateKey}}
<InfoTableRow @label="Private key" @value={{@credentials.privateKey}}>
<div class="is-block">
<Hds::Alert data-test-warning @type="inline" @color="warning" @class="has-bottom-margin-s" as |A|>
<A.Title>Warning</A.Title>
<A.Description>You will not be able to access the private key later, so please copy the information below.</A.Description>
</Hds::Alert>
<MaskedInput @value={{@credentials.privateKey}} @name="Private key" @allowCopy={{true}} @displayOnly={{true}} />
</div>
</InfoTableRow>
{{/if}}
<InfoTableRow @label="Certificate" @value={{@credentials.certificate}}>
<MaskedInput @value={{@credentials.certificate}} @displayOnly={{true}} @allowCopy={{true}} />
</InfoTableRow>
<InfoTableRow @label="CA Chain" @value={{@credentials.caChain}}>
<div class="is-block">
{{#each @credentials.caChain as |chain|}}
<MaskedInput @value={{chain}} @displayOnly={{true}} @allowCopy={{true}} />
{{/each}}
</div>
</InfoTableRow>
</div>

View file

@ -0,0 +1,16 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
{{#if this.hasGenerated}}
<DetailsCredentials @credentials={{@credentials}} @onRevokeCredentials={{this.revokeCredentials}} />
{{else}}
<EditForm
@model={{@credentials}}
@onSave={{fn (mut this.hasGenerated) true}}
@callOnSaveAfterRender={{true}}
@successMessage="Successfully generated credentials from role: {{@credentials.role}}!"
@cancelLinkParams={{array "credentials.index"}}
/>
{{/if}}

View file

@ -0,0 +1,30 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import errorMessage from 'vault/utils/error-message';
export default class KmipPageCredentialsGenerate extends Component {
@service flashMessages;
@service('app-router') router;
@tracked hasGenerated = false;
@action
async revokeCredentials() {
const { scope, role } = this.args.credentials;
try {
await this.args.credentials.destroyRecord();
this.flashMessages.success('Successfully revoked credentials.');
this.router.transitionTo('vault.cluster.secrets.backend.kmip.credentials.index', scope, role);
} catch (e) {
const message = errorMessage(e);
this.flashMessages.danger(`There was an error revoking credentials: ${message}.`);
this.args.credentials.rollbackAttributes();
}
}
}

View file

@ -15,10 +15,10 @@ export default class CredentialsShowController extends Controller {
async revokeCredentials() {
try {
await this.model.destroyRecord();
this.flashMessages.success('Successfully revoked credentials');
this.flashMessages.success('Successfully revoked credentials.');
this.router.transitionTo('vault.cluster.secrets.backend.kmip.credentials.index', this.scope, this.role);
} catch (e) {
this.flashMessages.danger(`There was an error revoking credentials: ${e.errors.join(' ')}`);
this.flashMessages.danger(`There was an error revoking credentials: ${e.errors.join(' ')}.`);
this.model.rollbackAttributes();
}
}

View file

@ -13,10 +13,6 @@
</h1>
</p.levelLeft>
</PageHeader>
<EditForm
@model={{this.model}}
@onSave={{transition-to "vault.cluster.secrets.backend.kmip.credentials.show" this.model.id}}
@callOnSaveAfterRender={{true}}
@successMessage={{concat "Successfully generated credentials from role:" this.role "!"}}
@cancelLinkParams={{array "credentials.index"}}
/>
{{! this.model is the ember data model kmip/credential }}
<Page::CredentialsGenerate @credentials={{this.model}} />

View file

@ -13,54 +13,5 @@
</h1>
</p.levelLeft>
</PageHeader>
<Toolbar>
<ToolbarActions>
{{#if this.model.deletePath.canDelete}}
<ConfirmAction
@buttonText="Revoke credentials"
class="toolbar-button"
@buttonColor="secondary"
@onConfirmAction={{this.revokeCredentials}}
@confirmTitle="Revoke this?"
@confirmMessage="Any client using these credentials will no longer be able to."
/>
<div class="toolbar-separator"></div>
{{/if}}
<ToolbarLink @route="credentials.index" @models={{array this.scope this.role}} data-test-kmip-link-back-to-role>
Back to role
</ToolbarLink>
<Hds::Copy::Button
@text="Copy certificate"
@textToCopy={{this.model.certificate}}
@onError={{(fn (set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger"))}}
class="toolbar-button"
data-test-copy-button
/>
</ToolbarActions>
</Toolbar>
<div class="box is-shadowless is-fullwidth is-sideless">
<InfoTableRow @label="Serial number" @value={{this.model.id}}>
<MaskedInput @value={{this.model.id}} @displayOnly={{true}} @allowCopy={{true}} />
</InfoTableRow>
{{#if this.model.privateKey}}
<InfoTableRow @label="Private key" @value={{this.model.privateKey}}>
<div class="is-block">
<Hds::Alert data-test-warning @type="inline" @color="warning" @class="has-bottom-margin-s" as |A|>
<A.Title>Warning</A.Title>
<A.Description>You will not be able to access the private key later, so please copy the information below.</A.Description>
</Hds::Alert>
<MaskedInput @value={{this.model.privateKey}} @name="Private key" @allowCopy={{true}} @displayOnly={{true}} />
</div>
</InfoTableRow>
{{/if}}
<InfoTableRow @label="Certificate" @value={{this.model.certificate}}>
<MaskedInput @value={{this.model.certificate}} @displayOnly={{true}} @allowCopy={{true}} />
</InfoTableRow>
<InfoTableRow @label="CA Chain" @value={{this.model.caChain}}>
<div class="is-block">
{{#each this.model.caChain as |chain|}}
<MaskedInput @value={{chain}} @displayOnly={{true}} @allowCopy={{true}} />
{{/each}}
</div>
</InfoTableRow>
</div>
<DetailsCredentials @credentials={{this.model}} @onRevokeCredentials={{this.revokeCredentials}} />

View file

@ -335,14 +335,38 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) {
await settled();
assert.strictEqual(
currentRouteName(),
'vault.cluster.secrets.backend.kmip.credentials.show',
'generate redirects to the show page'
'vault.cluster.secrets.backend.kmip.credentials.generate',
'it remains in the generate route'
);
assert
.dom(GENERAL.infoRowValue('Private key'))
.hasText(
'Warning You will not be able to access the private key later, so please copy the information below. ***********',
'it renders private key after generating'
);
await credentialsPage.backToRoleLink();
await settled();
assert.strictEqual(credentialsPage.listItemLinks.length, 1, 'renders a single credential');
});
test('it can revoke a credential from the generate view', async function (assert) {
const { backend, scope, role } = await createRole(this.backend);
await credentialsPage.visit({ backend, scope, role });
await credentialsPage.generateCredentialsLink();
await credentialsPage.submit();
// revoke the credentials
await waitUntil(() => find('[data-test-confirm-action-trigger]'));
assert.dom('[data-test-confirm-action-trigger]').exists('delete button exists');
await credentialsPage.delete().confirmDelete();
await settled();
assert.strictEqual(
currentURL(),
`/vault/secrets/${backend}/kmip/scopes/${scope}/roles/${role}/credentials`,
'redirects to the credentials list'
);
assert.true(credentialsPage.isEmpty, 'renders an empty credentials page');
});
test('it can revoke a credential from the list', async function (assert) {
const { backend, scope, role } = await generateCreds(this.backend);
await credentialsPage.visit({ backend, scope, role });