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 <lane.wetmore@hashicorp.com>
Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
Vault Automation 2025-10-27 12:41:57 -04:00 committed by GitHub
parent 117beded49
commit 8346f0638c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 87 additions and 45 deletions

3
changelog/_10045.txt Normal file
View file

@ -0,0 +1,3 @@
```release-note:bug
ui: remove unnecessary 'credential type' form input when generating AWS secrets
```

View file

@ -110,11 +110,21 @@
</div>
{{else}}
<form {{on "submit" this.create}} data-test-secret-generate-form>
<div class="box is-sideless is-fullwidth is-marginless {{if this.helpText 'no-padding-top'}}">
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="generate" @noun="credential" />
<MessageError @model={{this.model}} />
{{#if @awsRoleType}}
<Hds::Text::Body
@tag="p"
@size="300"
class="has-bottom-padding-s"
data-test-credential-type={{@awsRoleType}}
>Generating credentials of type:
<strong>{{@awsRoleType}}</strong>
</Hds::Text::Body>
{{/if}}
{{#if this.helpText}}
<p class="is-hint">{{this.helpText}}</p>
<p class="is-hint has-bottom-padding-s" data-test-help-text>{{this.helpText}}</p>
{{/if}}
{{#each this.formFields as |key|}}
<FormField data-test-field @attr={{get this.model.allByKey key}} @model={{this.model}} />

View file

@ -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<Args> {
@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);

View file

@ -41,7 +41,7 @@
<div class="toolbar-separator"></div>
{{/if}}
{{#if this.model.canGenerate}}
<ToolbarSecretLink @secret={{this.model.id}} @mode="credentials" data-test-backend-credentials="iam">
<ToolbarSecretLink @secret={{this.model.id}} @mode="credentials" data-test-backend-credentials>
Generate credentials
</ToolbarSecretLink>
{{/if}}

View file

@ -40,16 +40,11 @@
<div class="toolbar-separator"></div>
{{/if}}
{{#if (eq this.model.keyType "otp")}}
<ToolbarSecretLink
@secret={{this.model.id}}
@mode="credentials"
data-test-backend-credentials={{true}}
@replace={{true}}
>
<ToolbarSecretLink @secret={{this.model.id}} @mode="credentials" data-test-backend-credentials @replace={{true}}>
Generate Credential
</ToolbarSecretLink>
{{else}}
<ToolbarSecretLink @secret={{this.model.id}} @mode="sign" data-test-backend-credentials={{true}} @replace={{true}}>
<ToolbarSecretLink @secret={{this.model.id}} @mode="sign" data-test-backend-credentials @replace={{true}}>
Sign Keys
</ToolbarSecretLink>
{{/if}}

View file

@ -23,10 +23,6 @@
padding-right: 0;
}
&.no-padding-top {
padding-top: 0;
}
&.has-slim-padding {
padding: 9px 0;
}

View file

@ -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);

View file

@ -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]`,