UI/Database Secrets Engine cleanup (#10949)

* Update role toolbar, serialization for special mongo values

* Only show defaultShown if no value on info table row

* Remove root_rotation_statements from mongo connection fields

* Wrap this.router in try/catch if in then statement

* Add changelog
This commit is contained in:
Chelsea Shaw 2021-02-19 14:04:51 -06:00 committed by GitHub
parent 6f3d179635
commit 889d82aca5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 81 additions and 49 deletions

3
changelog/10949.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: Customize MongoDB input fields on Database Secrets Engine
```

View File

@ -52,7 +52,7 @@ export default class DatabaseListItem extends Component {
adapter adapter
.rotateRootCredentials(backend, id) .rotateRootCredentials(backend, id)
.then(() => { .then(() => {
this.flashMessages.success(`Success: ${id} connection was reset`); this.flashMessages.success(`Success: ${id} connection was rotated`);
}) })
.catch(e => { .catch(e => {
this.flashMessages.danger(e.errors); this.flashMessages.danger(e.errors);

View File

@ -43,7 +43,11 @@ export default class DatabaseRoleEdit extends Component {
secret secret
.destroyRecord() .destroyRecord()
.then(() => { .then(() => {
this.router.transitionTo(LIST_ROOT_ROUTE, backend, { queryParams: { tab: 'role' } }); try {
this.router.transitionTo(LIST_ROOT_ROUTE, backend, { queryParams: { tab: 'role' } });
} catch (e) {
console.debug(e);
}
}) })
.catch(e => { .catch(e => {
this.flashMessages.danger(e.errors?.join('. ')); this.flashMessages.danger(e.errors?.join('. '));
@ -59,7 +63,11 @@ export default class DatabaseRoleEdit extends Component {
let path = roleSecret.type === 'static' ? 'static-roles' : 'roles'; let path = roleSecret.type === 'static' ? 'static-roles' : 'roles';
roleSecret.set('path', path); roleSecret.set('path', path);
roleSecret.save().then(() => { roleSecret.save().then(() => {
this.router.transitionTo(SHOW_ROUTE, `role/${secretId}`); try {
this.router.transitionTo(SHOW_ROUTE, `role/${secretId}`);
} catch (e) {
console.debug(e);
}
}); });
} }
@ -75,7 +83,11 @@ export default class DatabaseRoleEdit extends Component {
roleSecret.set('path', path); roleSecret.set('path', path);
} }
roleSecret.save().then(() => { roleSecret.save().then(() => {
this.router.transitionTo(SHOW_ROUTE, `role/${secretId}`); try {
this.router.transitionTo(SHOW_ROUTE, `role/${secretId}`);
} catch (e) {
console.debug(e);
}
}); });
} }
} }

View File

@ -35,7 +35,7 @@ const STATEMENT_FIELDS = {
}, },
dynamic: { dynamic: {
default: ['creation_statements', 'revocation_statements', 'rotation_statements'], default: ['creation_statements', 'revocation_statements', 'rotation_statements'],
'mongodb-database-plugin': ['creation_statement'], 'mongodb-database-plugin': ['creation_statement', 'revocation_statement'],
}, },
}; };
@ -44,25 +44,21 @@ export default class DatabaseRoleSettingForm extends Component {
const type = this.args.roleType; const type = this.args.roleType;
if (!type) return null; if (!type) return null;
const db = this.args.dbType || 'default'; const db = this.args.dbType || 'default';
const fields = ROLE_FIELDS[type][db]; const dbValidFields = ROLE_FIELDS[type][db];
if (!Array.isArray(fields)) return fields; if (!Array.isArray(dbValidFields)) return dbValidFields;
const filtered = this.args.attrs.filter(a => { return this.args.attrs.filter(a => {
const includes = fields.includes(a.name); return dbValidFields.includes(a.name);
return includes;
}); });
return filtered;
} }
get statementFields() { get statementFields() {
const type = this.args.roleType; const type = this.args.roleType;
if (!type) return null; if (!type) return null;
const db = this.args.dbType || 'default'; const db = this.args.dbType || 'default';
const fields = STATEMENT_FIELDS[type][db]; const dbValidFields = STATEMENT_FIELDS[type][db];
if (!Array.isArray(fields)) return fields; if (!Array.isArray(dbValidFields)) return dbValidFields;
const filtered = this.args.attrs.filter(a => { return this.args.attrs.filter(a => {
const includes = fields.includes(a.name); return dbValidFields.includes(a.name);
return includes;
}); });
return filtered;
} }
} }

View File

@ -79,10 +79,9 @@ export default Model.extend({
subText: 'x509 CA file for validating the certificate presented by the MongoDB server.', subText: 'x509 CA file for validating the certificate presented by the MongoDB server.',
editType: 'file', editType: 'file',
}), }),
root_rotation_statements: attr('string', { 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.`, 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: 'json', editType: 'stringArray',
theme: 'hashi short',
defaultShown: 'Default', defaultShown: 'Default',
}), }),
@ -115,15 +114,7 @@ export default Model.extend({
// for both create and edit fields // for both create and edit fields
mainFields: computed('plugin_name', function() { mainFields: computed('plugin_name', function() {
return [ return ['plugin_name', 'name', 'connection_url', 'verify_connection', 'password_policy', 'pluginConfig'];
'plugin_name',
'name',
'connection_url',
'verify_connection',
'password_policy',
'pluginConfig',
'root_rotation_statements',
];
}), }),
showAttrs: computed('plugin_name', function() { showAttrs: computed('plugin_name', function() {
@ -133,13 +124,15 @@ export default Model.extend({
'connection_url', 'connection_url',
'write_concern', 'write_concern',
'verify_connection', 'verify_connection',
'root_rotation_statements',
'allowed_roles', 'allowed_roles',
]; ];
return expandAttributeMeta(this, f); return expandAttributeMeta(this, f);
}), }),
pluginFieldGroups: computed('plugin_name', function() { pluginFieldGroups: computed('plugin_name', function() {
if (!this.plugin_name) {
return null;
}
let groups = [{ default: ['username', 'password', 'write_concern'] }]; let groups = [{ default: ['username', 'password', 'write_concern'] }];
// TODO: Get plugin options based on plugin // TODO: Get plugin options based on plugin
groups.push({ groups.push({

View File

@ -64,7 +64,7 @@ export default Model.extend({
theme: 'hashi short', theme: 'hashi short',
defaultShown: 'Default', defaultShown: 'Default',
}), }),
rotation_statement: attr('string', { revocation_statement: attr('string', {
editType: 'json', editType: 'json',
theme: 'hashi short', theme: 'hashi short',
defaultShown: 'Default', defaultShown: 'Default',
@ -72,7 +72,18 @@ export default Model.extend({
/* FIELD ATTRIBUTES */ /* FIELD ATTRIBUTES */
get fieldAttrs() { get fieldAttrs() {
let fields = ['database', 'name', 'type']; // Main fields on edit/create form
let fields = ['name', 'database', 'type'];
return expandAttributeMeta(this, fields);
},
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']);
}
return expandAttributeMeta(this, fields); return expandAttributeMeta(this, fields);
}, },
@ -86,8 +97,8 @@ export default Model.extend({
'creation_statements', 'creation_statements',
'creation_statement', // only for MongoDB (styling difference) 'creation_statement', // only for MongoDB (styling difference)
'revocation_statements', 'revocation_statements',
'revocation_statement', // only for MongoDB (styling difference)
'rotation_statements', 'rotation_statements',
'rotation_statement', // only for MongoDB (styling difference)
]; ];
return expandAttributeMeta(this, allRoleSettingFields); return expandAttributeMeta(this, allRoleSettingFields);
}), }),

View File

@ -24,12 +24,17 @@ export default RESTSerializer.extend({
if (payload.data.db_name) { if (payload.data.db_name) {
database = [payload.data.db_name]; database = [payload.data.db_name];
} }
// Copy to singular for MongoDB
const creation_statement = payload.data.creation_statements[0];
const revocation_statement = payload.data.revocation_statements[0];
return { return {
id: payload.secret, id: payload.secret,
name: payload.secret, name: payload.secret,
backend: payload.backend, backend: payload.backend,
database, database,
path, path,
creation_statement,
revocation_statement,
...payload.data, ...payload.data,
}; };
}, },
@ -64,6 +69,18 @@ export default RESTSerializer.extend({
data.db_name = db; data.db_name = db;
delete data.database; delete data.database;
} }
// This is necessary because the input for MongoDB is a json string
// rather than an array, so we transpose that here
if (data.creation_statement) {
const singleStatement = data.creation_statement;
data.creation_statements = [singleStatement];
delete data.creation_statement;
}
if (data.revocation_statement) {
const singleStatement = data.revocation_statement;
data.revocation_statements = [singleStatement];
delete data.revocation_statement;
}
return data; return data;
}, },

View File

@ -87,7 +87,7 @@
{{!-- Plugin Config Section --}} {{!-- Plugin Config Section --}}
<div class="form-section"> <div class="form-section">
<h3 class="title is-5">Plugin config</h3> <h3 class="title is-5">Plugin config</h3>
{{#unless @model.plugin_name}} {{#unless @model.pluginFieldGroups}}
<EmptyState <EmptyState
@title="No plugin selected" @title="No plugin selected"
@message="Select a plugin type to be able to configure it." @message="Select a plugin type to be able to configure it."
@ -97,7 +97,6 @@
{{#each-in fieldGroup as |group fields|}} {{#each-in fieldGroup as |group fields|}}
{{#if (eq group "default")}} {{#if (eq group "default")}}
{{#each fields as |attr|}} {{#each fields as |attr|}}
{{!-- TODO: special password edit mode --}}
{{form-field data-test-field attr=attr model=@model}} {{form-field data-test-field attr=attr model=@model}}
{{/each}} {{/each}}
{{else}} {{else}}

View File

@ -18,6 +18,19 @@
{{#if (eq @mode 'show')}} {{#if (eq @mode 'show')}}
<Toolbar> <Toolbar>
<ToolbarActions> <ToolbarActions>
{{#if @model.canDelete}}
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{action 'delete'}}
@confirmTitle="Delete role?"
@confirmMessage="This role will be permanently deleted. You will need to recreate it to use it again."
@confirmButtonText="Delete"
data-test-database-role-delete
>
Delete role
</ConfirmAction>
<div class="toolbar-separator" />
{{/if}}
{{#if @model.canGenerateCredentials}} {{#if @model.canGenerateCredentials}}
<button <button
type="button" type="button"
@ -28,18 +41,6 @@
Generate credentials Generate credentials
</button> </button>
{{/if}} {{/if}}
{{#if @model.canDelete}}
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{action 'delete'}}
@confirmTitle="Delete role?"
@confirmMessage="This role will be permanently deleted. You will need to re-create it to use it again."
@confirmButtonText="Delete"
data-test-database-role-delete
>
Delete role
</ConfirmAction>
{{/if}}
{{#if @model.canEditRole}} {{#if @model.canEditRole}}
<ToolbarSecretLink <ToolbarSecretLink
@secret={{concat 'role/' @model.id}} @secret={{concat 'role/' @model.id}}
@ -53,7 +54,7 @@
{{/if}} {{/if}}
</ToolbarActions> </ToolbarActions>
</Toolbar> </Toolbar>
{{#each @model.fieldAttrs as |attr|}} {{#each @model.showFields as |attr|}}
{{#let attr.options.defaultDisplay as |defaultDisplay|}} {{#let attr.options.defaultDisplay as |defaultDisplay|}}
{{#if (eq attr.type "object")}} {{#if (eq attr.type "object")}}
<InfoTableRow <InfoTableRow

View File

@ -28,7 +28,7 @@
@glyph="cancel-square-outline" @glyph="cancel-square-outline"
/> No /> No
{{/if}} {{/if}}
{{else if (and alwaysRender defaultShown)}} {{else if (and (not value) (and alwaysRender defaultShown))}}
{{defaultShown}} {{defaultShown}}
{{else}} {{else}}
{{#if (eq type 'array')}} {{#if (eq type 'array')}}