UI: Add the wizard to the database secret engine (#10982)

* wizard setup

* cleanup

* add changelog

* fix names from save to create role and create database

* fix missing progress bar
This commit is contained in:
Angel Garbarino 2021-02-23 13:52:39 -07:00 committed by GitHub
parent e60cc11f33
commit af2b9af24e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 292 additions and 201 deletions

3
changelog/10982.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
ui: Adds the wizard to the Database Secret Engine
```

View File

@ -19,6 +19,7 @@ export default class DatabaseConnectionEdit extends Component {
@service store;
@service router;
@service flashMessages;
@service wizard;
@tracked
showPasswordField = false; // used for edit mode
@ -26,6 +27,13 @@ export default class DatabaseConnectionEdit extends Component {
@tracked
showSaveModal = false; // used for create mode
constructor() {
super(...arguments);
if (this.wizard.featureState === 'details' || this.wizard.featureState === 'connection') {
this.wizard.transitionFeatureMachine(this.wizard.featureState, 'CONTINUE', 'database');
}
}
rotateCredentials(backend, name) {
let adapter = this.store.adapterFor('database/connection');
return adapter.rotateRootCredentials(backend, name);

View File

@ -9,6 +9,18 @@ const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
export default class DatabaseRoleEdit extends Component {
@service router;
@service flashMessages;
@service wizard;
constructor() {
super(...arguments);
console.log(this.wizard.featureState, 'featureSTate');
if (
this.wizard.featureState === 'displayConnection' ||
this.wizard.featureState === 'displayRoleDatabase'
) {
this.wizard.transitionFeatureMachine(this.wizard.featureState, 'CONTINUE', 'database');
}
}
@tracked loading = false;

View File

@ -20,6 +20,7 @@ export default Component.extend({
nextStep: computed('fullNextStep', function() {
return this.fullNextStep.split('.').lastObject;
}),
needsConnection: equal('mountSubtype', 'database'),
needsEncryption: equal('mountSubtype', 'transit'),
stepComponent: alias('wizard.stepComponent'),
detailsComponent: computed('currentMachine', 'mountSubtype', function() {

View File

@ -39,6 +39,9 @@ export default {
],
on: {
CONTINUE: {
connection: {
cond: type => type === 'database',
},
role: {
cond: type => ['pki', 'aws', 'ssh'].includes(type),
},
@ -51,6 +54,15 @@ export default {
},
},
},
connection: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/secrets-connection' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
CONTINUE: 'displayConnection',
},
},
encryption: {
onEntry: [
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
@ -87,6 +99,24 @@ export default {
CONTINUE: 'credentials',
},
},
displayConnection: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/secrets-connection-show' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
CONTINUE: 'displayRoleDatabase',
},
},
displayRoleDatabase: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/secrets-display-database-role' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
CONTINUE: 'display',
},
},
secret: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/secrets-secret' },
@ -103,6 +133,10 @@ export default {
],
on: {
REPEAT: {
connection: {
cond: type => type === 'database',
actions: [{ type: 'routeTransition', params: ['vault.cluster.secrets.backend.create-root'] }],
},
role: {
cond: type => ['pki', 'aws', 'ssh'].includes(type),
actions: [{ type: 'routeTransition', params: ['vault.cluster.secrets.backend.create-root'] }],

View File

@ -16,212 +16,212 @@
</PageHeader>
{{#if (eq @mode "show")}}
<Toolbar>
<ToolbarActions>
{{#if @model.canDelete}}
<button
type="button"
class="toolbar-link"
{{on 'click' this.delete}}
data-test-database-connection-delete
>
Delete connection
</button>
{{/if}}
{{#if @model.canReset}}
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{action 'reset'}}
@confirmTitle="Reset connection?"
@confirmMessage="This will close the connection and its underlying plugin and restart it with the configuration stored in the barrier."
@confirmButtonText="Reset"
data-test-database-connection-reset
>
Reset connection
</ConfirmAction>
{{/if}}
{{#if (and @model.canReset @model.canDelete)}}
<div class="toolbar-separator" />
{{/if}}
{{#if @model.canRotate }}
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{this.rotate}}
@confirmTitle="Rotate credentials?"
@confirmMessage={{'This will rotate the "root" user credentials stored for the database connection. The password will not be accessible once rotated.'}}
@confirmButtonText="Rotate"
data-test-database-connection-rotate
>
Rotate root credentials
</ConfirmAction>
{{/if}}
{{#if @model.canAddRole}}
<ToolbarSecretLink
@secret=''
@mode="create"
@type="add"
@queryParams={{query-params initialKey=(or filter baseKey.id) itemType="role"}}
@data-test-secret-create=true
>
Add role
</ToolbarSecretLink>
{{/if}}
{{#if @model.canEdit}}
<ToolbarSecretLink
@secret={{@model.id}}
@mode="edit"
@data-test-edit-link=true
@replace=true
>
Edit configuration
</ToolbarSecretLink>
{{/if}}
</ToolbarActions>
</Toolbar>
<Toolbar>
<ToolbarActions>
{{#if @model.canDelete}}
<button
type="button"
class="toolbar-link"
{{on 'click' this.delete}}
data-test-database-connection-delete
>
Delete connection
</button>
{{/if}}
{{#if @model.canReset}}
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{action 'reset'}}
@confirmTitle="Reset connection?"
@confirmMessage="This will close the connection and its underlying plugin and restart it with the configuration stored in the barrier."
@confirmButtonText="Reset"
data-test-database-connection-reset
>
Reset connection
</ConfirmAction>
{{/if}}
{{#if (and @model.canReset @model.canDelete)}}
<div class="toolbar-separator" />
{{/if}}
{{#if @model.canRotate }}
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{this.rotate}}
@confirmTitle="Rotate credentials?"
@confirmMessage={{'This will rotate the "root" user credentials stored for the database connection. The password will not be accessible once rotated.'}}
@confirmButtonText="Rotate"
data-test-database-connection-rotate
>
Rotate root credentials
</ConfirmAction>
{{/if}}
{{#if @model.canAddRole}}
<ToolbarSecretLink
@secret=''
@mode="create"
@type="add"
@queryParams={{query-params initialKey=(or filter baseKey.id) itemType="role"}}
@data-test-secret-create=true
>
Add role
</ToolbarSecretLink>
{{/if}}
{{#if @model.canEdit}}
<ToolbarSecretLink
@secret={{@model.id}}
@mode="edit"
@data-test-edit-link=true
@replace=true
>
Edit configuration
</ToolbarSecretLink>
{{/if}}
</ToolbarActions>
</Toolbar>
{{/if}}
{{#if (eq @mode 'create')}}
<form {{on 'submit' this.handleCreateConnection}}>
{{#each @model.fieldAttrs as |attr|}}
{{#if (eq attr.name "pluginConfig")}}
{{!-- Plugin Config Section --}}
<div class="form-section">
<h3 class="title is-5">Plugin config</h3>
{{#unless @model.pluginFieldGroups}}
<EmptyState
@title="No plugin selected"
@message="Select a plugin type to be able to configure it."
/>
{{else}}
{{#each @model.pluginFieldGroups as |fieldGroup|}}
{{#each-in fieldGroup as |group fields|}}
{{#if (eq group "default")}}
{{#each fields as |attr|}}
{{form-field data-test-field attr=attr model=@model}}
{{/each}}
{{else}}
<ToggleButton @class="is-block" @toggleAttr={{concat "show" (camelize group)}} @toggleTarget={{this}} @openLabel={{concat "Hide " group}} @closedLabel={{group}} @data-test-toggle-group={{group}} />
{{#if (get this (concat "show" (camelize group)))}}
<div class="box is-marginless">
{{#each fields as |attr|}}
{{form-field data-test-field attr=attr model=@model}}
{{/each}}
</div>
{{/if}}
{{/if}}
{{/each-in}}
{{/each}}
{{/unless}}
</div>
{{else if (not-eq attr.options.readOnly true)}}
{{form-field data-test-field attr=attr model=@model}}
{{/if}}
{{/each}}
<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless">
<div class="field is-grouped">
<div class="control">
<button
data-test-secret-save
type="submit"
disabled={{buttonDisabled}}
class="button is-primary"
>
Save
</button>
</div>
<div class="control">
<SecretLink @mode="list" @class="button">
Cancel
</SecretLink>
</div>
</div>
</div>
</form>
{{else if (eq @mode 'edit')}}
<form {{on 'submit' this.handleUpdateConnection}}>
{{#each @model.fieldAttrs as |attr|}}
{{#if (eq attr.name "pluginConfig")}}
<div class="form-section">
<h3 class="title is-5">Plugin config</h3>
{{#each @model.pluginFieldGroups as |fieldGroup|}}
{{#each-in fieldGroup as |group fields|}}
{{#if (eq group "default")}}
{{#each fields as |attr|}}
{{#if (eq attr.name "password")}}
<label for="{{attr.name}}" class="is-label">
{{capitalize (or attr.options.label attr.name)}}
</label>
<div class="field">
<Toggle
@name="show-{{attr.name}}"
@status="success"
@size="small"
@onChange={{fn this.updateShowPassword (not this.showPasswordField)}}
@checked={{this.showPasswordField}}
data-test-toggle={{attr.name}}
>
<span class="ttl-picker-label has-text-grey">Update password</span><br/>
<div class="description has-text-grey">
<span>{{if this.showPasswordField 'The new password that will be used when connecting to the database' 'Vault will use the existing password'}}</span>
</div>
{{#if this.showPasswordField}}
<Input
{{on "change" (fn this.updatePassword attr.name)}}
@type="password"
@value={{get @model attr.name}}
@name={{attr.name}}
class="input"
{{!-- Prevents browsers from auto-filling --}}
@autocomplete="new-password"
@spellcheck="false" />
{{/if}}
</Toggle>
</div>
{{else}}
<form {{on 'submit' this.handleCreateConnection}}>
{{#each @model.fieldAttrs as |attr|}}
{{#if (eq attr.name "pluginConfig")}}
{{!-- Plugin Config Section --}}
<div class="form-section">
<h3 class="title is-5">Plugin config</h3>
{{#unless @model.pluginFieldGroups}}
<EmptyState
@title="No plugin selected"
@message="Select a plugin type to be able to configure it."
/>
{{else}}
{{#each @model.pluginFieldGroups as |fieldGroup|}}
{{#each-in fieldGroup as |group fields|}}
{{#if (eq group "default")}}
{{#each fields as |attr|}}
{{form-field data-test-field attr=attr model=@model}}
{{/each}}
{{else}}
<ToggleButton @class="is-block" @toggleAttr={{concat "show" (camelize group)}} @toggleTarget={{this}} @openLabel={{concat "Hide " group}} @closedLabel={{group}} @data-test-toggle-group={{group}} />
{{#if (get this (concat "show" (camelize group)))}}
<div class="box is-marginless">
{{#each fields as |attr|}}
{{form-field data-test-field attr=attr model=@model}}
{{/each}}
</div>
{{/if}}
{{/each}}
{{else}}
<ToggleButton @class="is-block" @toggleAttr={{concat "show" (camelize group)}} @toggleTarget={{this}} @openLabel={{concat "Hide " group}} @closedLabel={{group}} @data-test-toggle-group={{group}} />
{{#if (get this (concat "show" (camelize group)))}}
<div class="box is-marginless">
{{#each fields as |attr|}}
{{form-field data-test-field attr=attr model=@model}}
{{/each}}
</div>
{{/if}}
{{/if}}
{{/each-in}}
{{/each}}
</div>
{{else if (or (eq attr.name 'name') (eq attr.name 'plugin_name'))}}
<ReadonlyFormField @attr={{attr}} @value={{get @model attr.name}} />
{{else if (not-eq attr.options.readOnly true)}}
{{form-field data-test-field attr=attr model=@model}}
{{/if}}
{{/each}}
{{/each-in}}
{{/each}}
{{/unless}}
</div>
{{else if (not-eq attr.options.readOnly true)}}
{{form-field data-test-field attr=attr model=@model}}
{{/if}}
{{/each}}
<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless">
<div class="field is-grouped">
<div class="control">
<button
data-test-secret-save
type="submit"
disabled={{buttonDisabled}}
class="button is-primary"
>
Save
</button>
</div>
<div class="control">
<SecretLink @mode="list" @class="button">
Cancel
</SecretLink>
<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless">
<div class="field is-grouped">
<div class="control">
<button
data-test-secret-save
type="submit"
disabled={{buttonDisabled}}
class="button is-primary"
>
Create database
</button>
</div>
<div class="control">
<SecretLink @mode="list" @class="button">
Cancel
</SecretLink>
</div>
</div>
</div>
</div>
</form>
</form>
{{else if (eq @mode 'edit')}}
<form {{on 'submit' this.handleUpdateConnection}}>
{{#each @model.fieldAttrs as |attr|}}
{{#if (eq attr.name "pluginConfig")}}
<div class="form-section">
<h3 class="title is-5">Plugin config</h3>
{{#each @model.pluginFieldGroups as |fieldGroup|}}
{{#each-in fieldGroup as |group fields|}}
{{#if (eq group "default")}}
{{#each fields as |attr|}}
{{#if (eq attr.name "password")}}
<label for="{{attr.name}}" class="is-label">
{{capitalize (or attr.options.label attr.name)}}
</label>
<div class="field">
<Toggle
@name="show-{{attr.name}}"
@status="success"
@size="small"
@onChange={{fn this.updateShowPassword (not this.showPasswordField)}}
@checked={{this.showPasswordField}}
data-test-toggle={{attr.name}}
>
<span class="ttl-picker-label has-text-grey">Update password</span><br/>
<div class="description has-text-grey">
<span>{{if this.showPasswordField 'The new password that will be used when connecting to the database' 'Vault will use the existing password'}}</span>
</div>
{{#if this.showPasswordField}}
<Input
{{on "change" (fn this.updatePassword attr.name)}}
@type="password"
@value={{get @model attr.name}}
@name={{attr.name}}
class="input"
{{!-- Prevents browsers from auto-filling --}}
@autocomplete="new-password"
@spellcheck="false" />
{{/if}}
</Toggle>
</div>
{{else}}
{{form-field data-test-field attr=attr model=@model}}
{{/if}}
{{/each}}
{{else}}
<ToggleButton @class="is-block" @toggleAttr={{concat "show" (camelize group)}} @toggleTarget={{this}} @openLabel={{concat "Hide " group}} @closedLabel={{group}} @data-test-toggle-group={{group}} />
{{#if (get this (concat "show" (camelize group)))}}
<div class="box is-marginless">
{{#each fields as |attr|}}
{{form-field data-test-field attr=attr model=@model}}
{{/each}}
</div>
{{/if}}
{{/if}}
{{/each-in}}
{{/each}}
</div>
{{else if (or (eq attr.name 'name') (eq attr.name 'plugin_name'))}}
<ReadonlyFormField @attr={{attr}} @value={{get @model attr.name}} />
{{else if (not-eq attr.options.readOnly true)}}
{{form-field data-test-field attr=attr model=@model}}
{{/if}}
{{/each}}
<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless">
<div class="field is-grouped">
<div class="control">
<button
data-test-secret-save
type="submit"
disabled={{buttonDisabled}}
class="button is-primary"
>
Save
</button>
</div>
<div class="control">
<SecretLink @mode="list" @class="button">
Cancel
</SecretLink>
</div>
</div>
</div>
</form>
{{else}}
{{#each @model.showAttrs as |attr|}}
{{#let attr.options.defaultDisplay as |defaultDisplay|}}

View File

@ -108,7 +108,11 @@
disabled={{this.loading}}
class="button is-primary {{if this.loading 'is-loading'}}"
>
Save
{{#if (eq @mode 'create')}}
Create role
{{else}}
Save
{{/if}}
</button>
{{else}}
<ToolTip @horizontalPosition="left" as |T|>

View File

@ -1,5 +1,6 @@
<div class="progress-container">
{{#each progressBar as |bar|}}
{{#each @progressBar as |bar|}}
<div class="feature-progress-container">
<span class="progress-bar">
<span class="feature-progress" style={{bar.style}} {{! template-lint-disable }}></span>

View File

@ -6,6 +6,7 @@
actionText=actionText
nextFeature=nextFeature
nextStep=nextStep
needsConnection=needsConnection
needsEncryption=needsEncryption
isSupported=isSupported
onDone=onDone

View File

@ -0,0 +1,8 @@
<WizardSection
@headerText="Create a Role"
@instructions='Click “Add role.”'
>
<p>
Now that we've setup a database let's connect a role.
</p>
</WizardSection>

View File

@ -0,0 +1,8 @@
<WizardSection
@headerText="Connect a database"
@instructions='Enter the details of your database and click “Create database.”'
>
<p>
Here you can specify the details of your database plugin and include any root rotation statements.
</p>
</WizardSection>

View File

@ -6,5 +6,8 @@
{{#if @needsEncryption}}
The Transit Secrets Engine uses encryption keys to provide "encryption as a service". Click on "Create Encryption Key" at the top to create one.
{{/if}}
{{#if @needsConnection}}
Now that the engine has been mounted, lets connect a {{@mountSubtype}}.
{{/if}}
</p>
</WizardSection>

View File

@ -0,0 +1,8 @@
<WizardSection
@headerText="Create a Role"
@instructions='Edit the details of your role and click “Create role.”'
>
<p>
Roles are what generate database credentials. They can be static or dynamic.
</p>
</WizardSection>

View File

@ -1,6 +1,6 @@
<WizardSection
@headerText="Adding a role"
@instructions='Enter your role details and click "Save"'
@instructions='Enter your role details and click "Create role"'
>
<p>
A role grants permissions that specify what an identity can and cannot do. A role is typically shared among many users who are then granted credentials with that are granted the policy permissions.

View File

@ -93,7 +93,7 @@
{{else}}
{{#if (eq baseKey.id '')}}
<EmptyState
@title="No {{pluralize options.item}} in this backend yet"
@title="No {{pluralize options.item}} in this backend"
@message="Secrets in this backend will be listed here. Add a secret to get started."
>
<SecretLink @mode="create" @secret="" @queryParams={{query-params initialKey=(or filter baseKey.id) itemType=tab}} @class="link">