UI: Update PostgreSQL database params (#29200)

* add testing before model changes

* add enterprise self_managed attr, update tests

* add postgres params

* add changelog

* update test

* cleanup filter function

* fix nits
This commit is contained in:
claire bontempo
2024-12-18 12:21:18 -06:00
committed by GitHub
parent 96f32adb00
commit 13e4d0f230
5 changed files with 529 additions and 13 deletions

3
changelog/29200.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
ui: Adds params to postgresql database to improve editing a connection in the web browser.
```

View File

@@ -9,6 +9,7 @@ import { alias, or } from '@ember/object/computed';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import { AVAILABLE_PLUGIN_TYPES } from '../../utils/model-helpers/database-helpers';
import { service } from '@ember/service';
/**
* fieldsToGroups helper fn
@@ -31,6 +32,8 @@ const fieldsToGroups = function (arr, key = 'subgroup') {
};
export default Model.extend({
version: service(),
backend: attr('string', {
readOnly: true,
}),
@@ -77,6 +80,11 @@ export default Model.extend({
subText: 'The password to use when connecting with the above username.',
editType: 'password',
}),
disable_escaping: attr('boolean', {
defaultValue: false,
subText: 'Turns off the escaping of special characters inside of the username and password fields.',
docLink: 'https://developer.hashicorp.com/vault/docs/secrets/databases#disable-character-escaping',
}),
// optional
ca_cert: attr('string', {
@@ -125,12 +133,56 @@ export default Model.extend({
label: 'Disable SSL verification',
defaultValue: false,
}),
password_authentication: attr('string', {
defaultValue: 'password',
editType: 'radio',
subText: 'The default is "password."',
possibleValues: [
{
value: 'password',
helpText:
'Passwords will be sent to PostgreSQL in plaintext format and may appear in PostgreSQL logs as-is.',
},
{
value: 'scram-sha-256',
helpText:
'When set to "scram-sha-256", passwords will be hashed by Vault and stored as-is by PostgreSQL. Using "scram-sha-256" requires a minimum version of PostgreSQL 10.',
},
],
docLink:
'https://developer.hashicorp.com/vault/api-docs/secret/databases/postgresql#password_authentication',
}),
auth_type: attr('string', {
subText: 'If set to "gcp_iam", will enable IAM authentication to a Google CloudSQL instance.',
docLink: 'https://developer.hashicorp.com/vault/api-docs/secret/databases/postgresql#auth_type',
}),
service_account_json: attr('string', {
label: 'Service account JSON',
subText:
'JSON encoded credentials for a GCP Service Account to use for IAM authentication. Requires "auth_type" to be "gcp_iam".',
editType: 'file',
}),
use_private_ip: attr('boolean', {
label: 'Use private IP',
subText:
'Enables the option to connect to CloudSQL Instances with Private IP. Requires auth_type to be "gcp_iam".',
defaultValue: false,
}),
private_key: attr('string', {
helpText: 'The secret key used for the x509 client certificate. Must be PEM encoded.',
editType: 'file',
}),
tls: attr('string', {
label: 'TLS Certificate Key',
helpText:
'x509 certificate for connecting to the database. This must be a PEM encoded version of the private key and the certificate combined.',
editType: 'file',
}),
tls_certificate: attr('string', {
label: 'TLS Certificate Key',
helpText: 'The x509 certificate for connecting to the database. Must be PEM encoded.',
editType: 'file',
}),
tls_ca: attr('string', {
label: 'TLS CA',
helpText:
@@ -147,25 +199,28 @@ export default Model.extend({
defaultShown: 'Default',
}),
// ENTERPRISE ONLY
self_managed: attr('boolean', {
subText:
'Allows onboarding static roles with a rootless connection configuration. Mutually exclusive with username and password. If true, will force verify_connection to be false.',
defaultValue: false,
}),
isAvailablePlugin: computed('plugin_name', function () {
return !!AVAILABLE_PLUGIN_TYPES.find((a) => a.value === this.plugin_name);
}),
showAttrs: computed('plugin_name', function () {
const fields = AVAILABLE_PLUGIN_TYPES.find((a) => a.value === this.plugin_name)
.fields.filter((f) => f.show !== false)
.map((f) => f.attr);
const fields = this._filterFields((f) => f.show !== false).map((f) => f.attr);
fields.push('allowed_roles');
return expandAttributeMeta(this, fields);
}),
// for both create and edit fields
fieldAttrs: computed('plugin_name', function () {
// for both create and edit fields
let fields = ['plugin_name', 'name', 'connection_url', 'verify_connection', 'password_policy'];
if (this.plugin_name) {
fields = AVAILABLE_PLUGIN_TYPES.find((a) => a.value === this.plugin_name)
.fields.filter((f) => !f.group)
.map((field) => field.attr);
fields = this._filterFields((f) => !f.group).map((f) => f.attr);
}
return expandAttributeMeta(this, fields);
}),
@@ -174,9 +229,7 @@ export default Model.extend({
if (!this.plugin_name) {
return null;
}
const pluginFields = AVAILABLE_PLUGIN_TYPES.find((a) => a.value === this.plugin_name).fields.filter(
(f) => f.group === 'pluginConfig'
);
const pluginFields = this._filterFields((f) => f.group === 'pluginConfig');
const groups = fieldsToGroups(pluginFields, 'subgroup');
return fieldToAttrs(this, groups);
}),
@@ -185,12 +238,21 @@ export default Model.extend({
if (!this.plugin_name) {
return expandAttributeMeta(this, ['root_rotation_statements']);
}
const fields = AVAILABLE_PLUGIN_TYPES.find((a) => a.value === this.plugin_name)
.fields.filter((f) => f.group === 'statements')
.map((field) => field.attr);
const fields = this._filterFields((f) => f.group === 'statements').map((f) => f.attr);
return expandAttributeMeta(this, fields);
}),
// after checking for enterprise, filter callback fires and returns
_filterFields(filterCallback) {
const plugin = AVAILABLE_PLUGIN_TYPES.find((a) => a.value === this.plugin_name);
return plugin.fields.filter((field) => {
// return if attribute is enterprise only and we're on community
if (field?.isEnterprise && !this.version.isEnterprise) return false;
// filter by group, or if there isn't a group
return filterCallback(field);
});
},
/* CAPABILITIES */
editConnectionPath: lazyCapabilities(apiPath`${'backend'}/config/${'id'}`, 'backend', 'id'),
canEdit: alias('editConnectionPath.canUpdate'),

View File

@@ -169,11 +169,20 @@ export const AVAILABLE_PLUGIN_TYPES = [
{ attr: 'connection_url', group: 'pluginConfig' },
{ attr: 'username', group: 'pluginConfig', show: false },
{ attr: 'password', group: 'pluginConfig', show: false },
{ attr: 'password_authentication', group: 'pluginConfig' },
{ attr: 'max_open_connections', group: 'pluginConfig' },
{ attr: 'max_idle_connections', group: 'pluginConfig' },
{ attr: 'max_connection_lifetime', group: 'pluginConfig' },
{ attr: 'auth_type', group: 'pluginConfig' },
{ attr: 'service_account_json', group: 'pluginConfig' },
{ attr: 'use_private_ip', group: 'pluginConfig' },
{ attr: 'username_template', group: 'pluginConfig' },
{ attr: 'disable_escaping', group: 'pluginConfig' },
{ attr: 'root_rotation_statements', group: 'statements' },
{ attr: 'self_managed', group: 'pluginConfig', isEnterprise: true },
{ attr: 'private_key', group: 'pluginConfig', subgroup: 'TLS options' },
{ attr: 'tls_ca', group: 'pluginConfig', subgroup: 'TLS options' },
{ attr: 'tls_certificate', group: 'pluginConfig', subgroup: 'TLS options' },
],
},
];

View File

@@ -0,0 +1,347 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
// If we refactor the database/connection model we can move this const to a util and use it in a form component.
// The ideal (future) scenario is the API provides this and we don't have to manually write the schema :)
export const EXPECTED_FIELDS = {
'elasticsearch-database-plugin': {
default: [
'url',
'username',
'password',
'ca_cert',
'ca_path',
'client_cert',
'client_key',
'tls_server_name',
'insecure',
'username_template',
],
showAttrs: [
'plugin_name',
'name',
'password_policy',
'url',
'ca_cert',
'ca_path',
'client_cert',
'client_key',
'tls_server_name',
'insecure',
'username_template',
'allowed_roles',
],
fieldAttrs: ['plugin_name', 'name', 'verify_connection', 'password_policy'],
pluginFieldGroups: undefined,
statementFields: [],
},
'mongodb-database-plugin': {
default: ['username', 'password', 'write_concern', 'username_template'],
showAttrs: [
'plugin_name',
'name',
'connection_url',
'password_policy',
'write_concern',
'username_template',
'tls',
'tls_ca',
'root_rotation_statements',
'allowed_roles',
],
fieldAttrs: ['plugin_name', 'name', 'connection_url', 'verify_connection', 'password_policy'],
pluginFieldGroups: ['username', 'password', 'write_concern', 'username_template'],
statementFields: [
{
name: 'root_rotation_statements',
options: {
defaultShown: 'Default',
editType: 'stringArray',
subText:
"The database statements to be executed to rotate the root user's credentials. If nothing is entered, Vault will use a reasonable default.",
},
type: undefined,
},
],
'TLS options': ['tls', 'tls_ca'],
},
'mssql-database-plugin': {
default: [
'username',
'password',
'username_template',
'max_open_connections',
'max_idle_connections',
'max_connection_lifetime',
],
showAttrs: [
'plugin_name',
'name',
'connection_url',
'password_policy',
'username_template',
'max_open_connections',
'max_idle_connections',
'max_connection_lifetime',
'root_rotation_statements',
'allowed_roles',
],
fieldAttrs: ['plugin_name', 'name', 'connection_url', 'verify_connection', 'password_policy'],
statementFields: [
{
name: 'root_rotation_statements',
options: {
defaultShown: 'Default',
editType: 'stringArray',
subText:
"The database statements to be executed to rotate the root user's credentials. If nothing is entered, Vault will use a reasonable default.",
},
type: undefined,
},
],
},
'mysql-aurora-database-plugin': {
default: [
'connection_url',
'username',
'password',
'max_open_connections',
'max_idle_connections',
'max_connection_lifetime',
'username_template',
],
showAttrs: [
'plugin_name',
'name',
'password_policy',
'connection_url',
'max_open_connections',
'max_idle_connections',
'max_connection_lifetime',
'username_template',
'tls',
'tls_ca',
'root_rotation_statements',
'allowed_roles',
],
fieldAttrs: ['plugin_name', 'name', 'verify_connection', 'password_policy'],
statementFields: [
{
name: 'root_rotation_statements',
options: {
defaultShown: 'Default',
editType: 'stringArray',
subText:
"The database statements to be executed to rotate the root user's credentials. If nothing is entered, Vault will use a reasonable default.",
},
type: undefined,
},
],
'TLS options': ['tls', 'tls_ca'],
},
'mysql-legacy-database-plugin': {
default: [
'connection_url',
'username',
'password',
'max_open_connections',
'max_idle_connections',
'max_connection_lifetime',
'username_template',
],
showAttrs: [
'plugin_name',
'name',
'password_policy',
'connection_url',
'max_open_connections',
'max_idle_connections',
'max_connection_lifetime',
'username_template',
'tls',
'tls_ca',
'root_rotation_statements',
'allowed_roles',
],
fieldAttrs: ['plugin_name', 'name', 'verify_connection', 'password_policy'],
statementFields: [
{
name: 'root_rotation_statements',
options: {
defaultShown: 'Default',
editType: 'stringArray',
subText:
"The database statements to be executed to rotate the root user's credentials. If nothing is entered, Vault will use a reasonable default.",
},
type: undefined,
},
],
'TLS options': ['tls', 'tls_ca'],
},
'mysql-database-plugin': {
default: [
'connection_url',
'username',
'password',
'max_open_connections',
'max_idle_connections',
'max_connection_lifetime',
'username_template',
],
showAttrs: [
'plugin_name',
'name',
'password_policy',
'connection_url',
'max_open_connections',
'max_idle_connections',
'max_connection_lifetime',
'username_template',
'tls',
'tls_ca',
'root_rotation_statements',
'allowed_roles',
],
fieldAttrs: ['plugin_name', 'name', 'verify_connection', 'password_policy'],
statementFields: [
{
name: 'root_rotation_statements',
options: {
defaultShown: 'Default',
editType: 'stringArray',
subText:
"The database statements to be executed to rotate the root user's credentials. If nothing is entered, Vault will use a reasonable default.",
},
type: undefined,
},
],
'TLS options': ['tls', 'tls_ca'],
},
'mysql-rds-database-plugin': {
default: [
'connection_url',
'username',
'password',
'max_open_connections',
'max_idle_connections',
'max_connection_lifetime',
'username_template',
],
showAttrs: [
'plugin_name',
'name',
'password_policy',
'connection_url',
'max_open_connections',
'max_idle_connections',
'max_connection_lifetime',
'username_template',
'tls',
'tls_ca',
'root_rotation_statements',
'allowed_roles',
],
fieldAttrs: ['plugin_name', 'name', 'verify_connection', 'password_policy'],
statementFields: [
{
name: 'root_rotation_statements',
options: {
defaultShown: 'Default',
editType: 'stringArray',
subText:
"The database statements to be executed to rotate the root user's credentials. If nothing is entered, Vault will use a reasonable default.",
},
type: undefined,
},
],
'TLS options': ['tls', 'tls_ca'],
},
'vault-plugin-database-oracle': {
default: [
'connection_url',
'username',
'password',
'max_open_connections',
'max_idle_connections',
'max_connection_lifetime',
'username_template',
],
showAttrs: [
'plugin_name',
'name',
'password_policy',
'connection_url',
'max_open_connections',
'max_idle_connections',
'max_connection_lifetime',
'username_template',
'root_rotation_statements',
'allowed_roles',
],
fieldAttrs: ['plugin_name', 'name', 'verify_connection', 'password_policy'],
statementFields: [
{
name: 'root_rotation_statements',
options: {
defaultShown: 'Default',
editType: 'stringArray',
subText:
"The database statements to be executed to rotate the root user's credentials. If nothing is entered, Vault will use a reasonable default.",
},
type: undefined,
},
],
},
'postgresql-database-plugin': {
default: [
'connection_url',
'username',
'password',
'password_authentication',
'max_open_connections',
'max_idle_connections',
'max_connection_lifetime',
'auth_type',
'service_account_json',
'use_private_ip',
'username_template',
'disable_escaping',
],
showAttrs: [
'plugin_name',
'name',
'password_policy',
'connection_url',
'password_authentication',
'max_open_connections',
'max_idle_connections',
'max_connection_lifetime',
'auth_type',
'service_account_json',
'use_private_ip',
'username_template',
'disable_escaping',
'root_rotation_statements',
'private_key',
'tls_ca',
'tls_certificate',
'allowed_roles',
],
fieldAttrs: ['plugin_name', 'name', 'verify_connection', 'password_policy'],
statementFields: [
{
name: 'root_rotation_statements',
options: {
defaultShown: 'Default',
editType: 'stringArray',
subText:
"The database statements to be executed to rotate the root user's credentials. If nothing is entered, Vault will use a reasonable default.",
},
type: undefined,
},
],
enterpriseOnly: 'self_managed',
},
};

View File

@@ -0,0 +1,95 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { EXPECTED_FIELDS } from 'vault/tests/helpers/secret-engine/database-helpers';
import { AVAILABLE_PLUGIN_TYPES } from 'vault/utils/model-helpers/database-helpers';
module('Unit | Model | database/connection', function (hooks) {
setupTest(hooks);
hooks.beforeEach(async function () {
this.store = this.owner.lookup('service:store');
this.version = this.owner.lookup('service:version');
// setting version here so tests can be run locally on CE or ENT
this.version.type = 'community';
this.createModel = (plugin) =>
this.store.createRecord('database/connection', {
plugin_name: plugin,
});
});
for (const plugin of AVAILABLE_PLUGIN_TYPES.map((p) => p.value)) {
module(`it computes fields for plugin_type: ${plugin}`, function (hooks) {
hooks.beforeEach(function () {
this.model = this.createModel(plugin);
this.getActual = (group) => this.model[group];
this.getExpected = (group) => EXPECTED_FIELDS[plugin][group];
const pluginFields = AVAILABLE_PLUGIN_TYPES.find((o) => o.value === plugin).fields;
this.pluginHasTlsOptions = pluginFields.some((f) => f.subgroup === 'TLS options');
this.pluginHasEnterpriseAttrs = pluginFields.some((f) => f.isEnterprise);
});
test('it computes showAttrs', function (assert) {
const actual = this.getActual('showAttrs').map((a) => a.name);
const expected = this.getExpected('showAttrs');
assert.propEqual(actual, expected, 'actual computed attrs match expected');
});
test('it computes fieldAttrs', function (assert) {
const actual = this.getActual('fieldAttrs').map((a) => a.name);
const expected = this.getExpected('fieldAttrs');
assert.propEqual(actual, expected, 'actual computed attrs match expected');
});
test('it computes default group', function (assert) {
// pluginFieldGroups is an array of group objects
const [actualDefault] = this.getActual('pluginFieldGroups');
assert.propEqual(
actualDefault.default.map((a) => a.name),
this.getExpected('default'),
'it has expected default group attributes'
);
});
test('it computes statementFields', function (assert) {
const actual = this.getActual('statementFields');
const expected = this.getExpected('statementFields');
assert.propEqual(actual, expected, 'actual computed attrs match expected');
});
if (this.pluginHasTlsOptions) {
test('it computes TLS options group', function (assert) {
// pluginFieldGroups is an array of group objects
const [, actualTlsOptions] = this.getActual('pluginFieldGroups');
assert.propEqual(
actualTlsOptions['TLS options'].map((a) => a.name),
this.getExpected('TLS options'),
'it has expected TLS options'
);
});
}
if (this.pluginHasEnterpriseAttrs) {
test('it includes enterprise fields', function (assert) {
this.version.type = 'enterprise';
const [actualDefault] = this.getActual('pluginFieldGroups');
const expected = this.getExpected('default').push(this.getExpected('enterpriseOnly'));
assert.propEqual(
actualDefault.default.map((a) => a.name),
expected,
'it includes enterprise attributes'
);
});
}
});
}
});