vault/ui/tests/acceptance/secrets/backend/database/secret-test.js
claire bontempo 76e742ba32
ui: HDS adoption replace <AlertBanner> component (#21375)
* UI: HDS adoption replace AlertBanner part 1 (#21163)

* rename test selector

* replace db banner

* add class

* replace db role edit

* db creds

* generate creds

* simpler class

* license banner component

* oidc callback plash

* raft

* aws

* secret create or update

* change to compact alert for form field

* change back to inline

* combine alert banners

* wrap in conditional

* remove references to message class

* UI: HDS adoption replace AlertBanner part 2 (#21243)

* token expire warning

* delete css

* edit form

* item details distribute mfa step 2 transit verify

* back to secondary

* distribute

* oidc lease error

* sign

* kv obj and repl dash

* more repl

* update test selector

* show, creds

* shamir

* pki csr

* pki banners

* add hds library to ember engines

* woops comma

* fix k8 test

* update message error component for last!

* hold off MessageError changes until next pr

* revert test selectors

* update pki tests

* UI: part 3 remove alert banner (#21334)

* final component swap

* and actual final of MessageError

* update MessageError selectors

* delete alert-banner and remove references

* update next step alerts to highlight color

* finishing touches, auth form test and client dashboard inline link

* fix more selectors

* fix shamir flow test

* ui: part 4 final cleanup (#21365)

* replace AlertPopup

* add test tag

* move tag

* one more message error tag

* delete alert popup

* final css cleanup

* move preformatted flash into <p> tag

* ui: address comments for sidebranch  (#21388)

* add periods, move link to trailing

* more periods and typo fix
2023-06-21 11:37:11 -07:00

553 lines
24 KiB
JavaScript
Raw 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 (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { currentURL, settled, click, visit, fillIn, typeIn } from '@ember/test-helpers';
import { create } from 'ember-cli-page-object';
import { selectChoose, clickTrigger } from 'ember-power-select/test-support/helpers';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
import connectionPage from 'vault/tests/pages/secrets/backend/database/connection';
import rolePage from 'vault/tests/pages/secrets/backend/database/role';
import apiStub from 'vault/tests/helpers/noop-all-api-requests';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
import searchSelect from 'vault/tests/pages/components/search-select';
const searchSelectComponent = create(searchSelect);
const consoleComponent = create(consoleClass);
const MODEL = {
engineType: 'database',
id: 'database-name',
};
const mount = async () => {
const path = `database-${Date.now()}`;
await mountSecrets.enable('database', path);
await settled();
return path;
};
const newConnection = async (backend, plugin = 'mongodb-database-plugin') => {
const name = `connection-${Date.now()}`;
await connectionPage.visitCreate({ backend });
await connectionPage.dbPlugin(plugin);
await connectionPage.name(name);
await connectionPage.connectionUrl(`mongodb://127.0.0.1:4321/${name}`);
await connectionPage.toggleVerify();
await connectionPage.save();
await connectionPage.enable();
return name;
};
const connectionTests = [
{
name: 'elasticsearch-connection',
plugin: 'elasticsearch-database-plugin',
elasticUser: 'username',
elasticPassword: 'password',
url: 'http://127.0.0.1:9200',
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
assert.dom('[data-test-input="ca_cert"]').exists(`CA certificate field exists for ${name}`);
assert.dom('[data-test-input="ca_path"]').exists(`CA path field exists for ${name}`);
assert.dom('[data-test-input="client_cert"]').exists(`Client certificate field exists for ${name}`);
assert.dom('[data-test-input="client_key"]').exists(`Client key field exists for ${name}`);
assert.dom('[data-test-input="tls_server_name"]').exists(`TLS server name field exists for ${name}`);
assert.dom('[data-test-input="insecure"]').exists(`Insecure checkbox exists for ${name}`);
assert
.dom('[data-test-toggle-input="show-username_template"]')
.exists(`Username template toggle exists for ${name}`);
},
},
{
name: 'mongodb-connection',
plugin: 'mongodb-database-plugin',
url: `mongodb://127.0.0.1:4321/test`,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
assert.dom('[data-test-input="write_concern"]').exists(`Write concern field exists for ${name}`);
assert.dom('[data-test-toggle-group="TLS options"]').exists('TLS options toggle exists');
assert
.dom('[data-test-input="root_rotation_statements"]')
.exists(`Root rotation statements exists for ${name}`);
},
},
{
name: 'mssql-connection',
plugin: 'mssql-database-plugin',
url: `mssql://127.0.0.1:4321/test`,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
assert
.dom('[data-test-input="max_open_connections"]')
.exists(`Max open connections exists for ${name}`);
assert
.dom('[data-test-input="max_idle_connections"]')
.exists(`Max idle connections exists for ${name}`);
assert
.dom('[data-test-input="max_connection_lifetime"]')
.exists(`Max connection lifetime exists for ${name}`);
assert
.dom('[data-test-input="root_rotation_statements"]')
.exists(`Root rotation statements exists for ${name}`);
},
},
{
name: 'mysql-connection',
plugin: 'mysql-database-plugin',
url: `{{username}}:{{password}}@tcp(127.0.0.1:3306)/test`,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
assert
.dom('[data-test-input="max_open_connections"]')
.exists(`Max open connections exists for ${name}`);
assert
.dom('[data-test-input="max_idle_connections"]')
.exists(`Max idle connections exists for ${name}`);
assert
.dom('[data-test-input="max_connection_lifetime"]')
.exists(`Max connection lifetime exists for ${name}`);
assert.dom('[data-test-toggle-group="TLS options"]').exists('TLS options toggle exists');
assert
.dom('[data-test-input="root_rotation_statements"]')
.exists(`Root rotation statements exists for ${name}`);
},
},
{
name: 'mysql-aurora-connection',
plugin: 'mysql-aurora-database-plugin',
url: `{{username}}:{{password}}@tcp(127.0.0.1:3306)/test`,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
assert
.dom('[data-test-input="max_open_connections"]')
.exists(`Max open connections exists for ${name}`);
assert
.dom('[data-test-input="max_idle_connections"]')
.exists(`Max idle connections exists for ${name}`);
assert
.dom('[data-test-input="max_connection_lifetime"]')
.exists(`Max connection lifetime exists for ${name}`);
assert.dom('[data-test-toggle-group="TLS options"]').exists('TLS options toggle exists');
assert
.dom('[data-test-input="root_rotation_statements"]')
.exists(`Root rotation statements exists for ${name}`);
},
},
{
name: 'mysql-rds-connection',
plugin: 'mysql-rds-database-plugin',
url: `{{username}}:{{password}}@tcp(127.0.0.1:3306)/test`,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
assert
.dom('[data-test-input="max_open_connections"]')
.exists(`Max open connections exists for ${name}`);
assert
.dom('[data-test-input="max_idle_connections"]')
.exists(`Max idle connections exists for ${name}`);
assert
.dom('[data-test-input="max_connection_lifetime"]')
.exists(`Max connection lifetime exists for ${name}`);
assert.dom('[data-test-toggle-group="TLS options"]').exists('TLS options toggle exists');
assert
.dom('[data-test-input="root_rotation_statements"]')
.exists(`Root rotation statements exists for ${name}`);
},
},
{
name: 'mysql-legacy-connection',
plugin: 'mysql-legacy-database-plugin',
url: `{{username}}:{{password}}@tcp(127.0.0.1:3306)/test`,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
assert
.dom('[data-test-input="max_open_connections"]')
.exists(`Max open connections exists for ${name}`);
assert
.dom('[data-test-input="max_idle_connections"]')
.exists(`Max idle connections exists for ${name}`);
assert
.dom('[data-test-input="max_connection_lifetime"]')
.exists(`Max connection lifetime exists for ${name}`);
assert.dom('[data-test-toggle-group="TLS options"]').exists('TLS options toggle exists');
assert
.dom('[data-test-input="root_rotation_statements"]')
.exists(`Root rotation statements exists for ${name}`);
},
},
{
name: 'postgresql-connection',
plugin: 'postgresql-database-plugin',
url: `postgresql://{{username}}:{{password}}@localhost:5432/postgres?sslmode=disable`,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
assert
.dom('[data-test-input="max_open_connections"]')
.exists(`Max open connections exists for ${name}`);
assert
.dom('[data-test-input="max_idle_connections"]')
.exists(`Max idle connections exists for ${name}`);
assert
.dom('[data-test-input="max_connection_lifetime"]')
.exists(`Max connection lifetime exists for ${name}`);
assert
.dom('[data-test-input="root_rotation_statements"]')
.exists(`Root rotation statements exists for ${name}`);
assert
.dom('[data-test-toggle-input="show-username_template"]')
.exists(`Username template toggle exists for ${name}`);
},
},
// keep oracle as last DB because it is skipped in some tests (line 285) the UI doesn't return to empty state after
{
name: 'oracle-connection',
plugin: 'vault-plugin-database-oracle',
url: `{{username}}/{{password}}@localhost:1521/OraDoc.localhost`,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
assert
.dom('[data-test-input="max_open_connections"]')
.exists(`Max open connections exists for ${name}`);
assert
.dom('[data-test-input="max_idle_connections"]')
.exists(`Max idle connections exists for ${name}`);
assert
.dom('[data-test-input="max_connection_lifetime"]')
.exists(`Max connection lifetime exists for ${name}`);
assert
.dom('[data-test-input="root_rotation_statements"]')
.exists(`Root rotation statements exists for ${name}`);
assert
.dom('[data-test-database-oracle-alert]')
.hasTextContaining(
`Warning Please ensure that your Oracle plugin has the default name of vault-plugin-database-oracle. Custom naming is not supported in the UI at this time. If the plugin is already named vault-plugin-database-oracle, disregard this warning.`,
'warning banner displays for oracle plugin name'
);
},
},
];
module('Acceptance | secrets/database/*', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(async function () {
this.server = apiStub({ usePassthrough: true });
return authPage.login();
});
hooks.afterEach(function () {
this.server.shutdown();
});
test('can enable the database secrets engine', async function (assert) {
const backend = `database-${Date.now()}`;
await mountSecrets.enable('database', backend);
await settled();
assert.strictEqual(
currentURL(),
`/vault/secrets/${backend}/list`,
'Mounts and redirects to connection list page'
);
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
assert
.dom('.active[data-test-secret-list-tab="Connections"]')
.exists('Has Connections tab which is active');
await click('[data-test-tab="overview"]');
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/overview`, 'Tab links to overview page');
assert.dom('[data-test-component="empty-state"]').exists('Empty state also exists on overview page');
assert.dom('[data-test-secret-list-tab="Roles"]').exists('Has Roles tab');
});
test('Connection create and edit form for each plugin', async function (assert) {
assert.expect(161);
const backend = await mount();
for (const testCase of connectionTests) {
await connectionPage.visitCreate({ backend });
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/create`, 'Correct creation URL');
assert
.dom('[data-test-empty-state-title]')
.hasText('No plugin selected', 'No plugin is selected by default and empty state shows');
await connectionPage.dbPlugin(testCase.plugin);
assert.dom('[data-test-empty-state]').doesNotExist('Empty state goes away after plugin selected');
await connectionPage.name(testCase.name);
if (testCase.plugin === 'elasticsearch-database-plugin') {
await connectionPage.url(testCase.url);
await connectionPage.username(testCase.elasticUser);
await connectionPage.password(testCase.elasticPassword);
} else {
await connectionPage.connectionUrl(testCase.url);
}
// skip adding oracle db connection since plugin doesn't exist
if (testCase.plugin === 'vault-plugin-database-oracle') {
testCase.requiredFields(assert, testCase.name);
continue;
}
testCase.requiredFields(assert, testCase.name);
await connectionPage.toggleVerify();
await connectionPage.save();
await settled();
assert
.dom('.modal.is-active .title')
.hasText('Rotate your root credentials?', 'Modal appears asking to rotate root credentials');
await connectionPage.enable();
assert.ok(
currentURL().startsWith(`/vault/secrets/${backend}/show/${testCase.name}`),
`Saves connection and takes you to show page for ${testCase.name}`
);
assert
.dom(`[data-test-row-value="Password"]`)
.doesNotExist(`Does not show Password value on show page for ${testCase.name}`);
await connectionPage.edit();
assert.ok(
currentURL().startsWith(`/vault/secrets/${backend}/edit/${testCase.name}`),
`Edit connection button and takes you to edit page for ${testCase.name}`
);
assert.dom(`[data-test-input="name"]`).hasAttribute('readonly');
assert.dom(`[data-test-input="plugin_name"]`).hasAttribute('readonly');
assert.dom('[data-test-input="password"]').doesNotExist('Password is not displayed on edit form');
assert.dom('[data-test-toggle-input="show-password"]').exists('Update password toggle exists');
await connectionPage.toggleVerify();
await connectionPage.save();
// click "Add Role"
await connectionPage.addRole();
await settled();
assert.strictEqual(
searchSelectComponent.selectedOptions[0].text,
testCase.name,
'Database connection is pre-selected on the form'
);
await click('[data-test-secret-breadcrumb]');
}
});
test('Can create and delete a connection', async function (assert) {
const backend = await mount();
const connectionDetails = {
plugin: 'mongodb-database-plugin',
id: 'horses-db',
fields: [
{ label: 'Connection name', name: 'name', value: 'horses-db' },
{ label: 'Connection URL', name: 'connection_url', value: 'mongodb://127.0.0.1:235/horses' },
{ label: 'Username', name: 'username', value: 'user', hideOnShow: true },
{ label: 'Password', name: 'password', password: 'so-secure', hideOnShow: true },
{ label: 'Write concern', name: 'write_concern' },
],
};
assert.strictEqual(
currentURL(),
`/vault/secrets/${backend}/list`,
'Mounts and redirects to connection list page'
);
await connectionPage.createLink();
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/create`, 'Create link goes to create page');
assert
.dom('[data-test-empty-state-title]')
.hasText('No plugin selected', 'No plugin is selected by default and empty state shows');
await connectionPage.dbPlugin(connectionDetails.plugin);
assert.dom('[data-test-empty-state]').doesNotExist('Empty state goes away after plugin selected');
connectionDetails.fields.forEach(async ({ name, value }) => {
assert
.dom(`[data-test-input="${name}"]`)
.exists(`Field ${name} exists for ${connectionDetails.plugin}`);
if (value) {
await fillIn(`[data-test-input="${name}"]`, value);
}
});
// uncheck verify for the save step to work
await connectionPage.toggleVerify();
await connectionPage.save();
await settled();
assert
.dom('.modal.is-active .title')
.hasText('Rotate your root credentials?', 'Modal appears asking to ');
await connectionPage.enable();
assert.strictEqual(
currentURL(),
`/vault/secrets/${backend}/show/${connectionDetails.id}`,
'Saves connection and takes you to show page'
);
connectionDetails.fields.forEach(({ label, name, value, hideOnShow }) => {
if (hideOnShow) {
assert
.dom(`[data-test-row-value="${label}"]`)
.doesNotExist(`Does not show ${name} value on show page for ${connectionDetails.plugin}`);
} else if (!value) {
assert.dom(`[data-test-row-value="${label}"]`).hasText('Default');
} else {
assert.dom(`[data-test-row-value="${label}"]`).hasText(value);
}
});
await connectionPage.delete();
assert
.dom('.modal.is-active .title')
.hasText('Delete connection?', 'Modal appears asking to confirm delete action');
await fillIn('[data-test-confirmation-modal-input="Delete connection?"]', connectionDetails.id);
await click('[data-test-confirm-button]');
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/list`, 'Redirects to connection list page');
assert
.dom('[data-test-empty-state-title]')
.hasText('No connections in this backend', 'No connections listed because it was deleted');
});
test('buttons show up for managing connection', async function (assert) {
const backend = await mount();
const connection = await newConnection(backend);
await connectionPage.visitShow({ backend, id: connection });
assert
.dom('[data-test-database-connection-delete]')
.hasText('Delete connection', 'Delete connection button exists with correct text');
assert
.dom('[data-test-database-connection-reset]')
.hasText('Reset connection', 'Reset button exists with correct text');
assert.dom('[data-test-secret-create]').hasText('Add role', 'Add role button exists with correct text');
assert.dom('[data-test-edit-link]').hasText('Edit configuration', 'Edit button exists with correct text');
const CONNECTION_VIEW_ONLY = `
path "${backend}/*" {
capabilities = ["deny"]
}
path "${backend}/config" {
capabilities = ["list"]
}
path "${backend}/config/*" {
capabilities = ["read"]
}
`;
await consoleComponent.runCommands([
`write sys/mounts/${backend} type=database`,
`write sys/policies/acl/test-policy policy=${btoa(CONNECTION_VIEW_ONLY)}`,
'write -field=client_token auth/token/create policies=test-policy ttl=1h',
]);
const token = consoleComponent.lastTextOutput;
await logout.visit();
await authPage.login(token);
await connectionPage.visitShow({ backend, id: connection });
assert.strictEqual(
currentURL(),
`/vault/secrets/${backend}/show/${connection}`,
'Allows reading connection'
);
assert
.dom('[data-test-database-connection-delete]')
.doesNotExist('Delete button does not show due to permissions');
assert
.dom('[data-test-database-connection-reset]')
.doesNotExist('Reset button does not show due to permissions');
assert.dom('[data-test-secret-create]').doesNotExist('Add role button does not show due to permissions');
assert.dom('[data-test-edit-link]').doesNotExist('Edit button does not show due to permissions');
await visit(`/vault/secrets/${backend}/overview`);
assert.dom('[data-test-selectable-card="Connections"]').exists('Connections card exists on overview');
assert
.dom('[data-test-selectable-card="Roles"]')
.doesNotExist('Roles card does not exist on overview w/ policy');
assert.dom('.title-number').hasText('1', 'Lists the correct number of connections');
// confirm get credentials card is an option to select. Regression bug.
await typeIn('.ember-text-field', 'blah');
assert.dom('[data-test-get-credentials]').isEnabled();
});
test('Role create form', async function (assert) {
const backend = await mount();
// Connection needed for role fields
await newConnection(backend);
await rolePage.visitCreate({ backend });
await rolePage.name('bar');
assert
.dom('[data-test-component="empty-state"]')
.exists({ count: 2 }, 'Two empty states exist before selections made');
await clickTrigger('#database');
assert.strictEqual(searchSelectComponent.options.length, 1, 'list shows existing connections so far');
await selectChoose('#database', '.ember-power-select-option', 0);
assert
.dom('[data-test-component="empty-state"]')
.exists({ count: 2 }, 'Two empty states exist before selections made');
await rolePage.roleType('static');
assert.dom('[data-test-component="empty-state"]').doesNotExist('Empty states go away');
assert.dom('[data-test-input="username"]').exists('Username field appears for static role');
assert
.dom('[data-test-toggle-input="Rotation period"]')
.exists('Rotation period field appears for static role');
await rolePage.roleType('dynamic');
assert
.dom('[data-test-toggle-input="Generated credentialss Time-to-Live (TTL)"]')
.exists('TTL field exists for dynamic');
assert
.dom('[data-test-toggle-input="Generated credentialss maximum Time-to-Live (Max TTL)"]')
.exists('Max TTL field exists for dynamic');
// Real connection (actual running db) required to save role, so we aren't testing that flow yet
});
test('root and limited access', async function (assert) {
this.set('model', MODEL);
const backend = 'database';
const NO_ROLES_POLICY = `
path "database/roles/*" {
capabilities = ["delete"]
}
path "database/static-roles/*" {
capabilities = ["delete"]
}
path "database/config/*" {
capabilities = ["list", "create", "read", "update"]
}
path "database/creds/*" {
capabilities = ["list", "create", "read", "update"]
}
`;
await consoleComponent.runCommands([
`write sys/mounts/${backend} type=database`,
`write sys/policies/acl/test-policy policy=${btoa(NO_ROLES_POLICY)}`,
'write -field=client_token auth/token/create policies=test-policy ttl=1h',
]);
const token = consoleComponent.lastTextOutput;
// test root user flow
await settled();
// await click('[data-test-secret-backend-row="database"]');
// skipping the click because occasionally is shows up on the second page and cannot be found
await visit(`/vault/secrets/database/overview`);
assert.dom('[data-test-component="empty-state"]').exists('renders empty state');
assert.dom('[data-test-secret-list-tab="Connections"]').exists('renders connections tab');
assert.dom('[data-test-secret-list-tab="Roles"]').exists('renders connections tab');
await click('[data-test-secret-create="connections"]');
assert.strictEqual(currentURL(), '/vault/secrets/database/create');
// Login with restricted policy
await logout.visit();
await authPage.login(token);
await settled();
// skipping the click because occasionally is shows up on the second page and cannot be found
await visit(`/vault/secrets/database/overview`);
assert.dom('[data-test-tab="overview"]').exists('renders overview tab');
assert.dom('[data-test-secret-list-tab="Connections"]').exists('renders connections tab');
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-selectable-card="Connections"]')
.exists({ count: 1 }, 'renders only the connection card');
await click('[data-test-action-text="Configure new"]');
assert.strictEqual(currentURL(), '/vault/secrets/database/create?itemType=connection');
});
});