diff --git a/changelog/24947.txt b/changelog/24947.txt new file mode 100644 index 0000000000..498158e2c5 --- /dev/null +++ b/changelog/24947.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Fixed minor bugs with database secrets engine +``` \ No newline at end of file diff --git a/ui/app/adapters/database/connection.js b/ui/app/adapters/database/connection.js index 5555d74cb6..beea13f36f 100644 --- a/ui/app/adapters/database/connection.js +++ b/ui/app/adapters/database/connection.js @@ -56,6 +56,7 @@ export default ApplicationAdapter.extend({ return { data: { id, + name: id, ...data, }, }; diff --git a/ui/app/adapters/database/role.js b/ui/app/adapters/database/role.js index 2a3002c1d1..560def403c 100644 --- a/ui/app/adapters/database/role.js +++ b/ui/app/adapters/database/role.js @@ -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(); diff --git a/ui/app/components/database-connection.js b/ui/app/components/database-connection.js index 060a7702b2..a4e6c8c759 100644 --- a/ui/app/components/database-connection.js +++ b/ui/app/components/database-connection.js @@ -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(() => { diff --git a/ui/app/models/database/connection.js b/ui/app/models/database/connection.js index c6037eb4cf..fc1a595bcc 100644 --- a/ui/app/models/database/connection.js +++ b/ui/app/models/database/connection.js @@ -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', diff --git a/ui/app/serializers/database/connection.js b/ui/app/serializers/database/connection.js index b8271bca54..96aaae8a0d 100644 --- a/ui/app/serializers/database/connection.js +++ b/ui/app/serializers/database/connection.js @@ -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]; } } diff --git a/ui/app/templates/components/generate-credentials-database.hbs b/ui/app/templates/components/generate-credentials-database.hbs index 78b448fcf2..a40b257b92 100644 --- a/ui/app/templates/components/generate-credentials-database.hbs +++ b/ui/app/templates/components/generate-credentials-database.hbs @@ -40,7 +40,7 @@ {{/unless}} {{#if (and (not @model.errorMessage) (eq @roleType "dynamic"))}} - + Warning You will not be able to access these credentials later, so please copy them now. diff --git a/ui/mirage/factories/database-connection.js b/ui/mirage/factories/database-connection.js new file mode 100644 index 0000000000..2f6e8db22a --- /dev/null +++ b/ui/mirage/factories/database-connection.js @@ -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'@'%'", + ], +}); diff --git a/ui/mirage/handlers/database.js b/ui/mirage/handlers/database.js new file mode 100644 index 0000000000..e4c22aac59 --- /dev/null +++ b/ui/mirage/handlers/database.js @@ -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', + }; + }); +} diff --git a/ui/mirage/handlers/db.js b/ui/mirage/handlers/db.js deleted file mode 100644 index b00383c4f6..0000000000 --- a/ui/mirage/handlers/db.js +++ /dev/null @@ -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); - }); -} diff --git a/ui/mirage/handlers/index.js b/ui/mirage/handlers/index.js index b7e9df950e..1c1e0d3b06 100644 --- a/ui/mirage/handlers/index.js +++ b/ui/mirage/handlers/index.js @@ -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, diff --git a/ui/tests/acceptance/secrets/backend/database/workflow-test.js b/ui/tests/acceptance/secrets/backend/database/workflow-test.js new file mode 100644 index 0000000000..4536e0f1af --- /dev/null +++ b/ui/tests/acceptance/secrets/backend/database/workflow-test.js @@ -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'); + }); + }); +}); diff --git a/ui/tests/unit/serializers/database/connection-test.js b/ui/tests/unit/serializers/database/connection-test.js new file mode 100644 index 0000000000..13b73871aa --- /dev/null +++ b/ui/tests/unit/serializers/database/connection-test.js @@ -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`); + }); +});