Skip to content

Commit 8cf2228

Browse files
Fix auth method config submit following ember data migration (#9755) (#9793)
* fix broken form after ember data migration * convert to typescript, add tests * only transition on success * use test.each * use AuthMethodResource * add tests and refactor fallback for engine-display-data * fix token_type submitting for token auth methods * fix imports * fix conditional for token_type * update comments add check for token_type * fix test and add comment to clarify different setting types * revert and keep unknown type, blowing the scope out too much! Co-authored-by: claire bontempo <[email protected]>
1 parent 109ff8d commit 8cf2228

File tree

17 files changed

+440
-150
lines changed

17 files changed

+440
-150
lines changed

ui/app/components/auth-config-form/config.hbs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
<form {{on "submit" (perform this.saveModel)}}>
77
<div class="box is-sideless is-fullwidth is-marginless">
88
<NamespaceReminder @mode="save" @noun="Auth Method" />
9-
<MessageError @model={{@form}} />
10-
<FormFieldGroups @model={{@form}} @groupName="formFieldGroups" @mode={{this.mode}} />
9+
<MessageError @model={{@form}} @errorMessage={{this.errorMessage}} />
10+
<FormFieldGroups @model={{@form}} @groupName="formFieldGroups" />
1111
</div>
1212

1313
<div class="field is-grouped box is-fullwidth is-bottomless">

ui/app/components/auth-config-form/config.js

Lines changed: 0 additions & 44 deletions
This file was deleted.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: BUSL-1.1
4+
*/
5+
6+
import { service } from '@ember/service';
7+
import Component from '@glimmer/component';
8+
import { task } from 'ember-concurrency';
9+
import { waitFor } from '@ember/test-waiters';
10+
import { tracked } from '@glimmer/tracking';
11+
12+
import type AuthMethodForm from 'vault/forms/auth/method';
13+
import type RouterService from '@ember/routing/router-service';
14+
import type FlashMessageService from 'ember-cli-flash/services/flash-messages';
15+
import type ApiService from 'vault/services/api';
16+
import type { HTMLElementEvent } from 'vault/forms';
17+
import {
18+
AwsConfigureClientRequest,
19+
AwsConfigureIdentityAccessListTidyOperationRequest,
20+
AwsConfigureRoleTagDenyListTidyOperationRequest,
21+
AzureConfigureAuthRequest,
22+
GithubConfigureRequest,
23+
GoogleCloudConfigureAuthRequest,
24+
JwtConfigureRequest,
25+
KubernetesConfigureAuthRequest,
26+
LdapConfigureAuthRequest,
27+
OktaConfigureRequest,
28+
RadiusConfigureRequest,
29+
} from '@hashicorp/vault-client-typescript';
30+
import AuthMethodResource from 'vault/resources/auth/method';
31+
32+
/**
33+
* @module AuthConfigForm/Config
34+
* The `AuthConfigForm/Config` is the form for auth methods that need additional configuration.
35+
* AuthConfigForm::Options handle the backend's mount configuration.
36+
*
37+
* @example
38+
* <AuthConfigForm::Config @form={{this.form}} />
39+
*
40+
* @property form=null {AuthMethodForm} - The corresponding auth method that is being configured.
41+
*
42+
*/
43+
44+
type ConfigPayload =
45+
| AwsConfigureClientRequest
46+
| AwsConfigureIdentityAccessListTidyOperationRequest
47+
| AwsConfigureRoleTagDenyListTidyOperationRequest
48+
| AzureConfigureAuthRequest
49+
| GithubConfigureRequest
50+
| GoogleCloudConfigureAuthRequest
51+
| JwtConfigureRequest
52+
| KubernetesConfigureAuthRequest
53+
| LdapConfigureAuthRequest
54+
| OktaConfigureRequest
55+
| RadiusConfigureRequest
56+
| Record<string, unknown>; // Add other payload types as needed
57+
58+
interface Args {
59+
form: AuthMethodForm;
60+
section: 'configuration' | 'client' | 'identity-accesslist' | 'roletag-denylist';
61+
method: AuthMethodResource;
62+
}
63+
64+
export default class AuthConfigBase extends Component<Args> {
65+
@service declare readonly api: ApiService;
66+
@service declare readonly flashMessages: FlashMessageService;
67+
@service declare readonly router: RouterService;
68+
69+
@tracked errorMessage = '';
70+
71+
configMethod(path: string, payload: ConfigPayload) {
72+
const { section, method } = this.args;
73+
74+
switch (method.methodType) {
75+
case 'aws':
76+
switch (section) {
77+
case 'client':
78+
return this.api.auth.awsConfigureClient(path, payload as AwsConfigureClientRequest);
79+
case 'identity-accesslist':
80+
return this.api.auth.awsConfigureIdentityAccessListTidyOperation(
81+
path,
82+
payload as AwsConfigureIdentityAccessListTidyOperationRequest
83+
);
84+
case 'roletag-denylist':
85+
return this.api.auth.awsConfigureRoleTagDenyListTidyOperation(
86+
path,
87+
payload as AwsConfigureRoleTagDenyListTidyOperationRequest
88+
);
89+
default:
90+
throw new Error(`Unsupported AWS section: ${section}`);
91+
}
92+
case 'azure':
93+
return this.api.auth.azureConfigureAuth(path, payload as AzureConfigureAuthRequest);
94+
case 'github':
95+
return this.api.auth.githubConfigure(path, payload as GithubConfigureRequest);
96+
case 'gcp':
97+
return this.api.auth.googleCloudConfigureAuth(path, payload as GoogleCloudConfigureAuthRequest);
98+
case 'jwt':
99+
case 'oidc':
100+
return this.api.auth.jwtConfigure(path, payload as JwtConfigureRequest);
101+
case 'kubernetes':
102+
return this.api.auth.kubernetesConfigureAuth(path, payload as KubernetesConfigureAuthRequest);
103+
case 'ldap':
104+
return this.api.auth.ldapConfigureAuth(path, payload as LdapConfigureAuthRequest);
105+
case 'okta':
106+
return this.api.auth.oktaConfigure(path, payload as OktaConfigureRequest);
107+
case 'radius':
108+
return this.api.auth.radiusConfigure(path, payload as RadiusConfigureRequest);
109+
default:
110+
throw new Error(`Configuration of the ${method.methodType} method is not supported by the Vault UI.`);
111+
}
112+
}
113+
114+
@task
115+
@waitFor
116+
*saveModel(evt: HTMLElementEvent<HTMLFormElement>) {
117+
evt.preventDefault();
118+
this.errorMessage = '';
119+
try {
120+
const { form, method } = this.args;
121+
const { data } = form.toJSON();
122+
yield this.configMethod(method.path, data as ConfigPayload);
123+
this.router.transitionTo('vault.cluster.access.methods').followRedirects();
124+
this.flashMessages.success('The configuration was saved successfully.');
125+
} catch (err) {
126+
const { message } = yield this.api.parseError(err);
127+
this.errorMessage = message;
128+
}
129+
}
130+
}

ui/app/components/auth-config-form/options.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export default class AuthConfigOptions extends Component<Args> {
6666
const {
6767
data: { description, config, user_lockout_config },
6868
} = form.toJSON();
69+
6970
const payload = {
7071
description,
7172
...config,
@@ -75,13 +76,18 @@ export default class AuthConfigOptions extends Component<Args> {
7576
payload.user_lockout_config = user_lockout_config;
7677
}
7778

79+
// 'token_type' cannot be set for the 'token' auth mount
80+
if (form.normalizedType === 'token' && payload?.token_type) {
81+
delete payload.token_type;
82+
}
83+
7884
await this.api.sys.mountsAuthTuneConfigurationParameters(form.data.path, payload);
85+
this.router.transitionTo('vault.cluster.access.methods').followRedirects();
86+
this.flashMessages.success('The configuration was saved successfully.');
7987
} catch (err) {
8088
const { message } = await this.api.parseError(err);
8189
this.errorMessage = message;
8290
}
83-
this.router.transitionTo('vault.cluster.access.methods').followRedirects();
84-
this.flashMessages.success('The configuration was saved successfully.');
8591
})
8692
);
8793
}

ui/app/forms/auth/method.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,12 @@ export default class AuthMethodForm extends MountForm<AuthMethodFormData> {
3939

4040
get tuneFields() {
4141
const readOnly = ['local', 'seal_wrap'];
42+
// 'token_type' cannot be set for the 'token' auth method
43+
if (this.normalizedType === 'token') {
44+
readOnly.push('config.token_type');
45+
}
4246
return this.formFieldGroups[1]?.['Method Options']?.filter((field) => {
43-
const isTuneable = !readOnly.includes(field.name);
44-
return isTuneable || (field.name === 'token_type' && this.normalizedType === 'token');
47+
return !readOnly.includes(field.name);
4548
});
4649
}
4750

ui/app/helpers/engines-display-data.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: BUSL-1.1
44
*/
55

6-
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
6+
import { ALL_ENGINES, type EngineDisplayData } from 'vault/utils/all-engines-metadata';
77

88
/**
99
* Helper function to retrieve engine metadata for a given `methodType`.
@@ -21,12 +21,11 @@ import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
2121
* @returns {Object|undefined} - The engine metadata, which includes information about its mount type (e.g., secret or auth)
2222
* and whether it requires an enterprise license. Returns undefined if no match is found.
2323
*/
24-
export default function engineDisplayData(methodType: string) {
24+
export default function engineDisplayData(methodType: string): EngineDisplayData {
2525
const engine = ALL_ENGINES?.find((t) => t.type === methodType);
26-
if (!engine && methodType) {
27-
// Fallback to a unknown engine if no match found but type is provided
26+
if (!engine) {
2827
return {
29-
displayName: 'Unknown plugin',
28+
displayName: methodType || 'Unknown plugin',
3029
type: 'unknown',
3130
isOldEngine: true,
3231
glyph: 'lock',

ui/app/routes/vault/cluster/settings/auth/configure.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55

66
import Route from '@ember/routing/route';
77
import { service } from '@ember/service';
8+
import AuthMethodResource from 'vault/resources/auth/method';
89

910
import type ApiService from 'vault/services/api';
1011
import type { ModelFrom } from 'vault/route';
12+
import type { Mount } from 'vault/vault/mount';
1113

1214
export type ClusterSettingsAuthConfigureRouteModel = ModelFrom<ClusterSettingsAuthConfigureRoute>;
1315

@@ -16,12 +18,11 @@ export default class ClusterSettingsAuthConfigureRoute extends Route {
1618

1719
async model(params: { method: string }) {
1820
const path = params.method;
19-
const methodOptions = await this.api.sys.authReadConfiguration(path);
20-
21+
const methodOptions = (await this.api.sys.authReadConfiguration(path)) as Mount;
22+
const method = new AuthMethodResource({ ...methodOptions, path }, this);
2123
return {
2224
methodOptions,
23-
type: methodOptions.type as string,
24-
id: path,
25+
method,
2526
};
2627
}
2728
}

ui/app/routes/vault/cluster/settings/auth/configure/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export default class SettingsAuthConfigureRoute extends Route {
1111
@service router;
1212

1313
beforeModel() {
14-
const model = this.modelFor('vault.cluster.settings.auth.configure');
15-
const section = tabsForAuthSection([model])[0].routeParams.slice().pop();
14+
const { method } = this.modelFor('vault.cluster.settings.auth.configure');
15+
const section = tabsForAuthSection([method])[0].routeParams.slice().pop();
1616
return this.router.transitionTo('vault.cluster.settings.auth.configure.section', section);
1717
}
1818
}

ui/app/routes/vault/cluster/settings/auth/configure/section.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,19 @@ export default class ClusterSettingsAuthConfigureRoute extends Route {
2626
}
2727

2828
modelForOptions() {
29-
const { methodOptions, id, type } = this.configRouteModel;
29+
const { methodOptions, method } = this.configRouteModel;
3030
const config = methodOptions.config as MountConfig;
3131
const listing_visibility = config.listing_visibility === 'unauth' ? true : false;
3232

3333
const form = new AuthMethodForm({
3434
...methodOptions,
35-
path: id,
36-
config: { ...methodOptions.config, listing_visibility },
35+
path: method.id,
36+
config: { ...config, listing_visibility },
3737
user_lockout_config: {},
3838
});
39-
form.type = type;
39+
// `type` is a tracked property on the form class and not a field param
40+
// so we need to set it here even though it is spread in methodOptions above.
41+
form.type = methodOptions.type;
4042

4143
return {
4244
form,
@@ -45,13 +47,13 @@ export default class ClusterSettingsAuthConfigureRoute extends Route {
4547
}
4648

4749
get configFieldGroupsMap() {
48-
const { type } = this.configRouteModel;
50+
const { method } = this.configRouteModel;
4951
return {
5052
kubernetes: {
5153
default: ['kubernetes_host', 'kubernetes_ca_cert', 'disable_local_ca_jwt'],
5254
'Kubernetes Options': ['token_reviewer_jwt', 'pem_keys', 'use_annotations_as_alias_metadata'],
5355
},
54-
}[type];
56+
}[method.methodType];
5557
}
5658

5759
fetchConfig(type: string, section: string, path: string, help = false) {
@@ -95,13 +97,13 @@ export default class ClusterSettingsAuthConfigureRoute extends Route {
9597
}
9698

9799
async modelForConfiguration(section: string) {
98-
const { id: path, type } = this.configRouteModel;
100+
const { path, methodType } = this.configRouteModel.method;
99101

100102
const formOptions = { isNew: false };
101103
let formData;
102104
// make request to fetch configuration data for method
103105
try {
104-
const { data } = await this.fetchConfig(type, section, path);
106+
const { data } = await this.fetchConfig(methodType, section, path);
105107
formData = data as object;
106108
} catch (e) {
107109
const { message, status } = await this.api.parseError(e);
@@ -113,15 +115,15 @@ export default class ClusterSettingsAuthConfigureRoute extends Route {
113115
}
114116
// make request to fetch OpenAPI properties with help query param
115117
const helpResponse = (await this.fetchConfig(
116-
type,
118+
methodType,
117119
section,
118120
path,
119121
true
120122
)) as unknown as OpenApiHelpResponse;
121123
const form = new OpenApiForm(helpResponse, formData, formOptions);
122124
// for jwt and oidc types, the jwks_pairs field is not deprecated but we do not render it in the UI
123125
// remove the field from the group before rendering the form
124-
if (['jwt', 'oidc'].includes(type)) {
126+
if (['jwt', 'oidc'].includes(methodType)) {
125127
const defaultGroup = form.formFieldGroups[0]?.['default'] || [];
126128
const index = defaultGroup.findIndex((field) => field.name === 'jwks_pairs');
127129
if (index !== undefined && index >= 0) {
@@ -131,7 +133,8 @@ export default class ClusterSettingsAuthConfigureRoute extends Route {
131133

132134
return {
133135
form,
134-
section: 'configuration',
136+
section,
137+
method: this.configRouteModel.method,
135138
};
136139
}
137140

0 commit comments

Comments
 (0)