UI: Ember Data migration: DB overview / credentials (#14912) (#15040)

* migrating db overview page

* fix toolbar alignment on remaining links

* migrating database creds + minor secrets table fix

* update totp key fetch

* removing store for aws

* fix workflow test

* removed commented code

* fix return line

* [UI] Ember Data Migration - Core Addon (#14891)

* removes store service from confirm-leave decorator

* updates secret list header tab component to use capabilities service for database type

* removes store service from edit-form component

* removes ember data fetch support from InfoTableItemArray component

* removes store from shamir components

* removes store from replication components in core addon

* adds missing service injection to shamir flow component

* fixes reduced disclosure test

* fixes issues with seal/unseal workflow

* reverts assertion change in info-table-item-array test

* fixes database test

* updates shamir flow test

* removes commented out code

* fix pathfors

* dont throw messages that dont need to be thrown :)

* updating to use allSettled

* matching whats in adapter

* fix

* updating to use enums

* [UI] Ember Data Migration - TOTP Secrets Engine Views | VAULT-44225 (#14933)

* VAULT-44225 - edm secrets totp views

* fixed review comments and updated validations to match original

* fixed review comments

* fix 2

* update to parseError

* fix

---------

Co-authored-by: Dan Rivera <dan.rivera@hashicorp.com>
Co-authored-by: Jordan Reimer <zofskeez@gmail.com>
Co-authored-by: mohit-hashicorp <mohit.ojha@hashicorp.com>
This commit is contained in:
Vault Automation 2026-05-28 09:10:35 -06:00 committed by GitHub
parent 60e61741f9
commit b00064cba2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 110 additions and 60 deletions

View file

@ -41,22 +41,22 @@
<InfoTableRow @label="Password" @value={{@model.password}}>
<MaskedInput @value={{@model.password}} @name="Password" @displayOnly={{true}} @allowCopy={{true}} />
</InfoTableRow>
<InfoTableRow @label="Lease ID" @value={{@model.leaseId}} />
<InfoTableRow @label="Lease Duration" @value={{format-duration @model.leaseDuration}} />
<InfoTableRow @label="Lease ID" @value={{@model.lease_id}} />
<InfoTableRow @label="Lease Duration" @value={{format-duration @model.lease_duration}} />
{{/if}}
{{! STATIC ROLE }}
{{#if (and (eq @roleType "static") @model.username)}}
<InfoTableRow
@label="Last Vault rotation"
@value={{date-format @model.lastVaultRotation "MMMM d yyyy, h:mm:ss a"}}
@tooltipText={{@model.lastVaultRotation}}
@value={{date-format @model.last_vault_rotation "MMMM d yyyy, h:mm:ss a"}}
@tooltipText={{@model.last_vault_rotation}}
@addCopyButton={{true}}
/>
<InfoTableRow @label="Password" @value={{@model.password}}>
<MaskedInput @value={{@model.password}} @name="Password" @displayOnly={{true}} @allowCopy={{true}} />
</InfoTableRow>
<InfoTableRow @label="Username" @value={{@model.username}} />
<InfoTableRow @label="Rotation Period" @value={{format-duration @model.rotationPeriod}} />
<InfoTableRow @label="Rotation Period" @value={{format-duration @model.rotation_period}} />
<InfoTableRow @label="Time Remaining" @value={{format-duration @model.ttl}} />
{{/if}}
</div>

View file

@ -62,23 +62,23 @@ export default class SecretEngineList extends Component<Args> {
{
key: 'accessor',
label: 'Accessor',
width: '175px',
width: '205px',
},
{
key: 'description',
label: 'Description',
width: '300px',
width: '320px',
},
{
key: 'running_plugin_version',
label: 'Version',
isSortable: true,
width: '170px',
width: '175px',
},
{
key: 'popupMenu',
label: 'Action',
width: '75px',
width: '80px',
},
];

View file

@ -14,5 +14,5 @@
...attributes
>
{{yield}}
<Icon @name={{this.glyph}} />
<Icon class="toolbar-icon" @name={{this.glyph}} />
</SecretLink>

View file

@ -5,7 +5,6 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import ControlGroupError from 'vault/lib/control-group-error';
const SUPPORTED_DYNAMIC_BACKENDS = ['database', 'ssh', 'aws', 'totp'];
@ -13,7 +12,6 @@ export default Route.extend({
templateName: 'vault/cluster/secrets/backend/credentials',
pathHelp: service('path-help'),
router: service(),
store: service(),
api: service(),
beforeModel(transition) {
@ -34,21 +32,42 @@ export default Route.extend({
}
},
getDatabaseCredential(backend, secret, roleType = '') {
return this.store.queryRecord('database/credential', { backend, secret, roleType }).catch((error) => {
if (error instanceof ControlGroupError) {
throw error;
async getDatabaseCredential(backend, secret, roleType = '') {
try {
if (roleType === 'static') {
const { last_vault_rotation, lease_duration, data } =
await this.api.secrets.databaseReadStaticRoleCredentials(secret, backend);
return {
last_vault_rotation,
lease_duration,
...data,
};
} else {
const { data, lease_id, lease_duration } = await this.api.secrets.databaseGenerateCredentials(
secret,
backend
);
return {
...data,
lease_id,
lease_duration,
};
}
} catch (error) {
const { response } = await this.api.parseError(error);
if (response.isControlGroupError) {
throw response;
}
// Unless it's a control group error, we want to pass back error info
// so we can render it on the GenerateCredentialsDatabase component
return error;
});
return response;
}
},
async getAwsRole(backend, id) {
try {
const role = await this.store.queryRecord('role-aws', { backend, id });
return role;
const { data } = await this.api.secrets.awsReadRole(id, backend);
return data;
} catch (e) {
// swallow error, non-essential data
return;
@ -57,8 +76,8 @@ export default Route.extend({
async getTotpKey(backend, keyName) {
try {
const resp = await this.api.secrets.totpReadKey(keyName, backend);
return resp.data || {};
const { data } = await this.api.secrets.totpReadKey(keyName, backend);
return data || {};
} catch (e) {
// swallow error, non-essential data
return {};
@ -86,19 +105,11 @@ export default Route.extend({
roleName: role,
roleType,
dbCred,
awsRoleType: awsRole?.credentialType,
awsRoleType: awsRole?.credential_type,
};
},
resetController(controller) {
controller.reset();
},
actions: {
willTransition() {
// we do not want to save any of the credential information in the store.
// once the user navigates away from this page, remove all credential info.
this.store.unloadAll('database/credential');
},
},
});

View file

@ -7,54 +7,96 @@ import Route from '@ember/routing/route';
import { hash } from 'rsvp';
import { service } from '@ember/service';
import { getEnginePathParam } from 'vault/utils/backend-route-helpers';
import {
SecretsApiDatabaseListStaticRolesListEnum,
SecretsApiDatabaseListRolesListEnum,
SecretsApiDatabaseListConnectionsListEnum,
} from '@hashicorp/vault-client-typescript';
export default Route.extend({
store: service(),
capabilities: service(),
api: service(),
type: '',
// this only grabs connections for current db backend, only used to populate # of connections
async fetchConnection(queryOptions) {
try {
return await this.store.query('database/connection', queryOptions);
} catch (e) {
return e.httpStatus;
const { keys } = await this.api.secrets.databaseListConnections(
queryOptions.backend,
SecretsApiDatabaseListConnectionsListEnum.TRUE
);
return keys;
} catch (error) {
const { status } = await this.api.parseError(error);
if (status === 404) {
return status;
}
}
},
// this grabs both dynamic and static roles for current db backend, only used to populate # of roles
async fetchAllRoles(queryOptions) {
try {
return await this.store.query('database/role', queryOptions);
} catch (e) {
return e.httpStatus;
}
},
const roles = [];
const { backend } = queryOptions;
const [staticResp, dynamicResp] = await Promise.allSettled([
this.api.secrets.databaseListStaticRoles(backend, SecretsApiDatabaseListStaticRolesListEnum.TRUE),
this.api.secrets.databaseListRoles(backend, SecretsApiDatabaseListRolesListEnum.TRUE),
]);
pathQuery(backend, endpoint) {
return {
id: `${backend}/${endpoint}/`,
};
if (staticResp.status === 'rejected' && dynamicResp.status === 'rejected') {
const { response: staticError, status: staticStatus } = await this.api.parseError(staticResp.reason);
const { response: dynamicError, status: dynamicStatus } = await this.api.parseError(
dynamicResp.reason
);
if (staticError?.isControlGroupError) {
throw staticError;
}
throw staticStatus < dynamicStatus ? dynamicError : staticError;
} else {
if (staticResp.value) {
roles.push(...staticResp.value.keys);
}
if (dynamicResp.value) {
roles.push(...dynamicResp.value.keys);
}
return roles;
}
} catch (error) {
const { status } = await this.api.parseError(error);
if (status === 404) {
return status;
}
}
},
async fetchCapabilitiesRole(queryOptions) {
return this.store.queryRecord('capabilities', this.pathQuery(queryOptions.backend, 'roles'));
const paths = [this.capabilities.pathFor('databaseRoles', { backend: queryOptions.backend })];
const capabilities = paths ? await this.capabilities.fetch(paths) : {};
return capabilities[paths[0]];
},
async fetchCapabilitiesStaticRole(queryOptions) {
return this.store.queryRecord('capabilities', this.pathQuery(queryOptions.backend, 'static-roles'));
const paths = [this.capabilities.pathFor('databaseStaticRoles', { backend: queryOptions.backend })];
const capabilities = paths ? await this.capabilities.fetch(paths) : {};
return capabilities[paths[0]];
},
async fetchCapabilitiesConnection(queryOptions) {
return this.store.queryRecord('capabilities', this.pathQuery(queryOptions.backend, 'config'));
const paths = [this.capabilities.pathFor('databaseConfig', { backend: queryOptions.backend })];
const capabilities = paths ? await this.capabilities.fetch(paths) : {};
return capabilities[paths[0]];
},
model() {
async model() {
const backend = getEnginePathParam(this);
const queryOptions = { backend, id: '' };
const connection = this.fetchConnection(queryOptions);
const role = this.fetchAllRoles(queryOptions);
const roleCapabilities = this.fetchCapabilitiesRole(queryOptions);
const staticRoleCapabilities = this.fetchCapabilitiesStaticRole(queryOptions);
const connectionCapabilities = this.fetchCapabilitiesConnection(queryOptions);
const connection = await this.fetchConnection(queryOptions);
const role = await this.fetchAllRoles(queryOptions);
const roleCapabilities = await this.fetchCapabilitiesRole(queryOptions);
const staticRoleCapabilities = await this.fetchCapabilitiesStaticRole(queryOptions);
const connectionCapabilities = await this.fetchCapabilitiesConnection(queryOptions);
return hash({
backend,
@ -71,7 +113,7 @@ export default Route.extend({
setupController(controller, model) {
this._super(...arguments);
const showEmptyState = model.connections === 404 && model.roles === 404;
const showEmptyState = model.connections === 404 && (model.roles === undefined || model.roles === 404);
const noConnectionCapabilities =
!model.connectionCapabilities.canList &&
!model.connectionCapabilities.canCreate &&

View file

@ -7,7 +7,7 @@
<GenerateCredentialsDatabase
@backendPath={{this.model.backendPath}}
@roleName={{this.model.roleName}}
@roleType={{this.model.dbCred.roleType}}
@roleType={{this.model.roleType}}
@model={{this.model.dbCred}}
/>
{{else if (eq this.model.backendType "totp")}}

View file

@ -644,10 +644,7 @@ module('Acceptance | secrets/database/*', function (hooks) {
assert
.dom('[data-test-secret-list-tab="Roles"]')
.doesNotExist(`does not show the roles tab because it does not have permissions`);
assert
.dom('[data-test-overview-card="Connections"]')
.exists({ count: 1 }, 'renders only the connection card');
await click('[data-test-action-text="Configure new"]');
await click(SES.createSecretLink);
assert.strictEqual(currentURL(), `/vault/secrets-engines/${backend}/create?itemType=connection`);
});
});

View file

@ -335,7 +335,7 @@ module('Acceptance | database workflow', function (hooks) {
.hasText('generated-password', 'Password is generated');
assert
.dom(GENERAL.infoRowValue('Lease Duration'))
.hasText('3600', 'shows lease duration from response');
.hasText('1 hour', 'shows lease duration from response');
assert
.dom(GENERAL.infoRowValue('Lease ID'))
.hasText(`database/creds/${roleName}/abcd`, 'shows lease ID from response');