mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 18:17:55 +00:00
UI: glimmerize generate credentials component (#27405)
This commit is contained in:
3
changelog/27405.txt
Normal file
3
changelog/27405.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:improvement
|
||||||
|
ui: AWS credentials form sets credential_type from backing role
|
||||||
|
```
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
@value={{@accessKey}}
|
@value={{@accessKey}}
|
||||||
data-test-aws-input="accessKey"
|
data-test-input="accessKey"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
name="secret"
|
name="secret"
|
||||||
class="input"
|
class="input"
|
||||||
@value={{@secretKey}}
|
@value={{@secretKey}}
|
||||||
data-test-aws-input="secretKey"
|
data-test-input="secretKey"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div class="box is-bottomless is-fullwidth">
|
<div class="box is-bottomless is-fullwidth">
|
||||||
<Hds::Button @text="Save" data-test-aws-input="root-save" type="submit" />
|
<Hds::Button @text="Save" data-test-save type="submit" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</T.Panel>
|
</T.Panel>
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
@onChange={{fn this.handleTtlChange "leaseMax"}}
|
@onChange={{fn this.handleTtlChange "leaseMax"}}
|
||||||
/>
|
/>
|
||||||
<div class="box is-bottomless is-fullwidth">
|
<div class="box is-bottomless is-fullwidth">
|
||||||
<Hds::Button @text="Save" data-test-aws-input="lease-save" type="submit" />
|
<Hds::Button @text="Save" data-test-save type="submit" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</T.Panel>
|
</T.Panel>
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
<p.top>
|
<p.top>
|
||||||
<Hds::Breadcrumb>
|
<Hds::Breadcrumb>
|
||||||
<Hds::Breadcrumb::Item
|
<Hds::Breadcrumb::Item
|
||||||
@text={{this.backendPath}}
|
@text={{@backendPath}}
|
||||||
@route="vault.cluster.secrets.backend"
|
@route="vault.cluster.secrets.backend"
|
||||||
@model={{this.backendPath}}
|
@model={{@backendPath}}
|
||||||
data-test-link="role-list"
|
data-test-link="role-list"
|
||||||
/>
|
/>
|
||||||
<Hds::Breadcrumb::Item @text="Credentials" @route="vault.cluster.secrets.backend" @model={{this.backendPath}} />
|
<Hds::Breadcrumb::Item @text="Credentials" @route="vault.cluster.secrets.backend" @model={{@backendPath}} />
|
||||||
<Hds::Breadcrumb::Item @text={{this.roleName}} @route="vault.cluster.secrets.backend.show" @model={{this.roleName}} />
|
<Hds::Breadcrumb::Item @text={{@roleName}} @route="vault.cluster.secrets.backend.show" @model={{@roleName}} />
|
||||||
<Hds::Breadcrumb::Item @text={{this.options.title}} @current={{true}} />
|
<Hds::Breadcrumb::Item @text={{this.options.title}} @current={{true}} />
|
||||||
</Hds::Breadcrumb>
|
</Hds::Breadcrumb>
|
||||||
</p.top>
|
</p.top>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</p.levelLeft>
|
</p.levelLeft>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
{{#if this.model.hasGenerated}}
|
{{#if this.hasGenerated}}
|
||||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||||
<MessageError @model={{this.model}} />
|
<MessageError @model={{this.model}} />
|
||||||
{{#unless this.model.isError}}
|
{{#unless this.model.isError}}
|
||||||
@@ -35,48 +35,42 @@
|
|||||||
</A.Description>
|
</A.Description>
|
||||||
</Hds::Alert>
|
</Hds::Alert>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
{{#each this.model.attrs as |attr|}}
|
{{#each this.displayFields as |key|}}
|
||||||
{{#if (eq attr.type "object")}}
|
{{#let (get this.model.allByKey key) as |attr|}}
|
||||||
<InfoTableRow
|
{{#if (eq attr.type "object")}}
|
||||||
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
<InfoTableRow
|
||||||
@value={{stringify (get this.model attr.name)}}
|
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
||||||
/>
|
@value={{stringify (get this.model attr.name)}}
|
||||||
{{else}}
|
/>
|
||||||
{{#if
|
{{else}}
|
||||||
(or
|
{{#if attr.options.masked}}
|
||||||
(eq attr.name "key")
|
{{#if (get this.model attr.name)}}
|
||||||
(eq attr.name "secretKey")
|
<InfoTableRow
|
||||||
(eq attr.name "securityToken")
|
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
||||||
(eq attr.name "privateKey")
|
@value={{get this.model attr.name}}
|
||||||
attr.options.masked
|
>
|
||||||
)
|
<MaskedInput
|
||||||
}}
|
@value={{get this.model attr.name}}
|
||||||
{{#if (get this.model attr.name)}}
|
@name={{attr.name}}
|
||||||
|
@displayOnly={{true}}
|
||||||
|
@allowCopy={{true}}
|
||||||
|
/>
|
||||||
|
</InfoTableRow>
|
||||||
|
{{/if}}
|
||||||
|
{{else if (and (get this.model attr.name) (or (eq attr.name "issueDate") (eq attr.name "expiryDate")))}}
|
||||||
|
<InfoTableRow
|
||||||
|
data-test-table-row
|
||||||
|
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
||||||
|
@value={{date-format (get this.model attr.name) "MMM dd, yyyy hh:mm:ss a"}}
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
<InfoTableRow
|
<InfoTableRow
|
||||||
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
||||||
@value={{get this.model attr.name}}
|
@value={{get this.model attr.name}}
|
||||||
>
|
/>
|
||||||
<MaskedInput
|
|
||||||
@value={{get this.model attr.name}}
|
|
||||||
@name={{attr.name}}
|
|
||||||
@displayOnly={{true}}
|
|
||||||
@allowCopy={{true}}
|
|
||||||
/>
|
|
||||||
</InfoTableRow>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{else if (and (get this.model attr.name) (or (eq attr.name "issueDate") (eq attr.name "expiryDate")))}}
|
|
||||||
<InfoTableRow
|
|
||||||
data-test-table-row
|
|
||||||
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
|
||||||
@value={{date-format (get this.model attr.name) "MMM dd, yyyy hh:mm:ss a"}}
|
|
||||||
/>
|
|
||||||
{{else}}
|
|
||||||
<InfoTableRow
|
|
||||||
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
|
||||||
@value={{get this.model attr.name}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/let}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||||
@@ -106,34 +100,25 @@
|
|||||||
@text="Back"
|
@text="Back"
|
||||||
@color="secondary"
|
@color="secondary"
|
||||||
@route="vault.cluster.secrets.backend.list-root"
|
@route="vault.cluster.secrets.backend.list-root"
|
||||||
@model={{this.backendPath}}
|
@model={{@backendPath}}
|
||||||
data-test-secret-generate-back={{true}}
|
data-test-back-button
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
<Hds::Button
|
<Hds::Button @text="Back" @color="secondary" {{on "click" this.reset}} data-test-back-button />
|
||||||
@text="Back"
|
|
||||||
@color="secondary"
|
|
||||||
{{on "click" (action "newModel")}}
|
|
||||||
data-test-secret-generate-back="true"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<form {{action "create" on="submit"}} data-test-secret-generate-form="true">
|
<form {{on "submit" this.create}} data-test-secret-generate-form>
|
||||||
<div class="box is-sideless no-padding-top is-fullwidth is-marginless">
|
<div class="box is-sideless no-padding-top is-fullwidth is-marginless">
|
||||||
<NamespaceReminder @mode="generate" @noun="credential" />
|
<NamespaceReminder @mode="generate" @noun="credential" />
|
||||||
<MessageError @model={{this.model}} />
|
<MessageError @model={{this.model}} />
|
||||||
{{#if this.model.helpText}}
|
{{#if this.helpText}}
|
||||||
<p class="is-hint">{{this.model.helpText}}</p>
|
<p class="is-hint">{{this.helpText}}</p>
|
||||||
{{/if}}
|
|
||||||
{{#if this.model.fieldGroups}}
|
|
||||||
<FormFieldGroupsLoop @model={{this.model}} @mode={{this.mode}} />
|
|
||||||
{{else}}
|
|
||||||
{{#each this.model.attrs as |attr|}}
|
|
||||||
<FormField data-test-field={{true}} @attr={{attr}} @model={{this.model}} />
|
|
||||||
{{/each}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#each this.formFields as |key|}}
|
||||||
|
<FormField data-test-field @attr={{get this.model.allByKey key}} @model={{this.model}} />
|
||||||
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||||
<Hds::ButtonSet>
|
<Hds::ButtonSet>
|
||||||
@@ -142,14 +127,14 @@
|
|||||||
@icon={{if this.loading "loading"}}
|
@icon={{if this.loading "loading"}}
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={{this.loading}}
|
disabled={{this.loading}}
|
||||||
data-test-secret-generate={{true}}
|
data-test-save
|
||||||
/>
|
/>
|
||||||
<Hds::Button
|
<Hds::Button
|
||||||
@text="Cancel"
|
@text="Cancel"
|
||||||
@route="vault.cluster.secrets.backend.list-root"
|
@route="vault.cluster.secrets.backend.list-root"
|
||||||
@color="secondary"
|
@color="secondary"
|
||||||
@model={{this.backendPath}}
|
@model={{@backendPath}}
|
||||||
data-test-secret-generate-cancel={{true}}
|
data-test-cancel
|
||||||
/>
|
/>
|
||||||
</Hds::ButtonSet>
|
</Hds::ButtonSet>
|
||||||
</div>
|
</div>
|
||||||
@@ -4,56 +4,49 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
import { computed, set } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import Component from '@ember/component';
|
import Component from '@glimmer/component';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
|
||||||
const MODEL_TYPES = {
|
const CREDENTIAL_TYPES = {
|
||||||
'ssh-sign': {
|
ssh: {
|
||||||
model: 'ssh-sign',
|
|
||||||
},
|
|
||||||
'ssh-creds': {
|
|
||||||
model: 'ssh-otp-credential',
|
model: 'ssh-otp-credential',
|
||||||
title: 'Generate SSH Credentials',
|
title: 'Generate SSH Credentials',
|
||||||
|
formFields: ['username', 'ip'],
|
||||||
|
displayFields: ['username', 'ip', 'key', 'keyType', 'port'],
|
||||||
},
|
},
|
||||||
'aws-creds': {
|
aws: {
|
||||||
model: 'aws-credential',
|
model: 'aws-credential',
|
||||||
title: 'Generate AWS Credentials',
|
title: 'Generate AWS Credentials',
|
||||||
backIsListLink: true,
|
backIsListLink: true,
|
||||||
|
displayFields: ['accessKey', 'secretKey', 'securityToken', 'leaseId', 'renewable', 'leaseDuration'],
|
||||||
|
// aws form fields are dynamic
|
||||||
|
formFields: (model) => {
|
||||||
|
return {
|
||||||
|
iam_user: ['credentialType'],
|
||||||
|
assumed_role: ['credentialType', 'ttl', 'roleArn'],
|
||||||
|
federation_token: ['credentialType', 'ttl'],
|
||||||
|
session_token: ['credentialType', 'ttl'],
|
||||||
|
}[model.credentialType];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Component.extend({
|
export default class GenerateCredentials extends Component {
|
||||||
controlGroup: service(),
|
@service controlGroup;
|
||||||
store: service(),
|
@service store;
|
||||||
router: service(),
|
@service router;
|
||||||
// set on the component
|
|
||||||
backendType: null,
|
|
||||||
backendPath: null,
|
|
||||||
roleName: null,
|
|
||||||
action: null,
|
|
||||||
|
|
||||||
model: null,
|
@tracked model;
|
||||||
loading: false,
|
@tracked loading = false;
|
||||||
emptyData: '{\n}',
|
@tracked hasGenerated = false;
|
||||||
|
emptyData = '{\n}';
|
||||||
|
|
||||||
modelForType() {
|
constructor() {
|
||||||
const type = this.options;
|
super(...arguments);
|
||||||
if (type) {
|
const modelType = this.modelForType();
|
||||||
return type.model;
|
this.model = this.generateNewModel(modelType);
|
||||||
}
|
}
|
||||||
// if we don't have a mode for that type then redirect them back to the backend list
|
|
||||||
this.router.transitionTo('vault.cluster.secrets.backend.list-root', this.backendPath);
|
|
||||||
},
|
|
||||||
|
|
||||||
options: computed('action', 'backendType', function () {
|
|
||||||
const action = this.action || 'creds';
|
|
||||||
return MODEL_TYPES[`${this.backendType}-${action}`];
|
|
||||||
}),
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this._super(...arguments);
|
|
||||||
this.createOrReplaceModel();
|
|
||||||
},
|
|
||||||
|
|
||||||
willDestroy() {
|
willDestroy() {
|
||||||
// components are torn down after store is unloaded and will cause an error if attempt to unload record
|
// components are torn down after store is unloaded and will cause an error if attempt to unload record
|
||||||
@@ -61,20 +54,46 @@ export default Component.extend({
|
|||||||
if (noTeardown && !this.model.isDestroyed && !this.model.isDestroying) {
|
if (noTeardown && !this.model.isDestroyed && !this.model.isDestroying) {
|
||||||
this.model.unloadRecord();
|
this.model.unloadRecord();
|
||||||
}
|
}
|
||||||
this._super(...arguments);
|
super.willDestroy();
|
||||||
},
|
}
|
||||||
|
|
||||||
createOrReplaceModel() {
|
modelForType() {
|
||||||
const modelType = this.modelForType();
|
const type = this.options;
|
||||||
const model = this.model;
|
if (type) {
|
||||||
const roleName = this.roleName;
|
return type.model;
|
||||||
const backendPath = this.backendPath;
|
}
|
||||||
|
// if we don't have a mode for that type then redirect them back to the backend list
|
||||||
|
this.router.transitionTo('vault.cluster.secrets.backend.list-root', this.args.backendPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
get helpText() {
|
||||||
|
if (this.options?.model === 'aws-credential') {
|
||||||
|
return 'For Vault roles of credential type iam_user, there are no inputs, just submit the form. Choose a type to change the input options.';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
get options() {
|
||||||
|
return CREDENTIAL_TYPES[this.args.backendType];
|
||||||
|
}
|
||||||
|
|
||||||
|
get formFields() {
|
||||||
|
const typeOpts = this.options;
|
||||||
|
if (typeof typeOpts.formFields === 'function') {
|
||||||
|
return typeOpts.formFields(this.model);
|
||||||
|
}
|
||||||
|
return typeOpts.formFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
get displayFields() {
|
||||||
|
return this.options.displayFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateNewModel(modelType) {
|
||||||
if (!modelType) {
|
if (!modelType) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (model) {
|
const { roleName, backendPath, awsRoleType } = this.args;
|
||||||
model.unloadRecord();
|
|
||||||
}
|
|
||||||
const attrs = {
|
const attrs = {
|
||||||
role: {
|
role: {
|
||||||
backend: backendPath,
|
backend: backendPath,
|
||||||
@@ -82,44 +101,60 @@ export default Component.extend({
|
|||||||
},
|
},
|
||||||
id: `${backendPath}-${roleName}`,
|
id: `${backendPath}-${roleName}`,
|
||||||
};
|
};
|
||||||
const newModel = this.store.createRecord(modelType, attrs);
|
if (awsRoleType) {
|
||||||
this.set('model', newModel);
|
// this is only set from route if backendType = aws
|
||||||
},
|
attrs.credentialType = awsRoleType;
|
||||||
|
}
|
||||||
|
return this.store.createRecord(modelType, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
actions: {
|
replaceModel() {
|
||||||
create() {
|
const modelType = this.modelForType();
|
||||||
const model = this.model;
|
if (!modelType) {
|
||||||
this.set('loading', true);
|
return;
|
||||||
this.model
|
}
|
||||||
.save()
|
if (this.model) {
|
||||||
.then(() => {
|
this.model.unloadRecord();
|
||||||
model.set('hasGenerated', true);
|
}
|
||||||
})
|
this.model = this.generateNewModel(modelType);
|
||||||
.catch((error) => {
|
}
|
||||||
// Handle control group AdapterError
|
|
||||||
if (error.message === 'Control Group encountered') {
|
|
||||||
this.controlGroup.saveTokenFromError(error);
|
|
||||||
const err = this.controlGroup.logFromError(error);
|
|
||||||
error.errors = [err.content];
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.set('loading', false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
codemirrorUpdated(attr, val, codemirror) {
|
@action
|
||||||
codemirror.performLint();
|
create(evt) {
|
||||||
const hasErrors = codemirror.state.lint.marked.length > 0;
|
evt.preventDefault();
|
||||||
|
this.loading = true;
|
||||||
|
this.model
|
||||||
|
.save()
|
||||||
|
.then(() => {
|
||||||
|
this.hasGenerated = true;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// Handle control group AdapterError
|
||||||
|
if (error.message === 'Control Group encountered') {
|
||||||
|
this.controlGroup.saveTokenFromError(error);
|
||||||
|
const err = this.controlGroup.logFromError(error);
|
||||||
|
error.errors = [err.content];
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasErrors) {
|
@action
|
||||||
set(this.model, attr, JSON.parse(val));
|
codemirrorUpdated(attr, val, codemirror) {
|
||||||
}
|
codemirror.performLint();
|
||||||
},
|
const hasErrors = codemirror.state.lint.marked.length > 0;
|
||||||
|
|
||||||
newModel() {
|
if (!hasErrors) {
|
||||||
this.createOrReplaceModel();
|
this.model[attr] = JSON.parse(val);
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
|
||||||
|
@action
|
||||||
|
reset() {
|
||||||
|
this.hasGenerated = false;
|
||||||
|
this.replaceModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,11 +6,10 @@
|
|||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
queryParams: ['action', 'roleType'],
|
queryParams: ['roleType'],
|
||||||
action: '',
|
// used for database credentials
|
||||||
roleType: '',
|
roleType: '',
|
||||||
reset() {
|
reset() {
|
||||||
this.set('action', '');
|
|
||||||
this.set('roleType', '');
|
this.set('roleType', '');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Model, { attr } from '@ember-data/model';
|
import Model, { attr } from '@ember-data/model';
|
||||||
import { computed } from '@ember/object';
|
import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes';
|
||||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
|
||||||
const CREDENTIAL_TYPES = [
|
const CREDENTIAL_TYPES = [
|
||||||
{
|
{
|
||||||
value: 'iam_user',
|
value: 'iam_user',
|
||||||
@@ -25,27 +25,28 @@ const CREDENTIAL_TYPES = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const DISPLAY_FIELDS = ['accessKey', 'secretKey', 'securityToken', 'leaseId', 'renewable', 'leaseDuration'];
|
@withExpandedAttributes()
|
||||||
export default Model.extend({
|
export default class AwsCredential extends Model {
|
||||||
helpText:
|
@attr('object', {
|
||||||
'For Vault roles of credential type iam_user, there are no inputs, just submit the form. Choose a type to change the input options.',
|
|
||||||
role: attr('object', {
|
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
}),
|
})
|
||||||
|
role;
|
||||||
|
|
||||||
credentialType: attr('string', {
|
@attr('string', {
|
||||||
defaultValue: 'iam_user',
|
defaultValue: 'iam_user',
|
||||||
possibleValues: CREDENTIAL_TYPES,
|
possibleValues: CREDENTIAL_TYPES,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
}),
|
})
|
||||||
|
credentialType;
|
||||||
|
|
||||||
roleArn: attr('string', {
|
@attr('string', {
|
||||||
label: 'Role ARN',
|
label: 'Role ARN',
|
||||||
helpText:
|
helpText:
|
||||||
'The ARN of the role to assume if credential_type on the Vault role is assumed_role. Optional if the role has a single role ARN; required otherwise.',
|
'The ARN of the role to assume if credential_type on the Vault role is assumed_role. Optional if the role has a single role ARN; required otherwise.',
|
||||||
}),
|
})
|
||||||
|
roleArn;
|
||||||
|
|
||||||
ttl: attr({
|
@attr({
|
||||||
editType: 'ttl',
|
editType: 'ttl',
|
||||||
defaultValue: '3600s',
|
defaultValue: '3600s',
|
||||||
setDefault: true,
|
setDefault: true,
|
||||||
@@ -53,29 +54,17 @@ export default Model.extend({
|
|||||||
label: 'TTL',
|
label: 'TTL',
|
||||||
helpText:
|
helpText:
|
||||||
'Specifies the TTL for the use of the STS token. Valid only when credential_type is assumed_role, federation_token, or session_token.',
|
'Specifies the TTL for the use of the STS token. Valid only when credential_type is assumed_role, federation_token, or session_token.',
|
||||||
}),
|
})
|
||||||
leaseId: attr('string'),
|
ttl;
|
||||||
renewable: attr('boolean'),
|
|
||||||
leaseDuration: attr('number'),
|
|
||||||
accessKey: attr('string'),
|
|
||||||
secretKey: attr('string'),
|
|
||||||
securityToken: attr('string'),
|
|
||||||
|
|
||||||
attrs: computed('credentialType', 'accessKey', 'securityToken', function () {
|
@attr('string') leaseId;
|
||||||
const type = this.credentialType;
|
@attr('boolean') renewable;
|
||||||
const fieldsForType = {
|
@attr('number') leaseDuration;
|
||||||
iam_user: ['credentialType'],
|
@attr('string') accessKey;
|
||||||
assumed_role: ['credentialType', 'ttl', 'roleArn'],
|
@attr('string', { masked: true }) secretKey;
|
||||||
federation_token: ['credentialType', 'ttl'],
|
@attr('string', { masked: true }) securityToken;
|
||||||
session_token: ['credentialType', 'ttl'],
|
|
||||||
};
|
|
||||||
if (this.accessKey || this.securityToken) {
|
|
||||||
return expandAttributeMeta(this, DISPLAY_FIELDS.slice(0));
|
|
||||||
}
|
|
||||||
return expandAttributeMeta(this, fieldsForType[type].slice(0));
|
|
||||||
}),
|
|
||||||
|
|
||||||
toCreds: computed('accessKey', 'secretKey', 'securityToken', 'leaseId', function () {
|
get toCreds() {
|
||||||
const props = {
|
const props = {
|
||||||
accessKey: this.accessKey,
|
accessKey: this.accessKey,
|
||||||
secretKey: this.secretKey,
|
secretKey: this.secretKey,
|
||||||
@@ -90,5 +79,5 @@ export default Model.extend({
|
|||||||
return ret;
|
return ret;
|
||||||
}, {});
|
}, {});
|
||||||
return JSON.stringify(propsWithVals, null, 2);
|
return JSON.stringify(propsWithVals, null, 2);
|
||||||
}),
|
}
|
||||||
});
|
}
|
||||||
|
|||||||
@@ -3,27 +3,25 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { reads } from '@ember/object/computed';
|
|
||||||
import Model, { attr } from '@ember-data/model';
|
import Model, { attr } from '@ember-data/model';
|
||||||
import { computed } from '@ember/object';
|
import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes';
|
||||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
|
||||||
const CREATE_FIELDS = ['username', 'ip'];
|
|
||||||
|
|
||||||
const DISPLAY_FIELDS = ['username', 'ip', 'key', 'keyType', 'port'];
|
@withExpandedAttributes()
|
||||||
export default Model.extend({
|
export default class SshOtpCredential extends Model {
|
||||||
role: attr('object', {
|
@attr('object', {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
}),
|
})
|
||||||
ip: attr('string', {
|
role;
|
||||||
|
@attr('string', {
|
||||||
label: 'IP Address',
|
label: 'IP Address',
|
||||||
}),
|
})
|
||||||
username: attr('string'),
|
ip;
|
||||||
key: attr('string'),
|
@attr('string') username;
|
||||||
keyType: attr('string'),
|
@attr('string', { masked: true }) key;
|
||||||
port: attr('number'),
|
@attr('string') keyType;
|
||||||
attrs: computed('key', function () {
|
@attr('number') port;
|
||||||
const keys = this.key ? DISPLAY_FIELDS.slice(0) : CREATE_FIELDS.slice(0);
|
|
||||||
return expandAttributeMeta(this, keys);
|
get toCreds() {
|
||||||
}),
|
return this.key;
|
||||||
toCreds: reads('key'),
|
}
|
||||||
});
|
}
|
||||||
|
|||||||
@@ -17,12 +17,15 @@ export default Route.extend({
|
|||||||
store: service(),
|
store: service(),
|
||||||
|
|
||||||
beforeModel() {
|
beforeModel() {
|
||||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
const { id: backendPath, type: backendType } = this.modelFor('vault.cluster.secrets.backend');
|
||||||
if (backend != 'ssh') {
|
// redirect if the backend type does not support credentials
|
||||||
return;
|
if (!SUPPORTED_DYNAMIC_BACKENDS.includes(backendType)) {
|
||||||
|
return this.router.transitionTo('vault.cluster.secrets.backend.list-root', backendPath);
|
||||||
|
}
|
||||||
|
// hydrate model if backend type is ssh
|
||||||
|
if (backendType === 'ssh') {
|
||||||
|
this.pathHelp.getNewModel('ssh-otp-credential', backendPath);
|
||||||
}
|
}
|
||||||
const modelType = 'ssh-otp-credential';
|
|
||||||
return this.pathHelp.getNewModel(modelType, backend);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getDatabaseCredential(backend, secret, roleType = '') {
|
getDatabaseCredential(backend, secret, roleType = '') {
|
||||||
@@ -51,23 +54,34 @@ export default Route.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getAwsRole(backend, id) {
|
||||||
|
try {
|
||||||
|
const role = await this.store.queryRecord('role-aws', { backend, id });
|
||||||
|
return role;
|
||||||
|
} catch (e) {
|
||||||
|
// swallow error, non-essential data
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async model(params) {
|
async model(params) {
|
||||||
const role = params.secret;
|
const role = params.secret;
|
||||||
const { id: backendPath, type: backendType } = this.modelFor('vault.cluster.secrets.backend');
|
const { id: backendPath, type: backendType } = this.modelFor('vault.cluster.secrets.backend');
|
||||||
const roleType = params.roleType;
|
const roleType = params.roleType;
|
||||||
let dbCred;
|
let dbCred, awsRole;
|
||||||
if (backendType === 'database') {
|
if (backendType === 'database') {
|
||||||
dbCred = await this.getDatabaseCredential(backendPath, role, roleType);
|
dbCred = await this.getDatabaseCredential(backendPath, role, roleType);
|
||||||
|
} else if (backendType === 'aws') {
|
||||||
|
awsRole = await this.getAwsRole(backendPath, role);
|
||||||
}
|
}
|
||||||
if (!SUPPORTED_DYNAMIC_BACKENDS.includes(backendType)) {
|
|
||||||
return this.router.transitionTo('vault.cluster.secrets.backend.list-root', backendPath);
|
|
||||||
}
|
|
||||||
return resolve({
|
return resolve({
|
||||||
backendPath,
|
backendPath,
|
||||||
backendType,
|
backendType,
|
||||||
roleName: role,
|
roleName: role,
|
||||||
roleType,
|
roleType,
|
||||||
dbCred,
|
dbCred,
|
||||||
|
awsRoleType: awsRole?.credentialType,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
||||||
<Hds::ButtonSet>
|
<Hds::ButtonSet>
|
||||||
<Hds::Button @text={{if (eq this.mode "create") "Create role" "Save"}} type="submit" data-test-role-aws-create />
|
<Hds::Button @text={{if (eq this.mode "create") "Create role" "Save"}} type="submit" data-test-save />
|
||||||
{{#if (eq this.mode "create")}}
|
{{#if (eq this.mode "create")}}
|
||||||
<Hds::Button
|
<Hds::Button
|
||||||
@text="Cancel"
|
@text="Cancel"
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
@model={{this.model.dbCred}}
|
@model={{this.model.dbCred}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{! TODO smells a little to have action off of query param requiring a conditional }}
|
|
||||||
<GenerateCredentials
|
<GenerateCredentials
|
||||||
@backendPath={{this.model.backendPath}}
|
@backendPath={{this.model.backendPath}}
|
||||||
@backendType={{this.model.backendType}}
|
@backendType={{this.model.backendType}}
|
||||||
@roleName={{this.model.roleName}}
|
@roleName={{this.model.roleName}}
|
||||||
@action={{if this.action this.action ""}}
|
@awsRoleType={{this.model.awsRoleType}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -67,12 +67,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<Hds::Button
|
<Hds::Button @text="Back" @color="secondary" {{on "click" (action "newModel")}} data-test-back-button />
|
||||||
@text="Back"
|
|
||||||
@color="secondary"
|
|
||||||
{{on "click" (action "newModel")}}
|
|
||||||
data-test-secret-generate-back={{true}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
@@ -113,14 +108,14 @@
|
|||||||
@icon={{if this.loading "loading"}}
|
@icon={{if this.loading "loading"}}
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={{this.loading}}
|
disabled={{this.loading}}
|
||||||
data-test-secret-generate
|
data-test-save
|
||||||
/>
|
/>
|
||||||
<Hds::Button
|
<Hds::Button
|
||||||
@text="Cancel"
|
@text="Cancel"
|
||||||
@color="secondary"
|
@color="secondary"
|
||||||
@route="vault.cluster.secrets.backend.list-root"
|
@route="vault.cluster.secrets.backend.list-root"
|
||||||
@model={{this.backend.id}}
|
@model={{this.backend.id}}
|
||||||
data-test-secret-generate-cancel
|
data-test-cancel
|
||||||
/>
|
/>
|
||||||
</Hds::ButtonSet>
|
</Hds::ButtonSet>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { click, fillIn, currentURL, find, settled, waitUntil } from '@ember/test-helpers';
|
import { click, fillIn, currentURL, find, settled, waitUntil, visit } from '@ember/test-helpers';
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupApplicationTest } from 'ember-qunit';
|
import { setupApplicationTest } from 'ember-qunit';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
@@ -13,7 +13,63 @@ import { GENERAL } from '../helpers/general-selectors';
|
|||||||
import authPage from 'vault/tests/pages/auth';
|
import authPage from 'vault/tests/pages/auth';
|
||||||
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
|
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
|
||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
|
import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands';
|
||||||
|
import { overrideResponse } from 'vault/tests/helpers/stubs';
|
||||||
|
|
||||||
|
const AWS_CREDS = {
|
||||||
|
configTab: '[data-test-configuration-tab]',
|
||||||
|
configure: '[data-test-secret-backend-configure]',
|
||||||
|
awsForm: '[data-test-aws-root-creds-form]',
|
||||||
|
viewBackend: '[data-test-backend-view-link]',
|
||||||
|
createSecret: '[data-test-secret-create]',
|
||||||
|
secretHeader: '[data-test-secret-header]',
|
||||||
|
secretLink: (name) => (name ? `[data-test-secret-link="${name}"]` : '[data-test-secret-link]'),
|
||||||
|
crumb: (path) => `[data-test-secret-breadcrumb="${path}"] a`,
|
||||||
|
ttlToggle: '[data-test-ttl-toggle="TTL"]',
|
||||||
|
warning: '[data-test-warning]',
|
||||||
|
delete: (role) => `[data-test-aws-role-delete="${role}"]`,
|
||||||
|
backButton: '[data-test-back-button]',
|
||||||
|
generateLink: '[data-test-backend-credentials]',
|
||||||
|
};
|
||||||
|
const ROLE_TYPES = [
|
||||||
|
{
|
||||||
|
credentialType: 'iam_user',
|
||||||
|
async fillOutForm(assert) {
|
||||||
|
// nothing to fill out
|
||||||
|
assert.dom('[data-test-field]').exists({ count: 1 });
|
||||||
|
},
|
||||||
|
expectedPayload: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
credentialType: 'assumed_role',
|
||||||
|
async fillOutForm(assert) {
|
||||||
|
await click(GENERAL.toggleInput('TTL'));
|
||||||
|
assert.dom(GENERAL.toggleInput('TTL')).isNotChecked();
|
||||||
|
await fillIn(GENERAL.inputByAttr('roleArn'), 'foobar');
|
||||||
|
},
|
||||||
|
expectedPayload: {
|
||||||
|
role_arn: 'foobar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
credentialType: 'federation_token',
|
||||||
|
async fillOutForm(assert) {
|
||||||
|
assert.dom(GENERAL.toggleInput('TTL')).isChecked();
|
||||||
|
await fillIn(GENERAL.ttl.input('TTL'), '3');
|
||||||
|
},
|
||||||
|
expectedPayload: {
|
||||||
|
ttl: '10800s',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
credentialType: 'session_token',
|
||||||
|
async fillOutForm(assert) {
|
||||||
|
await click(GENERAL.toggleInput('TTL'));
|
||||||
|
assert.dom(GENERAL.toggleInput('TTL')).isNotChecked();
|
||||||
|
},
|
||||||
|
expectedPayload: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
module('Acceptance | aws secret backend', function (hooks) {
|
module('Acceptance | aws secret backend', function (hooks) {
|
||||||
setupApplicationTest(hooks);
|
setupApplicationTest(hooks);
|
||||||
setupMirage(hooks);
|
setupMirage(hooks);
|
||||||
@@ -30,28 +86,23 @@ module('Acceptance | aws secret backend', function (hooks) {
|
|||||||
test('aws backend', async function (assert) {
|
test('aws backend', async function (assert) {
|
||||||
const path = `aws-${this.uid}`;
|
const path = `aws-${this.uid}`;
|
||||||
const roleName = 'awsrole';
|
const roleName = 'awsrole';
|
||||||
this.server.post(`/${path}/creds/${roleName}`, (_, req) => {
|
|
||||||
const payload = JSON.parse(req.requestBody);
|
|
||||||
assert.deepEqual(payload, { role_arn: 'foobar' }, 'does not send TTL when unchecked');
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
|
|
||||||
await enablePage.enable('aws', path);
|
await enablePage.enable('aws', path);
|
||||||
await settled();
|
await settled();
|
||||||
await click('[data-test-configuration-tab]');
|
await click(AWS_CREDS.configTab);
|
||||||
|
|
||||||
await click('[data-test-secret-backend-configure]');
|
await click(AWS_CREDS.configure);
|
||||||
|
|
||||||
assert.strictEqual(currentURL(), `/vault/settings/secrets/configure/${path}`);
|
assert.strictEqual(currentURL(), `/vault/settings/secrets/configure/${path}`);
|
||||||
|
|
||||||
assert.dom('[data-test-aws-root-creds-form]').exists();
|
assert.dom(AWS_CREDS.awsForm).exists();
|
||||||
assert.dom(GENERAL.tab('access-to-aws')).exists('renders the root creds tab');
|
assert.dom(GENERAL.tab('access-to-aws')).exists('renders the root creds tab');
|
||||||
assert.dom(GENERAL.tab('lease')).exists('renders the leases config tab');
|
assert.dom(GENERAL.tab('lease')).exists('renders the leases config tab');
|
||||||
|
|
||||||
await fillIn('[data-test-aws-input="accessKey"]', 'foo');
|
await fillIn(GENERAL.inputByAttr('accessKey'), 'foo');
|
||||||
await fillIn('[data-test-aws-input="secretKey"]', 'bar');
|
await fillIn(GENERAL.inputByAttr('secretKey'), 'bar');
|
||||||
|
|
||||||
await click('[data-test-aws-input="root-save"]');
|
await click(GENERAL.saveButton);
|
||||||
|
|
||||||
assert.true(
|
assert.true(
|
||||||
this.flashSuccessSpy.calledWith('The backend configuration saved successfully!'),
|
this.flashSuccessSpy.calledWith('The backend configuration saved successfully!'),
|
||||||
@@ -60,53 +111,133 @@ module('Acceptance | aws secret backend', function (hooks) {
|
|||||||
|
|
||||||
await click(GENERAL.tab('lease'));
|
await click(GENERAL.tab('lease'));
|
||||||
|
|
||||||
await click('[data-test-aws-input="lease-save"]');
|
await click(GENERAL.saveButton);
|
||||||
|
|
||||||
assert.true(
|
assert.true(
|
||||||
this.flashSuccessSpy.calledTwice,
|
this.flashSuccessSpy.calledTwice,
|
||||||
'a new success flash message is rendered upon saving lease'
|
'a new success flash message is rendered upon saving lease'
|
||||||
);
|
);
|
||||||
|
|
||||||
await click('[data-test-backend-view-link]');
|
await click(AWS_CREDS.viewBackend);
|
||||||
|
|
||||||
assert.strictEqual(currentURL(), `/vault/secrets/${path}/list`, 'navigates to the roles list');
|
assert.strictEqual(currentURL(), `/vault/secrets/${path}/list`, 'navigates to the roles list');
|
||||||
|
|
||||||
await click('[data-test-secret-create]');
|
await click(AWS_CREDS.createSecret);
|
||||||
|
|
||||||
assert.dom('[data-test-secret-header]').hasText('Create an AWS Role', 'aws: renders the create page');
|
assert.dom(AWS_CREDS.secretHeader).hasText('Create an AWS Role', 'aws: renders the create page');
|
||||||
|
|
||||||
await fillIn('[data-test-input="name"]', roleName);
|
await fillIn(GENERAL.inputByAttr('name'), roleName);
|
||||||
|
|
||||||
// save the role
|
// save the role
|
||||||
await click('[data-test-role-aws-create]');
|
await click(GENERAL.saveButton);
|
||||||
await waitUntil(() => currentURL() === `/vault/secrets/${path}/show/${roleName}`); // flaky without this
|
await waitUntil(() => currentURL() === `/vault/secrets/${path}/show/${roleName}`); // flaky without this
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`/vault/secrets/${path}/show/${roleName}`,
|
`/vault/secrets/${path}/show/${roleName}`,
|
||||||
'aws: navigates to the show page on creation'
|
'aws: navigates to the show page on creation'
|
||||||
);
|
);
|
||||||
await click(`[data-test-secret-breadcrumb="${path}"] a`);
|
await click(AWS_CREDS.crumb(path));
|
||||||
|
|
||||||
assert.strictEqual(currentURL(), `/vault/secrets/${path}/list`);
|
assert.strictEqual(currentURL(), `/vault/secrets/${path}/list`);
|
||||||
assert.dom(`[data-test-secret-link="${roleName}"]`).exists();
|
assert.dom(AWS_CREDS.secretLink(roleName)).exists();
|
||||||
|
|
||||||
// check that generates credentials flow is correct
|
|
||||||
await click(`[data-test-secret-link="${roleName}"]`);
|
|
||||||
assert.dom('h1').hasText('Generate AWS Credentials');
|
|
||||||
assert.dom('[data-test-input="credentialType"]').hasValue('iam_user');
|
|
||||||
await fillIn('[data-test-input="credentialType"]', 'assumed_role');
|
|
||||||
await click('[data-test-ttl-toggle="TTL"]');
|
|
||||||
assert.dom('[data-test-ttl-toggle="TTL"]').isNotChecked();
|
|
||||||
await fillIn('[data-test-input="roleArn"]', 'foobar');
|
|
||||||
await click('[data-test-secret-generate]');
|
|
||||||
assert.dom('[data-test-warning]').exists('Shows access warning after generation');
|
|
||||||
await click('[data-test-secret-generate-back]');
|
|
||||||
|
|
||||||
//and delete
|
//and delete
|
||||||
await click(`[data-test-secret-link="${roleName}"] [data-test-popup-menu-trigger]`);
|
await click(`${AWS_CREDS.secretLink(roleName)} [data-test-popup-menu-trigger]`);
|
||||||
await waitUntil(() => find(`[data-test-aws-role-delete="${roleName}"]`)); // flaky without
|
await waitUntil(() => find(AWS_CREDS.delete(roleName))); // flaky without
|
||||||
await click(`[data-test-aws-role-delete="${roleName}"]`);
|
await click(AWS_CREDS.delete(roleName));
|
||||||
await click(GENERAL.confirmButton);
|
await click(GENERAL.confirmButton);
|
||||||
assert.dom(`[data-test-secret-link="${roleName}"]`).doesNotExist('aws: role is no longer in the list');
|
assert.dom(AWS_CREDS.secretLink(roleName)).doesNotExist('aws: role is no longer in the list');
|
||||||
|
});
|
||||||
|
|
||||||
|
ROLE_TYPES.forEach((scenario) => {
|
||||||
|
test(`aws credentials - type ${scenario.credentialType}`, async function (assert) {
|
||||||
|
const path = `aws-cred-${this.uid}`;
|
||||||
|
const roleName = `awsrole-${scenario.credentialType}`;
|
||||||
|
this.server.post(`/${path}/creds/${roleName}`, (_, req) => {
|
||||||
|
const payload = JSON.parse(req.requestBody);
|
||||||
|
assert.deepEqual(payload, scenario.expectedPayload);
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
access_key: 'AKIA...',
|
||||||
|
secret_key: 'xlCs...',
|
||||||
|
security_token: 'some-token',
|
||||||
|
arn: 'arn:aws:sts::123456789012:assumed-role/DeveloperRole/some-user-supplied-role-session-name',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.server.get(`/${path}/creds/${roleName}`, () => {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
access_key: 'AKIA...',
|
||||||
|
secret_key: 'xlCs...',
|
||||||
|
security_token: 'some-token',
|
||||||
|
arn: 'arn:aws:sts::123456789012:assumed-role/DeveloperRole/some-user-supplied-role-session-name',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await runCmd(mountEngineCmd('aws', path));
|
||||||
|
|
||||||
|
await visit(`/vault/secrets/${path}/create`);
|
||||||
|
assert.dom('h1').hasText('Create an AWS Role');
|
||||||
|
await fillIn(GENERAL.inputByAttr('name'), roleName);
|
||||||
|
await fillIn(GENERAL.inputByAttr('credentialType'), scenario.credentialType);
|
||||||
|
await click(GENERAL.saveButton);
|
||||||
|
await waitUntil(() => currentURL() === `/vault/secrets/${path}/show/${roleName}`); // flaky without this
|
||||||
|
assert.strictEqual(currentURL(), `/vault/secrets/${path}/show/${roleName}`);
|
||||||
|
await click(AWS_CREDS.generateLink);
|
||||||
|
assert
|
||||||
|
.dom(GENERAL.inputByAttr('credentialType'))
|
||||||
|
.hasValue(scenario.credentialType, 'credentialType matches backing role');
|
||||||
|
|
||||||
|
// based on credentialType, fill out form
|
||||||
|
await scenario.fillOutForm(assert);
|
||||||
|
|
||||||
|
await click(GENERAL.saveButton);
|
||||||
|
assert.dom(AWS_CREDS.warning).exists('Shows access warning after generation');
|
||||||
|
assert.dom(GENERAL.infoRowValue('Access key')).exists();
|
||||||
|
assert.dom(GENERAL.infoRowValue('Secret key')).exists();
|
||||||
|
assert.dom(GENERAL.infoRowValue('Security token')).exists();
|
||||||
|
await visit('/vault/dashboard');
|
||||||
|
|
||||||
|
await runCmd(deleteEngineCmd(path));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`aws credentials without role read access`, async function (assert) {
|
||||||
|
const path = `aws-cred-${this.uid}`;
|
||||||
|
const roleName = `awsrole-noread`;
|
||||||
|
this.server.post(`/${path}/creds/${roleName}`, () => {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
access_key: 'AKIA...',
|
||||||
|
secret_key: 'xlCs...',
|
||||||
|
security_token: 'some-token',
|
||||||
|
arn: 'arn:aws:sts::123456789012:assumed-role/DeveloperRole/some-user-supplied-role-session-name',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.server.get(`/${path}/roles/${roleName}`, () => overrideResponse(403));
|
||||||
|
await runCmd(mountEngineCmd('aws', path));
|
||||||
|
await runCmd(`write ${path}/roles/${roleName} credential_type=assumed_role`);
|
||||||
|
|
||||||
|
await visit(`/vault/secrets/${path}/list`);
|
||||||
|
assert.dom(AWS_CREDS.secretLink(roleName)).exists();
|
||||||
|
await click(AWS_CREDS.secretLink(roleName));
|
||||||
|
|
||||||
|
assert.strictEqual(currentURL(), `/vault/secrets/${path}/credentials/${roleName}`);
|
||||||
|
assert
|
||||||
|
.dom(GENERAL.inputByAttr('credentialType'))
|
||||||
|
.hasValue('iam_user', 'credentialType defaults to first in list due to no role read permissions');
|
||||||
|
|
||||||
|
await fillIn(GENERAL.inputByAttr('credentialType'), 'assumed_role');
|
||||||
|
|
||||||
|
await click(GENERAL.saveButton);
|
||||||
|
assert.dom(AWS_CREDS.warning).exists('Shows access warning after generation');
|
||||||
|
assert.dom(GENERAL.infoRowValue('Access key')).exists();
|
||||||
|
assert.dom(GENERAL.infoRowValue('Secret key')).exists();
|
||||||
|
assert.dom(GENERAL.infoRowValue('Security token')).exists();
|
||||||
|
await visit('/vault/dashboard');
|
||||||
|
|
||||||
|
await runCmd(deleteEngineCmd(path));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,14 +3,23 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { click, fillIn, findAll, currentURL, find, settled, waitUntil } from '@ember/test-helpers';
|
import {
|
||||||
|
click,
|
||||||
|
fillIn,
|
||||||
|
currentURL,
|
||||||
|
find,
|
||||||
|
settled,
|
||||||
|
waitUntil,
|
||||||
|
currentRouteName,
|
||||||
|
waitFor,
|
||||||
|
} from '@ember/test-helpers';
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupApplicationTest } from 'ember-qunit';
|
import { setupApplicationTest } from 'ember-qunit';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import authPage from 'vault/tests/pages/auth';
|
import authPage from 'vault/tests/pages/auth';
|
||||||
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
|
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
|
||||||
import { setRunOptions } from 'ember-a11y-testing/test-support';
|
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||||
|
|
||||||
module('Acceptance | ssh secret backend', function (hooks) {
|
module('Acceptance | ssh secret backend', function (hooks) {
|
||||||
setupApplicationTest(hooks);
|
setupApplicationTest(hooks);
|
||||||
@@ -26,6 +35,7 @@ module('Acceptance | ssh secret backend', function (hooks) {
|
|||||||
{
|
{
|
||||||
type: 'ca',
|
type: 'ca',
|
||||||
name: 'carole',
|
name: 'carole',
|
||||||
|
credsRoute: 'vault.cluster.secrets.backend.sign',
|
||||||
async fillInCreate() {
|
async fillInCreate() {
|
||||||
await click('[data-test-input="allowUserCertificates"]');
|
await click('[data-test-input="allowUserCertificates"]');
|
||||||
},
|
},
|
||||||
@@ -61,6 +71,7 @@ module('Acceptance | ssh secret backend', function (hooks) {
|
|||||||
{
|
{
|
||||||
type: 'otp',
|
type: 'otp',
|
||||||
name: 'otprole',
|
name: 'otprole',
|
||||||
|
credsRoute: 'vault.cluster.secrets.backend.credentials',
|
||||||
async fillInCreate() {
|
async fillInCreate() {
|
||||||
await fillIn('[data-test-input="defaultUser"]', 'admin');
|
await fillIn('[data-test-input="defaultUser"]', 'admin');
|
||||||
await click('[data-test-toggle-group="Options"]');
|
await click('[data-test-toggle-group="Options"]');
|
||||||
@@ -84,13 +95,7 @@ module('Acceptance | ssh secret backend', function (hooks) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
test('ssh backend', async function (assert) {
|
test('ssh backend', async function (assert) {
|
||||||
// Popup menu causes flakiness
|
assert.expect(30);
|
||||||
setRunOptions({
|
|
||||||
rules: {
|
|
||||||
'color-contrast': { enabled: false },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
assert.expect(28);
|
|
||||||
const sshPath = `ssh-${this.uid}`;
|
const sshPath = `ssh-${this.uid}`;
|
||||||
|
|
||||||
await enablePage.enable('ssh', sshPath);
|
await enablePage.enable('ssh', sshPath);
|
||||||
@@ -100,27 +105,24 @@ module('Acceptance | ssh secret backend', function (hooks) {
|
|||||||
await click('[data-test-secret-backend-configure]');
|
await click('[data-test-secret-backend-configure]');
|
||||||
|
|
||||||
assert.strictEqual(currentURL(), `/vault/settings/secrets/configure/${sshPath}`);
|
assert.strictEqual(currentURL(), `/vault/settings/secrets/configure/${sshPath}`);
|
||||||
assert.ok(findAll('[data-test-ssh-configure-form]').length, 'renders the empty configuration form');
|
assert.dom('[data-test-ssh-configure-form]').exists('renders the empty configuration form');
|
||||||
|
|
||||||
// default has generate CA checked so we just submit the form
|
// default has generate CA checked so we just submit the form
|
||||||
await click('[data-test-ssh-input="configure-submit"]');
|
await click('[data-test-ssh-input="configure-submit"]');
|
||||||
|
|
||||||
assert.ok(
|
await waitFor('[data-test-ssh-input="public-key"]');
|
||||||
await waitUntil(() => findAll('[data-test-ssh-input="public-key"]').length),
|
assert.dom('[data-test-ssh-input="public-key"]').exists();
|
||||||
'a public key is fetched'
|
|
||||||
);
|
|
||||||
await click('[data-test-backend-view-link]');
|
await click('[data-test-backend-view-link]');
|
||||||
|
|
||||||
assert.strictEqual(currentURL(), `/vault/secrets/${sshPath}/list`, `redirects to ssh index`);
|
assert.strictEqual(currentURL(), `/vault/secrets/${sshPath}/list`, `redirects to ssh index`);
|
||||||
|
|
||||||
for (const role of ROLES) {
|
for (const role of ROLES) {
|
||||||
// create a role
|
// create a role
|
||||||
await click('[ data-test-secret-create]');
|
await click('[data-test-secret-create]');
|
||||||
|
|
||||||
assert.ok(
|
assert
|
||||||
find('[data-test-secret-header]').textContent.includes('SSH Role'),
|
.dom('[data-test-secret-header]')
|
||||||
`${role.type}: renders the create page`
|
.includesText('SSH Role', `${role.type}: renders the create page`);
|
||||||
);
|
|
||||||
|
|
||||||
await fillIn('[data-test-input="name"]', role.name);
|
await fillIn('[data-test-input="name"]', role.name);
|
||||||
await fillIn('[data-test-input="keyType"]', role.type);
|
await fillIn('[data-test-input="keyType"]', role.type);
|
||||||
@@ -138,7 +140,7 @@ module('Acceptance | ssh secret backend', function (hooks) {
|
|||||||
|
|
||||||
// sign a key with this role
|
// sign a key with this role
|
||||||
await click('[data-test-backend-credentials]');
|
await click('[data-test-backend-credentials]');
|
||||||
|
assert.strictEqual(currentRouteName(), role.credsRoute);
|
||||||
await role.fillInGenerate();
|
await role.fillInGenerate();
|
||||||
if (role.type === 'ca') {
|
if (role.type === 'ca') {
|
||||||
await settled();
|
await settled();
|
||||||
@@ -146,29 +148,23 @@ module('Acceptance | ssh secret backend', function (hooks) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generate creds
|
// generate creds
|
||||||
await click('[data-test-secret-generate]');
|
await click(GENERAL.saveButton);
|
||||||
await settled(); // eslint-disable-line
|
await settled(); // eslint-disable-line
|
||||||
role.assertAfterGenerate(assert, sshPath);
|
role.assertAfterGenerate(assert, sshPath);
|
||||||
|
|
||||||
// click the "Back" button
|
// click the "Back" button
|
||||||
await click('[data-test-secret-generate-back]');
|
await click('[data-test-back-button]');
|
||||||
|
|
||||||
assert.ok(
|
assert.dom('[data-test-secret-generate-form]').exists(`${role.type}: back takes you back to the form`);
|
||||||
findAll('[data-test-secret-generate-form]').length,
|
|
||||||
`${role.type}: back takes you back to the form`
|
|
||||||
);
|
|
||||||
|
|
||||||
await click('[data-test-secret-generate-cancel]');
|
await click(GENERAL.cancelButton);
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`/vault/secrets/${sshPath}/list`,
|
`/vault/secrets/${sshPath}/list`,
|
||||||
`${role.type}: cancel takes you to ssh index`
|
`${role.type}: cancel takes you to ssh index`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.dom(`[data-test-secret-link="${role.name}"]`).exists(`${role.type}: role shows in the list`);
|
||||||
findAll(`[data-test-secret-link="${role.name}"]`).length,
|
|
||||||
`${role.type}: role shows in the list`
|
|
||||||
);
|
|
||||||
|
|
||||||
//and delete
|
//and delete
|
||||||
await click(`[data-test-secret-link="${role.name}"] [data-test-popup-menu-trigger]`);
|
await click(`[data-test-secret-link="${role.name}"] [data-test-popup-menu-trigger]`);
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) HashiCorp, Inc.
|
|
||||||
* SPDX-License-Identifier: BUSL-1.1
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Base } from '../create';
|
|
||||||
import { settled } from '@ember/test-helpers';
|
|
||||||
import { clickable, visitable, create, fillable } from 'ember-cli-page-object';
|
|
||||||
|
|
||||||
export default create({
|
|
||||||
...Base,
|
|
||||||
visitEdit: visitable('/vault/secrets/:backend/edit/:id'),
|
|
||||||
visitEditRoot: visitable('/vault/secrets/:backend/edit'),
|
|
||||||
toggleDomain: clickable('[data-test-toggle-group="Domain Handling"]'),
|
|
||||||
toggleOptions: clickable('[data-test-toggle-group="Options"]'),
|
|
||||||
name: fillable('[data-test-input="name"]'),
|
|
||||||
allowAnyName: clickable('[data-test-input="allowAnyName"]'),
|
|
||||||
allowedDomains: fillable('[data-test-input="allowedDomains"] .input'),
|
|
||||||
save: clickable('[data-test-role-create]'),
|
|
||||||
|
|
||||||
async createRole(name, allowedDomains) {
|
|
||||||
await this.toggleDomain();
|
|
||||||
await settled();
|
|
||||||
await this.toggleOptions();
|
|
||||||
await settled();
|
|
||||||
await this.name(name);
|
|
||||||
await settled();
|
|
||||||
await this.allowAnyName();
|
|
||||||
await settled();
|
|
||||||
await this.allowedDomains(allowedDomains);
|
|
||||||
await settled();
|
|
||||||
await this.save();
|
|
||||||
await settled();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) HashiCorp, Inc.
|
|
||||||
* SPDX-License-Identifier: BUSL-1.1
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Base } from '../credentials';
|
|
||||||
import { clickable, text, value, create, fillable, isPresent } from 'ember-cli-page-object';
|
|
||||||
|
|
||||||
export default create({
|
|
||||||
...Base,
|
|
||||||
title: text('[data-test-title]'),
|
|
||||||
commonName: fillable('[data-test-input="commonName"]'),
|
|
||||||
commonNameValue: value('[data-test-input="commonName"]'),
|
|
||||||
csr: fillable('[data-test-input="csr"]'),
|
|
||||||
submit: clickable('[data-test-secret-generate]'),
|
|
||||||
back: clickable('[data-test-secret-generate-back]'),
|
|
||||||
certificate: text('[data-test-row-value="Certificate"]'),
|
|
||||||
toggleOptions: clickable('[data-test-toggle-group]'),
|
|
||||||
enableTtl: clickable('[data-test-toggle-input]'),
|
|
||||||
hasCert: isPresent('[data-test-row-value="Certificate"]'),
|
|
||||||
fillInTime: fillable('[data-test-ttl-value]'),
|
|
||||||
fillInField: fillable('[data-test-select="ttl-unit"]'),
|
|
||||||
issueCert: async function (commonName) {
|
|
||||||
await this.commonName(commonName).toggleOptions().enableTtl().fillInField('h').fillInTime('30').submit();
|
|
||||||
},
|
|
||||||
|
|
||||||
sign: async function (commonName, csr) {
|
|
||||||
return this.csr(csr)
|
|
||||||
.commonName(commonName)
|
|
||||||
.toggleOptions()
|
|
||||||
.enableTtl()
|
|
||||||
.fillInField('h')
|
|
||||||
.fillInTime('30')
|
|
||||||
.submit();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) HashiCorp, Inc.
|
|
||||||
* SPDX-License-Identifier: BUSL-1.1
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Base } from '../show';
|
|
||||||
import { settled } from '@ember/test-helpers';
|
|
||||||
import { create, clickable, collection, text, isPresent } from 'ember-cli-page-object';
|
|
||||||
|
|
||||||
export default create({
|
|
||||||
...Base,
|
|
||||||
rows: collection('data-test-row-label'),
|
|
||||||
certificate: text('[data-test-row-value="Certificate"]'),
|
|
||||||
hasCert: isPresent('[data-test-row-value="Certificate"]'),
|
|
||||||
edit: clickable('[data-test-edit-link]'),
|
|
||||||
generateCert: clickable('[data-test-credentials-link]'),
|
|
||||||
deleteBtn: clickable('[data-test-role-delete] button'),
|
|
||||||
confirmBtn: clickable('[data-test-confirm-button]'),
|
|
||||||
async deleteRole() {
|
|
||||||
await this.deleteBtn();
|
|
||||||
await settled();
|
|
||||||
await this.confirmBtn();
|
|
||||||
await settled();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -14,8 +14,8 @@ export default create({
|
|||||||
ip: fillable('[data-test-input="ip"]'),
|
ip: fillable('[data-test-input="ip"]'),
|
||||||
warningIsPresent: isPresent('[data-test-warning]'),
|
warningIsPresent: isPresent('[data-test-warning]'),
|
||||||
commonNameValue: value('[data-test-input="commonName"]'),
|
commonNameValue: value('[data-test-input="commonName"]'),
|
||||||
submit: clickable('[data-test-secret-generate]'),
|
submit: clickable('[data-test-save]'),
|
||||||
back: clickable('[data-test-secret-generate-back]'),
|
back: clickable('[data-test-back-button]'),
|
||||||
generateOTP: async function () {
|
generateOTP: async function () {
|
||||||
await this.user('admin').ip('192.168.1.1').submit();
|
await this.user('admin').ip('192.168.1.1').submit();
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user