From 8346f0638cb9d47db96a25bc21b938b87a236e03 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 27 Oct 2025 12:41:57 -0400 Subject: [PATCH] UI: update aws generate credential form inputs to rely on credentialType (#10045) (#10351) * update aws generate credential form inputs to rely on credentialType * update tests * show credential type + style updates * Update ui/app/components/generate-credentials.ts * update test, naming and help text * add changelog * rename changelog --------- Co-authored-by: lane-wetmore Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> --- changelog/_10045.txt | 3 + ui/app/components/generate-credentials.hbs | 14 ++- ...credentials.js => generate-credentials.ts} | 89 +++++++++++++------ ui/app/components/role-aws-edit.hbs | 2 +- ui/app/components/role-ssh-edit.hbs | 9 +- ui/app/styles/core/box.scss | 4 - .../secrets/backend/aws/aws-test.js | 9 +- ui/tests/helpers/general-selectors.ts | 2 +- 8 files changed, 87 insertions(+), 45 deletions(-) create mode 100644 changelog/_10045.txt rename ui/app/components/{generate-credentials.js => generate-credentials.ts} (53%) diff --git a/changelog/_10045.txt b/changelog/_10045.txt new file mode 100644 index 0000000000..b9499071ae --- /dev/null +++ b/changelog/_10045.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: remove unnecessary 'credential type' form input when generating AWS secrets +``` diff --git a/ui/app/components/generate-credentials.hbs b/ui/app/components/generate-credentials.hbs index 161e225199..5ca853534a 100644 --- a/ui/app/components/generate-credentials.hbs +++ b/ui/app/components/generate-credentials.hbs @@ -110,11 +110,21 @@ {{else}}
-
+
+ {{#if @awsRoleType}} + Generating credentials of type: + {{@awsRoleType}} + + {{/if}} {{#if this.helpText}} -

{{this.helpText}}

+

{{this.helpText}}

{{/if}} {{#each this.formFields as |key|}} diff --git a/ui/app/components/generate-credentials.js b/ui/app/components/generate-credentials.ts similarity index 53% rename from ui/app/components/generate-credentials.js rename to ui/app/components/generate-credentials.ts index b24ec29134..481ba84ec0 100644 --- a/ui/app/components/generate-credentials.js +++ b/ui/app/components/generate-credentials.ts @@ -8,6 +8,13 @@ import { action } from '@ember/object'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; +import type AdapterError from 'vault/@ember-data/adapter/error'; +import type ApiService from 'vault/services/api'; +import type AwsCredential from 'vault/models/aws-credential'; +import type ControlGroupService from 'vault/vault/services/control-group'; +import type RouterService from '@ember/routing/router-service'; +import type Store from '@ember-data/store'; + const CREDENTIAL_TYPES = { ssh: { model: 'ssh-otp-credential', @@ -21,31 +28,46 @@ const CREDENTIAL_TYPES = { backIsListLink: true, displayFields: ['accessKey', 'secretKey', 'securityToken', 'leaseId', 'renewable', 'leaseDuration'], // aws form fields are dynamic - formFields: (model) => { + formFields: (model: AwsCredential) => { return { iam_user: ['credentialType'], assumed_role: ['credentialType', 'ttl', 'roleArn'], federation_token: ['credentialType', 'ttl'], session_token: ['credentialType', 'ttl'], - }[model.credentialType]; + }[model.credentialType as string]; }, }, }; -export default class GenerateCredentials extends Component { - @service controlGroup; - @service store; - @service router; +interface Args { + awsRoleType: string | undefined; + backendPath: string; + backendType: 'ssh' | 'aws'; + roleName: string; +} + +export default class GenerateCredentials extends Component { + @service declare readonly api: ApiService; + @service declare readonly controlGroup: ControlGroupService; + @service declare readonly store: Store; + @service declare readonly router: RouterService; @tracked model; @tracked loading = false; @tracked hasGenerated = false; + + cannotReadAwsRole = false; emptyData = '{\n}'; - constructor() { - super(...arguments); + constructor(owner: unknown, args: Args) { + super(owner, args); const modelType = this.modelForType(); this.model = this.generateNewModel(modelType); + + // if user lacks role read permissions, awsRoleType will be undefined + // the role type dictates which form inputs are available, so this case + // will need special handling when generating credentials + this.cannotReadAwsRole = this.args.backendType == 'aws' && !this.args.awsRoleType; } willDestroy() { @@ -57,30 +79,45 @@ export default class GenerateCredentials extends Component { super.willDestroy(); } - modelForType() { + modelForType(): string | undefined { const type = this.options; if (type) { return type.model; } // if we don't have a model for that type then redirect them back to the backend list this.router.transitionTo('vault.cluster.secrets.backend.list-root', this.args.backendPath); + return undefined; } - 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.'; + get helpText(): string { + let message = ''; + if (this.cannotReadAwsRole) { + message = + 'You do not have permissions to read this role so Vault cannot infer the credential type. Select the credential type you want to generate. '; } - return ''; + if (this.options?.model === 'aws-credential' && this.model.credentialType === 'iam_user') + message += 'For Vault roles of credential type iam_user, there are no inputs, just submit the form.'; + return message; } get options() { return CREDENTIAL_TYPES[this.args.backendType]; } - get formFields() { + get formFields(): string[] | undefined { const typeOpts = this.options; + if (typeof typeOpts.formFields === 'function') { - return typeOpts.formFields(this.model); + // without read access to the role, awsRoleType will be undefined and will default to iam_user + // so we will need to show credentialType input for user selection + // otherwise, we can omit that input + const fields = typeOpts.formFields(this.model) ?? []; + + if (!this.cannotReadAwsRole) { + return fields.filter((f) => f !== 'credentialType'); + } + + return fields; } return typeOpts.formFields; } @@ -89,22 +126,21 @@ export default class GenerateCredentials extends Component { return this.options.displayFields; } - generateNewModel(modelType) { + generateNewModel(modelType?: string) { if (!modelType) { return; } const { roleName, backendPath, awsRoleType } = this.args; + // conditionally add credentialType so that if not present, it will default to iam_user const attrs = { role: { backend: backendPath, name: roleName, }, id: `${backendPath}-${roleName}`, + ...(awsRoleType ? { credentialType: awsRoleType } : {}), }; - if (awsRoleType) { - // this is only set from route if backendType = aws - attrs.credentialType = awsRoleType; - } + return this.store.createRecord(modelType, attrs); } @@ -120,7 +156,7 @@ export default class GenerateCredentials extends Component { } @action - create(evt) { + create(evt: Event) { evt.preventDefault(); this.loading = true; this.model @@ -128,11 +164,12 @@ export default class GenerateCredentials extends Component { .then(() => { this.hasGenerated = true; }) - .catch((error) => { + .catch(async (error: AdapterError) => { + const { response } = await this.api.parseError(error); // Handle control group AdapterError - if (error.message === 'Control Group encountered') { - this.controlGroup.saveTokenFromError(error); - const err = this.controlGroup.logFromError(error); + if (response?.isControlGroupError) { + this.controlGroup.saveTokenFromError(response); + const err = this.controlGroup.logFromError(response); error.errors = [err.content]; } throw error; @@ -143,7 +180,7 @@ export default class GenerateCredentials extends Component { } @action - editorUpdated(attr, val) { + editorUpdated(attr: string, val: string) { // wont set invalid JSON to the model try { this.model[attr] = JSON.parse(val); diff --git a/ui/app/components/role-aws-edit.hbs b/ui/app/components/role-aws-edit.hbs index 90efc32276..4b5302c9b7 100644 --- a/ui/app/components/role-aws-edit.hbs +++ b/ui/app/components/role-aws-edit.hbs @@ -41,7 +41,7 @@
{{/if}} {{#if this.model.canGenerate}} - + Generate credentials {{/if}} diff --git a/ui/app/components/role-ssh-edit.hbs b/ui/app/components/role-ssh-edit.hbs index 5f70c165e8..17f87b270c 100644 --- a/ui/app/components/role-ssh-edit.hbs +++ b/ui/app/components/role-ssh-edit.hbs @@ -40,16 +40,11 @@
{{/if}} {{#if (eq this.model.keyType "otp")}} - + Generate Credential {{else}} - + Sign Keys {{/if}} diff --git a/ui/app/styles/core/box.scss b/ui/app/styles/core/box.scss index cd40b32ca2..9946ff85a7 100644 --- a/ui/app/styles/core/box.scss +++ b/ui/app/styles/core/box.scss @@ -23,10 +23,6 @@ padding-right: 0; } - &.no-padding-top { - padding-top: 0; - } - &.has-slim-padding { padding: 9px 0; } diff --git a/ui/tests/acceptance/secrets/backend/aws/aws-test.js b/ui/tests/acceptance/secrets/backend/aws/aws-test.js index aeb6473361..776856d81e 100644 --- a/ui/tests/acceptance/secrets/backend/aws/aws-test.js +++ b/ui/tests/acceptance/secrets/backend/aws/aws-test.js @@ -22,7 +22,9 @@ const ROLE_TYPES = [ credentialType: 'iam_user', async fillOutForm(assert) { // nothing to fill out - assert.dom('[data-test-field]').exists({ count: 1 }); + assert + .dom(GENERAL.helpText) + .hasText('For Vault roles of credential type iam_user, there are no inputs, just submit the form.'); }, expectedPayload: {}, }, @@ -140,8 +142,8 @@ module('Acceptance | aws secret backend', function (hooks) { assert.strictEqual(currentURL(), `/vault/secrets-engines/${path}/show/${roleName}`); await click(SES.generateLink); assert - .dom(GENERAL.inputByAttr('credentialType')) - .hasValue(scenario.credentialType, 'credentialType matches backing role'); + .dom(`[data-test-credential-type=${scenario.credentialType}]`) + .exists(scenario.credentialType, 'credentialType matches backing role'); // based on credentialType, fill out form await scenario.fillOutForm(assert); @@ -182,7 +184,6 @@ module('Acceptance | aws secret backend', function (hooks) { 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.submitButton); diff --git a/ui/tests/helpers/general-selectors.ts b/ui/tests/helpers/general-selectors.ts index b17cbb1df6..6b456b35a0 100644 --- a/ui/tests/helpers/general-selectors.ts +++ b/ui/tests/helpers/general-selectors.ts @@ -60,7 +60,7 @@ export const GENERAL = { fieldLabel: () => `[data-test-form-field-label]`, fieldLabelbyAttr: (attr: string) => `[data-test-form-field-label="${attr}"]`, groupControlByIndex: (index: number) => `.hds-form-group__control-field:nth-of-type(${index})`, - helpText: () => `[data-test-help-text]`, + helpText: '[data-test-help-text]', helpTextByAttr: (attr: string) => `[data-test-help-text="${attr}"]`, helpTextByGroupControlIndex: (index: number) => `.hds-form-group__control-field:nth-of-type(${index}) [data-test-help-text]`,