mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
UI: Database fixes (#24947)
This commit is contained in:
3
changelog/24947.txt
Normal file
3
changelog/24947.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:bug
|
||||
ui: Fixed minor bugs with database secrets engine
|
||||
```
|
||||
@@ -56,6 +56,7 @@ export default ApplicationAdapter.extend({
|
||||
return {
|
||||
data: {
|
||||
id,
|
||||
name: id,
|
||||
...data,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -144,7 +144,7 @@ export default ApplicationAdapter.extend({
|
||||
|
||||
async _updateAllowedRoles(store, { role, backend, db, type = 'add' }) {
|
||||
const connection = await store.queryRecord('database/connection', { backend, id: db });
|
||||
const roles = [...connection.allowed_roles];
|
||||
const roles = [...(connection.allowed_roles || [])];
|
||||
const allowedRoles = type === 'add' ? addToArray([roles, role]) : removeFromArray([roles, role]);
|
||||
connection.allowed_roles = allowedRoles;
|
||||
return connection.save();
|
||||
|
||||
@@ -59,8 +59,6 @@ export default class DatabaseConnectionEdit extends Component {
|
||||
async handleCreateConnection(evt) {
|
||||
evt.preventDefault();
|
||||
const secret = this.args.model;
|
||||
const secretId = secret.name;
|
||||
secret.set('id', secretId);
|
||||
secret
|
||||
.save()
|
||||
.then(() => {
|
||||
|
||||
@@ -49,9 +49,7 @@ export default Model.extend({
|
||||
label: 'Connection will be verified',
|
||||
defaultValue: true,
|
||||
}),
|
||||
allowed_roles: attr('array', {
|
||||
readOnly: true,
|
||||
}),
|
||||
allowed_roles: attr('array'),
|
||||
password_policy: attr('string', {
|
||||
label: 'Use custom password policy',
|
||||
editType: 'optionalText',
|
||||
|
||||
@@ -38,7 +38,7 @@ export default RESTSerializer.extend({
|
||||
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
|
||||
const nullResponses = ['updateRecord', 'createRecord', 'deleteRecord'];
|
||||
const connections = nullResponses.includes(requestType)
|
||||
? { name: id, backend: payload.backend }
|
||||
? { name: payload.data.name, backend: payload.data.backend }
|
||||
: this.normalizeSecrets(payload);
|
||||
const { modelName } = primaryModelClass;
|
||||
let transformedPayload = { [modelName]: connections };
|
||||
@@ -63,7 +63,8 @@ export default RESTSerializer.extend({
|
||||
// filter data to only allow plugin specific attrs
|
||||
const allowedAttributes = Object.keys(data).filter((dataAttrs) => pluginAttributes.includes(dataAttrs));
|
||||
for (const key in data) {
|
||||
if (!allowedAttributes.includes(key)) {
|
||||
// All connections allow allowed_roles but it's not shown on the form
|
||||
if (key !== 'allowed_roles' && !allowedAttributes.includes(key)) {
|
||||
delete data[key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</EmptyState>
|
||||
{{/unless}}
|
||||
{{#if (and (not @model.errorMessage) (eq @roleType "dynamic"))}}
|
||||
<Hds::Alert @type="inline" @color="warning" class="has-top-bottom-margin" as |A|>
|
||||
<Hds::Alert @type="inline" @color="warning" class="has-top-bottom-margin" data-test-credentials-warning as |A|>
|
||||
<A.Title>Warning</A.Title>
|
||||
<A.Description>
|
||||
You will not be able to access these credentials later, so please copy them now.
|
||||
|
||||
24
ui/mirage/factories/database-connection.js
Normal file
24
ui/mirage/factories/database-connection.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
|
||||
// For the purposes of testing, we only use a subset of fields relevant to mysql
|
||||
export default Factory.extend({
|
||||
backend: 'database',
|
||||
name: 'connection',
|
||||
plugin_name: 'mysql-database-plugin',
|
||||
verify_connection: true,
|
||||
connection_url: '{{username}}:{{password}}@tcp(127.0.0.1:33060)/',
|
||||
username: 'admin',
|
||||
max_open_connections: 4,
|
||||
max_idle_connections: 0,
|
||||
max_connection_lifetime: '0s',
|
||||
allowed_roles: () => [],
|
||||
root_rotation_statements: () => [
|
||||
'SELECT user from mysql.user',
|
||||
"GRANT ALL PRIVILEGES ON *.* to 'sudo'@'%'",
|
||||
],
|
||||
});
|
||||
124
ui/mirage/handlers/database.js
Normal file
124
ui/mirage/handlers/database.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { Response } from 'miragejs';
|
||||
|
||||
export default function (server) {
|
||||
const getRecord = (schema, req, dbKey) => {
|
||||
const { backend, name } = req.params;
|
||||
const record = schema.db[dbKey].findBy({ name, backend });
|
||||
if (record) {
|
||||
delete record.backend;
|
||||
delete record.id;
|
||||
}
|
||||
return record ? { data: record } : new Response(404, {}, { errors: [] });
|
||||
};
|
||||
const createOrUpdateRecord = (schema, req, key) => {
|
||||
const { backend, name } = req.params;
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
const record = schema[key].findOrCreateBy({ name, backend });
|
||||
record.update(payload);
|
||||
return new Response(204);
|
||||
};
|
||||
const deleteRecord = (schema, req, dbKey) => {
|
||||
const { name } = req.params;
|
||||
const record = schema.db[dbKey].findBy({ name });
|
||||
if (record) {
|
||||
schema.db[dbKey].remove(record.id);
|
||||
}
|
||||
return new Response(204);
|
||||
};
|
||||
|
||||
// Connection mgmt
|
||||
server.get('/:backend/config/:name', (schema, req) => {
|
||||
return getRecord(schema, req, 'database/connections');
|
||||
});
|
||||
server.get('/:backend/config', (schema) => {
|
||||
const keys = schema.db['databaseConnections'].map((record) => record.name);
|
||||
if (!keys.length) {
|
||||
return new Response(404, {}, { errors: [] });
|
||||
}
|
||||
return {
|
||||
data: {
|
||||
keys,
|
||||
},
|
||||
};
|
||||
});
|
||||
server.post('/:backend/config/:name', (schema, req) => {
|
||||
const { name } = req.params;
|
||||
const { username } = JSON.parse(req.requestBody);
|
||||
if (name === 'bad-connection') {
|
||||
return new Response(
|
||||
500,
|
||||
{},
|
||||
{
|
||||
errors: [
|
||||
`error creating database object: error verifying - ping: Error 1045 (28000): Access denied for user '${username}'@'192.168.65.1' (using password: YES)`,
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
return createOrUpdateRecord(schema, req, 'database/connections');
|
||||
});
|
||||
server.delete('/:backend/config/:name', (schema, req) => {
|
||||
return deleteRecord(schema, req, 'database-connection');
|
||||
});
|
||||
// Rotate root
|
||||
server.post('/:backend/rotate-root/:name', (schema, req) => {
|
||||
const { name } = req.params;
|
||||
if (name === 'fail-rotate') {
|
||||
return new Response(
|
||||
500,
|
||||
{},
|
||||
{
|
||||
errors: [
|
||||
"1 error occurred:\n\t* failed to update user: failed to change password: Error 1045 (28000): Access denied for user 'admin'@'%' (using password: YES)\n\n",
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
return new Response(204);
|
||||
});
|
||||
|
||||
// Generate credentials
|
||||
server.get('/:backend/creds/:role', (schema, req) => {
|
||||
const { role } = req.params;
|
||||
if (role === 'static-role') {
|
||||
// static creds
|
||||
return {
|
||||
request_id: 'static-1234',
|
||||
lease_id: '',
|
||||
renewable: false,
|
||||
lease_duration: 0,
|
||||
data: {
|
||||
last_vault_rotation: '2024-01-18T10:45:47.227193-06:00',
|
||||
password: 'generated-password',
|
||||
rotation_period: 86400,
|
||||
ttl: 3600,
|
||||
username: 'static-username',
|
||||
},
|
||||
wrap_info: null,
|
||||
warnings: null,
|
||||
auth: null,
|
||||
mount_type: 'database',
|
||||
};
|
||||
}
|
||||
// dynamic creds
|
||||
return {
|
||||
request_id: 'dynamic-1234',
|
||||
lease_id: `database/creds/${role}/abcd`,
|
||||
renewable: true,
|
||||
lease_duration: 3600,
|
||||
data: {
|
||||
password: 'generated-password',
|
||||
username: 'generated-username',
|
||||
},
|
||||
wrap_info: null,
|
||||
warnings: null,
|
||||
auth: null,
|
||||
mount_type: 'database',
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export default function (server) {
|
||||
server.get('/database/static-roles', function () {
|
||||
return {
|
||||
data: { keys: ['dev-static', 'prod-static'] },
|
||||
};
|
||||
});
|
||||
|
||||
server.get('/database/static-roles/:rolename', function (db, req) {
|
||||
if (req.params.rolename.includes('tester')) {
|
||||
return new Response(400);
|
||||
}
|
||||
return {
|
||||
data: {
|
||||
rotation_statements: [
|
||||
'{ "db": "admin", "roles": [{ "role": "readWrite" }, {"role": "read", "db": "foo"}] }',
|
||||
],
|
||||
db_name: 'connection',
|
||||
username: 'alice',
|
||||
rotation_period: '1h',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
server.post('/database/rotate-role/:rolename', function () {
|
||||
return new Response(204);
|
||||
});
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import base from './base';
|
||||
import chrootNamespace from './chroot-namespace';
|
||||
import customMessages from './custom-messages';
|
||||
import clients from './clients';
|
||||
import db from './db';
|
||||
import database from './database';
|
||||
import hcpLink from './hcp-link';
|
||||
import kms from './kms';
|
||||
import kubernetes from './kubernetes';
|
||||
@@ -24,7 +24,7 @@ export {
|
||||
base,
|
||||
chrootNamespace,
|
||||
clients,
|
||||
db,
|
||||
database,
|
||||
hcpLink,
|
||||
kms,
|
||||
kubernetes,
|
||||
|
||||
336
ui/tests/acceptance/secrets/backend/database/workflow-test.js
Normal file
336
ui/tests/acceptance/secrets/backend/database/workflow-test.js
Normal file
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* 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 ENV from 'vault/config/environment';
|
||||
import { setupApplicationTest } from 'vault/tests/helpers';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import flashMessage from 'vault/tests/pages/components/flash-message';
|
||||
import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands';
|
||||
|
||||
const flash = create(flashMessage);
|
||||
|
||||
const PAGE = {
|
||||
// GENERIC
|
||||
emptyStateTitle: '[data-test-empty-state-title]',
|
||||
emptyStateAction: '[data-test-secret-create="connections"]',
|
||||
infoRow: '[data-test-component="info-table-row"]',
|
||||
infoRowLabel: (label) => `[data-test-row-label="${label}"]`,
|
||||
infoRowValue: (label) => `[data-test-row-value="${label}"]`,
|
||||
infoRowValueDiv: (label) => `[data-test-value-div="${label}"]`,
|
||||
// CONNECTIONS
|
||||
rotateModal: '[data-test-db-connection-modal-title]',
|
||||
confirmRotate: '[data-test-enable-rotate-connection]',
|
||||
skipRotate: '[data-test-enable-connection]',
|
||||
// ROLES
|
||||
addRole: '[data-test-secret-create]',
|
||||
roleSettingsSection: '[data-test-role-settings-section]',
|
||||
statementsSection: '[data-test-statements-section]',
|
||||
editRole: '[data-test-edit-link]',
|
||||
generateCredentials: (type = 'dynamic') => `[data-test-database-role-creds="${type}"]`,
|
||||
};
|
||||
|
||||
const FORM = {
|
||||
inputByAttr: (attr) => `[data-test-input="${attr}"]`,
|
||||
creationStatement: (idx = 0) =>
|
||||
`[data-test-input="creation_statements"] [data-test-string-list-input="${idx}"]`,
|
||||
saveBtn: '[data-test-secret-save]',
|
||||
};
|
||||
|
||||
async function fillOutConnection(name) {
|
||||
await fillIn(FORM.inputByAttr('name'), name);
|
||||
await fillIn(FORM.inputByAttr('plugin_name'), 'mysql-database-plugin');
|
||||
await fillIn(FORM.inputByAttr('connection_url'), '{{username}}:{{password}}@tcp(127.0.0.1:33060)/');
|
||||
await fillIn(FORM.inputByAttr('username'), 'admin');
|
||||
await fillIn(FORM.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.before(function () {
|
||||
ENV['ember-cli-mirage'].handler = 'database';
|
||||
});
|
||||
hooks.after(function () {
|
||||
ENV['ember-cli-mirage'].handler = null;
|
||||
});
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
this.backend = `db-workflow-${uuidv4()}`;
|
||||
this.store = this.owner.lookup('service:store');
|
||||
await authPage.login();
|
||||
await runCmd(mountEngineCmd('database', this.backend), false);
|
||||
});
|
||||
|
||||
hooks.afterEach(async function () {
|
||||
await authPage.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`,
|
||||
},
|
||||
];
|
||||
});
|
||||
test('create with rotate', async function (assert) {
|
||||
assert.expect(24);
|
||||
this.server.post('/:backend/rotate-root/:name', () => {
|
||||
assert.ok(true, 'rotate root called');
|
||||
new Response(204);
|
||||
});
|
||||
await visit(`/vault/secrets/${this.backend}/overview`);
|
||||
assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct');
|
||||
await click(PAGE.emptyStateAction);
|
||||
assert.strictEqual(currentURL(), `/vault/secrets/${this.backend}/create`, 'Takes you to create page');
|
||||
|
||||
// fill in connection details
|
||||
await fillOutConnection(`connect-${this.backend}`);
|
||||
await click(FORM.saveBtn);
|
||||
|
||||
assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown');
|
||||
await click(PAGE.confirmRotate);
|
||||
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${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(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`);
|
||||
assert.dom(PAGE.infoRowValue(label)).hasText(value, `Value for ${label} is correct`);
|
||||
});
|
||||
});
|
||||
test('create without rotate', async function (assert) {
|
||||
assert.expect(23);
|
||||
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/${this.backend}/overview`);
|
||||
assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct');
|
||||
await click(PAGE.emptyStateAction);
|
||||
assert.strictEqual(currentURL(), `/vault/secrets/${this.backend}/create`, 'Takes you to create page');
|
||||
|
||||
// fill in connection details
|
||||
await fillOutConnection(`connect-${this.backend}`);
|
||||
await click(FORM.saveBtn);
|
||||
|
||||
assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown');
|
||||
await click(PAGE.skipRotate);
|
||||
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${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(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`);
|
||||
assert.dom(PAGE.infoRowValue(label)).hasText(value, `Value for ${label} is correct`);
|
||||
});
|
||||
});
|
||||
test('create failure', async function (assert) {
|
||||
assert.expect(25);
|
||||
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/${this.backend}/overview`);
|
||||
assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct');
|
||||
await click(PAGE.emptyStateAction);
|
||||
assert.strictEqual(currentURL(), `/vault/secrets/${this.backend}/create`, 'Takes you to create page');
|
||||
|
||||
// fill in connection details
|
||||
await fillOutConnection(`bad-connection`);
|
||||
await click(FORM.saveBtn);
|
||||
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(FORM.inputByAttr('name'), `connect-${this.backend}`);
|
||||
await click(FORM.saveBtn);
|
||||
assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown');
|
||||
await click(PAGE.confirmRotate);
|
||||
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${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(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`);
|
||||
assert.dom(PAGE.infoRowValue(label)).hasText(value, `Value for ${label} is correct`);
|
||||
});
|
||||
});
|
||||
|
||||
test('create connection with rotate failure', async function (assert) {
|
||||
await visit(`/vault/secrets/${this.backend}/overview`);
|
||||
assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct');
|
||||
await click(PAGE.emptyStateAction);
|
||||
assert.strictEqual(currentURL(), `/vault/secrets/${this.backend}/create`, 'Takes you to create page');
|
||||
|
||||
// fill in connection details
|
||||
await fillOutConnection(`fail-rotate`);
|
||||
await click(FORM.saveBtn);
|
||||
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/${this.backend}/show/fail-rotate`,
|
||||
'Takes you to details page for connection'
|
||||
);
|
||||
});
|
||||
});
|
||||
module('roles', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.connection = `connect-${this.backend}`;
|
||||
await visit(`/vault/secrets/${this.backend}/create`);
|
||||
await fillOutConnection(this.connection);
|
||||
await click(FORM.saveBtn);
|
||||
await visit(`/vault/secrets/${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/${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(FORM.inputByAttr('name'), roleName);
|
||||
assert.dom('[data-test-selected-option]').hasText(this.connection, 'Connection is selected by default');
|
||||
|
||||
await fillIn(FORM.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(FORM.saveBtn);
|
||||
// DETAILS
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${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 credentials’s Time-to-Live (TTL)', value: '1 hour' },
|
||||
{ label: 'Generated credentials’s 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 }) => {
|
||||
const valueSelector =
|
||||
label === 'Creation statements' ? PAGE.infoRowValueDiv(label) : PAGE.infoRowValue(label);
|
||||
assert.dom(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`);
|
||||
assert.dom(valueSelector).hasText(value, `Value for ${label} is correct`);
|
||||
});
|
||||
// EDIT
|
||||
await click(PAGE.editRole);
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${this.backend}/edit/role/${roleName}?itemType=role`,
|
||||
'Takes you to edit page for role'
|
||||
);
|
||||
// TODO: these should be readonly not disabled
|
||||
assert.dom(FORM.inputByAttr('name')).isDisabled('Name is read-only');
|
||||
assert.dom(FORM.inputByAttr('database')).isDisabled('Database is read-only');
|
||||
assert.dom(FORM.inputByAttr('type')).isDisabled('Type is read-only');
|
||||
await fillIn('[data-test-ttl-value="Generated credentials’s Time-to-Live (TTL)"]', '2');
|
||||
await click(FORM.saveBtn);
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${this.backend}/show/role/${roleName}`,
|
||||
'Takes you to details page for role after save'
|
||||
);
|
||||
assert
|
||||
.dom(PAGE.infoRowValue('Generated credentials’s Time-to-Live (TTL)'))
|
||||
.hasText('2 hours', 'Shows updated TTL');
|
||||
|
||||
// CREDENTIALS
|
||||
await click(PAGE.generateCredentials());
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${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(`[data-test-value-div="Username"] [data-test-masked-input]`)
|
||||
.hasText('***********', 'Username is masked');
|
||||
await click(`[data-test-value-div="Username"] [data-test-button="toggle-masked"]`);
|
||||
assert
|
||||
.dom(`[data-test-value-div="Username"] [data-test-masked-input]`)
|
||||
.hasText('generated-username', 'Username is generated');
|
||||
|
||||
assert
|
||||
.dom(`[data-test-value-div="Password"] [data-test-masked-input]`)
|
||||
.hasText('***********', 'Password is masked');
|
||||
await click(`[data-test-value-div="Password"] [data-test-button="toggle-masked"]`);
|
||||
assert
|
||||
.dom(`[data-test-value-div="Password"] [data-test-masked-input]`)
|
||||
.hasText('generated-password', 'Password is generated');
|
||||
assert.dom(PAGE.infoRowValue('Lease Duration')).hasText('3600', 'shows lease duration from response');
|
||||
assert
|
||||
.dom(PAGE.infoRowValue('Lease ID'))
|
||||
.hasText(`database/creds/${roleName}/abcd`, 'shows lease ID from response');
|
||||
});
|
||||
});
|
||||
});
|
||||
101
ui/tests/unit/serializers/database/connection-test.js
Normal file
101
ui/tests/unit/serializers/database/connection-test.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
module('Unit | Serializer | database/connection', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.uid = uuidv4();
|
||||
this.store = this.owner.lookup('service:store');
|
||||
});
|
||||
test('it should serialize only keys that are valid for the database type (elasticsearch)', function (assert) {
|
||||
const backend = `db-serializer-test-${this.uid}`;
|
||||
const name = `elastic-test-${this.uid}`;
|
||||
const record = this.store.createRecord('database/connection', {
|
||||
plugin_name: 'elasticsearch-database-plugin',
|
||||
backend,
|
||||
name,
|
||||
allowed_roles: ['readonly'],
|
||||
connection_url: 'http://localhost:9200',
|
||||
url: 'http://localhost:9200',
|
||||
username: 'elastic',
|
||||
password: 'changeme',
|
||||
tls_ca: 'some-value',
|
||||
ca_cert: undefined, // does not send undefined values
|
||||
});
|
||||
const expectedResult = {
|
||||
plugin_name: 'elasticsearch-database-plugin',
|
||||
backend,
|
||||
name,
|
||||
verify_connection: true,
|
||||
allowed_roles: ['readonly'],
|
||||
url: 'http://localhost:9200',
|
||||
username: 'elastic',
|
||||
password: 'changeme',
|
||||
insecure: false,
|
||||
};
|
||||
|
||||
const serializedRecord = record.serialize();
|
||||
assert.deepEqual(
|
||||
serializedRecord,
|
||||
expectedResult,
|
||||
'invalid elasticsearch options were not added to the payload'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should normalize values for the database type (elasticsearch)', function (assert) {
|
||||
const serializer = this.owner.lookup('serializer:database/connection');
|
||||
const normalized = serializer.normalizeSecrets({
|
||||
request_id: 'request-id',
|
||||
lease_id: '',
|
||||
renewable: false,
|
||||
lease_duration: 0,
|
||||
data: {
|
||||
allowed_roles: ['readonly'],
|
||||
connection_details: {
|
||||
backend: 'database',
|
||||
insecure: false,
|
||||
url: 'https://localhost:9200',
|
||||
username: 'root',
|
||||
},
|
||||
password_policy: '',
|
||||
plugin_name: 'elasticsearch-database-plugin',
|
||||
plugin_version: '',
|
||||
root_credentials_rotate_statements: [],
|
||||
},
|
||||
wrap_info: null,
|
||||
warnings: null,
|
||||
auth: null,
|
||||
mount_type: 'database',
|
||||
backend: 'database',
|
||||
id: 'elastic-test',
|
||||
});
|
||||
const expectedResult = {
|
||||
allowed_roles: ['readonly'],
|
||||
backend: 'database',
|
||||
connection_details: {
|
||||
backend: 'database',
|
||||
insecure: false,
|
||||
url: 'https://localhost:9200',
|
||||
username: 'root',
|
||||
},
|
||||
id: 'elastic-test',
|
||||
insecure: false,
|
||||
name: 'elastic-test',
|
||||
password_policy: '',
|
||||
plugin_name: 'elasticsearch-database-plugin',
|
||||
plugin_version: '',
|
||||
root_credentials_rotate_statements: [],
|
||||
root_rotation_statements: [],
|
||||
url: 'https://localhost:9200',
|
||||
username: 'root',
|
||||
};
|
||||
assert.deepEqual(normalized, expectedResult, `Normalizes and flattens database response`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user