UI: Database fixes (#24947)

This commit is contained in:
Chelsea Shaw
2024-01-24 12:04:44 -06:00
committed by GitHub
parent b87318b35e
commit a4611fbfaa
13 changed files with 597 additions and 43 deletions

3
changelog/24947.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:bug
ui: Fixed minor bugs with database secrets engine
```

View File

@@ -56,6 +56,7 @@ export default ApplicationAdapter.extend({
return { return {
data: { data: {
id, id,
name: id,
...data, ...data,
}, },
}; };

View File

@@ -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();

View File

@@ -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(() => {

View File

@@ -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',

View File

@@ -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];
} }
} }

View File

@@ -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.

View 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'@'%'",
],
});

View 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',
};
});
}

View File

@@ -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);
});
}

View File

@@ -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,

View 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 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 }) => {
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 credentialss 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 credentialss 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');
});
});
});

View 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`);
});
});