mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-04 20:06:27 +02:00
* updates auth method options route to use form and api client * updates auth method config and section routes to use api client and open api form * updates display attrs for auth method configs * fixes plugin identity util fields tests * fixes js lint error * updates enable-tune-form tests * hides specific form field for jwt/oidc auth config types * Revert "updates display attrs for auth method configs" This reverts commit 5d382f79276f56b3fdbe64fcbc9c8365c5f4b421. * Revert "fixes plugin identity util fields tests" This reverts commit 6d4acbe3228c796745f2dea6279c1540bb053c62. * fixes config section test * bumps api client version * updates auth config form options component to use proper endpoint * fixes enable tune form tests * fixes auth config form options tests * fixes type errors in snapshot-manage component * updates recover_source_path arg to undefined so it is not included in the query params * fixes remaining test failures related to user_lockout_config --------- Co-authored-by: Vault Automation <github-team-secure-vault-core@hashicorp.com>
This commit is contained in:
parent
d283ef5897
commit
d8ecd066b8
@ -6,15 +6,10 @@
|
||||
<form {{on "submit" (perform this.saveModel)}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="save" @noun="Auth Method" />
|
||||
<MessageError @model={{@model}} />
|
||||
{{#if @model.attrs}}
|
||||
{{#each @model.attrs as |attr|}}
|
||||
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />
|
||||
{{/each}}
|
||||
{{else if @model.fieldGroups}}
|
||||
<FormFieldGroups @model={{@model}} @mode={{this.mode}} />
|
||||
{{/if}}
|
||||
<MessageError @model={{@form}} />
|
||||
<FormFieldGroups @model={{@form}} @groupName="formFieldGroups" @mode={{this.mode}} />
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<Hds::Button
|
||||
@text="Save"
|
||||
|
||||
@ -3,42 +3,39 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<form {{on "submit" (perform this.saveModel)}}>
|
||||
<form {{on "submit" (perform this.onSubmit)}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @model={{@model}} @errorMessage={{this.errorMessage}} />
|
||||
<MessageError @model={{@form}} @errorMessage={{this.errorMessage}} />
|
||||
<NamespaceReminder @mode="save" @noun="Auth Method" />
|
||||
|
||||
{{#each @model.tuneAttrs as |attr|}}
|
||||
{{#if (not (includes attr.name @model.userLockoutConfig.modelAttrs))}}
|
||||
<FormField data-test-field @attr={{attr}} @model={{@model}} />
|
||||
{{#if (and (eq attr.name "config.listingVisibility") @model.directLoginLink)}}
|
||||
<div class="has-top-margin-negative-s has-bottom-margin-l is-flex-center">
|
||||
<Hds::Text::Body @tag="p" @color="faint">UI login link:</Hds::Text::Body>
|
||||
<Hds::Copy::Snippet @textToCopy={{@model.directLoginLink}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#each @form.tuneFields as |field|}}
|
||||
<FormField data-test-field @attr={{field}} @model={{@form}} />
|
||||
{{#if (and (eq field.name "config.listing_visibility") this.directLoginLink)}}
|
||||
<div class="has-top-margin-negative-s has-bottom-margin-l is-flex-center">
|
||||
<Hds::Text::Body @tag="p" @color="faint">UI login link:</Hds::Text::Body>
|
||||
<Hds::Copy::Snippet @textToCopy={{this.directLoginLink}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
{{#if @model.supportsUserLockoutConfig}}
|
||||
{{#if this.supportsUserLockoutConfig}}
|
||||
<hr class="has-top-margin-xl has-bottom-margin-l has-background-gray-200" />
|
||||
<Hds::Text::Display @tag="h2" @size="400" @weight="bold" data-test-user-lockout-section>User lockout configuration</Hds::Text::Display>
|
||||
<Hds::Text::Body @tag="p" @size="100" @color="faint" class="has-bottom-margin-m">
|
||||
Specifies the user lockout settings for this auth mount.
|
||||
</Hds::Text::Body>
|
||||
{{#each @model.tuneAttrs as |attr|}}
|
||||
{{#if (includes attr.name @model.userLockoutConfig.modelAttrs)}}
|
||||
<FormField @attr={{attr}} @model={{@model}} />
|
||||
{{/if}}
|
||||
{{#each @form.userLockoutConfigFields as |field|}}
|
||||
<FormField @attr={{field}} @model={{@form}} />
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<Hds::Button
|
||||
@text="Update options"
|
||||
@icon={{if this.saveModel.isRunning "loading"}}
|
||||
@icon={{if this.onSubmit.isRunning "loading"}}
|
||||
type="submit"
|
||||
disabled={{this.saveModel.isRunning}}
|
||||
disabled={{this.onSubmit.isRunning}}
|
||||
data-test-submit
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import AdapterError from '@ember-data/adapter/error';
|
||||
import AuthConfigComponent from './config';
|
||||
import { service } from '@ember/service';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
/**
|
||||
* @module AuthConfigForm/Options
|
||||
* The `AuthConfigForm/Options` is options portion of the auth config form.
|
||||
*
|
||||
* @example
|
||||
* <AuthConfigForm::Options @model={{this.model}} />
|
||||
*
|
||||
* @property model=null {DS.Model} - The corresponding auth model that is being configured.
|
||||
*
|
||||
*/
|
||||
|
||||
export default class AuthConfigOptions extends AuthConfigComponent {
|
||||
@service flashMessages;
|
||||
@service router;
|
||||
|
||||
@tracked errorMessage;
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*saveModel(evt) {
|
||||
evt.preventDefault();
|
||||
this.errorMessage = null;
|
||||
const data = this.args.model.config.serialize();
|
||||
data.description = this.args.model.description;
|
||||
|
||||
if (this.args.model.supportsUserLockoutConfig) {
|
||||
data.user_lockout_config = {};
|
||||
this.args.model.userLockoutConfig.apiParams.forEach((attr) => {
|
||||
if (Object.keys(data).includes(attr)) {
|
||||
data.user_lockout_config[attr] = data[attr];
|
||||
delete data[attr];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// token_type should not be tuneable for the token auth method.
|
||||
if (this.args.model.methodType === 'token') {
|
||||
delete data.token_type;
|
||||
}
|
||||
|
||||
try {
|
||||
yield this.args.model.tune(data);
|
||||
} catch (err) {
|
||||
if (err instanceof AdapterError) {
|
||||
// because we're not calling model.save the model never updates with
|
||||
// the error, so we set it manually in the component instead.
|
||||
this.errorMessage = errorMessage(err);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
this.router.transitionTo('vault.cluster.access.methods').followRedirects();
|
||||
this.flashMessages.success('The configuration was saved successfully.');
|
||||
}
|
||||
}
|
||||
87
ui/app/components/auth-config-form/options.ts
Normal file
87
ui/app/components/auth-config-form/options.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { supportedTypes } from 'vault/utils/auth-form-helpers';
|
||||
|
||||
import type AuthMethodForm from 'vault/forms/auth/method';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type { HTMLElementEvent } from 'vault/forms';
|
||||
import type NamespaceService from 'vault/services/namespace';
|
||||
import type VersionService from 'vault/services/version';
|
||||
import type { MountsAuthTuneConfigurationParametersRequest } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
/**
|
||||
* @module AuthConfigForm/Options
|
||||
* The `AuthConfigForm/Options` is options portion of the auth config form.
|
||||
*
|
||||
* @example
|
||||
* <AuthConfigForm::Options @form={{this.form}} />
|
||||
*
|
||||
* @property form=null {AuthMethodForm} - The corresponding auth method that is being configured.
|
||||
*
|
||||
*/
|
||||
|
||||
type Args = {
|
||||
form: AuthMethodForm;
|
||||
};
|
||||
|
||||
export default class AuthConfigOptions extends Component<Args> {
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly router: RouterService;
|
||||
@service declare readonly namespace: NamespaceService;
|
||||
@service declare readonly version: VersionService;
|
||||
|
||||
@tracked errorMessage: string | null = null;
|
||||
|
||||
get directLoginLink() {
|
||||
const ns = this.namespace.path;
|
||||
const nsQueryParam = ns ? `namespace=${encodeURIComponent(ns)}&` : '';
|
||||
const { normalizedType, data } = this.args.form;
|
||||
const isSupported = supportedTypes(this.version.isEnterprise).includes(normalizedType);
|
||||
return isSupported
|
||||
? `${window.origin}/ui/vault/auth?${nsQueryParam}with=${encodeURIComponent(data.path)}`
|
||||
: '';
|
||||
}
|
||||
|
||||
get supportsUserLockoutConfig() {
|
||||
return ['approle', 'ldap', 'userpass'].includes(this.args.form.normalizedType);
|
||||
}
|
||||
|
||||
onSubmit = task(
|
||||
waitFor(async (evt: HTMLElementEvent<HTMLFormElement>) => {
|
||||
evt.preventDefault();
|
||||
this.errorMessage = null;
|
||||
try {
|
||||
const { form } = this.args;
|
||||
const {
|
||||
data: { description, config, user_lockout_config },
|
||||
} = form.toJSON();
|
||||
const payload = {
|
||||
description,
|
||||
...config,
|
||||
} as MountsAuthTuneConfigurationParametersRequest;
|
||||
|
||||
if (Object.keys(user_lockout_config).length) {
|
||||
payload.user_lockout_config = user_lockout_config;
|
||||
}
|
||||
|
||||
await this.api.sys.mountsAuthTuneConfigurationParameters(form.data.path, payload);
|
||||
} catch (err) {
|
||||
const { message } = await this.api.parseError(err);
|
||||
this.errorMessage = message;
|
||||
}
|
||||
this.router.transitionTo('vault.cluster.access.methods').followRedirects();
|
||||
this.flashMessages.success('The configuration was saved successfully.');
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -261,12 +261,19 @@ export default class SnapshotManage extends Component<Args> {
|
||||
const headers = this.api.buildHeaders({ namespace });
|
||||
switch (mountType) {
|
||||
case SupportedSecretBackendsEnum.KV: {
|
||||
await this.api.secrets.kvV1Write(this.resourcePath, this.mountPath, {}, snapshot_id, headers);
|
||||
await this.api.secrets.kvV1Write(
|
||||
this.resourcePath,
|
||||
this.mountPath,
|
||||
{},
|
||||
snapshot_id,
|
||||
undefined,
|
||||
headers
|
||||
);
|
||||
break;
|
||||
}
|
||||
case SupportedSecretBackendsEnum.CUBBYHOLE: {
|
||||
this.api.buildHeaders({ namespace: namespace || this.namespace.path });
|
||||
await this.api.secrets.cubbyholeWrite(this.resourcePath, {}, snapshot_id, headers);
|
||||
await this.api.secrets.cubbyholeWrite(this.resourcePath, {}, snapshot_id, undefined, headers);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
||||
@ -10,6 +10,41 @@ import FormFieldGroup from 'vault/utils/forms/field-group';
|
||||
import type { AuthMethodFormData } from 'vault/auth/methods';
|
||||
|
||||
export default class AuthMethodForm extends MountForm<AuthMethodFormData> {
|
||||
fieldProps = ['tuneFields', 'userLockoutConfigFields'];
|
||||
|
||||
userLockoutConfigFields = [
|
||||
new FormField('user_lockout_config.lockout_threshold', 'string', {
|
||||
label: 'Lockout threshold',
|
||||
subText: 'Specifies the number of failed login attempts after which the user is locked out, e.g. 15.',
|
||||
}),
|
||||
new FormField('user_lockout_config.lockout_duration', undefined, {
|
||||
label: 'Lockout duration',
|
||||
helperTextEnabled: 'The duration for which a user will be locked out, e.g. "5s" or "30m".',
|
||||
editType: 'ttl',
|
||||
helperTextDisabled: 'No lockout duration configured.',
|
||||
}),
|
||||
|
||||
new FormField('user_lockout_config.lockout_counter_reset', undefined, {
|
||||
label: 'Lockout counter reset',
|
||||
helperTextEnabled:
|
||||
'The duration after which the lockout counter is reset with no failed login attempts, e.g. "5s" or "30m".',
|
||||
editType: 'ttl',
|
||||
helperTextDisabled: 'No reset duration configured.',
|
||||
}),
|
||||
new FormField('user_lockout_config.lockout_disable', 'boolean', {
|
||||
label: 'Disable lockout for this mount',
|
||||
subText: 'If checked, disables the user lockout feature for this mount.',
|
||||
}),
|
||||
];
|
||||
|
||||
get tuneFields() {
|
||||
const readOnly = ['local', 'seal_wrap'];
|
||||
return this.formFieldGroups[1]?.['Method Options']?.filter((field) => {
|
||||
const isTuneable = !readOnly.includes(field.name);
|
||||
return isTuneable || (field.name === 'token_type' && this.normalizedType === 'token');
|
||||
});
|
||||
}
|
||||
|
||||
formFieldGroups = [
|
||||
new FormFieldGroup('default', [this.fields.path]),
|
||||
new FormFieldGroup('Method Options', [
|
||||
|
||||
@ -18,6 +18,11 @@ export default class Form<T extends object> {
|
||||
declare validations: Validations;
|
||||
declare isNew: boolean;
|
||||
|
||||
// used by proxy to determine if the property being accessed is a form field
|
||||
// override these in subclasses to define additional/different fields defined on the class
|
||||
fieldProps = ['formFields'];
|
||||
fieldGroupProps = ['formFieldGroups'];
|
||||
|
||||
constructor(data: Partial<T> = {}, options: FormOptions = {}, validations?: Validations) {
|
||||
this.data = { ...data } as T;
|
||||
this.isNew = options.isNew || false;
|
||||
@ -31,18 +36,25 @@ export default class Form<T extends object> {
|
||||
const proxyTarget = (target: this, prop: string) => {
|
||||
try {
|
||||
// check if the property that is being accessed is a form field
|
||||
const { formFields, formFieldGroups } = target as {
|
||||
formFields?: FormField[];
|
||||
formFieldGroups?: FormFieldGroup[];
|
||||
};
|
||||
const fields = Array.isArray(formFields) ? formFields : [];
|
||||
const fields = this.fieldProps.reduce((fields: FormField[], prop) => {
|
||||
const formFields = target[prop as keyof this];
|
||||
if (Array.isArray(formFields)) {
|
||||
fields.push(...formFields);
|
||||
}
|
||||
return fields;
|
||||
}, []);
|
||||
// in the case of formFieldGroups we need extract the fields out into a flat array
|
||||
const groupFields = Array.isArray(formFieldGroups)
|
||||
? formFieldGroups.reduce((arr: FormField[], group) => {
|
||||
const groupFields = this.fieldGroupProps.reduce((groupFields: FormField[], prop) => {
|
||||
const formFieldGroups = target[prop as keyof this];
|
||||
if (Array.isArray(formFieldGroups)) {
|
||||
const fields = formFieldGroups.reduce((arr: FormField[], group: FormFieldGroup) => {
|
||||
const values = Object.values(group)[0] || [];
|
||||
return [...arr, ...values];
|
||||
}, [])
|
||||
: [];
|
||||
}, []);
|
||||
groupFields.push(...fields);
|
||||
}
|
||||
return groupFields;
|
||||
}, []);
|
||||
// combine the formFields and formGroupFields into a single array
|
||||
const allFields = [...fields, ...groupFields];
|
||||
const formDataKeys = allFields.map((field) => field.name) || [];
|
||||
|
||||
57
ui/app/forms/open-api.ts
Normal file
57
ui/app/forms/open-api.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Form from 'vault/forms/form';
|
||||
import FormField from 'vault/utils/forms/field';
|
||||
import FormFieldGroup from 'vault/utils/forms/field-group';
|
||||
import { propsForSchema } from 'vault/utils/openapi-helpers';
|
||||
|
||||
import type { OpenApiHelpResponse } from 'vault/utils/openapi-helpers';
|
||||
|
||||
export default class OpenApiForm<T extends object> extends Form<T> {
|
||||
declare formFieldGroups: FormFieldGroup[];
|
||||
|
||||
constructor(helpResponse: OpenApiHelpResponse, ...formArgs: ConstructorParameters<typeof Form>) {
|
||||
super(...formArgs);
|
||||
// create formFieldGroups from the OpenAPI properties
|
||||
const props = propsForSchema(helpResponse);
|
||||
const groups: { [groupName: string]: FormField[] } = {};
|
||||
// iterate over the properties and organize them into groups
|
||||
for (const [name, prop] of Object.entries(props)) {
|
||||
// disabling lint rule since we need to ignore certain options returned from expandOpenApiProps util
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { fieldGroup, fieldValue, type, defaultValue, ...options } = prop;
|
||||
// groupName from groupsMap takes precedence over fieldGroup from the property
|
||||
const group = fieldGroup || 'default';
|
||||
// organize the form fields so we can create formFieldGroups later
|
||||
if (!(group in groups)) {
|
||||
groups[group] = [];
|
||||
}
|
||||
// create a new FormField for the property and associate it with the appropriate fieldGroup
|
||||
// props marked as `identifier` are primary fields that should be rendered first in the form
|
||||
const arrMethod = options.identifier ? 'unshift' : 'push';
|
||||
groups[group]?.[arrMethod](new FormField(name, type, options));
|
||||
// set the default value on the data object
|
||||
if (defaultValue && this.data[name as keyof typeof this.data] === undefined) {
|
||||
this.data = { ...this.data, [name]: defaultValue };
|
||||
}
|
||||
}
|
||||
|
||||
// ensure default group is the first item in the formFieldGroups
|
||||
|
||||
// create formFieldGroups from the expanded groups
|
||||
this.formFieldGroups = Object.entries(groups).reduce<FormFieldGroup[]>(
|
||||
(formFieldGroups, [groupName, fields]) => {
|
||||
const group = new FormFieldGroup(groupName, fields);
|
||||
// ensure the default group is the first group to render
|
||||
if (groupName === 'default') {
|
||||
return [group, ...formFieldGroups];
|
||||
}
|
||||
return [...formFieldGroups, group];
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default Route.extend({
|
||||
store: service(),
|
||||
|
||||
model() {
|
||||
const { method } = this.paramsFor(this.routeName);
|
||||
return this.store.findAll('auth-method').then(() => {
|
||||
return this.store.peekRecord('auth-method', method);
|
||||
});
|
||||
},
|
||||
});
|
||||
27
ui/app/routes/vault/cluster/settings/auth/configure.ts
Normal file
27
ui/app/routes/vault/cluster/settings/auth/configure.ts
Normal file
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type { ModelFrom } from 'vault/route';
|
||||
|
||||
export type ClusterSettingsAuthConfigureRouteModel = ModelFrom<ClusterSettingsAuthConfigureRoute>;
|
||||
|
||||
export default class ClusterSettingsAuthConfigureRoute extends Route {
|
||||
@service declare readonly api: ApiService;
|
||||
|
||||
async model(params: { method: string }) {
|
||||
const path = params.method;
|
||||
const methodOptions = await this.api.sys.authReadConfiguration(path);
|
||||
|
||||
return {
|
||||
methodOptions,
|
||||
type: methodOptions.type as string,
|
||||
id: path,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,110 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import AdapterError from '@ember-data/adapter/error';
|
||||
import { service } from '@ember/service';
|
||||
import { set } from '@ember/object';
|
||||
import Route from '@ember/routing/route';
|
||||
import RSVP from 'rsvp';
|
||||
import UnloadModelRoute from 'vault/mixins/unload-model-route';
|
||||
import { getHelpUrlForModel } from 'vault/utils/openapi-helpers';
|
||||
|
||||
export default Route.extend(UnloadModelRoute, {
|
||||
modelPath: 'model.model',
|
||||
pathHelp: service('path-help'),
|
||||
store: service(),
|
||||
|
||||
modelType(backendType, section) {
|
||||
const MODELS = {
|
||||
'aws-client': 'auth-config/aws/client',
|
||||
'aws-identity-accesslist': 'auth-config/aws/identity-accesslist',
|
||||
'aws-roletag-denylist': 'auth-config/aws/roletag-denylist',
|
||||
'azure-configuration': 'auth-config/azure',
|
||||
'github-configuration': 'auth-config/github',
|
||||
'gcp-configuration': 'auth-config/gcp',
|
||||
'jwt-configuration': 'auth-config/jwt',
|
||||
'oidc-configuration': 'auth-config/oidc',
|
||||
'kubernetes-configuration': 'auth-config/kubernetes',
|
||||
'ldap-configuration': 'auth-config/ldap',
|
||||
'okta-configuration': 'auth-config/okta',
|
||||
'radius-configuration': 'auth-config/radius',
|
||||
};
|
||||
return MODELS[`${backendType}-${section}`];
|
||||
},
|
||||
|
||||
beforeModel() {
|
||||
const { section_name } = this.paramsFor(this.routeName);
|
||||
if (section_name === 'options') {
|
||||
return;
|
||||
}
|
||||
const { method } = this.paramsFor('vault.cluster.settings.auth.configure');
|
||||
const backend = this.modelFor('vault.cluster.settings.auth.configure');
|
||||
const modelType = this.modelType(backend.type, section_name);
|
||||
// If this method returns a string it means we expect to hydrate it with OpenAPI
|
||||
if (getHelpUrlForModel(modelType)) {
|
||||
return this.pathHelp.hydrateModel(modelType, method);
|
||||
}
|
||||
// if no helpUrl is defined, this is a fully generated model
|
||||
return this.pathHelp.getNewModel(modelType, method, backend.apiPath);
|
||||
},
|
||||
|
||||
model(params) {
|
||||
const backend = this.modelFor('vault.cluster.settings.auth.configure');
|
||||
const { section_name: section } = params;
|
||||
if (section === 'options') {
|
||||
return RSVP.hash({
|
||||
model: backend,
|
||||
section,
|
||||
});
|
||||
}
|
||||
const modelType = this.modelType(backend.type, section);
|
||||
if (!modelType) {
|
||||
const error = new AdapterError();
|
||||
set(error, 'httpStatus', 404);
|
||||
throw error;
|
||||
}
|
||||
const model = this.store.peekRecord(modelType, backend.id);
|
||||
if (model) {
|
||||
return RSVP.hash({
|
||||
model,
|
||||
section,
|
||||
});
|
||||
}
|
||||
return this.store
|
||||
.findRecord(modelType, backend.id)
|
||||
.then((config) => {
|
||||
config.set('backend', backend);
|
||||
return RSVP.hash({
|
||||
model: config,
|
||||
section,
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
let config;
|
||||
// if you haven't saved a config, the API 404s, so create one here to edit and return it
|
||||
if (e.httpStatus === 404) {
|
||||
config = this.store.createRecord(modelType, {
|
||||
mutableId: backend.id,
|
||||
});
|
||||
config.set('backend', backend);
|
||||
|
||||
return RSVP.hash({
|
||||
model: config,
|
||||
section,
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
willTransition() {
|
||||
if (this.currentModel.model.constructor.modelName !== 'auth-method') {
|
||||
this.unloadModel();
|
||||
return true;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
142
ui/app/routes/vault/cluster/settings/auth/configure/section.ts
Normal file
142
ui/app/routes/vault/cluster/settings/auth/configure/section.ts
Normal file
@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { service } from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
import AuthMethodForm from 'vault/forms/auth/method';
|
||||
import OpenApiForm from 'vault/forms/open-api';
|
||||
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type PathHelpService from 'vault/services/path-help';
|
||||
import type Store from '@ember-data/store';
|
||||
import type { ClusterSettingsAuthConfigureRouteModel } from '../configure';
|
||||
import type { MountConfig } from 'vault/mount';
|
||||
import type { HTTPRequestInit, RequestOpts } from '@hashicorp/vault-client-typescript';
|
||||
import type { OpenApiHelpResponse } from 'vault/utils/openapi-helpers';
|
||||
|
||||
export default class ClusterSettingsAuthConfigureRoute extends Route {
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly pathHelp: PathHelpService;
|
||||
@service declare readonly store: Store;
|
||||
|
||||
get configRouteModel() {
|
||||
return this.modelFor('vault.cluster.settings.auth.configure') as ClusterSettingsAuthConfigureRouteModel;
|
||||
}
|
||||
|
||||
modelForOptions() {
|
||||
const { methodOptions, id, type } = this.configRouteModel;
|
||||
const config = methodOptions.config as MountConfig;
|
||||
const listing_visibility = config.listing_visibility === 'unauth' ? true : false;
|
||||
|
||||
const form = new AuthMethodForm({
|
||||
...methodOptions,
|
||||
path: id,
|
||||
config: { ...methodOptions.config, listing_visibility },
|
||||
user_lockout_config: {},
|
||||
});
|
||||
form.type = type;
|
||||
|
||||
return {
|
||||
form,
|
||||
section: 'options',
|
||||
};
|
||||
}
|
||||
|
||||
get configFieldGroupsMap() {
|
||||
const { type } = this.configRouteModel;
|
||||
return {
|
||||
kubernetes: {
|
||||
default: ['kubernetes_host', 'kubernetes_ca_cert', 'disable_local_ca_jwt'],
|
||||
'Kubernetes Options': ['token_reviewer_jwt', 'pem_keys', 'use_annotations_as_alias_metadata'],
|
||||
},
|
||||
}[type];
|
||||
}
|
||||
|
||||
fetchConfig(type: string, section: string, path: string, help = false) {
|
||||
const initOverride = help
|
||||
? (context: { init: HTTPRequestInit; context: RequestOpts }) =>
|
||||
this.api.addQueryParams(context, { help: 1 })
|
||||
: undefined;
|
||||
|
||||
switch (type) {
|
||||
case 'aws': {
|
||||
switch (section) {
|
||||
case 'client':
|
||||
return this.api.auth.awsReadClientConfiguration(path, initOverride);
|
||||
case 'identity-accesslist':
|
||||
return this.api.auth.awsReadIdentityAccessListTidySettings(path, initOverride);
|
||||
case 'roletag-denylist':
|
||||
return this.api.auth.awsReadRoleTagDenyListTidySettings(path, initOverride);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'azure':
|
||||
return this.api.auth.azureReadAuthConfiguration(path, initOverride);
|
||||
case 'github':
|
||||
return this.api.auth.githubReadConfiguration(path, initOverride);
|
||||
case 'gcp':
|
||||
return this.api.auth.googleCloudReadAuthConfiguration(path, initOverride);
|
||||
case 'jwt':
|
||||
case 'oidc':
|
||||
return this.api.auth.jwtReadConfiguration(path, initOverride);
|
||||
case 'kubernetes':
|
||||
return this.api.auth.kubernetesReadAuthConfiguration(path, initOverride);
|
||||
case 'ldap':
|
||||
return this.api.auth.ldapReadAuthConfiguration(path, initOverride);
|
||||
case 'okta':
|
||||
return this.api.auth.oktaReadConfiguration(path, initOverride);
|
||||
case 'radius':
|
||||
return this.api.auth.radiusReadConfiguration(path, initOverride);
|
||||
}
|
||||
|
||||
throw { httpStatus: 404 };
|
||||
}
|
||||
|
||||
async modelForConfiguration(section: string) {
|
||||
const { id: path, type } = this.configRouteModel;
|
||||
|
||||
const formOptions = { isNew: false };
|
||||
let formData;
|
||||
// make request to fetch configuration data for method
|
||||
try {
|
||||
const { data } = await this.fetchConfig(type, section, path);
|
||||
formData = data as object;
|
||||
} catch (e) {
|
||||
const { message, status } = await this.api.parseError(e);
|
||||
if (status === 404) {
|
||||
formOptions.isNew = true;
|
||||
} else {
|
||||
throw { message, httpsStatus: status };
|
||||
}
|
||||
}
|
||||
// make request to fetch OpenAPI properties with help query param
|
||||
const helpResponse = (await this.fetchConfig(
|
||||
type,
|
||||
section,
|
||||
path,
|
||||
true
|
||||
)) as unknown as OpenApiHelpResponse;
|
||||
const form = new OpenApiForm(helpResponse, formData, formOptions);
|
||||
// for jwt and oidc types, the jwks_pairs field is not deprecated but we do not render it in the UI
|
||||
// remove the field from the group before rendering the form
|
||||
if (['jwt', 'oidc'].includes(type)) {
|
||||
const defaultGroup = form.formFieldGroups[0]?.['default'] || [];
|
||||
const index = defaultGroup.findIndex((field) => field.name === 'jwks_pairs');
|
||||
if (index !== undefined && index >= 0) {
|
||||
defaultGroup.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
section: 'configuration',
|
||||
};
|
||||
}
|
||||
|
||||
model(params: { section_name: 'options' | 'configuration' }) {
|
||||
const { section_name: section } = params;
|
||||
return section === 'options' ? this.modelForOptions() : this.modelForConfiguration(section);
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ export default class VaultClusterSettingsAuthEnableRoute extends Route {
|
||||
model() {
|
||||
const defaults = {
|
||||
config: { listing_visibility: false },
|
||||
user_lockout_config: {},
|
||||
};
|
||||
return new AuthMethodForm(defaults, { isNew: true });
|
||||
}
|
||||
|
||||
@ -148,9 +148,13 @@ export default class ApiService extends Service {
|
||||
// convenience method for updating the query params object on the request context
|
||||
// eg. this.api.sys.uiConfigListCustomMessages(true, ({ context: { query } }) => { query.authenticated = true });
|
||||
// -> this.api.sys.uiConfigListCustomMessages(true, (context) => this.api.addQueryParams(context, { authenticated: true }));
|
||||
addQueryParams(requestContext: { init: HTTPRequestInit; context: RequestOpts }, params: HTTPQuery = {}) {
|
||||
const { context } = requestContext;
|
||||
async addQueryParams(
|
||||
requestContext: { init: HTTPRequestInit; context: RequestOpts },
|
||||
params: HTTPQuery = {}
|
||||
) {
|
||||
const { context, init } = requestContext;
|
||||
context.query = { ...context.query, ...params };
|
||||
return init;
|
||||
}
|
||||
|
||||
// accepts an error response and returns { status, message, response, path }
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
}}
|
||||
|
||||
{{#if (eq this.model.section "options")}}
|
||||
<AuthConfigForm::Options @model={{this.model.model}} />
|
||||
<AuthConfigForm::Options @form={{this.model.form}} />
|
||||
{{else}}
|
||||
<AuthConfigForm::Config @model={{this.model.model}} />
|
||||
<AuthConfigForm::Config @form={{this.model.form}} />
|
||||
{{/if}}
|
||||
@ -42,16 +42,87 @@ interface DisplayAttrs {
|
||||
sensitive?: boolean;
|
||||
}
|
||||
interface OpenApiAction {
|
||||
operationId: string;
|
||||
tags?: string[];
|
||||
responses: Record<string, { description: string }>;
|
||||
requestBody?: {
|
||||
content: Record<string, { schema: { $ref: string } }>;
|
||||
required: boolean;
|
||||
};
|
||||
parameters: Array<{ name: string }>;
|
||||
}
|
||||
interface OpenApiPath {
|
||||
description?: string;
|
||||
parameters: OpenApiParameter[];
|
||||
parameters?: OpenApiParameter[];
|
||||
'x-vault-displayAttrs': DisplayAttrs;
|
||||
get?: OpenApiAction;
|
||||
post?: OpenApiAction;
|
||||
delete?: OpenApiAction;
|
||||
}
|
||||
interface Attribute {
|
||||
name: string;
|
||||
type: string | undefined;
|
||||
options: {
|
||||
editType?: string;
|
||||
fieldGroup?: string;
|
||||
fieldValue?: string;
|
||||
label?: string;
|
||||
readonly?: boolean;
|
||||
};
|
||||
}
|
||||
interface OpenApiProp {
|
||||
description: string;
|
||||
type: string;
|
||||
'x-vault-displayAttrs': {
|
||||
name?: string;
|
||||
value?: string | number;
|
||||
group?: string;
|
||||
sensitive?: boolean;
|
||||
editType?: string;
|
||||
description?: string;
|
||||
identifier?: boolean;
|
||||
};
|
||||
items?: { type: string };
|
||||
format?: string;
|
||||
isId?: boolean;
|
||||
deprecated?: boolean;
|
||||
enum?: string[];
|
||||
}
|
||||
interface MixedAttr {
|
||||
type?: string;
|
||||
helpText?: string;
|
||||
editType?: string;
|
||||
fieldGroup: string;
|
||||
fieldValue?: string;
|
||||
label?: string;
|
||||
readonly?: boolean;
|
||||
possibleValues?: string[];
|
||||
defaultValue?: string | number | (() => string | number);
|
||||
sensitive?: boolean;
|
||||
readOnly?: boolean;
|
||||
name?: string;
|
||||
identifier?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type OpenApiProps = Record<string, OpenApiProp>;
|
||||
|
||||
export type OpenApiHelpResponse = {
|
||||
help: string;
|
||||
openapi: {
|
||||
paths: OpenApiPath[];
|
||||
components: {
|
||||
schemas: Record<string, { properties: OpenApiProps; type: string }>;
|
||||
};
|
||||
info: {
|
||||
title: string;
|
||||
version: string;
|
||||
description: string;
|
||||
license: { name: string; url: string };
|
||||
};
|
||||
openapi: string;
|
||||
};
|
||||
};
|
||||
|
||||
// Take object entries from the OpenAPI response and consolidate them into an object which includes itemTypes, operations, and paths
|
||||
export function reducePathsByPathName(pathsInfo: PathInfo, currentPath: [string, OpenApiPath]): PathInfo {
|
||||
@ -160,59 +231,18 @@ const OPENAPI_POWERED_MODELS = {
|
||||
'role-ssh': (backend: string) => `/v1/${backend}/roles/example?help=1`,
|
||||
};
|
||||
|
||||
export function getHelpUrlForModel(modelType: string, backend: string) {
|
||||
const urlFn = OPENAPI_POWERED_MODELS[modelType as keyof typeof OPENAPI_POWERED_MODELS] as (
|
||||
backend: string
|
||||
) => string;
|
||||
if (!urlFn) return null;
|
||||
return urlFn(backend);
|
||||
export function getHelpUrlForModel(modelType: string, backend?: string) {
|
||||
if (modelType in OPENAPI_POWERED_MODELS && backend) {
|
||||
const urlFn = OPENAPI_POWERED_MODELS[modelType as keyof typeof OPENAPI_POWERED_MODELS];
|
||||
return urlFn(backend);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface Attribute {
|
||||
name: string;
|
||||
type: string | undefined;
|
||||
options: {
|
||||
editType?: string;
|
||||
fieldGroup?: string;
|
||||
fieldValue?: string;
|
||||
label?: string;
|
||||
readonly?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface OpenApiProp {
|
||||
description: string;
|
||||
type: string;
|
||||
'x-vault-displayAttrs': {
|
||||
name: string;
|
||||
value: string | number;
|
||||
group: string;
|
||||
sensitive: boolean;
|
||||
editType?: string;
|
||||
description?: string;
|
||||
};
|
||||
items?: { type: string };
|
||||
format?: string;
|
||||
isId?: boolean;
|
||||
deprecated?: boolean;
|
||||
enum?: string[];
|
||||
}
|
||||
interface MixedAttr {
|
||||
type?: string;
|
||||
helpText?: string;
|
||||
editType?: string;
|
||||
fieldGroup: string;
|
||||
fieldValue?: string;
|
||||
label?: string;
|
||||
readonly?: boolean;
|
||||
possibleValues?: string[];
|
||||
defaultValue?: string | number | (() => string | number);
|
||||
sensitive?: boolean;
|
||||
readOnly?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export const expandOpenApiProps = function (props: Record<string, OpenApiProp>): Record<string, MixedAttr> {
|
||||
export const expandOpenApiProps = function (
|
||||
props: OpenApiProps,
|
||||
outputFormat: 'model' | 'form' = 'model'
|
||||
): Record<string, MixedAttr> {
|
||||
const attrs: Record<string, MixedAttr> = {};
|
||||
// expand all attributes
|
||||
for (const propName in props) {
|
||||
@ -229,6 +259,7 @@ export const expandOpenApiProps = function (props: Record<string, OpenApiProp>):
|
||||
sensitive,
|
||||
editType,
|
||||
description: displayDescription,
|
||||
identifier,
|
||||
} = prop['x-vault-displayAttrs'] || {};
|
||||
|
||||
if (type === 'integer') {
|
||||
@ -255,9 +286,10 @@ export const expandOpenApiProps = function (props: Record<string, OpenApiProp>):
|
||||
fieldGroup: group || 'default',
|
||||
readOnly: isId,
|
||||
defaultValue: value || undefined,
|
||||
identifier,
|
||||
};
|
||||
|
||||
if (type === 'object' && !!value) {
|
||||
if (type === 'object' && !!value && outputFormat !== 'form') {
|
||||
attrDefn.defaultValue = () => {
|
||||
return value;
|
||||
};
|
||||
@ -285,11 +317,33 @@ export const expandOpenApiProps = function (props: Record<string, OpenApiProp>):
|
||||
delete attrDefn[attrProp];
|
||||
}
|
||||
}
|
||||
attrs[camelize(propName)] = attrDefn;
|
||||
|
||||
const key = outputFormat === 'model' ? camelize(propName) : propName;
|
||||
attrs[key] = attrDefn;
|
||||
}
|
||||
return attrs;
|
||||
};
|
||||
|
||||
/*
|
||||
* extract props for post request schema from OpenAPI help response
|
||||
* returns expanded OpenAPI props to be used in forms
|
||||
*/
|
||||
export const propsForSchema = function (helpResponse: OpenApiHelpResponse) {
|
||||
const { openapi } = helpResponse;
|
||||
// paths is an array but it will have a single entry with the scope we're in
|
||||
const path = Object.values(openapi.paths)[0];
|
||||
const schema = path?.post?.requestBody?.content['application/json']?.schema;
|
||||
if (schema?.$ref) {
|
||||
// $ref will be shaped like `#/components/schemas/MyResponseType
|
||||
// this maps to the location of the item in openapi.components.schemas
|
||||
const schemaRef = schema.$ref.replace('#/components/schemas/', '');
|
||||
const props = openapi.components.schemas[schemaRef]?.['properties'] as OpenApiProps;
|
||||
return expandOpenApiProps(props, 'form');
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
/**
|
||||
* combineOpenApiAttrs takes attributes defined on an existing models
|
||||
* and adds in the attributes found on an OpenAPI response. The values
|
||||
|
||||
@ -36,6 +36,29 @@ module('Acceptance | auth enable tune form test', function (hooks) {
|
||||
'config.allowed_response_headers',
|
||||
'config.plugin_version',
|
||||
];
|
||||
this.tokensGroup = {
|
||||
Tokens: [
|
||||
'token_bound_cidrs',
|
||||
'token_explicit_max_ttl',
|
||||
'token_max_ttl',
|
||||
'token_no_default_policy',
|
||||
'token_num_uses',
|
||||
'token_period',
|
||||
'token_policies',
|
||||
'token_ttl',
|
||||
'token_type',
|
||||
],
|
||||
};
|
||||
this.oidcJwtGroup = {
|
||||
'OIDC/JWT Options': [
|
||||
'oidc_client_id',
|
||||
'oidc_client_secret',
|
||||
'oidc_discovery_ca_pem',
|
||||
'jwt_validation_pubkeys',
|
||||
'jwt_supported_algs',
|
||||
'bound_issuer',
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
module('azure', function (hooks) {
|
||||
@ -44,16 +67,19 @@ module('Acceptance | auth enable tune form test', function (hooks) {
|
||||
this.path = `${this.type}-${uuidv4()}`;
|
||||
this.tuneFields = [
|
||||
'environment',
|
||||
'identityTokenAudience',
|
||||
'identityTokenTtl',
|
||||
'maxRetries',
|
||||
'maxRetryDelay',
|
||||
'identity_token_audience',
|
||||
'identity_token_ttl',
|
||||
'max_retries',
|
||||
'max_retry_delay',
|
||||
'resource',
|
||||
'retryDelay',
|
||||
'rootPasswordTtl',
|
||||
'tenantId',
|
||||
'retry_delay',
|
||||
'root_password_ttl',
|
||||
'tenant_id',
|
||||
];
|
||||
this.tuneToggles = { 'Azure Options': ['clientId', 'clientSecret'] };
|
||||
// until the vault-plugin-auth-azure changes are released, these fields will be in the default group
|
||||
// this test should then fail and the following line can be removed and the next line uncommented
|
||||
this.tuneFields.push('client_id', 'client_secret');
|
||||
// this.tuneToggles = { 'Azure Options': ['client_id', 'client_secret'] };
|
||||
await login();
|
||||
return visit('/vault/settings/auth/enable');
|
||||
});
|
||||
@ -68,29 +94,25 @@ module('Acceptance | auth enable tune form test', function (hooks) {
|
||||
this.type = 'jwt';
|
||||
this.path = `${this.type}-${uuidv4()}`;
|
||||
this.customSelectors = {
|
||||
providerConfig: `${GENERAL.fieldByAttr('providerConfig')} .cm-editor`,
|
||||
provider_config: `${GENERAL.fieldByAttr('provider_config')} .cm-editor`,
|
||||
};
|
||||
this.tuneFields = [
|
||||
'defaultRole',
|
||||
'jwksCaPem',
|
||||
'jwksUrl',
|
||||
'namespaceInState',
|
||||
'oidcDiscoveryUrl',
|
||||
'oidcResponseMode',
|
||||
'oidcResponseTypes',
|
||||
'providerConfig',
|
||||
'unsupportedCriticalCertExtensions',
|
||||
'default_role',
|
||||
'jwks_ca_pem',
|
||||
'jwks_url',
|
||||
'namespace_in_state',
|
||||
'oidc_discovery_url',
|
||||
'oidc_response_mode',
|
||||
'oidc_response_types',
|
||||
// provider_config will be updated to EditType: file in next version of vault-plugin-auth-jwt
|
||||
// commenting out for now to avoid test failure
|
||||
// 'provider_config',
|
||||
'unsupported_critical_cert_extensions',
|
||||
];
|
||||
this.tuneToggles = {
|
||||
'JWT Options': [
|
||||
'oidcClientId',
|
||||
'oidcClientSecret',
|
||||
'oidcDiscoveryCaPem',
|
||||
'jwtValidationPubkeys',
|
||||
'jwtSupportedAlgs',
|
||||
'boundIssuer',
|
||||
],
|
||||
};
|
||||
// until the vault-plugin-auth-jwt changes are released, these fields will be in the default group
|
||||
// this test should then fail and the following line can be removed and the next line uncommented
|
||||
this.tuneFields.push(...this.oidcJwtGroup['OIDC/JWT Options']);
|
||||
// this.tuneToggles = this.oidcJwtGroup;
|
||||
await login();
|
||||
return visit('/vault/settings/auth/enable');
|
||||
});
|
||||
@ -106,41 +128,33 @@ module('Acceptance | auth enable tune form test', function (hooks) {
|
||||
this.path = `${this.type}-${uuidv4()}`;
|
||||
this.tuneFields = [
|
||||
'url',
|
||||
'caseSensitiveNames',
|
||||
'connectionTimeout',
|
||||
'dereferenceAliases',
|
||||
'maxPageSize',
|
||||
'passwordPolicy',
|
||||
'requestTimeout',
|
||||
'tokenBoundCidrs',
|
||||
'tokenExplicitMaxTtl',
|
||||
'tokenMaxTtl',
|
||||
'tokenNoDefaultPolicy',
|
||||
'tokenNumUses',
|
||||
'tokenPeriod',
|
||||
'tokenPolicies',
|
||||
'tokenTtl',
|
||||
'tokenType',
|
||||
'usePre111GroupCnBehavior',
|
||||
'usernameAsAlias',
|
||||
'case_sensitive_names',
|
||||
'connection_timeout',
|
||||
'dereference_aliases',
|
||||
'max_page_size',
|
||||
'password_policy',
|
||||
'request_timeout',
|
||||
'use_pre111_group_cn_behavior',
|
||||
'username_as_alias',
|
||||
];
|
||||
this.tuneToggles = {
|
||||
'LDAP Options': [
|
||||
'starttls',
|
||||
'insecureTls',
|
||||
'insecure_tls',
|
||||
'discoverdn',
|
||||
'denyNullBind',
|
||||
'tlsMinVersion',
|
||||
'tlsMaxVersion',
|
||||
'deny_null_bind',
|
||||
'tls_min_version',
|
||||
'tls_max_version',
|
||||
'certificate',
|
||||
'clientTlsCert',
|
||||
'clientTlsKey',
|
||||
'client_tls_cert',
|
||||
'client_tls_key',
|
||||
'userattr',
|
||||
'upndomain',
|
||||
'anonymousGroupSearch',
|
||||
'anonymous_group_search',
|
||||
],
|
||||
'Customize User Search': ['binddn', 'userdn', 'bindpass', 'userfilter'],
|
||||
'Customize Group Membership Search': ['groupfilter', 'groupattr', 'groupdn', 'useTokenGroups'],
|
||||
'Customize Group Membership Search': ['groupfilter', 'groupattr', 'groupdn', 'use_token_groups'],
|
||||
...this.tokensGroup,
|
||||
};
|
||||
await login();
|
||||
return visit('/vault/settings/auth/enable');
|
||||
@ -156,29 +170,23 @@ module('Acceptance | auth enable tune form test', function (hooks) {
|
||||
this.type = 'oidc';
|
||||
this.path = `${this.type}-${uuidv4()}`;
|
||||
this.customSelectors = {
|
||||
providerConfig: `${GENERAL.fieldByAttr('providerConfig')} .cm-editor`,
|
||||
provider_config: `${GENERAL.fieldByAttr('provider_config')} .cm-editor`,
|
||||
};
|
||||
this.tuneFields = [
|
||||
'oidcDiscoveryUrl',
|
||||
'defaultRole',
|
||||
'jwksCaPem',
|
||||
'jwksUrl',
|
||||
'oidcResponseMode',
|
||||
'oidcResponseTypes',
|
||||
'namespaceInState',
|
||||
'providerConfig',
|
||||
'unsupportedCriticalCertExtensions',
|
||||
'oidc_discovery_url',
|
||||
'default_role',
|
||||
'jwks_ca_pem',
|
||||
'jwks_url',
|
||||
'oidc_response_mode',
|
||||
'oidc_response_types',
|
||||
'namespace_in_state',
|
||||
'provider_config',
|
||||
'unsupported_critical_cert_extensions',
|
||||
];
|
||||
this.tuneToggles = {
|
||||
'OIDC Options': [
|
||||
'oidcClientId',
|
||||
'oidcClientSecret',
|
||||
'oidcDiscoveryCaPem',
|
||||
'jwtValidationPubkeys',
|
||||
'jwtSupportedAlgs',
|
||||
'boundIssuer',
|
||||
],
|
||||
};
|
||||
// until the vault-plugin-auth-jwt changes are released, these fields will be in the default group
|
||||
// this test should then fail and the following line can be removed and the next line uncommented
|
||||
this.tuneFields.push(...this.oidcJwtGroup['OIDC/JWT Options']);
|
||||
// this.tuneToggles = this.oidcJwtGroup;
|
||||
await login();
|
||||
return visit('/vault/settings/auth/enable');
|
||||
});
|
||||
@ -192,19 +200,8 @@ module('Acceptance | auth enable tune form test', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.type = 'okta';
|
||||
this.path = `${this.type}-${uuidv4()}`;
|
||||
this.tuneFields = [
|
||||
'orgName',
|
||||
'tokenBoundCidrs',
|
||||
'tokenExplicitMaxTtl',
|
||||
'tokenMaxTtl',
|
||||
'tokenNoDefaultPolicy',
|
||||
'tokenNumUses',
|
||||
'tokenPeriod',
|
||||
'tokenPolicies',
|
||||
'tokenTtl',
|
||||
'tokenType',
|
||||
];
|
||||
this.tuneToggles = { Options: ['apiToken', 'baseUrl', 'bypassOktaMfa'] };
|
||||
this.tuneFields = ['org_name', 'api_token', 'base_url', 'bypass_okta_mfa'];
|
||||
this.tuneToggles = this.tokensGroup;
|
||||
await login();
|
||||
return visit('/vault/settings/auth/enable');
|
||||
});
|
||||
|
||||
@ -31,7 +31,12 @@ module('Acceptance | settings/auth/configure/section', function (hooks) {
|
||||
|
||||
test('it can save options', async function (assert) {
|
||||
assert.expect(6);
|
||||
this.server.post(`/sys/mounts/auth/:path/tune`, function (schema, request) {
|
||||
|
||||
const path = `approle-save-${this.uid}`;
|
||||
const type = 'approle';
|
||||
const section = 'options';
|
||||
|
||||
this.server.post(`/sys/mounts/auth/${path}/tune`, function (schema, request) {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
const keys = Object.keys(body);
|
||||
assert.strictEqual(body.token_type, 'batch', 'passes new token type');
|
||||
@ -40,17 +45,16 @@ module('Acceptance | settings/auth/configure/section', function (hooks) {
|
||||
assert.true(keys.includes('description'), 'passes updated description on tune');
|
||||
return request.passthrough();
|
||||
});
|
||||
const path = `approle-save-${this.uid}`;
|
||||
const type = 'approle';
|
||||
const section = 'options';
|
||||
|
||||
await enablePage.enable(type, path);
|
||||
await page.visit({ path, section });
|
||||
await fillIn(GENERAL.inputByAttr('description'), 'This is Approle!');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('config.tokenType'))
|
||||
.dom(GENERAL.inputByAttr('config.token_type'))
|
||||
.hasValue('default-service', 'as default the token type selected is default-service.');
|
||||
await fillIn(GENERAL.inputByAttr('config.tokenType'), 'batch');
|
||||
await fillIn(GENERAL.inputByAttr('config.token_type'), 'batch');
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
assert.strictEqual(
|
||||
page.flash.latestMessage,
|
||||
`The configuration was saved successfully.`,
|
||||
|
||||
@ -10,6 +10,8 @@ import { click, fillIn, render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
|
||||
import AuthMethodForm from 'vault/forms/auth/method';
|
||||
import sinon from 'sinon';
|
||||
|
||||
const userLockoutSupported = ['approle', 'ldap', 'userpass'];
|
||||
const userLockoutUnsupported = filterEnginesByMountCategory({ mountCategory: 'auth', isEnterprise: false })
|
||||
@ -23,28 +25,26 @@ module('Integration | Component | auth-config-form options', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.owner.lookup('service:flash-messages').registerTypes(['success']);
|
||||
this.router = this.owner.lookup('service:router');
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.createModel = (path, type) => {
|
||||
this.model = this.store.createRecord('auth-method', { path, type });
|
||||
this.model.set('config', this.store.createRecord('mount-config'));
|
||||
this.transitionStub = sinon
|
||||
.stub(this.router, 'transitionTo')
|
||||
.returns({ followRedirects: () => Promise.resolve() });
|
||||
|
||||
this.renderComponent = (path, type) => {
|
||||
this.form = new AuthMethodForm({
|
||||
path,
|
||||
config: { listing_visibility: false },
|
||||
user_lockout_config: {},
|
||||
});
|
||||
this.form.type = type;
|
||||
return render(hbs`<AuthConfigForm::Options @form={{this.form}} />`);
|
||||
};
|
||||
});
|
||||
|
||||
for (const type of userLockoutSupported) {
|
||||
test(`it submits data correctly for ${type} method (supports user_lockout_config)`, async function (assert) {
|
||||
assert.expect(3);
|
||||
const path = `my-${type}-auth/`;
|
||||
this.createModel(path, type);
|
||||
|
||||
this.router.reopen({
|
||||
transitionTo() {
|
||||
return {
|
||||
followRedirects() {
|
||||
assert.ok(true, `saving ${type} calls transitionTo on save`);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
const path = `my-${type}-auth`;
|
||||
|
||||
this.server.post(`sys/mounts/auth/${path}/tune`, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
@ -62,30 +62,36 @@ module('Integration | Component | auth-config-form options', function (hooks) {
|
||||
assert.propEqual(payload, expected, `${type} method payload contains tune parameters`);
|
||||
return { payload };
|
||||
});
|
||||
await render(hbs`<AuthConfigForm::Options @model={{this.model}} />`);
|
||||
|
||||
await this.renderComponent(path, type);
|
||||
|
||||
assert.dom('[data-test-user-lockout-section]').hasText('User lockout configuration');
|
||||
|
||||
await click(GENERAL.toggleInput('toggle-config.listingVisibility'));
|
||||
await fillIn(GENERAL.inputByAttr('config.tokenType'), 'default-batch');
|
||||
await click(GENERAL.toggleInput('toggle-config.listing_visibility'));
|
||||
await fillIn(GENERAL.inputByAttr('config.token_type'), 'default-batch');
|
||||
|
||||
await click(GENERAL.ttl.toggle('Default Lease TTL'));
|
||||
await fillIn(GENERAL.ttl.input('Default Lease TTL'), '30');
|
||||
|
||||
await fillIn(GENERAL.inputByAttr('config.lockoutThreshold'), '7');
|
||||
await fillIn(GENERAL.inputByAttr('user_lockout_config.lockout_threshold'), '7');
|
||||
|
||||
await click(GENERAL.ttl.toggle('Lockout duration'));
|
||||
await fillIn(GENERAL.ttl.input('Lockout duration'), '10');
|
||||
await fillIn(
|
||||
`${GENERAL.inputByAttr('config.lockoutDuration')} ${GENERAL.selectByAttr('ttl-unit')}`,
|
||||
`${GENERAL.inputByAttr('user_lockout_config.lockout_duration')} ${GENERAL.selectByAttr('ttl-unit')}`,
|
||||
'm'
|
||||
);
|
||||
await click(GENERAL.ttl.toggle('Lockout counter reset'));
|
||||
await fillIn(GENERAL.ttl.input('Lockout counter reset'), '5');
|
||||
|
||||
await click(GENERAL.inputByAttr('config.lockoutDisable'));
|
||||
await click(GENERAL.inputByAttr('user_lockout_config.lockout_disable'));
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.access.methods'),
|
||||
'transitions to access methods list on save'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -95,18 +101,7 @@ module('Integration | Component | auth-config-form options', function (hooks) {
|
||||
test(`it submits data correctly for ${type} auth method`, async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const path = `my-${type}-auth/`;
|
||||
this.createModel(path, type);
|
||||
|
||||
this.router.reopen({
|
||||
transitionTo() {
|
||||
return {
|
||||
followRedirects() {
|
||||
assert.ok(true, `saving ${type} calls transitionTo on save`);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
const path = `my-${type}-auth`;
|
||||
|
||||
this.server.post(`sys/mounts/auth/${path}/tune`, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
@ -118,20 +113,21 @@ module('Integration | Component | auth-config-form options', function (hooks) {
|
||||
assert.propEqual(payload, expected, `${type} method payload contains tune parameters`);
|
||||
return { payload };
|
||||
});
|
||||
await render(hbs`<AuthConfigForm::Options @model={{this.model}} />`);
|
||||
|
||||
await this.renderComponent(path, type);
|
||||
|
||||
assert
|
||||
.dom('[data-test-user-lockout-section]')
|
||||
.doesNotExist(`${type} method does not render user lockout section`);
|
||||
|
||||
await click(GENERAL.toggleInput('toggle-config.listingVisibility'));
|
||||
await fillIn(GENERAL.inputByAttr('config.tokenType'), 'default-batch');
|
||||
await click(GENERAL.toggleInput('toggle-config.listing_visibility'));
|
||||
await fillIn(GENERAL.inputByAttr('config.token_type'), 'default-batch');
|
||||
|
||||
await click(GENERAL.ttl.toggle('Default Lease TTL'));
|
||||
await fillIn(GENERAL.ttl.input('Default Lease TTL'), '30');
|
||||
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('config.lockoutThreshold'))
|
||||
.dom(GENERAL.inputByAttr('user_lockout_config.lockout_threshold'))
|
||||
.doesNotExist(`${type} method does not render lockout threshold`);
|
||||
assert
|
||||
.dom(GENERAL.ttl.toggle('Lockout duration'))
|
||||
@ -140,28 +136,23 @@ module('Integration | Component | auth-config-form options', function (hooks) {
|
||||
.dom(GENERAL.ttl.toggle('Lockout counter reset'))
|
||||
.doesNotExist(`${type} method does not render lockout counter reset`);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('config.lockoutDisable'))
|
||||
.dom(GENERAL.inputByAttr('user_lockout_config.lockout_disable'))
|
||||
.doesNotExist(`${type} method does not render lockout disable`);
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.access.methods'),
|
||||
'transitions to access methods list on save'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('it submits data correctly for token auth method', async function (assert) {
|
||||
assert.expect(8);
|
||||
const type = 'token';
|
||||
const path = `my-${type}-auth/`;
|
||||
this.createModel(path, type);
|
||||
|
||||
this.router.reopen({
|
||||
transitionTo() {
|
||||
return {
|
||||
followRedirects() {
|
||||
assert.ok(true, `saving token calls transitionTo on save`);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
const type = 'token';
|
||||
const path = `my-${type}-auth`;
|
||||
|
||||
this.server.post(`sys/mounts/auth/${path}/tune`, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
@ -172,19 +163,20 @@ module('Integration | Component | auth-config-form options', function (hooks) {
|
||||
assert.propEqual(payload, expected, `${type} method payload contains tune parameters`);
|
||||
return { payload };
|
||||
});
|
||||
await render(hbs`<AuthConfigForm::Options @model={{this.model}} />`);
|
||||
|
||||
await this.renderComponent(path, type);
|
||||
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('config.tokenType'))
|
||||
.doesNotExist('does not render tokenType for token auth method');
|
||||
.dom(GENERAL.inputByAttr('config.token_type'))
|
||||
.doesNotExist('does not render token_type for token auth method');
|
||||
|
||||
await click(GENERAL.toggleInput('toggle-config.listingVisibility'));
|
||||
await click(GENERAL.toggleInput('toggle-config.listing_visibility'));
|
||||
await click(GENERAL.ttl.toggle('Default Lease TTL'));
|
||||
await fillIn(GENERAL.ttl.input('Default Lease TTL'), '30');
|
||||
|
||||
assert.dom('[data-test-user-lockout-section]').doesNotExist('token does not render user lockout section');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('config.lockoutThreshold'))
|
||||
.dom(GENERAL.inputByAttr('user_lockout_config.lockout_threshold'))
|
||||
.doesNotExist('token method does not render lockout threshold');
|
||||
assert
|
||||
.dom(GENERAL.ttl.toggle('Lockout duration'))
|
||||
@ -193,9 +185,14 @@ module('Integration | Component | auth-config-form options', function (hooks) {
|
||||
.dom(GENERAL.ttl.toggle('Lockout counter reset'))
|
||||
.doesNotExist('token method does not render lockout counter reset');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('config.lockoutDisable'))
|
||||
.dom(GENERAL.inputByAttr('user_lockout_config.lockout_disable'))
|
||||
.doesNotExist('token method does not render lockout disable');
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.access.methods'),
|
||||
'transitions to access methods list on save'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
8
ui/types/vault/auth/methods.d.ts
vendored
8
ui/types/vault/auth/methods.d.ts
vendored
@ -87,7 +87,15 @@ export interface UsernameLoginResponse extends ApiResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export type UserLockoutConfig = {
|
||||
lockout_threshold?: string;
|
||||
lockout_duration?: string;
|
||||
lockout_counter_reset?: string;
|
||||
lockout_disable?: boolean;
|
||||
};
|
||||
|
||||
export type AuthMethodFormData = AuthEnableMethodRequest & {
|
||||
path: string;
|
||||
config: MountConfig;
|
||||
user_lockout_config: UserLockoutConfig;
|
||||
};
|
||||
|
||||
2
ui/types/vault/services/path-help.d.ts
vendored
2
ui/types/vault/services/path-help.d.ts
vendored
@ -9,4 +9,6 @@ import type { PathInfo } from 'vault/utils/openapi-helpers';
|
||||
|
||||
export default class PathHelpService extends Service {
|
||||
getPaths(apiPath: string, backend: string, itemType?: string, itemID?: string): Promise<PathInfo>;
|
||||
hydrateModel(modelType: string, backend: string): Promise<void>;
|
||||
getNewModel(modelType: string, backend: string, apiPath: string, itemType?: string): Promise<void>;
|
||||
}
|
||||
|
||||
@ -2783,8 +2783,8 @@ __metadata:
|
||||
|
||||
"@hashicorp/vault-client-typescript@hashicorp/vault-client-typescript":
|
||||
version: 0.0.0
|
||||
resolution: "@hashicorp/vault-client-typescript@https://github.com/hashicorp/vault-client-typescript.git#commit=20c8c9bb516615b32bc1ab7edd890e0fbbfcae4e"
|
||||
checksum: e78c7f9c195290d8e7d4b730fb1353124cdfba88500aa95f196e682c49bc88a7c8a98784b4eba81447d9456a9c12396a6c3e829fea42f0dbff217640d33f870c
|
||||
resolution: "@hashicorp/vault-client-typescript@https://github.com/hashicorp/vault-client-typescript.git#commit=159865331f4ff0219264ab243b2e30a0087f972f"
|
||||
checksum: ae044d2a927c2d351330690ecedb36a6321106ce9383a77495f03a785e39bfd469a385274ef3248c21b5e5540da975297dd42949e66c5ce31b090128b822bce7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user