UI: Decode Oracle database connection_url (#29114)

* decode url in the serializer for oracle connection_url

* add serializer test

* add test for oracle

* add test back, remove decode-url helper

* update comment and test

* link jiras VAULT-32830 VAULT-29785

* add changelog

* add test
This commit is contained in:
claire bontempo 2024-12-10 09:31:09 -08:00 committed by GitHub
parent 59489a8882
commit 5ba4fb3df6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 176 additions and 25 deletions

3
changelog/29114.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
ui: Decode database url to fix editing failures for an oracle connection
```

View File

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

View File

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

View File

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

View File

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

View File

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