Vault Automation 0c6c13dd38
license: update headers to IBM Corp. (#10229) (#10233)
* license: update headers to IBM Corp.
* `make proto`
* update offset because source file changed

Signed-off-by: Ryan Cragun <me@ryan.ec>
Co-authored-by: Ryan Cragun <me@ryan.ec>
2025-10-21 15:20:20 -06:00

400 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { v4 as uuidv4 } from 'uuid';
import { Response } from 'miragejs';
import { click, currentURL, fillIn, visit } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { create } from 'ember-cli-page-object';
import databaseHandlers from 'vault/mirage/handlers/database';
import { setupApplicationTest } from 'vault/tests/helpers';
import { login } from 'vault/tests/helpers/auth/auth-helpers';
import flashMessage from 'vault/tests/pages/components/flash-message';
import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
const flash = create(flashMessage);
const PAGE = {
// GENERIC
emptyStateTitle: '[data-test-empty-state-title]',
infoRow: '[data-test-component="info-table-row"]',
// CONNECTIONS
rotateModal: '[data-test-db-connection-modal-title]',
confirmRotate: '[data-test-enable-rotate-connection]',
skipRotate: '[data-test-enable-connection]',
// ROLES
addRole: '[data-test-add-role]',
roleSettingsSection: '[data-test-role-settings-section]',
statementsSection: '[data-test-statements-section]',
editRole: '[data-test-edit-link]',
};
const FORM = {
creationStatement: (idx = 0) =>
`[data-test-input="creation_statements"] [data-test-string-list-input="${idx}"]`,
};
async function fillOutConnection(name) {
await fillIn(GENERAL.inputByAttr('name'), name);
await fillIn(GENERAL.inputByAttr('plugin_name'), 'mysql-database-plugin');
await fillIn(GENERAL.inputByAttr('connection_url'), '{{username}}:{{password}}@tcp(127.0.0.1:33060)/');
await fillIn(GENERAL.inputByAttr('username'), 'admin');
await fillIn(GENERAL.inputByAttr('password'), 'very-secure');
}
/**
* This test set is for testing the flow for database secrets engine.
*/
module('Acceptance | database workflow', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(async function () {
databaseHandlers(this.server);
this.backend = `db-workflow-${uuidv4()}`;
this.store = this.owner.lookup('service:store');
await login();
await runCmd(mountEngineCmd('database', this.backend), false);
});
hooks.afterEach(async function () {
await login();
return runCmd(deleteEngineCmd(this.backend));
});
module('connections', function (hooks) {
hooks.beforeEach(function () {
this.expectedRows = [
{ label: 'Database plugin', value: 'mysql-database-plugin' },
{ label: 'Connection name', value: `connect-${this.backend}` },
{ label: 'Use custom password policy', value: 'Default' },
{ label: 'Connection URL', value: '{{username}}:{{password}}@tcp(127.0.0.1:33060)/' },
{ label: 'Max open connections', value: '4' },
{ label: 'Max idle connections', value: '0' },
{ label: 'Max connection lifetime', value: '0s' },
{ label: 'Username template', value: 'Default' },
{
label: 'Root rotation statements',
value: `Default`,
},
{ label: 'Rotate static roles immediately', value: 'Yes' },
];
});
test('create with rotate', async function (assert) {
assert.expect(26);
this.server.post('/:backend/rotate-root/:name', () => {
assert.ok(true, 'rotate root called');
new Response(204);
});
await visit(`/vault/secrets-engines/${this.backend}/overview`);
assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct');
await click(SES.createSecretLink);
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/create?itemType=connection`,
'Takes you to create page'
);
// fill in connection details
await fillOutConnection(`connect-${this.backend}`);
await click(GENERAL.submitButton);
assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown');
await click(PAGE.confirmRotate);
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/show/connect-${this.backend}`,
'Takes you to details page for connection'
);
assert.dom(PAGE.infoRow).exists({ count: this.expectedRows.length }, 'correct number of rows');
this.expectedRows.forEach(({ label, value }) => {
assert.dom(GENERAL.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`);
assert.dom(GENERAL.infoRowValue(label)).hasText(value, `Value for ${label} is correct`);
});
});
test('create without rotate', async function (assert) {
assert.expect(25);
this.server.post('/:backend/rotate-root/:name', () => {
assert.notOk(true, 'rotate root called when it should not have been');
new Response(204);
});
await visit(`/vault/secrets-engines/${this.backend}/overview`);
assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct');
await click(SES.createSecretLink);
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/create?itemType=connection`,
'Takes you to create page'
);
// fill in connection details
await fillOutConnection(`connect-${this.backend}`);
await click(GENERAL.submitButton);
assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown');
await click(PAGE.skipRotate);
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/show/connect-${this.backend}`,
'Takes you to details page for connection'
);
assert.dom(PAGE.infoRow).exists({ count: this.expectedRows.length }, 'correct number of rows');
this.expectedRows.forEach(({ label, value }) => {
assert.dom(GENERAL.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`);
assert.dom(GENERAL.infoRowValue(label)).hasText(value, `Value for ${label} is correct`);
});
});
test('create failure', async function (assert) {
assert.expect(27);
this.server.post('/:backend/rotate-root/:name', (schema, req) => {
const okay = req.params.name !== 'bad-connection';
assert.ok(okay, 'rotate root called but not for bad-connection');
new Response(204);
});
await visit(`/vault/secrets-engines/${this.backend}/overview`);
assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct');
await click(SES.createSecretLink);
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/create?itemType=connection`,
'Takes you to create page'
);
// fill in connection details
await fillOutConnection(`bad-connection`);
await click(GENERAL.submitButton);
assert.strictEqual(
flash.latestMessage,
`error creating database object: error verifying - ping: Error 1045 (28000): Access denied for user 'admin'@'192.168.65.1' (using password: YES)`,
'shows the error message from API'
);
await fillIn(GENERAL.inputByAttr('name'), `connect-${this.backend}`);
await click(GENERAL.submitButton);
assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown');
await click(PAGE.confirmRotate);
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/show/connect-${this.backend}`,
'Takes you to details page for connection'
);
assert.dom(PAGE.infoRow).exists({ count: this.expectedRows.length }, 'correct number of rows');
this.expectedRows.forEach(({ label, value }) => {
assert.dom(GENERAL.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`);
assert.dom(GENERAL.infoRowValue(label)).hasText(value, `Value for ${label} is correct`);
});
});
test('create connection with rotate failure', async function (assert) {
await visit(`/vault/secrets-engines/${this.backend}/overview`);
assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct');
await click(SES.createSecretLink);
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/create?itemType=connection`,
'Takes you to create page'
);
// fill in connection details
await fillOutConnection(`fail-rotate`);
await click(GENERAL.submitButton);
assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown');
await click(PAGE.confirmRotate);
assert.strictEqual(
flash.latestMessage,
`Error rotating root credentials: 1 error occurred: * failed to update user: failed to change password: Error 1045 (28000): Access denied for user 'admin'@'%' (using password: YES)`,
'shows the error message from API'
);
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/show/fail-rotate`,
'Takes you to details page for connection'
);
});
});
module('dynamic roles', function (hooks) {
hooks.beforeEach(async function () {
this.connection = `connect-${this.backend}`;
await visit(`/vault/secrets-engines/${this.backend}/create`);
await fillOutConnection(this.connection);
await click(GENERAL.submitButton);
await visit(`/vault/secrets-engines/${this.backend}/show/${this.connection}`);
});
test('it creates a dynamic role attached to the current connection', async function (assert) {
const roleName = 'dynamic-role';
await click(PAGE.addRole);
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/create?initialKey=${this.connection}&itemType=role`,
'Takes you to create role page'
);
assert
.dom(`${PAGE.roleSettingsSection} ${PAGE.emptyStateTitle}`)
.hasText('No role type selected', 'roles section shows empty state before selecting role type');
assert
.dom(`${PAGE.statementsSection} ${PAGE.emptyStateTitle}`)
.hasText('No role type selected', 'statements section shows empty state before selecting role type');
await fillIn(GENERAL.inputByAttr('name'), roleName);
assert.dom('[data-test-selected-option]').hasText(this.connection, 'Connection is selected by default');
await fillIn(GENERAL.inputByAttr('type'), 'dynamic');
assert
.dom(`${PAGE.roleSettingsSection} ${PAGE.emptyStateTitle}`)
.doesNotExist('roles section no longer has empty state');
assert
.dom(`${PAGE.statementsSection} ${PAGE.emptyStateTitle}`)
.doesNotExist('statements section no longer has empty state');
// Fill in multiple creation statements
await fillIn(FORM.creationStatement(), `GRANT SELECT ON *.* TO '{{name}}'@'%'`);
await click(`[data-test-string-list-row="0"] [data-test-string-list-button="add"]`);
await fillIn(FORM.creationStatement(1), `GRANT CREATE ON *.* TO '{{name}}'@'%'`);
await click(GENERAL.submitButton);
// DETAILS
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/show/role/${roleName}`,
'Takes you to details page for role after save'
);
assert.dom(PAGE.infoRow).exists({ count: 7 }, 'correct number of info rows displayed');
[
{ label: 'Role name', value: roleName },
{ label: 'Connection name', value: this.connection },
{ label: 'Type of role', value: 'dynamic' },
{ label: 'Generated credentialss Time-to-Live (TTL)', value: '1 hour' },
{ label: 'Generated credentialss maximum Time-to-Live (Max TTL)', value: '1 day' },
{
label: 'Creation statements',
value: `GRANT SELECT ON *.* TO '{{name}}'@'%',GRANT CREATE ON *.* TO '{{name}}'@'%'`,
},
{ label: 'Revocation statements', value: 'Default' },
].forEach(({ label, value }) => {
assert.dom(GENERAL.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`);
assert.dom(GENERAL.infoRowValue(label)).hasText(value, `Value for ${label} is correct`);
});
// EDIT
await click(PAGE.editRole);
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/edit/role/${roleName}?itemType=role`,
'Takes you to edit page for role'
);
// TODO: these should be readonly not disabled
assert.dom(GENERAL.inputByAttr('name')).isDisabled('Name is read-only');
assert.dom(GENERAL.inputByAttr('database')).isDisabled('Database is read-only');
assert.dom(GENERAL.inputByAttr('type')).isDisabled('Type is read-only');
await fillIn('[data-test-ttl-value="Generated credentialss Time-to-Live (TTL)"]', '2');
await click(GENERAL.submitButton);
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/show/role/${roleName}`,
'Takes you to details page for role after save'
);
assert
.dom(GENERAL.infoRowValue('Generated credentialss Time-to-Live (TTL)'))
.hasText('2 hours', 'Shows updated TTL');
// CREDENTIALS
await click(GENERAL.button('dynamic'));
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/credentials/${roleName}?roleType=dynamic`,
'Takes you to credentials page for role'
);
assert
.dom('[data-test-credentials-warning]')
.exists('shows warning about credentials only being available once');
assert
.dom(`${GENERAL.infoRowValue('Username')} [data-test-masked-input]`)
.hasText('***********', 'Username is masked');
await click(`${GENERAL.infoRowValue('Username')} ${GENERAL.button('toggle-masked')}`);
assert
.dom(`${GENERAL.infoRowValue('Username')} [data-test-masked-input]`)
.hasText('generated-username', 'Username is generated');
assert
.dom(`${GENERAL.infoRowValue('Password')} [data-test-masked-input]`)
.hasText('***********', 'Password is masked');
await click(`${GENERAL.infoRowValue('Password')} ${GENERAL.button('toggle-masked')}`);
assert
.dom(`${GENERAL.infoRowValue('Password')} [data-test-masked-input]`)
.hasText('generated-password', 'Password is generated');
assert
.dom(GENERAL.infoRowValue('Lease Duration'))
.hasText('3600', 'shows lease duration from response');
assert
.dom(GENERAL.infoRowValue('Lease ID'))
.hasText(`database/creds/${roleName}/abcd`, 'shows lease ID from response');
});
});
module('static roles', function (hooks) {
hooks.beforeEach(async function () {
this.setup = async ({ toggleRotateOff = false }) => {
this.connection = `connect-${this.backend}`;
await visit(`/vault/secrets-engines/${this.backend}/create`);
await fillOutConnection(this.connection);
if (toggleRotateOff) {
await click(GENERAL.toggleInput('toggle-skip_static_role_rotation_import'));
}
await click(GENERAL.submitButton);
await visit(`/vault/secrets-engines/${this.backend}/show/${this.connection}`);
};
});
test('set parent db to rotate static roles immediately, verify static role reflects that default', async function (assert) {
await this.setup({ toggleRotateOff: false });
const roleName = 'static-role';
await click(PAGE.addRole);
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/create?initialKey=${this.connection}&itemType=role`,
'Takes you to create role page'
);
await fillIn(GENERAL.inputByAttr('name'), roleName);
await fillIn(GENERAL.inputByAttr('type'), 'static');
assert
.dom('[data-test-toggle-subtext]')
.containsText(`Vault will rotate the password for this static role on creation.`);
});
test('set parent db to not rotate static roles immediately, verify static role reflects that default', async function (assert) {
await this.setup({ toggleRotateOff: true });
const roleName = 'static-role';
await click(PAGE.addRole);
assert.strictEqual(
currentURL(),
`/vault/secrets-engines/${this.backend}/create?initialKey=${this.connection}&itemType=role`,
'Takes you to create role page'
);
await fillIn(GENERAL.inputByAttr('name'), roleName);
await fillIn(GENERAL.inputByAttr('type'), 'static');
assert
.dom('[data-test-toggle-subtext]')
.containsText(`Vault will not rotate this role's password on creation.`);
});
});
});