diff --git a/changelog/29114.txt b/changelog/29114.txt new file mode 100644 index 000000000000..6900093c564d --- /dev/null +++ b/changelog/29114.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Decode database url to fix editing failures for an oracle connection +``` \ No newline at end of file diff --git a/ui/app/helpers/decode-uri.js b/ui/app/helpers/decode-uri.js deleted file mode 100644 index be4f471770ab..000000000000 --- a/ui/app/helpers/decode-uri.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { helper as buildHelper } from '@ember/component/helper'; - -export function decodeUri(string) { - return decodeURI(string); -} - -export default buildHelper(decodeUri); diff --git a/ui/app/serializers/database/connection.js b/ui/app/serializers/database/connection.js index 92951ccf5da8..c1bbed7fd113 100644 --- a/ui/app/serializers/database/connection.js +++ b/ui/app/serializers/database/connection.js @@ -29,6 +29,16 @@ export default RESTSerializer.extend({ ...payload.data, ...payload.data.connection_details, }; + + // connection_details are spread above into the main body of response so we can remove redundant data + delete response.connection_details; + if (response?.connection_url) { + // this url can include interpolated data, such as: "{{username}}/{{password}}@localhost:1521/OraDoc.localhost" + // these curly brackets are returned by the API encoded: "%7B%7Busername%7D%7D/%7B%7Bpassword%7D%7D@localhost:1521/OraDoc.localhost" + // we decode here so the UI displays and submits the url in the correct format + response.connection_url = decodeURI(response.connection_url); + } + if (payload.data.root_credentials_rotate_statements) { response.root_rotation_statements = payload.data.root_credentials_rotate_statements; } diff --git a/ui/app/templates/components/database-connection.hbs b/ui/app/templates/components/database-connection.hbs index c7ef46b3f126..c1bafe8f2d14 100644 --- a/ui/app/templates/components/database-connection.hbs +++ b/ui/app/templates/components/database-connection.hbs @@ -353,7 +353,7 @@ @alwaysRender={{not (is-empty-value (get @model attr.name) hasDefault=defaultDisplay)}} @defaultShown={{defaultDisplay}} @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} - @value={{if (eq attr.name "connection_url") (decode-uri (get @model attr.name)) (get @model attr.name)}} + @value={{get @model attr.name}} /> {{/if}} {{/let}} diff --git a/ui/tests/acceptance/secrets/backend/database/secret-test.js b/ui/tests/acceptance/secrets/backend/database/secret-test.js index 638f7b3dccdf..612a70489d06 100644 --- a/ui/tests/acceptance/secrets/backend/database/secret-test.js +++ b/ui/tests/acceptance/secrets/backend/database/secret-test.js @@ -6,6 +6,7 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { currentURL, settled, click, visit, fillIn, typeIn, waitFor } from '@ember/test-helpers'; +import { setupMirage } from 'ember-cli-mirage/test-support'; import { create } from 'ember-cli-page-object'; import { selectChoose } from 'ember-power-select/test-support'; import { clickTrigger } from 'ember-power-select/test-support/helpers'; @@ -226,6 +227,7 @@ const connectionTests = [ module('Acceptance | secrets/database/*', function (hooks) { setupApplicationTest(hooks); + setupMirage(hooks); hooks.beforeEach(async function () { this.backend = `database-testing`; @@ -337,9 +339,11 @@ module('Acceptance | secrets/database/*', function (hooks) { await visit('/vault/secrets'); }); } - test('database connection create and edit: vault-plugin-database-oracle', async function (assert) { + + // keep oracle as separate test because it relies on an external plugin that isn't rolled into the vault binary + // https://github.com/hashicorp/vault-plugin-database-oracle + test('database connection create: vault-plugin-database-oracle', async function (assert) { assert.expect(11); - // keep oracle as separate test because it behaves differently than the others const testCase = { name: 'oracle-connection', plugin: 'vault-plugin-database-oracle', @@ -380,7 +384,52 @@ module('Acceptance | secrets/database/*', function (hooks) { await connectionPage.connectionUrl(testCase.url); testCase.requiredFields(assert, testCase.plugin); // Cannot save without plugin mounted - // TODO: add fake server response for fuller test coverage + // Edit tested separately with mocked server response + }); + + test('database connection edit: vault-plugin-database-oracle', async function (assert) { + assert.expect(2); + const connectionName = 'oracle-connection'; + // mock API so we can test edit (without mounting external oracle plugin) + this.server.get(`/${this.backend}/config/${connectionName}`, () => { + return { + request_id: 'f869f23e-15c0-389b-82ac-84035a2b6079', + lease_id: '', + renewable: false, + lease_duration: 0, + data: { + allowed_roles: ['*'], + connection_details: { + backend: 'database', + connection_url: '%7B%7Busername%7D%7D/%7B%7Bpassword%7D%7D@//localhost:1521/ORCLPDB1', + max_connection_lifetime: '0s', + max_idle_connections: 0, + max_open_connections: 3, + username: 'VAULTADMIN', + }, + password_policy: '', + plugin_name: 'vault-plugin-database-oracle', + plugin_version: '', + root_credentials_rotate_statements: [], + verify_connection: true, + }, + wrap_info: null, + warnings: null, + auth: null, + mount_type: 'database', + }; + }); + + await visit(`/vault/secrets/${this.backend}/show/${connectionName}`); + const decoded = '{{username}}/{{password}}@//localhost:1521/ORCLPDB1'; + assert + .dom('[data-test-row-value="Connection URL"]') + .hasText(decoded, 'connection_url is decoded in display'); + + await connectionPage.edit(); + assert + .dom('[data-test-input="connection_url"]') + .hasValue(decoded, 'connection_url is decoded when editing'); }); test('Can create and delete a connection', async function (assert) { @@ -504,17 +553,17 @@ module('Acceptance | secrets/database/*', function (hooks) { await visit('/vault/secrets'); }); - test('connection_url must be decoded', async function (assert) { + test('connection_url is decoded', async function (assert) { const backend = this.backend; const connection = await newConnection( backend, 'mongodb-database-plugin', - '{{username}}/{{password}}@oracle-xe:1521/XEPDB1' + '{{username}}/{{password}}@mongo:1521/XEPDB1' ); await navToConnection(backend, connection); assert .dom('[data-test-row-value="Connection URL"]') - .hasText('{{username}}/{{password}}@oracle-xe:1521/XEPDB1'); + .hasText('{{username}}/{{password}}@mongo:1521/XEPDB1'); }); test('Role create form', async function (assert) { diff --git a/ui/tests/unit/serializers/database/connection-test.js b/ui/tests/unit/serializers/database/connection-test.js index 13b73871aa90..90445ea1e016 100644 --- a/ui/tests/unit/serializers/database/connection-test.js +++ b/ui/tests/unit/serializers/database/connection-test.js @@ -79,12 +79,6 @@ module('Unit | Serializer | database/connection', function (hooks) { 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', @@ -98,4 +92,111 @@ module('Unit | Serializer | database/connection', function (hooks) { }; assert.deepEqual(normalized, expectedResult, `Normalizes and flattens database response`); }); + + test('it should normalize values for the database type (oracle)', 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: ['*'], + connection_details: { + backend: 'database', + connection_url: '%7B%7Busername%7D%7D/%7B%7Bpassword%7D%7D@//localhost:1521/ORCLPDB1', + max_connection_lifetime: '0s', + max_idle_connections: 0, + max_open_connections: 3, + username: 'VAULTADMIN', + }, + password_policy: '', + plugin_name: 'vault-plugin-database-oracle', + plugin_version: '', + root_credentials_rotate_statements: [], + verify_connection: true, + }, + wrap_info: null, + warnings: null, + auth: null, + mount_type: 'database', + backend: 'database', + id: 'oracle-test', + }); + const expectedResult = { + allowed_roles: ['*'], + backend: 'database', + connection_url: '{{username}}/{{password}}@//localhost:1521/ORCLPDB1', + id: 'oracle-test', + max_connection_lifetime: '0s', + max_idle_connections: 0, + max_open_connections: 3, + name: 'oracle-test', + password_policy: '', + plugin_name: 'vault-plugin-database-oracle', + plugin_version: '', + root_credentials_rotate_statements: [], + root_rotation_statements: [], + username: 'VAULTADMIN', + verify_connection: true, + }; + assert.deepEqual(normalized, expectedResult, `Normalizes and flattens database response`); + }); + + test('it should normalize values if some params do not exist', 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: ['*'], + connection_details: { backend: 'database' }, // no connection_url param intentionally + plugin_name: 'vault-postgres-db', + }, + wrap_info: null, + warnings: null, + auth: null, + mount_type: 'database', + backend: 'database', + id: 'db-test', + }); + const expectedResult = { + allowed_roles: ['*'], + backend: 'database', + id: 'db-test', + name: 'db-test', + plugin_name: 'vault-postgres-db', + }; + assert.deepEqual(normalized, expectedResult, `Normalizes and flattens database response`); + }); + + test('it should fail gracefully if no connection_details', 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: ['*'], + plugin_name: 'vault-postgres-db', + }, + wrap_info: null, + warnings: null, + auth: null, + mount_type: 'database', + backend: 'database', + id: 'db-test', + }); + const expectedResult = { + allowed_roles: ['*'], + backend: 'database', + id: 'db-test', + name: 'db-test', + plugin_name: 'vault-postgres-db', + }; + assert.deepEqual(normalized, expectedResult, `Normalizes and flattens database response`); + }); });