mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 11:08:10 +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 {
|
return {
|
||||||
data: {
|
data: {
|
||||||
id,
|
id,
|
||||||
|
name: id,
|
||||||
...data,
|
...data,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export default ApplicationAdapter.extend({
|
|||||||
|
|
||||||
async _updateAllowedRoles(store, { role, backend, db, type = 'add' }) {
|
async _updateAllowedRoles(store, { role, backend, db, type = 'add' }) {
|
||||||
const connection = await store.queryRecord('database/connection', { backend, id: db });
|
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]);
|
const allowedRoles = type === 'add' ? addToArray([roles, role]) : removeFromArray([roles, role]);
|
||||||
connection.allowed_roles = allowedRoles;
|
connection.allowed_roles = allowedRoles;
|
||||||
return connection.save();
|
return connection.save();
|
||||||
|
|||||||
@@ -59,8 +59,6 @@ export default class DatabaseConnectionEdit extends Component {
|
|||||||
async handleCreateConnection(evt) {
|
async handleCreateConnection(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const secret = this.args.model;
|
const secret = this.args.model;
|
||||||
const secretId = secret.name;
|
|
||||||
secret.set('id', secretId);
|
|
||||||
secret
|
secret
|
||||||
.save()
|
.save()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -49,9 +49,7 @@ export default Model.extend({
|
|||||||
label: 'Connection will be verified',
|
label: 'Connection will be verified',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
}),
|
}),
|
||||||
allowed_roles: attr('array', {
|
allowed_roles: attr('array'),
|
||||||
readOnly: true,
|
|
||||||
}),
|
|
||||||
password_policy: attr('string', {
|
password_policy: attr('string', {
|
||||||
label: 'Use custom password policy',
|
label: 'Use custom password policy',
|
||||||
editType: 'optionalText',
|
editType: 'optionalText',
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default RESTSerializer.extend({
|
|||||||
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
|
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
|
||||||
const nullResponses = ['updateRecord', 'createRecord', 'deleteRecord'];
|
const nullResponses = ['updateRecord', 'createRecord', 'deleteRecord'];
|
||||||
const connections = nullResponses.includes(requestType)
|
const connections = nullResponses.includes(requestType)
|
||||||
? { name: id, backend: payload.backend }
|
? { name: payload.data.name, backend: payload.data.backend }
|
||||||
: this.normalizeSecrets(payload);
|
: this.normalizeSecrets(payload);
|
||||||
const { modelName } = primaryModelClass;
|
const { modelName } = primaryModelClass;
|
||||||
let transformedPayload = { [modelName]: connections };
|
let transformedPayload = { [modelName]: connections };
|
||||||
@@ -63,7 +63,8 @@ export default RESTSerializer.extend({
|
|||||||
// filter data to only allow plugin specific attrs
|
// filter data to only allow plugin specific attrs
|
||||||
const allowedAttributes = Object.keys(data).filter((dataAttrs) => pluginAttributes.includes(dataAttrs));
|
const allowedAttributes = Object.keys(data).filter((dataAttrs) => pluginAttributes.includes(dataAttrs));
|
||||||
for (const key in data) {
|
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];
|
delete data[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
</EmptyState>
|
</EmptyState>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
{{#if (and (not @model.errorMessage) (eq @roleType "dynamic"))}}
|
{{#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.Title>Warning</A.Title>
|
||||||
<A.Description>
|
<A.Description>
|
||||||
You will not be able to access these credentials later, so please copy them now.
|
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 chrootNamespace from './chroot-namespace';
|
||||||
import customMessages from './custom-messages';
|
import customMessages from './custom-messages';
|
||||||
import clients from './clients';
|
import clients from './clients';
|
||||||
import db from './db';
|
import database from './database';
|
||||||
import hcpLink from './hcp-link';
|
import hcpLink from './hcp-link';
|
||||||
import kms from './kms';
|
import kms from './kms';
|
||||||
import kubernetes from './kubernetes';
|
import kubernetes from './kubernetes';
|
||||||
@@ -24,7 +24,7 @@ export {
|
|||||||
base,
|
base,
|
||||||
chrootNamespace,
|
chrootNamespace,
|
||||||
clients,
|
clients,
|
||||||
db,
|
database,
|
||||||
hcpLink,
|
hcpLink,
|
||||||
kms,
|
kms,
|
||||||
kubernetes,
|
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