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" | ||||
|             spellcheck="false" | ||||
|             @value={{@accessKey}} | ||||
|             data-test-aws-input="accessKey" | ||||
|             data-test-input="accessKey" | ||||
|           /> | ||||
|         </div> | ||||
|       </div> | ||||
| @@ -56,7 +56,7 @@ | ||||
|             name="secret" | ||||
|             class="input" | ||||
|             @value={{@secretKey}} | ||||
|             data-test-aws-input="secretKey" | ||||
|             data-test-input="secretKey" | ||||
|           /> | ||||
|         </div> | ||||
|       </div> | ||||
| @@ -104,7 +104,7 @@ | ||||
|       {{/if}} | ||||
|  | ||||
|       <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> | ||||
|     </form> | ||||
|   </T.Panel> | ||||
| @@ -134,7 +134,7 @@ | ||||
|         @onChange={{fn this.handleTtlChange "leaseMax"}} | ||||
|       /> | ||||
|       <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> | ||||
|     </form> | ||||
|   </T.Panel> | ||||
|   | ||||
| @@ -7,13 +7,13 @@ | ||||
|   <p.top> | ||||
|     <Hds::Breadcrumb> | ||||
|       <Hds::Breadcrumb::Item | ||||
|         @text={{this.backendPath}} | ||||
|         @text={{@backendPath}} | ||||
|         @route="vault.cluster.secrets.backend" | ||||
|         @model={{this.backendPath}} | ||||
|         @model={{@backendPath}} | ||||
|         data-test-link="role-list" | ||||
|       /> | ||||
|       <Hds::Breadcrumb::Item @text="Credentials" @route="vault.cluster.secrets.backend" @model={{this.backendPath}} /> | ||||
|       <Hds::Breadcrumb::Item @text={{this.roleName}} @route="vault.cluster.secrets.backend.show" @model={{this.roleName}} /> | ||||
|       <Hds::Breadcrumb::Item @text="Credentials" @route="vault.cluster.secrets.backend" @model={{@backendPath}} /> | ||||
|       <Hds::Breadcrumb::Item @text={{@roleName}} @route="vault.cluster.secrets.backend.show" @model={{@roleName}} /> | ||||
|       <Hds::Breadcrumb::Item @text={{this.options.title}} @current={{true}} /> | ||||
|     </Hds::Breadcrumb> | ||||
|   </p.top> | ||||
| @@ -24,7 +24,7 @@ | ||||
|   </p.levelLeft> | ||||
| </PageHeader> | ||||
| 
 | ||||
| {{#if this.model.hasGenerated}} | ||||
| {{#if this.hasGenerated}} | ||||
|   <div class="box is-fullwidth is-sideless is-paddingless is-marginless"> | ||||
|     <MessageError @model={{this.model}} /> | ||||
|     {{#unless this.model.isError}} | ||||
| @@ -35,48 +35,42 @@ | ||||
|         </A.Description> | ||||
|       </Hds::Alert> | ||||
|     {{/unless}} | ||||
|     {{#each this.model.attrs as |attr|}} | ||||
|       {{#if (eq attr.type "object")}} | ||||
|         <InfoTableRow | ||||
|           @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} | ||||
|           @value={{stringify (get this.model attr.name)}} | ||||
|         /> | ||||
|       {{else}} | ||||
|         {{#if | ||||
|           (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)}} | ||||
|     {{#each this.displayFields as |key|}} | ||||
|       {{#let (get this.model.allByKey key) as |attr|}} | ||||
|         {{#if (eq attr.type "object")}} | ||||
|           <InfoTableRow | ||||
|             @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} | ||||
|             @value={{stringify (get this.model attr.name)}} | ||||
|           /> | ||||
|         {{else}} | ||||
|           {{#if attr.options.masked}} | ||||
|             {{#if (get this.model attr.name)}} | ||||
|               <InfoTableRow | ||||
|                 @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} | ||||
|                 @value={{get this.model attr.name}} | ||||
|               > | ||||
|                 <MaskedInput | ||||
|                   @value={{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 | ||||
|               @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} | ||||
|               @value={{get this.model attr.name}} | ||||
|             > | ||||
|               <MaskedInput | ||||
|                 @value={{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 | ||||
|             @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} | ||||
|             @value={{get this.model attr.name}} | ||||
|           /> | ||||
|         {{/if}} | ||||
|       {{/if}} | ||||
|       {{/let}} | ||||
|     {{/each}} | ||||
|   </div> | ||||
|   <div class="field is-grouped box is-fullwidth is-bottomless"> | ||||
| @@ -106,34 +100,25 @@ | ||||
|           @text="Back" | ||||
|           @color="secondary" | ||||
|           @route="vault.cluster.secrets.backend.list-root" | ||||
|           @model={{this.backendPath}} | ||||
|           data-test-secret-generate-back={{true}} | ||||
|           @model={{@backendPath}} | ||||
|           data-test-back-button | ||||
|         /> | ||||
|       {{else}} | ||||
|         <Hds::Button | ||||
|           @text="Back" | ||||
|           @color="secondary" | ||||
|           {{on "click" (action "newModel")}} | ||||
|           data-test-secret-generate-back="true" | ||||
|         /> | ||||
|         <Hds::Button @text="Back" @color="secondary" {{on "click" this.reset}} data-test-back-button /> | ||||
|       {{/if}} | ||||
|     </div> | ||||
|   </div> | ||||
| {{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"> | ||||
|       <NamespaceReminder @mode="generate" @noun="credential" /> | ||||
|       <MessageError @model={{this.model}} /> | ||||
|       {{#if this.model.helpText}} | ||||
|         <p class="is-hint">{{this.model.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 this.helpText}} | ||||
|         <p class="is-hint">{{this.helpText}}</p> | ||||
|       {{/if}} | ||||
|       {{#each this.formFields as |key|}} | ||||
|         <FormField data-test-field @attr={{get this.model.allByKey key}} @model={{this.model}} /> | ||||
|       {{/each}} | ||||
|     </div> | ||||
|     <div class="field is-grouped box is-fullwidth is-bottomless"> | ||||
|       <Hds::ButtonSet> | ||||
| @@ -142,14 +127,14 @@ | ||||
|           @icon={{if this.loading "loading"}} | ||||
|           type="submit" | ||||
|           disabled={{this.loading}} | ||||
|           data-test-secret-generate={{true}} | ||||
|           data-test-save | ||||
|         /> | ||||
|         <Hds::Button | ||||
|           @text="Cancel" | ||||
|           @route="vault.cluster.secrets.backend.list-root" | ||||
|           @color="secondary" | ||||
|           @model={{this.backendPath}} | ||||
|           data-test-secret-generate-cancel={{true}} | ||||
|           @model={{@backendPath}} | ||||
|           data-test-cancel | ||||
|         /> | ||||
|       </Hds::ButtonSet> | ||||
|     </div> | ||||
| @@ -4,56 +4,49 @@ | ||||
|  */ | ||||
|  | ||||
| import { service } from '@ember/service'; | ||||
| import { computed, set } from '@ember/object'; | ||||
| import Component from '@ember/component'; | ||||
| import { action } from '@ember/object'; | ||||
| import Component from '@glimmer/component'; | ||||
| import { tracked } from '@glimmer/tracking'; | ||||
|  | ||||
| const MODEL_TYPES = { | ||||
|   'ssh-sign': { | ||||
|     model: 'ssh-sign', | ||||
|   }, | ||||
|   'ssh-creds': { | ||||
| const CREDENTIAL_TYPES = { | ||||
|   ssh: { | ||||
|     model: 'ssh-otp-credential', | ||||
|     title: 'Generate SSH Credentials', | ||||
|     formFields: ['username', 'ip'], | ||||
|     displayFields: ['username', 'ip', 'key', 'keyType', 'port'], | ||||
|   }, | ||||
|   'aws-creds': { | ||||
|   aws: { | ||||
|     model: 'aws-credential', | ||||
|     title: 'Generate AWS Credentials', | ||||
|     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({ | ||||
|   controlGroup: service(), | ||||
|   store: service(), | ||||
|   router: service(), | ||||
|   // set on the component | ||||
|   backendType: null, | ||||
|   backendPath: null, | ||||
|   roleName: null, | ||||
|   action: null, | ||||
| export default class GenerateCredentials extends Component { | ||||
|   @service controlGroup; | ||||
|   @service store; | ||||
|   @service router; | ||||
|  | ||||
|   model: null, | ||||
|   loading: false, | ||||
|   emptyData: '{\n}', | ||||
|   @tracked model; | ||||
|   @tracked loading = false; | ||||
|   @tracked hasGenerated = false; | ||||
|   emptyData = '{\n}'; | ||||
|  | ||||
|   modelForType() { | ||||
|     const type = this.options; | ||||
|     if (type) { | ||||
|       return type.model; | ||||
|     } | ||||
|     // 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(); | ||||
|   }, | ||||
|   constructor() { | ||||
|     super(...arguments); | ||||
|     const modelType = this.modelForType(); | ||||
|     this.model = this.generateNewModel(modelType); | ||||
|   } | ||||
|  | ||||
|   willDestroy() { | ||||
|     // 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) { | ||||
|       this.model.unloadRecord(); | ||||
|     } | ||||
|     this._super(...arguments); | ||||
|   }, | ||||
|     super.willDestroy(); | ||||
|   } | ||||
|  | ||||
|   createOrReplaceModel() { | ||||
|     const modelType = this.modelForType(); | ||||
|     const model = this.model; | ||||
|     const roleName = this.roleName; | ||||
|     const backendPath = this.backendPath; | ||||
|   modelForType() { | ||||
|     const type = this.options; | ||||
|     if (type) { | ||||
|       return type.model; | ||||
|     } | ||||
|     // 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) { | ||||
|       return; | ||||
|     } | ||||
|     if (model) { | ||||
|       model.unloadRecord(); | ||||
|     } | ||||
|     const { roleName, backendPath, awsRoleType } = this.args; | ||||
|     const attrs = { | ||||
|       role: { | ||||
|         backend: backendPath, | ||||
| @@ -82,44 +101,60 @@ export default Component.extend({ | ||||
|       }, | ||||
|       id: `${backendPath}-${roleName}`, | ||||
|     }; | ||||
|     const newModel = this.store.createRecord(modelType, attrs); | ||||
|     this.set('model', newModel); | ||||
|   }, | ||||
|     if (awsRoleType) { | ||||
|       // this is only set from route if backendType = aws | ||||
|       attrs.credentialType = awsRoleType; | ||||
|     } | ||||
|     return this.store.createRecord(modelType, attrs); | ||||
|   } | ||||
|  | ||||
|   actions: { | ||||
|     create() { | ||||
|       const model = this.model; | ||||
|       this.set('loading', true); | ||||
|       this.model | ||||
|         .save() | ||||
|         .then(() => { | ||||
|           model.set('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.set('loading', false); | ||||
|         }); | ||||
|     }, | ||||
|   replaceModel() { | ||||
|     const modelType = this.modelForType(); | ||||
|     if (!modelType) { | ||||
|       return; | ||||
|     } | ||||
|     if (this.model) { | ||||
|       this.model.unloadRecord(); | ||||
|     } | ||||
|     this.model = this.generateNewModel(modelType); | ||||
|   } | ||||
|  | ||||
|     codemirrorUpdated(attr, val, codemirror) { | ||||
|       codemirror.performLint(); | ||||
|       const hasErrors = codemirror.state.lint.marked.length > 0; | ||||
|   @action | ||||
|   create(evt) { | ||||
|     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) { | ||||
|         set(this.model, attr, JSON.parse(val)); | ||||
|       } | ||||
|     }, | ||||
|   @action | ||||
|   codemirrorUpdated(attr, val, codemirror) { | ||||
|     codemirror.performLint(); | ||||
|     const hasErrors = codemirror.state.lint.marked.length > 0; | ||||
|  | ||||
|     newModel() { | ||||
|       this.createOrReplaceModel(); | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
|     if (!hasErrors) { | ||||
|       this.model[attr] = JSON.parse(val); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @action | ||||
|   reset() { | ||||
|     this.hasGenerated = false; | ||||
|     this.replaceModel(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -6,11 +6,10 @@ | ||||
| import Controller from '@ember/controller'; | ||||
|  | ||||
| export default Controller.extend({ | ||||
|   queryParams: ['action', 'roleType'], | ||||
|   action: '', | ||||
|   queryParams: ['roleType'], | ||||
|   // used for database credentials | ||||
|   roleType: '', | ||||
|   reset() { | ||||
|     this.set('action', ''); | ||||
|     this.set('roleType', ''); | ||||
|   }, | ||||
| }); | ||||
|   | ||||
| @@ -4,8 +4,8 @@ | ||||
|  */ | ||||
|  | ||||
| import Model, { attr } from '@ember-data/model'; | ||||
| import { computed } from '@ember/object'; | ||||
| import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; | ||||
| import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes'; | ||||
|  | ||||
| const CREDENTIAL_TYPES = [ | ||||
|   { | ||||
|     value: 'iam_user', | ||||
| @@ -25,27 +25,28 @@ const CREDENTIAL_TYPES = [ | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| const DISPLAY_FIELDS = ['accessKey', 'secretKey', 'securityToken', 'leaseId', 'renewable', 'leaseDuration']; | ||||
| export default Model.extend({ | ||||
|   helpText: | ||||
|     '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', { | ||||
| @withExpandedAttributes() | ||||
| export default class AwsCredential extends Model { | ||||
|   @attr('object', { | ||||
|     readOnly: true, | ||||
|   }), | ||||
|   }) | ||||
|   role; | ||||
|  | ||||
|   credentialType: attr('string', { | ||||
|   @attr('string', { | ||||
|     defaultValue: 'iam_user', | ||||
|     possibleValues: CREDENTIAL_TYPES, | ||||
|     readOnly: true, | ||||
|   }), | ||||
|   }) | ||||
|   credentialType; | ||||
|  | ||||
|   roleArn: attr('string', { | ||||
|   @attr('string', { | ||||
|     label: 'Role ARN', | ||||
|     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.', | ||||
|   }), | ||||
|   }) | ||||
|   roleArn; | ||||
|  | ||||
|   ttl: attr({ | ||||
|   @attr({ | ||||
|     editType: 'ttl', | ||||
|     defaultValue: '3600s', | ||||
|     setDefault: true, | ||||
| @@ -53,29 +54,17 @@ export default Model.extend({ | ||||
|     label: 'TTL', | ||||
|     helpText: | ||||
|       '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'), | ||||
|   renewable: attr('boolean'), | ||||
|   leaseDuration: attr('number'), | ||||
|   accessKey: attr('string'), | ||||
|   secretKey: attr('string'), | ||||
|   securityToken: attr('string'), | ||||
|   }) | ||||
|   ttl; | ||||
|  | ||||
|   attrs: computed('credentialType', 'accessKey', 'securityToken', function () { | ||||
|     const type = this.credentialType; | ||||
|     const fieldsForType = { | ||||
|       iam_user: ['credentialType'], | ||||
|       assumed_role: ['credentialType', 'ttl', 'roleArn'], | ||||
|       federation_token: ['credentialType', 'ttl'], | ||||
|       session_token: ['credentialType', 'ttl'], | ||||
|     }; | ||||
|     if (this.accessKey || this.securityToken) { | ||||
|       return expandAttributeMeta(this, DISPLAY_FIELDS.slice(0)); | ||||
|     } | ||||
|     return expandAttributeMeta(this, fieldsForType[type].slice(0)); | ||||
|   }), | ||||
|   @attr('string') leaseId; | ||||
|   @attr('boolean') renewable; | ||||
|   @attr('number') leaseDuration; | ||||
|   @attr('string') accessKey; | ||||
|   @attr('string', { masked: true }) secretKey; | ||||
|   @attr('string', { masked: true }) securityToken; | ||||
|  | ||||
|   toCreds: computed('accessKey', 'secretKey', 'securityToken', 'leaseId', function () { | ||||
|   get toCreds() { | ||||
|     const props = { | ||||
|       accessKey: this.accessKey, | ||||
|       secretKey: this.secretKey, | ||||
| @@ -90,5 +79,5 @@ export default Model.extend({ | ||||
|       return ret; | ||||
|     }, {}); | ||||
|     return JSON.stringify(propsWithVals, null, 2); | ||||
|   }), | ||||
| }); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -3,27 +3,25 @@ | ||||
|  * SPDX-License-Identifier: BUSL-1.1 | ||||
|  */ | ||||
|  | ||||
| import { reads } from '@ember/object/computed'; | ||||
| import Model, { attr } from '@ember-data/model'; | ||||
| import { computed } from '@ember/object'; | ||||
| import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; | ||||
| const CREATE_FIELDS = ['username', 'ip']; | ||||
| import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes'; | ||||
|  | ||||
| const DISPLAY_FIELDS = ['username', 'ip', 'key', 'keyType', 'port']; | ||||
| export default Model.extend({ | ||||
|   role: attr('object', { | ||||
| @withExpandedAttributes() | ||||
| export default class SshOtpCredential extends Model { | ||||
|   @attr('object', { | ||||
|     readOnly: true, | ||||
|   }), | ||||
|   ip: attr('string', { | ||||
|   }) | ||||
|   role; | ||||
|   @attr('string', { | ||||
|     label: 'IP Address', | ||||
|   }), | ||||
|   username: attr('string'), | ||||
|   key: attr('string'), | ||||
|   keyType: attr('string'), | ||||
|   port: attr('number'), | ||||
|   attrs: computed('key', function () { | ||||
|     const keys = this.key ? DISPLAY_FIELDS.slice(0) : CREATE_FIELDS.slice(0); | ||||
|     return expandAttributeMeta(this, keys); | ||||
|   }), | ||||
|   toCreds: reads('key'), | ||||
| }); | ||||
|   }) | ||||
|   ip; | ||||
|   @attr('string') username; | ||||
|   @attr('string', { masked: true }) key; | ||||
|   @attr('string') keyType; | ||||
|   @attr('number') port; | ||||
|  | ||||
|   get toCreds() { | ||||
|     return this.key; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -17,12 +17,15 @@ export default Route.extend({ | ||||
|   store: service(), | ||||
|  | ||||
|   beforeModel() { | ||||
|     const { backend } = this.paramsFor('vault.cluster.secrets.backend'); | ||||
|     if (backend != 'ssh') { | ||||
|       return; | ||||
|     const { id: backendPath, type: backendType } = this.modelFor('vault.cluster.secrets.backend'); | ||||
|     // redirect if the backend type does not support credentials | ||||
|     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 = '') { | ||||
| @@ -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) { | ||||
|     const role = params.secret; | ||||
|     const { id: backendPath, type: backendType } = this.modelFor('vault.cluster.secrets.backend'); | ||||
|     const roleType = params.roleType; | ||||
|     let dbCred; | ||||
|     let dbCred, awsRole; | ||||
|     if (backendType === 'database') { | ||||
|       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({ | ||||
|       backendPath, | ||||
|       backendType, | ||||
|       roleName: role, | ||||
|       roleType, | ||||
|       dbCred, | ||||
|       awsRoleType: awsRole?.credentialType, | ||||
|     }); | ||||
|   }, | ||||
|  | ||||
|   | ||||
| @@ -76,7 +76,7 @@ | ||||
|     </div> | ||||
|     <div class="field is-grouped-split box is-fullwidth is-bottomless"> | ||||
|       <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")}} | ||||
|           <Hds::Button | ||||
|             @text="Cancel" | ||||
|   | ||||
| @@ -11,11 +11,10 @@ | ||||
|     @model={{this.model.dbCred}} | ||||
|   /> | ||||
| {{else}} | ||||
|   {{! TODO smells a little to have action off of query param requiring a conditional }} | ||||
|   <GenerateCredentials | ||||
|     @backendPath={{this.model.backendPath}} | ||||
|     @backendType={{this.model.backendType}} | ||||
|     @roleName={{this.model.roleName}} | ||||
|     @action={{if this.action this.action ""}} | ||||
|     @awsRoleType={{this.model.awsRoleType}} | ||||
|   /> | ||||
| {{/if}} | ||||
| @@ -67,12 +67,7 @@ | ||||
|       </div> | ||||
|     {{/if}} | ||||
|     <div class="control"> | ||||
|       <Hds::Button | ||||
|         @text="Back" | ||||
|         @color="secondary" | ||||
|         {{on "click" (action "newModel")}} | ||||
|         data-test-secret-generate-back={{true}} | ||||
|       /> | ||||
|       <Hds::Button @text="Back" @color="secondary" {{on "click" (action "newModel")}} data-test-back-button /> | ||||
|     </div> | ||||
|   </div> | ||||
| {{else}} | ||||
| @@ -113,14 +108,14 @@ | ||||
|           @icon={{if this.loading "loading"}} | ||||
|           type="submit" | ||||
|           disabled={{this.loading}} | ||||
|           data-test-secret-generate | ||||
|           data-test-save | ||||
|         /> | ||||
|         <Hds::Button | ||||
|           @text="Cancel" | ||||
|           @color="secondary" | ||||
|           @route="vault.cluster.secrets.backend.list-root" | ||||
|           @model={{this.backend.id}} | ||||
|           data-test-secret-generate-cancel | ||||
|           data-test-cancel | ||||
|         /> | ||||
|       </Hds::ButtonSet> | ||||
|     </div> | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|  * 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 { setupApplicationTest } from 'ember-qunit'; | ||||
| import { v4 as uuidv4 } from 'uuid'; | ||||
| @@ -13,7 +13,63 @@ import { GENERAL } from '../helpers/general-selectors'; | ||||
| import authPage from 'vault/tests/pages/auth'; | ||||
| import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; | ||||
| 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) { | ||||
|   setupApplicationTest(hooks); | ||||
|   setupMirage(hooks); | ||||
| @@ -30,28 +86,23 @@ module('Acceptance | aws secret backend', function (hooks) { | ||||
|   test('aws backend', async function (assert) { | ||||
|     const path = `aws-${this.uid}`; | ||||
|     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 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.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('lease')).exists('renders the leases config tab'); | ||||
|  | ||||
|     await fillIn('[data-test-aws-input="accessKey"]', 'foo'); | ||||
|     await fillIn('[data-test-aws-input="secretKey"]', 'bar'); | ||||
|     await fillIn(GENERAL.inputByAttr('accessKey'), 'foo'); | ||||
|     await fillIn(GENERAL.inputByAttr('secretKey'), 'bar'); | ||||
|  | ||||
|     await click('[data-test-aws-input="root-save"]'); | ||||
|     await click(GENERAL.saveButton); | ||||
|  | ||||
|     assert.true( | ||||
|       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('[data-test-aws-input="lease-save"]'); | ||||
|     await click(GENERAL.saveButton); | ||||
|  | ||||
|     assert.true( | ||||
|       this.flashSuccessSpy.calledTwice, | ||||
|       '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'); | ||||
|  | ||||
|     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 | ||||
|     await click('[data-test-role-aws-create]'); | ||||
|     await click(GENERAL.saveButton); | ||||
|     await waitUntil(() => currentURL() === `/vault/secrets/${path}/show/${roleName}`); // flaky without this | ||||
|     assert.strictEqual( | ||||
|       currentURL(), | ||||
|       `/vault/secrets/${path}/show/${roleName}`, | ||||
|       '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.dom(`[data-test-secret-link="${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]'); | ||||
|     assert.dom(AWS_CREDS.secretLink(roleName)).exists(); | ||||
|  | ||||
|     //and delete | ||||
|     await click(`[data-test-secret-link="${roleName}"] [data-test-popup-menu-trigger]`); | ||||
|     await waitUntil(() => find(`[data-test-aws-role-delete="${roleName}"]`)); // flaky without | ||||
|     await click(`[data-test-aws-role-delete="${roleName}"]`); | ||||
|     await click(`${AWS_CREDS.secretLink(roleName)} [data-test-popup-menu-trigger]`); | ||||
|     await waitUntil(() => find(AWS_CREDS.delete(roleName))); // flaky without | ||||
|     await click(AWS_CREDS.delete(roleName)); | ||||
|     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 | ||||
|  */ | ||||
|  | ||||
| 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 { setupApplicationTest } from 'ember-qunit'; | ||||
| import { v4 as uuidv4 } from 'uuid'; | ||||
|  | ||||
| import authPage from 'vault/tests/pages/auth'; | ||||
| 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) { | ||||
|   setupApplicationTest(hooks); | ||||
| @@ -26,6 +35,7 @@ module('Acceptance | ssh secret backend', function (hooks) { | ||||
|     { | ||||
|       type: 'ca', | ||||
|       name: 'carole', | ||||
|       credsRoute: 'vault.cluster.secrets.backend.sign', | ||||
|       async fillInCreate() { | ||||
|         await click('[data-test-input="allowUserCertificates"]'); | ||||
|       }, | ||||
| @@ -61,6 +71,7 @@ module('Acceptance | ssh secret backend', function (hooks) { | ||||
|     { | ||||
|       type: 'otp', | ||||
|       name: 'otprole', | ||||
|       credsRoute: 'vault.cluster.secrets.backend.credentials', | ||||
|       async fillInCreate() { | ||||
|         await fillIn('[data-test-input="defaultUser"]', 'admin'); | ||||
|         await click('[data-test-toggle-group="Options"]'); | ||||
| @@ -84,13 +95,7 @@ module('Acceptance | ssh secret backend', function (hooks) { | ||||
|     }, | ||||
|   ]; | ||||
|   test('ssh backend', async function (assert) { | ||||
|     // Popup menu causes flakiness | ||||
|     setRunOptions({ | ||||
|       rules: { | ||||
|         'color-contrast': { enabled: false }, | ||||
|       }, | ||||
|     }); | ||||
|     assert.expect(28); | ||||
|     assert.expect(30); | ||||
|     const sshPath = `ssh-${this.uid}`; | ||||
|  | ||||
|     await enablePage.enable('ssh', sshPath); | ||||
| @@ -100,27 +105,24 @@ module('Acceptance | ssh secret backend', function (hooks) { | ||||
|     await click('[data-test-secret-backend-configure]'); | ||||
|  | ||||
|     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 | ||||
|     await click('[data-test-ssh-input="configure-submit"]'); | ||||
|  | ||||
|     assert.ok( | ||||
|       await waitUntil(() => findAll('[data-test-ssh-input="public-key"]').length), | ||||
|       'a public key is fetched' | ||||
|     ); | ||||
|     await waitFor('[data-test-ssh-input="public-key"]'); | ||||
|     assert.dom('[data-test-ssh-input="public-key"]').exists(); | ||||
|     await click('[data-test-backend-view-link]'); | ||||
|  | ||||
|     assert.strictEqual(currentURL(), `/vault/secrets/${sshPath}/list`, `redirects to ssh index`); | ||||
|  | ||||
|     for (const role of ROLES) { | ||||
|       // create a role | ||||
|       await click('[ data-test-secret-create]'); | ||||
|       await click('[data-test-secret-create]'); | ||||
|  | ||||
|       assert.ok( | ||||
|         find('[data-test-secret-header]').textContent.includes('SSH Role'), | ||||
|         `${role.type}: renders the create page` | ||||
|       ); | ||||
|       assert | ||||
|         .dom('[data-test-secret-header]') | ||||
|         .includesText('SSH Role', `${role.type}: renders the create page`); | ||||
|  | ||||
|       await fillIn('[data-test-input="name"]', role.name); | ||||
|       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 | ||||
|       await click('[data-test-backend-credentials]'); | ||||
|  | ||||
|       assert.strictEqual(currentRouteName(), role.credsRoute); | ||||
|       await role.fillInGenerate(); | ||||
|       if (role.type === 'ca') { | ||||
|         await settled(); | ||||
| @@ -146,29 +148,23 @@ module('Acceptance | ssh secret backend', function (hooks) { | ||||
|       } | ||||
|  | ||||
|       // generate creds | ||||
|       await click('[data-test-secret-generate]'); | ||||
|       await click(GENERAL.saveButton); | ||||
|       await settled(); // eslint-disable-line | ||||
|       role.assertAfterGenerate(assert, sshPath); | ||||
|  | ||||
|       // click the "Back" button | ||||
|       await click('[data-test-secret-generate-back]'); | ||||
|       await click('[data-test-back-button]'); | ||||
|  | ||||
|       assert.ok( | ||||
|         findAll('[data-test-secret-generate-form]').length, | ||||
|         `${role.type}: back takes you back to the form` | ||||
|       ); | ||||
|       assert.dom('[data-test-secret-generate-form]').exists(`${role.type}: back takes you back to the form`); | ||||
|  | ||||
|       await click('[data-test-secret-generate-cancel]'); | ||||
|       await click(GENERAL.cancelButton); | ||||
|  | ||||
|       assert.strictEqual( | ||||
|         currentURL(), | ||||
|         `/vault/secrets/${sshPath}/list`, | ||||
|         `${role.type}: cancel takes you to ssh index` | ||||
|       ); | ||||
|       assert.ok( | ||||
|         findAll(`[data-test-secret-link="${role.name}"]`).length, | ||||
|         `${role.type}: role shows in the list` | ||||
|       ); | ||||
|       assert.dom(`[data-test-secret-link="${role.name}"]`).exists(`${role.type}: role shows in the list`); | ||||
|  | ||||
|       //and delete | ||||
|       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"]'), | ||||
|   warningIsPresent: isPresent('[data-test-warning]'), | ||||
|   commonNameValue: value('[data-test-input="commonName"]'), | ||||
|   submit: clickable('[data-test-secret-generate]'), | ||||
|   back: clickable('[data-test-secret-generate-back]'), | ||||
|   submit: clickable('[data-test-save]'), | ||||
|   back: clickable('[data-test-back-button]'), | ||||
|   generateOTP: async function () { | ||||
|     await this.user('admin').ip('192.168.1.1').submit(); | ||||
|   }, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Chelsea Shaw
					Chelsea Shaw