UI/Add Elasticsearch DB (#12672)
* displays empty state if database is not supported in the UI * adds elasticsearch db plugin * adds changelog * updates elasticsearch attrs * move tls_server_name to pluginConfig group * move role setting fields to util * updates comments and refactors using util function * adds tests for elasticsearch * fixes indentation * when local host needs https * adds line at bottom of hbs file
This commit is contained in:
parent
1b54217094
commit
4b709e8b3b
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
**Elasticsearch in the UI**: Elasticsearch DB is now supported by the UI
|
||||
```
|
|
@ -14,38 +14,12 @@
|
|||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { getStatementFields, getRoleFields } from '../utils/database-role-fields';
|
||||
|
||||
// Below fields are intended to be dynamic based on type of role and db.
|
||||
// example of usage: FIELDS[roleType][db]
|
||||
const ROLE_FIELDS = {
|
||||
static: ['username', 'rotation_period'],
|
||||
dynamic: ['ttl', 'max_ttl'],
|
||||
};
|
||||
|
||||
const STATEMENT_FIELDS = {
|
||||
static: {
|
||||
default: ['rotation_statements'],
|
||||
'mongodb-database-plugin': [],
|
||||
'mssql-database-plugin': [],
|
||||
'mysql-database-plugin': [],
|
||||
'mysql-aurora-database-plugin': [],
|
||||
'mysql-rds-database-plugin': [],
|
||||
'mysql-legacy-database-plugin': [],
|
||||
},
|
||||
dynamic: {
|
||||
default: ['creation_statements', 'revocation_statements', 'rollback_statements', 'renew_statements'],
|
||||
'mongodb-database-plugin': ['creation_statement', 'revocation_statement'],
|
||||
'mssql-database-plugin': ['creation_statements', 'revocation_statements'],
|
||||
'mysql-database-plugin': ['creation_statements', 'revocation_statements'],
|
||||
'mysql-aurora-database-plugin': ['creation_statements', 'revocation_statements'],
|
||||
'mysql-rds-database-plugin': ['creation_statements', 'revocation_statements'],
|
||||
'mysql-legacy-database-plugin': ['creation_statements', 'revocation_statements'],
|
||||
},
|
||||
};
|
||||
export default class DatabaseRoleSettingForm extends Component {
|
||||
get settingFields() {
|
||||
if (!this.args.roleType) return null;
|
||||
let dbValidFields = ROLE_FIELDS[this.args.roleType];
|
||||
let dbValidFields = getRoleFields(this.args.roleType);
|
||||
return this.args.attrs.filter(a => {
|
||||
return dbValidFields.includes(a.name);
|
||||
});
|
||||
|
@ -55,10 +29,7 @@ export default class DatabaseRoleSettingForm extends Component {
|
|||
const type = this.args.roleType;
|
||||
const plugin = this.args.dbType;
|
||||
if (!type) return null;
|
||||
let dbValidFields = STATEMENT_FIELDS[type].default;
|
||||
if (STATEMENT_FIELDS[type][plugin]) {
|
||||
dbValidFields = STATEMENT_FIELDS[type][plugin];
|
||||
}
|
||||
let dbValidFields = getStatementFields(type, plugin);
|
||||
return this.args.attrs.filter(a => {
|
||||
return dbValidFields.includes(a.name);
|
||||
});
|
||||
|
|
|
@ -121,6 +121,26 @@ const AVAILABLE_PLUGIN_TYPES = [
|
|||
{ attr: 'root_rotation_statements', group: 'statements' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'elasticsearch-database-plugin',
|
||||
displayName: 'Elasticsearch',
|
||||
fields: [
|
||||
{ attr: 'plugin_name' },
|
||||
{ attr: 'name' },
|
||||
{ attr: 'verify_connection' },
|
||||
{ attr: 'password_policy' },
|
||||
{ attr: 'url', group: 'pluginConfig' },
|
||||
{ attr: 'username', group: 'pluginConfig', show: false },
|
||||
{ attr: 'password', group: 'pluginConfig', show: false },
|
||||
{ attr: 'ca_cert', group: 'pluginConfig' },
|
||||
{ attr: 'ca_path', group: 'pluginConfig' },
|
||||
{ attr: 'client_cert', group: 'pluginConfig' },
|
||||
{ attr: 'client_key', group: 'pluginConfig' },
|
||||
{ attr: 'tls_server_name', group: 'pluginConfig' },
|
||||
{ attr: 'insecure', group: 'pluginConfig' },
|
||||
{ attr: 'username_template', group: 'pluginConfig' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -149,7 +169,7 @@ export default Model.extend({
|
|||
}),
|
||||
// required
|
||||
name: attr('string', {
|
||||
label: 'Connection Name',
|
||||
label: 'Connection name',
|
||||
}),
|
||||
plugin_name: attr('string', {
|
||||
label: 'Database plugin',
|
||||
|
@ -177,22 +197,38 @@ export default Model.extend({
|
|||
|
||||
// common fields
|
||||
connection_url: attr('string', {
|
||||
subText: 'The connection string used to connect to the database.',
|
||||
label: 'Connection URL',
|
||||
subText:
|
||||
'The connection string used to connect to the database. This allows for simple templating of username and password of the root user in the {{field_name}} format.',
|
||||
}),
|
||||
url: attr('string', {
|
||||
subText:
|
||||
'The connection string used to connect to the database. This allows for simple templating of username and password of the root user.',
|
||||
label: 'URL',
|
||||
subText: `The URL for Elasticsearch's API ("https://localhost:9200").`,
|
||||
}),
|
||||
username: attr('string', {
|
||||
subText: 'Optional. The name of the user to use as the "root" user when connecting to the database.',
|
||||
subText: `The name of the user to use as the "root" user when connecting to the database.`,
|
||||
}),
|
||||
password: attr('string', {
|
||||
subText:
|
||||
'Optional. The password to use when connecting to the database. Typically used in the connection_url field via the templating directive {{password}}.',
|
||||
subText: 'The password to use when connecting with the above username.',
|
||||
editType: 'password',
|
||||
}),
|
||||
|
||||
// optional
|
||||
ca_cert: attr('string', {
|
||||
label: 'CA certificate',
|
||||
subText: `The path to a PEM-encoded CA cert file to use to verify the Elasticsearch server's identity.`,
|
||||
}),
|
||||
ca_path: attr('string', {
|
||||
label: 'CA path',
|
||||
subText: `The path to a directory of PEM-encoded CA cert files to use to verify the Elasticsearch server's identity.`,
|
||||
}),
|
||||
client_cert: attr('string', {
|
||||
label: 'Client certificate',
|
||||
subText: 'The path to the certificate for the Elasticsearch client to present for communication.',
|
||||
}),
|
||||
client_key: attr('string', {
|
||||
subText: 'The path to the key for the Elasticsearch client to use for communication.',
|
||||
}),
|
||||
hosts: attr('string', {}),
|
||||
host: attr('string', {}),
|
||||
port: attr('string', {}),
|
||||
|
@ -220,6 +256,10 @@ export default Model.extend({
|
|||
max_connection_lifetime: attr('string', {
|
||||
defaultValue: '0s',
|
||||
}),
|
||||
insecure: attr('boolean', {
|
||||
defaultValue: false,
|
||||
label: 'Disable SSL verification',
|
||||
}),
|
||||
tls: attr('string', {
|
||||
label: 'TLS Certificate Key',
|
||||
helpText:
|
||||
|
@ -232,12 +272,20 @@ export default Model.extend({
|
|||
'x509 CA file for validating the certificate presented by the MongoDB server. Must be PEM encoded.',
|
||||
editType: 'file',
|
||||
}),
|
||||
tls_server_name: attr('string', {
|
||||
label: 'TLS server name',
|
||||
subText: 'If set, this name is used to set the SNI host when connecting via 1TLS.',
|
||||
}),
|
||||
root_rotation_statements: attr({
|
||||
subText: `The database statements to be executed to rotate the root user's credentials. If nothing is entered, Vault will use a reasonable default.`,
|
||||
editType: 'stringArray',
|
||||
defaultShown: 'Default',
|
||||
}),
|
||||
|
||||
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)
|
||||
|
|
|
@ -3,6 +3,7 @@ import { computed } from '@ember/object';
|
|||
import { alias } from '@ember/object/computed';
|
||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
import { getRoleFields } from '../../utils/database-role-fields';
|
||||
|
||||
export default Model.extend({
|
||||
idPrefix: 'role/',
|
||||
|
@ -90,11 +91,7 @@ export default Model.extend({
|
|||
|
||||
get showFields() {
|
||||
let fields = ['name', 'database', 'type'];
|
||||
if (this.type === 'dynamic') {
|
||||
fields = fields.concat(['ttl', 'max_ttl', 'creation_statements', 'revocation_statements']);
|
||||
} else {
|
||||
fields = fields.concat(['username', 'rotation_period']);
|
||||
}
|
||||
fields = fields.concat(getRoleFields(this.type)).concat(['creation_statements', 'revocation_statements']);
|
||||
return expandAttributeMeta(this, fields);
|
||||
},
|
||||
|
||||
|
@ -106,9 +103,9 @@ export default Model.extend({
|
|||
'username',
|
||||
'rotation_period',
|
||||
'creation_statements',
|
||||
'creation_statement', // only for MongoDB (styling difference)
|
||||
'creation_statement', // for editType: JSON
|
||||
'revocation_statements',
|
||||
'revocation_statement', // only for MongoDB (styling difference)
|
||||
'revocation_statement', // only for MongoDB (editType: JSON)
|
||||
'rotation_statements',
|
||||
'rollback_statements',
|
||||
'renew_statements',
|
||||
|
|
|
@ -42,6 +42,8 @@
|
|||
|
||||
.empty-state-actions {
|
||||
margin-top: $spacing-xs;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
a,
|
||||
.link,
|
||||
|
@ -54,6 +56,7 @@
|
|||
|
||||
> * + * {
|
||||
margin-left: $spacing-s;
|
||||
margin-right: $spacing-s;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
{{#if (eq @mode "show")}}
|
||||
{{#if @model.isAvailablePlugin}}
|
||||
{{#if (eq @mode "show")}}
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if @model.canDelete}}
|
||||
|
@ -78,6 +79,7 @@
|
|||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq @mode 'create')}}
|
||||
|
@ -174,7 +176,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{else if (eq @mode 'edit')}}
|
||||
{{else if (and (eq @mode 'edit') @model.isAvailablePlugin)}}
|
||||
<form {{on 'submit' this.handleUpdateConnection}}>
|
||||
{{#each @model.fieldAttrs as |attr|}}
|
||||
{{#if (or (eq attr.name 'name') (eq attr.name 'plugin_name'))}}
|
||||
|
@ -290,6 +292,19 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{else if (eq @model.isAvailablePlugin false)}}
|
||||
<EmptyState
|
||||
@title="Database type unavailable"
|
||||
@subTitle="Not supported in the UI"
|
||||
@icon="disabled"
|
||||
@message="This database type cannot be viewed in the UI. You will have to use the API or CLI to perform actions here."
|
||||
@bottomBorder={{true}}
|
||||
>
|
||||
<LinkTo @route="vault.cluster.secrets.backend.list-root" class="link">
|
||||
<Chevron @direction="left"/> Go back
|
||||
</LinkTo>
|
||||
<a href="https://www.vaultproject.io/api/secret/databases" target="_blank" rel="noreferrer noopener">Documentation</a>
|
||||
</EmptyState>
|
||||
{{else}}
|
||||
{{#each @model.showAttrs as |attr|}}
|
||||
{{#let attr.options.defaultDisplay as |defaultDisplay|}}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{{#linked-block
|
||||
"vault.cluster.secrets.backend.show"
|
||||
(if this.keyTypeValue (concat 'role/' @item.id) @item.id)
|
||||
<LinkedBlock
|
||||
@params={{array "vault.cluster.secrets.backend.show"
|
||||
(if this.keyTypeValue (concat 'role/' @item.id) @item.id)}}
|
||||
class="list-item-row"
|
||||
data-test-secret-link=@item.id
|
||||
encode=true
|
||||
queryParams=(secret-query-params @backendType @item.type)
|
||||
}}
|
||||
data-test-secret-link={{@item.id}}
|
||||
encode={{true}}
|
||||
queryParams={{secret-query-params @backendType @item.type}}
|
||||
>
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-10">
|
||||
<LinkTo
|
||||
|
@ -80,4 +80,4 @@
|
|||
</PopupMenu>
|
||||
</div>
|
||||
</div>
|
||||
{{/linked-block}}
|
||||
</LinkedBlock>
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
export const ROLE_FIELDS = {
|
||||
static: ['username', 'rotation_period'],
|
||||
dynamic: ['ttl', 'max_ttl'],
|
||||
};
|
||||
|
||||
export const STATEMENT_FIELDS = {
|
||||
static: {
|
||||
default: ['rotation_statements'],
|
||||
'mongodb-database-plugin': [],
|
||||
'mssql-database-plugin': [],
|
||||
'mysql-database-plugin': [],
|
||||
'mysql-aurora-database-plugin': [],
|
||||
'mysql-rds-database-plugin': [],
|
||||
'mysql-legacy-database-plugin': [],
|
||||
'elasticsearch-database-plugin': [],
|
||||
},
|
||||
dynamic: {
|
||||
default: ['creation_statements', 'revocation_statements', 'rollback_statements', 'renew_statements'],
|
||||
'mongodb-database-plugin': ['creation_statement', 'revocation_statement'],
|
||||
'mssql-database-plugin': ['creation_statements', 'revocation_statements'],
|
||||
'mysql-database-plugin': ['creation_statements', 'revocation_statements'],
|
||||
'mysql-aurora-database-plugin': ['creation_statements', 'revocation_statements'],
|
||||
'mysql-rds-database-plugin': ['creation_statements', 'revocation_statements'],
|
||||
'mysql-legacy-database-plugin': ['creation_statements', 'revocation_statements'],
|
||||
'elasticsearch-database-plugin': ['creation_statement'],
|
||||
},
|
||||
};
|
||||
|
||||
export function getStatementFields(type, plugin) {
|
||||
if (!type) return null;
|
||||
let dbValidFields = STATEMENT_FIELDS[type].default;
|
||||
if (STATEMENT_FIELDS[type][plugin]) {
|
||||
dbValidFields = STATEMENT_FIELDS[type][plugin];
|
||||
}
|
||||
return dbValidFields;
|
||||
}
|
||||
|
||||
export function getRoleFields(type) {
|
||||
if (!type) return null;
|
||||
return ROLE_FIELDS[type];
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<div class="test" ...attributes>
|
||||
<div ...attributes>
|
||||
{{#if @disabled }}
|
||||
<div class="list-item-row linkable-item is-no-underline">
|
||||
{{yield (hash content=(component 'linkable-item/content'))}}
|
||||
|
|
|
@ -34,7 +34,7 @@ const newConnection = async (backend, plugin = 'mongodb-database-plugin') => {
|
|||
await connectionPage.visitCreate({ backend });
|
||||
await connectionPage.dbPlugin(plugin);
|
||||
await connectionPage.name(name);
|
||||
await connectionPage.url(`mongodb://127.0.0.1:4321/${name}`);
|
||||
await connectionPage.connectionUrl(`mongodb://127.0.0.1:4321/${name}`);
|
||||
await connectionPage.toggleVerify();
|
||||
await connectionPage.save();
|
||||
await connectionPage.enable();
|
||||
|
@ -42,6 +42,26 @@ const newConnection = async (backend, plugin = 'mongodb-database-plugin') => {
|
|||
};
|
||||
|
||||
const connectionTests = [
|
||||
{
|
||||
name: 'elasticsearch-connection',
|
||||
plugin: 'elasticsearch-database-plugin',
|
||||
elasticUser: 'username',
|
||||
elasticPassword: 'password',
|
||||
url: 'http://127.0.0.1:9200',
|
||||
requiredFields: async (assert, name) => {
|
||||
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
|
||||
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
|
||||
assert.dom('[data-test-input="ca_cert"]').exists(`CA certificate field exists for ${name}`);
|
||||
assert.dom('[data-test-input="ca_path"]').exists(`CA path field exists for ${name}`);
|
||||
assert.dom('[data-test-input="client_cert"]').exists(`Client certificate field exists for ${name}`);
|
||||
assert.dom('[data-test-input="client_key"]').exists(`Client key field exists for ${name}`);
|
||||
assert.dom('[data-test-input="tls_server_name"]').exists(`TLS server name field exists for ${name}`);
|
||||
assert.dom('[data-test-input="insecure"]').exists(`Insecure checkbox exists for ${name}`);
|
||||
assert
|
||||
.dom('[data-test-toggle-input="show-username_template"]')
|
||||
.exists(`Username template toggle exists for ${name}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mongodb-connection',
|
||||
plugin: 'mongodb-database-plugin',
|
||||
|
@ -208,7 +228,13 @@ module('Acceptance | secrets/database/*', function(hooks) {
|
|||
await connectionPage.dbPlugin(testCase.plugin);
|
||||
assert.dom('[data-test-empty-state]').doesNotExist('Empty state goes away after plugin selected');
|
||||
await connectionPage.name(testCase.name);
|
||||
if (testCase.plugin === 'elasticsearch-database-plugin') {
|
||||
await connectionPage.url(testCase.url);
|
||||
await connectionPage.username(testCase.elasticUser);
|
||||
await connectionPage.password(testCase.elasticPassword);
|
||||
} else {
|
||||
await connectionPage.connectionUrl(testCase.url);
|
||||
}
|
||||
testCase.requiredFields(assert, testCase.name);
|
||||
await connectionPage.toggleVerify();
|
||||
await connectionPage.save();
|
||||
|
@ -253,8 +279,8 @@ module('Acceptance | secrets/database/*', function(hooks) {
|
|||
plugin: 'mongodb-database-plugin',
|
||||
id: 'horses-db',
|
||||
fields: [
|
||||
{ label: 'Connection Name', name: 'name', value: 'horses-db' },
|
||||
{ label: 'Connection url', name: 'connection_url', value: 'mongodb://127.0.0.1:235/horses' },
|
||||
{ label: 'Connection name', name: 'name', value: 'horses-db' },
|
||||
{ label: 'Connection URL', name: 'connection_url', value: 'mongodb://127.0.0.1:235/horses' },
|
||||
{ label: 'Username', name: 'username', value: 'user', hideOnShow: true },
|
||||
{ label: 'Password', name: 'password', password: 'so-secure', hideOnShow: true },
|
||||
{ label: 'Write concern', name: 'write_concern' },
|
||||
|
|
|
@ -19,6 +19,11 @@ const testCases = [
|
|||
'renew_statements',
|
||||
],
|
||||
},
|
||||
{
|
||||
pluginType: 'elasticsearch-database-plugin',
|
||||
staticRoleFields: ['username', 'rotation_period'],
|
||||
dynamicRoleFields: ['creation_statement', 'ttl', 'max_ttl'],
|
||||
},
|
||||
{
|
||||
pluginType: 'mongodb-database-plugin',
|
||||
staticRoleFields: ['username', 'rotation_period'],
|
||||
|
|
|
@ -10,7 +10,10 @@ export default create({
|
|||
dbPlugin: selectable('[data-test-input="plugin_name"]'),
|
||||
name: fillable('[data-test-input="name"]'),
|
||||
toggleVerify: clickable('[data-test-input="verify_connection"]'),
|
||||
url: fillable('[data-test-input="connection_url"'),
|
||||
connectionUrl: fillable('[data-test-input="connection_url"]'),
|
||||
url: fillable('[data-test-input="url"]'),
|
||||
username: fillable('[data-test-input="username"]'),
|
||||
password: fillable('[data-test-input="password"]'),
|
||||
save: clickable('[data-test-secret-save=""]'),
|
||||
addRole: clickable('[data-test-secret-create="true"]'), // only from connection show
|
||||
enable: clickable('[data-test-enable-connection=""]'),
|
||||
|
|
Loading…
Reference in New Issue