mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
* 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:
parent
117beded49
commit
8346f0638c
8 changed files with 87 additions and 45 deletions
3
changelog/_10045.txt
Normal file
3
changelog/_10045.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
ui: remove unnecessary 'credential type' form input when generating AWS secrets
|
||||
```
|
||||
|
|
@ -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}} />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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}}
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
|
|||
|
|
@ -23,10 +23,6 @@
|
|||
padding-right: 0;
|
||||
}
|
||||
|
||||
&.no-padding-top {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
&.has-slim-padding {
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue