Copy [UI] Ember Data Migration - Auth Method Configs into main (#9000) (#9099)

* 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:
Jordan Reimer 2025-09-03 18:11:41 -06:00 committed by GitHub
parent d283ef5897
commit d8ecd066b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 671 additions and 441 deletions

View File

@ -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"

View File

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

View File

@ -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.');
}
}

View 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.');
})
);
}

View File

@ -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: {

View File

@ -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', [

View File

@ -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
View 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];
},
[]
);
}
}

View File

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

View 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,
};
}
}

View File

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

View 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);
}
}

View File

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

View File

@ -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 }

View File

@ -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}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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