mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 10:37:56 +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,22 +35,15 @@ | |||||||
|         </A.Description> |         </A.Description> | ||||||
|       </Hds::Alert> |       </Hds::Alert> | ||||||
|     {{/unless}} |     {{/unless}} | ||||||
|     {{#each this.model.attrs as |attr|}} |     {{#each this.displayFields as |key|}} | ||||||
|  |       {{#let (get this.model.allByKey key) as |attr|}} | ||||||
|         {{#if (eq attr.type "object")}} |         {{#if (eq attr.type "object")}} | ||||||
|           <InfoTableRow |           <InfoTableRow | ||||||
|             @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} |             @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} | ||||||
|             @value={{stringify (get this.model attr.name)}} |             @value={{stringify (get this.model attr.name)}} | ||||||
|           /> |           /> | ||||||
|         {{else}} |         {{else}} | ||||||
|         {{#if |           {{#if attr.options.masked}} | ||||||
|           (or |  | ||||||
|             (eq attr.name "key") |  | ||||||
|             (eq attr.name "secretKey") |  | ||||||
|             (eq attr.name "securityToken") |  | ||||||
|             (eq attr.name "privateKey") |  | ||||||
|             attr.options.masked |  | ||||||
|           ) |  | ||||||
|         }} |  | ||||||
|             {{#if (get this.model attr.name)}} |             {{#if (get this.model attr.name)}} | ||||||
|               <InfoTableRow |               <InfoTableRow | ||||||
|                 @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} |                 @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} | ||||||
| @@ -77,6 +70,7 @@ | |||||||
|             /> |             /> | ||||||
|           {{/if}} |           {{/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}} | ||||||
|       {{#if this.model.fieldGroups}} |       {{#each this.formFields as |key|}} | ||||||
|         <FormFieldGroupsLoop @model={{this.model}} @mode={{this.mode}} /> |         <FormField data-test-field @attr={{get this.model.allByKey key}} @model={{this.model}} /> | ||||||
|       {{else}} |  | ||||||
|         {{#each this.model.attrs as |attr|}} |  | ||||||
|           <FormField data-test-field={{true}} @attr={{attr}} @model={{this.model}} /> |  | ||||||
|       {{/each}} |       {{/each}} | ||||||
|       {{/if}} |  | ||||||
|     </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,18 +101,32 @@ 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; | ||||||
|  |     } | ||||||
|  |     if (this.model) { | ||||||
|  |       this.model.unloadRecord(); | ||||||
|  |     } | ||||||
|  |     this.model = this.generateNewModel(modelType); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   create(evt) { | ||||||
|  |     evt.preventDefault(); | ||||||
|  |     this.loading = true; | ||||||
|     this.model |     this.model | ||||||
|       .save() |       .save() | ||||||
|       .then(() => { |       .then(() => { | ||||||
|           model.set('hasGenerated', true); |         this.hasGenerated = true; | ||||||
|       }) |       }) | ||||||
|       .catch((error) => { |       .catch((error) => { | ||||||
|         // Handle control group AdapterError |         // Handle control group AdapterError | ||||||
| @@ -105,21 +138,23 @@ export default Component.extend({ | |||||||
|         throw error; |         throw error; | ||||||
|       }) |       }) | ||||||
|       .finally(() => { |       .finally(() => { | ||||||
|           this.set('loading', false); |         this.loading = false; | ||||||
|       }); |       }); | ||||||
|     }, |   } | ||||||
|  |  | ||||||
|  |   @action | ||||||
|   codemirrorUpdated(attr, val, codemirror) { |   codemirrorUpdated(attr, val, codemirror) { | ||||||
|     codemirror.performLint(); |     codemirror.performLint(); | ||||||
|     const hasErrors = codemirror.state.lint.marked.length > 0; |     const hasErrors = codemirror.state.lint.marked.length > 0; | ||||||
|  |  | ||||||
|     if (!hasErrors) { |     if (!hasErrors) { | ||||||
|         set(this.model, attr, JSON.parse(val)); |       this.model[attr] = JSON.parse(val); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     newModel() { |   @action | ||||||
|       this.createOrReplaceModel(); |   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
	 Chelsea Shaw
					Chelsea Shaw