From c34e25fb76228833a9b550c072ca30f03f44ca23 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 5 Dec 2025 12:43:38 -0500 Subject: [PATCH] [UI] Ember Data Migration - LDAP Config (#11048) (#11194) * removes withConfig decorator and moves check to application route * updates backendModel references in ldap engine to secretsEngine * adds ldap config form class * updates ldap config type in application route * updates ldap configure and configuration routes to use api service Co-authored-by: Jordan Reimer --- ui/app/forms/secrets/ldap/config.ts | 70 +++++++++++ ui/app/utils/forms/field.ts | 2 + .../addon/components/page/configuration.hbs | 17 ++- .../addon/components/page/configuration.ts | 113 +++++++----------- .../ldap/addon/components/page/configure.hbs | 16 +-- .../ldap/addon/components/page/configure.ts | 84 ++++++------- .../ldap/addon/components/page/libraries.hbs | 2 +- .../ldap/addon/components/page/libraries.ts | 4 +- .../ldap/addon/components/page/overview.hbs | 2 +- ui/lib/ldap/addon/components/page/overview.ts | 6 +- ui/lib/ldap/addon/components/page/roles.hbs | 2 +- ui/lib/ldap/addon/components/page/roles.ts | 4 +- .../controllers/libraries/subdirectory.ts | 4 +- ui/lib/ldap/addon/routes/application.ts | 48 ++++++++ ui/lib/ldap/addon/routes/configuration.ts | 43 ++----- ui/lib/ldap/addon/routes/configure.ts | 37 ++---- ui/lib/ldap/addon/routes/libraries/index.ts | 19 ++- .../addon/routes/libraries/subdirectory.ts | 18 +-- ui/lib/ldap/addon/routes/overview.ts | 18 +-- ui/lib/ldap/addon/routes/roles/index.ts | 23 ++-- .../ldap/addon/routes/roles/subdirectory.ts | 17 +-- .../ldap/addon/templates/libraries/index.hbs | 2 +- .../templates/libraries/subdirectory.hbs | 2 +- ui/lib/ldap/addon/templates/overview.hbs | 2 +- ui/lib/ldap/addon/templates/roles/index.hbs | 2 +- .../addon/templates/roles/subdirectory.hbs | 4 +- ui/tests/helpers/ldap/ldap-helpers.js | 24 ++-- .../ldap/page/configuration-test.js | 66 +++++----- .../components/ldap/page/configure-test.js | 68 +++++------ .../components/ldap/page/libraries-test.js | 2 +- .../components/ldap/page/overview-test.js | 2 +- .../components/ldap/page/roles-test.js | 2 +- 32 files changed, 379 insertions(+), 346 deletions(-) create mode 100644 ui/app/forms/secrets/ldap/config.ts create mode 100644 ui/lib/ldap/addon/routes/application.ts diff --git a/ui/app/forms/secrets/ldap/config.ts b/ui/app/forms/secrets/ldap/config.ts new file mode 100644 index 0000000000..ebc3a3dc16 --- /dev/null +++ b/ui/app/forms/secrets/ldap/config.ts @@ -0,0 +1,70 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import OpenApiForm from 'vault/forms/open-api'; +import FormField from 'vault/utils/forms/field'; + +import type { LdapConfigureRequest } from '@hashicorp/vault-client-typescript'; +import type Form from 'vault/forms/form'; +import type { Validations } from 'vault/app-types'; +import FormFieldGroup from 'vault/utils/forms/field-group'; + +export default class LdapConfigForm extends OpenApiForm { + constructor(...args: ConstructorParameters) { + super('LdapConfigureRequest', ...args); + + this.formFields.forEach((field) => { + // password_policy field has special handling + if (field.name === 'password_policy') { + field.options = { + editType: 'optionalText', + label: 'Use custom password policy', + subText: 'Specify the name of an existing password policy.', + defaultSubText: 'Unless a custom policy is specified, Vault will use a default.', + defaultShown: 'Default', + docLink: '/vault/docs/concepts/password-policies', + }; + } else if (field.name === 'binddn') { + // binddn and bindpass subText mentions that they are optional but the docs have them marked as required + // update text to avoid confusion + field.options = { + ...field.options, + label: 'Administrator distinguished name', + subText: + 'Distinguished name of the administrator to bind (Bind DN) when performing user and group search. Example: cn=vault,ou=Users,dc=example,dc=com.', + }; + } else if (field.name === 'bindpass') { + field.options = { + ...field.options, + label: 'Administrator password', + subText: 'Password to use along with Bind DN when performing user search.', + }; + } + }); + + // set up formFieldGroups + const groupsMap = [ + { name: 'default', keys: ['binddn', 'bindpass', 'url', 'password_policy'] }, + { + name: 'TLS options', + keys: ['starttls', 'insecure_tls', 'certificate', 'client_tls_cert', 'client_tls_key'], + }, + { + name: 'More options', + keys: ['userdn', 'userattr', 'upndomain', 'connection_timeout', 'request_timeout'], + }, + ]; + + this.formFieldGroups = groupsMap.map(({ name, keys }) => { + const fields = keys.map((key) => this.formFields.find((field) => field.name === key) as FormField); + return new FormFieldGroup(name, fields); + }); + } + + validations: Validations = { + binddn: [{ type: 'presence', message: 'Administrator distinguished name is required.' }], + bindpass: [{ type: 'presence', message: 'Administrator password is required.' }], + }; +} diff --git a/ui/app/utils/forms/field.ts b/ui/app/utils/forms/field.ts index 10476c8347..9bf93454b7 100644 --- a/ui/app/utils/forms/field.ts +++ b/ui/app/utils/forms/field.ts @@ -28,6 +28,8 @@ export interface FieldOptions { labelDisabled?: string; mapToBoolean?: string; isOppositeValue?: boolean; + defaultSubText?: string; + defaultShown?: string; } export default class FormField { diff --git a/ui/lib/ldap/addon/components/page/configuration.hbs b/ui/lib/ldap/addon/components/page/configuration.hbs index 22b542403f..afd6efb189 100644 --- a/ui/lib/ldap/addon/components/page/configuration.hbs +++ b/ui/lib/ldap/addon/components/page/configuration.hbs @@ -4,13 +4,13 @@ }} <:toolbarActions> - {{#if @model.configModel}} + {{#if @model.config}} {{/if}} - {{#if @model.configModel}} + {{#if @model.config}} Edit configuration @@ -30,16 +30,21 @@ -{{#if @model.configModel}} +{{#if @model.config}} {{#each this.defaultFields as |field|}} - + {{/each}}

TLS Connection


{{#each this.connectionFields as |field|}} - + {{/each}} {{else if @model.configError}} diff --git a/ui/lib/ldap/addon/components/page/configuration.ts b/ui/lib/ldap/addon/components/page/configuration.ts index 9cf5b294fd..38e7837d43 100644 --- a/ui/lib/ldap/addon/components/page/configuration.ts +++ b/ui/lib/ldap/addon/components/page/configuration.ts @@ -7,83 +7,60 @@ import Component from '@glimmer/component'; import { service } from '@ember/service'; import { task } from 'ember-concurrency'; import { waitFor } from '@ember/test-waiters'; -import errorMessage from 'vault/utils/error-message'; +import { toLabel } from 'core/helpers/to-label'; -import type LdapConfigModel from 'vault/models/ldap/config'; -import type SecretEngineModel from 'vault/models/secret-engine'; -import type AdapterError from '@ember-data/adapter/error'; -import type { Breadcrumb } from 'vault/vault/app-types'; import type FlashMessageService from 'vault/services/flash-messages'; +import type ApiService from 'vault/services/api'; +import type SecretMountPath from 'vault/services/secret-mount-path'; +import type { Breadcrumb } from 'vault/vault/app-types'; +import type { LdapApplicationModel } from 'ldap/routes/application'; interface Args { - model: { - configModel: LdapConfigModel; - configError: AdapterError; - backendModel: SecretEngineModel; - }; + model: LdapApplicationModel; breadcrumbs: Array; } -interface Field { - label: string; - value: any; // eslint-disable-line @typescript-eslint/no-explicit-any - formatTtl?: boolean; -} - export default class LdapConfigurationPageComponent extends Component { @service declare readonly flashMessages: FlashMessageService; + @service declare readonly api: ApiService; + @service declare readonly secretMountPath: SecretMountPath; - get defaultFields(): Array { - const model = this.args.model.configModel; - const keys = [ - 'binddn', - 'url', - 'schema', - 'password_policy', - 'userdn', - 'userattr', - 'connection_timeout', - 'request_timeout', - ]; - return model.allFields.reduce>((filtered, field) => { - if (keys.includes(field.name)) { - const label = - { - schema: 'Schema', - password_policy: 'Password Policy', - }[field.name] || field.options.label; - filtered.splice(keys.indexOf(field.name), 0, { - label, - value: model[field.name as keyof typeof model], - formatTtl: field.name.includes('timeout'), - }); + defaultFields = [ + 'binddn', + 'url', + 'schema', + 'password_policy', + 'userdn', + 'userattr', + 'connection_timeout', + 'request_timeout', + ]; + + connectionFields = ['certificate', 'starttls', 'insecure_tls', 'client_tls_cert', 'client_tls_key']; + + label = (field: string) => { + return ( + { + binddn: 'Administrator distinguished name', + url: 'URL', + certificate: 'CA certificate', + starttls: 'Start TLS', + insecure_tls: 'Insecure TLS', + client_tls_cert: 'Client TLS certificate', + client_tls_key: 'Client TLS key', + }[field] || toLabel([field]) + ); + }; + + rotateRoot = task( + waitFor(async () => { + try { + await this.api.secrets.ldapRotateRootCredentials(this.secretMountPath.currentPath); + this.flashMessages.success('Root password successfully rotated.'); + } catch (error) { + const { message } = await this.api.parseError(error); + this.flashMessages.danger(`Error rotating root password \n ${message}`); } - return filtered; - }, []); - } - - get connectionFields(): Array { - const model = this.args.model.configModel; - const keys = ['certificate', 'starttls', 'insecure_tls', 'client_tls_cert', 'client_tls_key']; - return model.allFields.reduce>((filtered, field) => { - if (keys.includes(field.name)) { - filtered.splice(keys.indexOf(field.name), 0, { - label: field.options.label, - value: model[field.name as keyof typeof model], - }); - } - return filtered; - }, []); - } - - @task - @waitFor - *rotateRoot() { - try { - yield this.args.model.configModel.rotateRoot(); - this.flashMessages.success('Root password successfully rotated.'); - } catch (error) { - this.flashMessages.danger(`Error rotating root password \n ${errorMessage(error)}`); - } - } + }) + ); } diff --git a/ui/lib/ldap/addon/components/page/configure.hbs b/ui/lib/ldap/addon/components/page/configure.hbs index 93da1f44e4..87e6e74aa5 100644 --- a/ui/lib/ldap/addon/components/page/configure.hbs +++ b/ui/lib/ldap/addon/components/page/configure.hbs @@ -4,7 +4,7 @@ }} {{#each this.schemaOptions as |option|}} @@ -32,13 +32,9 @@

Schema Options


- {{#if @model.configModel.schema}} + {{#if @model.form.data.schema}}
- +
{{else}} ; } interface SchemaOption { @@ -30,6 +31,8 @@ interface SchemaOption { export default class LdapConfigurePageComponent extends Component { @service declare readonly flashMessages: FlashMessageService; @service('app-router') declare readonly router: RouterService; + @service declare readonly api: ApiService; + @service declare readonly secretMountPath: SecretMountPath; @tracked showRotatePrompt = false; @tracked modelValidations: ValidationMap | null = null; @@ -66,25 +69,19 @@ export default class LdapConfigurePageComponent extends Component { this.router.transitionTo(`vault.cluster.secrets.backend.ldap.${route}`); } - validate() { - const { isValid, state, invalidFormMessage } = this.args.model.configModel.validate(); - this.modelValidations = isValid ? null : state; - this.invalidFormMessage = isValid ? '' : invalidFormMessage; - return isValid; - } - async rotateRoot() { try { - await this.args.model.configModel.rotateRoot(); + await this.api.secrets.ldapRotateRootCredentials(this.secretMountPath.currentPath); } catch (error) { // since config save was successful at this point we only want to show the error in a flash message - this.flashMessages.danger(`Error rotating root password \n ${errorMessage(error)}`); + const { message } = await this.api.parseError(error); + this.flashMessages.danger(`Error rotating root password \n ${message}`); } } - async saveConfigModelAndRotateRoot(rotate: boolean) { + async saveConfigModelAndRotateRoot(data: LdapConfigureModel['form']['data'], rotate: boolean) { try { - await this.args.model.configModel.save(); + await this.api.secrets.ldapConfigure(this.secretMountPath.currentPath, data); // if save was triggered from confirm action in rotate password prompt we need to make an additional request if (rotate) { await this.rotateRoot(); @@ -92,40 +89,45 @@ export default class LdapConfigurePageComponent extends Component { this.flashMessages.success('Successfully configured LDAP engine'); this.leave('configuration'); } catch (error) { - this.error = errorMessage(error, 'Error saving configuration. Please try again or contact support.'); + const { message } = await this.api.parseError( + error, + 'Error saving configuration. Please try again or contact support.' + ); + this.error = message; } } - @task - @waitFor - *save(event: Event | null, rotate: boolean) { - if (event) { - event.preventDefault(); - } - const isValid = this.validate(); - // show rotate creds prompt for new models when form state is valid - this.showRotatePrompt = isValid && this.args.model.configModel.isNew && !this.showRotatePrompt; + save = task( + waitFor(async (event: Event | null, rotate: boolean) => { + if (event) { + event.preventDefault(); + } + const { form } = this.args.model; + const { isValid, state, invalidFormMessage, data } = form.toJSON(); - if (isValid && !this.showRotatePrompt) { - yield this.saveConfigModelAndRotateRoot(rotate); - } - } + this.modelValidations = isValid ? null : state; + this.invalidFormMessage = isValid ? '' : invalidFormMessage; + // show rotate creds prompt for new models when form state is valid + this.showRotatePrompt = isValid && form.isNew && !this.showRotatePrompt; + + if (isValid && !this.showRotatePrompt) { + await this.saveConfigModelAndRotateRoot(data, rotate); + } + }) + ); @action cancel() { - const { - model: { configModel }, - } = this.args; - const transitionRoute = configModel.isNew ? 'overview' : 'configuration'; - const cleanupMethod = configModel.isNew ? 'unloadRecord' : 'rollbackAttributes'; - configModel[cleanupMethod](); + const { isNew } = this.args.model.form; + const transitionRoute = isNew ? 'overview' : 'configuration'; this.leave(transitionRoute); } - @task - @waitFor - *saveAndClose(rotate: boolean, close: () => void) { - close(); - yield this.saveConfigModelAndRotateRoot(rotate); - } + saveAndClose = task( + waitFor(async (rotate: boolean, close: () => void) => { + close(); + const { data } = this.args.model.form.toJSON(); + await this.saveConfigModelAndRotateRoot(data, rotate); + }) + ); } diff --git a/ui/lib/ldap/addon/components/page/libraries.hbs b/ui/lib/ldap/addon/components/page/libraries.hbs index f13a99e9c4..4c20100330 100644 --- a/ui/lib/ldap/addon/components/page/libraries.hbs +++ b/ui/lib/ldap/addon/components/page/libraries.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 }} - + <:toolbarFilters> {{#if (and (not @promptConfig) @libraries)}} ; promptConfig: boolean; - backendModel: SecretEngineModel; + secretsEngine: SecretsEngineResource; breadcrumbs: Array; } diff --git a/ui/lib/ldap/addon/components/page/overview.hbs b/ui/lib/ldap/addon/components/page/overview.hbs index 46a576e076..dab72a0795 100644 --- a/ui/lib/ldap/addon/components/page/overview.hbs +++ b/ui/lib/ldap/addon/components/page/overview.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 }} - + {{#if @promptConfig}} diff --git a/ui/lib/ldap/addon/components/page/overview.ts b/ui/lib/ldap/addon/components/page/overview.ts index f6c52e5fed..f5c8c9b430 100644 --- a/ui/lib/ldap/addon/components/page/overview.ts +++ b/ui/lib/ldap/addon/components/page/overview.ts @@ -10,7 +10,7 @@ import { action } from '@ember/object'; import { restartableTask } from 'ember-concurrency'; import type LdapLibraryModel from 'vault/models/ldap/library'; -import type SecretEngineModel from 'vault/models/secret-engine'; +import type SecretsEngineResource from 'vault/resources/secrets/engine'; import type RouterService from '@ember/routing/router-service'; import type Store from '@ember-data/store'; import type { Breadcrumb } from 'vault/vault/app-types'; @@ -20,7 +20,7 @@ import { LdapLibraryAccountStatus } from 'vault/vault/adapters/ldap/library'; interface Args { roles: Array; promptConfig: boolean; - backendModel: SecretEngineModel; + secretsEngine: SecretsEngineResource; breadcrumbs: Array; } @@ -72,7 +72,7 @@ export default class LdapLibrariesPageComponent extends Component { } fetchLibraries = restartableTask(async () => { - const backend = this.args.backendModel.id; + const backend = this.args.secretsEngine.id; const allLibraries: Array = []; try { diff --git a/ui/lib/ldap/addon/components/page/roles.hbs b/ui/lib/ldap/addon/components/page/roles.hbs index 859e045e39..f677c038a8 100644 --- a/ui/lib/ldap/addon/components/page/roles.hbs +++ b/ui/lib/ldap/addon/components/page/roles.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 }} - + <:toolbarFilters> {{#if (and (not @promptConfig) @roles.meta.total)}} ; promptConfig: boolean; - backendModel: SecretEngineModel; + secretsEngine: SecretsEngineResource; breadcrumbs: Array; pageFilter: string; } diff --git a/ui/lib/ldap/addon/controllers/libraries/subdirectory.ts b/ui/lib/ldap/addon/controllers/libraries/subdirectory.ts index 3e5e811a4c..905a11b32d 100644 --- a/ui/lib/ldap/addon/controllers/libraries/subdirectory.ts +++ b/ui/lib/ldap/addon/controllers/libraries/subdirectory.ts @@ -7,10 +7,10 @@ import Controller from '@ember/controller'; import { tracked } from '@glimmer/tracking'; import type { Breadcrumb } from 'vault/vault/app-types'; import type LdapLibraryModel from 'vault/models/ldap/library'; -import type SecretEngineModel from 'vault/models/secret-engine'; +import type SecretsEngineResource from 'vault/resources/secrets/engine'; interface RouteModel { - backendModel: SecretEngineModel; + secretsEngine: SecretsEngineResource; path_to_library: string; libraries: Array; } diff --git a/ui/lib/ldap/addon/routes/application.ts b/ui/lib/ldap/addon/routes/application.ts new file mode 100644 index 0000000000..678c8edfc3 --- /dev/null +++ b/ui/lib/ldap/addon/routes/application.ts @@ -0,0 +1,48 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Route from '@ember/routing/route'; +import { service } from '@ember/service'; + +import type { ModelFrom } from 'vault/vault/route'; +import type ApiService from 'vault/services/api'; +import type Transition from '@ember/routing/transition'; +import type SecretsEngineResource from 'vault/resources/secrets/engine'; +import type { LdapConfigureRequest } from '@hashicorp/vault-client-typescript'; + +export type LdapApplicationModel = ModelFrom; + +export default class LdapApplicationRoute extends Route { + @service declare readonly api: ApiService; + + async model(params: Record, transition: Transition) { + const secretsEngine = super.model(params, transition) as SecretsEngineResource; + let config: LdapConfigureRequest | undefined; + let promptConfig = false; + let configError: unknown; + // check if engine is configured + // child routes will handle prompting for configuration if needed + try { + const { data } = await this.api.secrets.ldapReadConfiguration(secretsEngine.id); + config = data as LdapConfigureRequest; + } catch (error) { + const { response, status } = await this.api.parseError(error); + // not considering 404 an error since it triggers the cta + if (status === 404) { + promptConfig = true; + } else { + // ignore if the user does not have permission or other failures so as to not block the other operations + // this error is thrown in the configuration route so we can display the error in the view + configError = response; + } + } + return { + secretsEngine, + config, + configError, + promptConfig, + }; + } +} diff --git a/ui/lib/ldap/addon/routes/configuration.ts b/ui/lib/ldap/addon/routes/configuration.ts index fe180a2bf1..8b0e6096cd 100644 --- a/ui/lib/ldap/addon/routes/configuration.ts +++ b/ui/lib/ldap/addon/routes/configuration.ts @@ -5,62 +5,37 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; -import type Store from '@ember-data/store'; import type SecretMountPath from 'vault/services/secret-mount-path'; import type Transition from '@ember/routing/transition'; -import type LdapConfigModel from 'vault/models/ldap/config'; -import type SecretEngineModel from 'vault/models/secret-engine'; import type Controller from '@ember/controller'; import type { Breadcrumb } from 'vault/vault/app-types'; -import type AdapterError from '@ember-data/adapter/error'; -import RouterService from '@ember/routing/router-service'; - -interface RouteModel { - backendModel: SecretEngineModel; - configModel: LdapConfigModel; - configError: AdapterError; -} +import type RouterService from '@ember/routing/router-service'; +import type ApiService from 'vault/services/api'; +import type { LdapApplicationModel } from './application'; interface RouteController extends Controller { breadcrumbs: Array; - model: RouteModel; + model: LdapApplicationModel; } -@withConfig('ldap/config') export default class LdapConfigurationRoute extends Route { - @service declare readonly store: Store; + @service declare readonly api: ApiService; @service declare readonly secretMountPath: SecretMountPath; @service('app-router') declare readonly router: RouterService; - declare configModel: LdapConfigModel; - declare configError: AdapterError; - declare promptConfig: boolean; - - model() { - const backendModel: SecretEngineModel = this.modelFor('application') as SecretEngineModel; - - return { - backendModel, - promptConfig: this.promptConfig, - configModel: this.configModel, - configError: this.configError, - }; - } - - setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) { + setupController(controller: RouteController, resolvedModel: LdapApplicationModel, transition: Transition) { super.setupController(controller, resolvedModel, transition); controller.breadcrumbs = [ { label: 'Secrets', route: 'secrets', linkExternal: true }, - { label: resolvedModel.backendModel.id, route: 'overview', model: resolvedModel.backendModel.id }, + { label: resolvedModel.secretsEngine.id, route: 'overview', model: resolvedModel.secretsEngine.id }, { label: 'Configuration' }, ]; } - afterModel(resolvedModel: RouteModel) { - if (!resolvedModel.configModel) { + afterModel(resolvedModel: LdapApplicationModel) { + if (!resolvedModel.config) { this.router.transitionTo('vault.cluster.secrets.backend.ldap.configure'); } } diff --git a/ui/lib/ldap/addon/routes/configure.ts b/ui/lib/ldap/addon/routes/configure.ts index 5eb3e0d9cc..410e44afc5 100644 --- a/ui/lib/ldap/addon/routes/configure.ts +++ b/ui/lib/ldap/addon/routes/configure.ts @@ -5,54 +5,41 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; +import LdapConfigForm from 'vault/forms/secrets/ldap/config'; -import type Store from '@ember-data/store'; import type SecretMountPath from 'vault/services/secret-mount-path'; import type Transition from '@ember/routing/transition'; -import type LdapConfigModel from 'vault/models/ldap/config'; -import type SecretEngineModel from 'vault/models/secret-engine'; import type Controller from '@ember/controller'; import type { Breadcrumb } from 'vault/vault/app-types'; -import SecretsEngineResource from 'vault/resources/secrets/engine'; +import type { LdapApplicationModel } from './application'; +import type { ModelFrom } from 'vault/route'; interface RouteController extends Controller { breadcrumbs: Array; } -interface RouteModel { - backendModel: SecretEngineModel; - promptConfig: boolean; - configModel: LdapConfigModel; - engineDisplayData: SecretsEngineResource; -} +export type LdapConfigureModel = ModelFrom; -@withConfig('ldap/config') export default class LdapConfigureRoute extends Route { - @service declare readonly store: Store; @service declare readonly secretMountPath: SecretMountPath; - declare configModel: LdapConfigModel; - declare promptConfig: boolean; - model() { - const backend = this.secretMountPath.currentPath; - const backendModel: SecretEngineModel = this.modelFor('application') as SecretEngineModel; - + const { secretsEngine, promptConfig, config } = this.modelFor('application') as LdapApplicationModel; + const form = new LdapConfigForm(config, { isNew: !config }); return { - backendModel, - promptConfig: this.promptConfig, - configModel: this.configModel || this.store.createRecord('ldap/config', { backend }), + secretsEngine, + promptConfig, + form, }; } - setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) { + setupController(controller: RouteController, resolvedModel: LdapConfigureModel, transition: Transition) { super.setupController(controller, resolvedModel, transition); controller.breadcrumbs = [ { label: 'Secrets', route: 'secrets', linkExternal: true }, - { label: resolvedModel.backendModel.id, route: 'overview' }, - ...(this.promptConfig ? [] : [{ label: 'Configuration', route: 'configuration' }]), + { label: resolvedModel.secretsEngine.id, route: 'overview' }, + ...(resolvedModel.promptConfig ? [] : [{ label: 'Configuration', route: 'configuration' }]), { label: 'Configure' }, ]; } diff --git a/ui/lib/ldap/addon/routes/libraries/index.ts b/ui/lib/ldap/addon/routes/libraries/index.ts index 5c5e14db96..4f14dfe402 100644 --- a/ui/lib/ldap/addon/routes/libraries/index.ts +++ b/ui/lib/ldap/addon/routes/libraries/index.ts @@ -5,19 +5,19 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; import { hash } from 'rsvp'; import type Store from '@ember-data/store'; import type SecretMountPath from 'vault/services/secret-mount-path'; import type Transition from '@ember/routing/transition'; import type LdapLibraryModel from 'vault/models/ldap/library'; -import type SecretEngineModel from 'vault/models/secret-engine'; import type Controller from '@ember/controller'; import type { Breadcrumb } from 'vault/vault/app-types'; +import type SecretsEngineResource from 'vault/resources/secrets/engine'; +import type { LdapApplicationModel } from '../application'; interface LdapLibrariesRouteModel { - backendModel: SecretEngineModel; + secretsEngine: SecretsEngineResource; promptConfig: boolean; libraries: Array; } @@ -26,19 +26,16 @@ interface LdapLibrariesController extends Controller { model: LdapLibrariesRouteModel; } -@withConfig('ldap/config') export default class LdapLibrariesRoute extends Route { @service declare readonly store: Store; @service declare readonly secretMountPath: SecretMountPath; - declare promptConfig: boolean; - model() { - const backendModel = this.modelFor('application') as SecretEngineModel; + const { secretsEngine, promptConfig } = this.modelFor('application') as LdapApplicationModel; return hash({ - backendModel, - promptConfig: this.promptConfig, - libraries: this.store.query('ldap/library', { backend: backendModel.id }), + secretsEngine, + promptConfig, + libraries: this.store.query('ldap/library', { backend: secretsEngine.id }), }); } @@ -51,7 +48,7 @@ export default class LdapLibrariesRoute extends Route { controller.breadcrumbs = [ { label: 'Secrets', route: 'secrets', linkExternal: true }, - { label: resolvedModel.backendModel.id, route: 'overview', model: resolvedModel.backendModel.id }, + { label: resolvedModel.secretsEngine.id, route: 'overview', model: resolvedModel.secretsEngine.id }, { label: 'Libraries' }, ]; } diff --git a/ui/lib/ldap/addon/routes/libraries/subdirectory.ts b/ui/lib/ldap/addon/routes/libraries/subdirectory.ts index cab03c6b53..d83e8cd3ba 100644 --- a/ui/lib/ldap/addon/routes/libraries/subdirectory.ts +++ b/ui/lib/ldap/addon/routes/libraries/subdirectory.ts @@ -6,18 +6,18 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { hash } from 'rsvp'; +import { ldapBreadcrumbs, libraryRoutes } from 'ldap/utils/ldap-breadcrumbs'; import type Store from '@ember-data/store'; import type SecretMountPath from 'vault/services/secret-mount-path'; import type Transition from '@ember/routing/transition'; import type LdapLibraryModel from 'vault/models/ldap/library'; -import type SecretEngineModel from 'vault/models/secret-engine'; - -import { ldapBreadcrumbs, libraryRoutes } from 'ldap/utils/ldap-breadcrumbs'; import type LdapLibrariesSubdirectoryController from 'ldap/controllers/libraries/subdirectory'; +import type SecretsEngineResource from 'vault/resources/secrets/engine'; +import type { LdapApplicationModel } from '../application'; interface RouteModel { - backendModel: SecretEngineModel; + secretsEngine: SecretsEngineResource; path_to_library: string; libraries: Array; } @@ -32,17 +32,17 @@ export default class LdapLibrariesSubdirectoryRoute extends Route { @service declare readonly secretMountPath: SecretMountPath; model(params: RouteParams) { - const backendModel = this.modelFor('application') as SecretEngineModel; + const { secretsEngine } = this.modelFor('application') as LdapApplicationModel; const { path_to_library } = params; // Ensure path_to_library has trailing slash for proper API calls and model construction const normalizedPath = path_to_library?.endsWith('/') ? path_to_library : `${path_to_library}/`; return hash({ - backendModel, + secretsEngine, path_to_library: normalizedPath, libraries: this.store.query('ldap/library', { - backend: backendModel.id, + backend: secretsEngine.id, path_to_library: normalizedPath, }), }); @@ -52,14 +52,14 @@ export default class LdapLibrariesSubdirectoryRoute extends Route { super.setupController(controller, resolvedModel, transition); const routeParams = (childResource: string) => { - return [resolvedModel.backendModel.id, childResource]; + return [resolvedModel.secretsEngine.id, childResource]; }; const currentLevelPath = resolvedModel.path_to_library; controller.breadcrumbs = [ { label: 'Secrets', route: 'secrets', linkExternal: true }, - { label: resolvedModel.backendModel.id, route: 'overview' }, + { label: resolvedModel.secretsEngine.id, route: 'overview' }, { label: 'Libraries', route: 'libraries' }, ...ldapBreadcrumbs(currentLevelPath, routeParams, libraryRoutes, true), ]; diff --git a/ui/lib/ldap/addon/routes/overview.ts b/ui/lib/ldap/addon/routes/overview.ts index 86b3eea162..2f65cbb213 100644 --- a/ui/lib/ldap/addon/routes/overview.ts +++ b/ui/lib/ldap/addon/routes/overview.ts @@ -5,31 +5,30 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; import { hash } from 'rsvp'; import type Store from '@ember-data/store'; import type SecretMountPath from 'vault/services/secret-mount-path'; import type Transition from '@ember/routing/transition'; -import type SecretEngineModel from 'vault/models/secret-engine'; import type LdapRoleModel from 'vault/models/ldap/role'; import type LdapLibraryModel from 'vault/models/ldap/library'; import type Controller from '@ember/controller'; -import type { Breadcrumb } from 'vault/vault/app-types'; -import { LdapLibraryAccountStatus } from 'vault/vault/adapters/ldap/library'; +import type { Breadcrumb } from 'vault/app-types'; +import type { LdapLibraryAccountStatus } from 'vault/adapters/ldap/library'; +import type { LdapApplicationModel } from './application'; +import type SecretsEngineResource from 'vault/resources/secrets/engine'; interface RouteController extends Controller { breadcrumbs: Array; } interface RouteModel { - backendModel: SecretEngineModel; + secretsEngine: SecretsEngineResource; promptConfig: boolean; roles: Array; libraries: Array; librariesStatus: Array; } -@withConfig('ldap/config') export default class LdapOverviewRoute extends Route { @service declare readonly store: Store; @service declare readonly secretMountPath: SecretMountPath; @@ -37,10 +36,11 @@ export default class LdapOverviewRoute extends Route { declare promptConfig: boolean; async model() { + const { promptConfig, secretsEngine } = this.modelFor('application') as LdapApplicationModel; const backend = this.secretMountPath.currentPath; return hash({ - promptConfig: this.promptConfig, - backendModel: this.modelFor('application'), + promptConfig, + secretsEngine, roles: this.store.query('ldap/role', { backend }).catch(() => []), }); } @@ -50,7 +50,7 @@ export default class LdapOverviewRoute extends Route { controller.breadcrumbs = [ { label: 'Secrets', route: 'secrets', linkExternal: true }, - { label: resolvedModel.backendModel.id }, + { label: resolvedModel.secretsEngine.id }, ]; } } diff --git a/ui/lib/ldap/addon/routes/roles/index.ts b/ui/lib/ldap/addon/routes/roles/index.ts index 0868034e14..c40557817c 100644 --- a/ui/lib/ldap/addon/routes/roles/index.ts +++ b/ui/lib/ldap/addon/routes/roles/index.ts @@ -4,19 +4,17 @@ */ import LdapRolesRoute from '../roles'; -import { service } from '@ember/service'; -import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; import { hash } from 'rsvp'; -import type Store from '@ember-data/store'; import type Transition from '@ember/routing/transition'; import type LdapRoleModel from 'vault/models/ldap/role'; -import type SecretEngineModel from 'vault/models/secret-engine'; import type Controller from '@ember/controller'; import type { Breadcrumb } from 'vault/vault/app-types'; +import type SecretsEngineResource from 'vault/resources/secrets/engine'; +import { LdapApplicationModel } from '../application'; interface RouteModel { - backendModel: SecretEngineModel; + secretsEngine: SecretsEngineResource; promptConfig: boolean; roles: Array; } @@ -26,12 +24,7 @@ interface RouteController extends Controller { model: RouteModel; } -@withConfig('ldap/config') export default class LdapRolesIndexRoute extends LdapRolesRoute { - @service declare readonly store: Store; // necessary for @withConfig decorator - - declare promptConfig: boolean; - queryParams = { pageFilter: { refreshModel: true, @@ -42,11 +35,11 @@ export default class LdapRolesIndexRoute extends LdapRolesRoute { }; model(params: { page?: string; pageFilter: string }) { - const backendModel = this.modelFor('application') as SecretEngineModel; + const { secretsEngine, promptConfig } = this.modelFor('application') as LdapApplicationModel; return hash({ - backendModel, - promptConfig: this.promptConfig, - roles: this.lazyQuery(backendModel.id, params, { showPartialError: true }), + secretsEngine, + promptConfig, + roles: this.lazyQuery(secretsEngine.id, params, { showPartialError: true }), }); } @@ -55,7 +48,7 @@ export default class LdapRolesIndexRoute extends LdapRolesRoute { controller.breadcrumbs = [ { label: 'Secrets', route: 'secrets', linkExternal: true }, - { label: resolvedModel.backendModel.id, route: 'overview' }, + { label: resolvedModel.secretsEngine.id, route: 'overview' }, { label: 'Roles' }, ]; } diff --git a/ui/lib/ldap/addon/routes/roles/subdirectory.ts b/ui/lib/ldap/addon/routes/roles/subdirectory.ts index 0cae502af4..c20ee3a1f0 100644 --- a/ui/lib/ldap/addon/routes/roles/subdirectory.ts +++ b/ui/lib/ldap/addon/routes/roles/subdirectory.ts @@ -10,11 +10,12 @@ import { ldapBreadcrumbs, roleRoutes } from 'ldap/utils/ldap-breadcrumbs'; import type { Breadcrumb } from 'vault/vault/app-types'; import type Controller from '@ember/controller'; import type LdapRoleModel from 'vault/models/ldap/role'; -import type SecretEngineModel from 'vault/models/secret-engine'; import type Transition from '@ember/routing/transition'; +import type SecretsEngineResource from 'vault/resources/secrets/engine'; +import type { LdapApplicationModel } from '../application'; interface RouteModel { - backendModel: SecretEngineModel; + secretsEngine: SecretsEngineResource; roleAncestry: { path_to_role: string; type: string }; roles: Array; } @@ -42,27 +43,27 @@ export default class LdapRolesSubdirectoryRoute extends LdapRolesRoute { }; model(params: RouteParams) { - const backendModel = this.modelFor('application') as SecretEngineModel; + const { secretsEngine } = this.modelFor('application') as LdapApplicationModel; const { path_to_role, type } = params; const roleAncestry = { path_to_role, type }; return hash({ - backendModel, + secretsEngine, roleAncestry, - roles: this.lazyQuery(backendModel.id, params, { roleAncestry }), + roles: this.lazyQuery(secretsEngine.id, params, { roleAncestry }), }); } setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) { super.setupController(controller, resolvedModel, transition); - const { backendModel, roleAncestry } = resolvedModel; + const { secretsEngine, roleAncestry } = resolvedModel; const routeParams = (childResource: string) => { - return [backendModel.id, roleAncestry.type, childResource]; + return [secretsEngine.id, roleAncestry.type, childResource]; }; const crumbs = [ { label: 'Secrets', route: 'secrets', linkExternal: true }, - { label: backendModel.id, route: 'overview' }, + { label: secretsEngine.id, route: 'overview' }, { label: 'Roles', route: 'roles' }, ...ldapBreadcrumbs(roleAncestry.path_to_role, routeParams, roleRoutes, true), ]; diff --git a/ui/lib/ldap/addon/templates/libraries/index.hbs b/ui/lib/ldap/addon/templates/libraries/index.hbs index 6e1c1177f9..3aa7b61451 100644 --- a/ui/lib/ldap/addon/templates/libraries/index.hbs +++ b/ui/lib/ldap/addon/templates/libraries/index.hbs @@ -6,6 +6,6 @@ \ No newline at end of file diff --git a/ui/lib/ldap/addon/templates/libraries/subdirectory.hbs b/ui/lib/ldap/addon/templates/libraries/subdirectory.hbs index e484e1bb42..b68a5e04da 100644 --- a/ui/lib/ldap/addon/templates/libraries/subdirectory.hbs +++ b/ui/lib/ldap/addon/templates/libraries/subdirectory.hbs @@ -6,6 +6,6 @@ \ No newline at end of file diff --git a/ui/lib/ldap/addon/templates/overview.hbs b/ui/lib/ldap/addon/templates/overview.hbs index 14d50f9062..015687f691 100644 --- a/ui/lib/ldap/addon/templates/overview.hbs +++ b/ui/lib/ldap/addon/templates/overview.hbs @@ -5,7 +5,7 @@ \ No newline at end of file diff --git a/ui/lib/ldap/addon/templates/roles/index.hbs b/ui/lib/ldap/addon/templates/roles/index.hbs index 1a29172964..fbc4449406 100644 --- a/ui/lib/ldap/addon/templates/roles/index.hbs +++ b/ui/lib/ldap/addon/templates/roles/index.hbs @@ -6,7 +6,7 @@ \ No newline at end of file diff --git a/ui/lib/ldap/addon/templates/roles/subdirectory.hbs b/ui/lib/ldap/addon/templates/roles/subdirectory.hbs index 5a1edeca1c..84743ecd85 100644 --- a/ui/lib/ldap/addon/templates/roles/subdirectory.hbs +++ b/ui/lib/ldap/addon/templates/roles/subdirectory.hbs @@ -7,9 +7,9 @@ {{/let}} \ No newline at end of file diff --git a/ui/tests/helpers/ldap/ldap-helpers.js b/ui/tests/helpers/ldap/ldap-helpers.js index 3c0059c43d..d5d2951001 100644 --- a/ui/tests/helpers/ldap/ldap-helpers.js +++ b/ui/tests/helpers/ldap/ldap-helpers.js @@ -4,17 +4,23 @@ */ import { visit, currentURL } from '@ember/test-helpers'; +import SecretsEngineResource from 'vault/resources/secrets/engine'; export const createSecretsEngine = (store) => { - store.pushPayload('secret-engine', { - modelName: 'secret-engine', - data: { - accessor: 'ldap_7e838627', - path: 'ldap-test/', - type: 'ldap', - }, - }); - return store.peekRecord('secret-engine', 'ldap-test'); + const data = { + accessor: 'ldap_7e838627', + path: 'ldap-test/', + type: 'ldap', + }; + if (store) { + store.pushPayload('secret-engine', { + modelName: 'secret-engine', + data, + }); + return store.peekRecord('secret-engine', 'ldap-test'); + } else { + return new SecretsEngineResource(data); + } }; export const generateBreadcrumbs = (backend, childRoute) => { diff --git a/ui/tests/integration/components/ldap/page/configuration-test.js b/ui/tests/integration/components/ldap/page/configuration-test.js index 6032f1c834..ebefe50142 100644 --- a/ui/tests/integration/components/ldap/page/configuration-test.js +++ b/ui/tests/integration/components/ldap/page/configuration-test.js @@ -15,6 +15,7 @@ import { setRunOptions } from 'ember-a11y-testing/test-support'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import engineDisplayData from 'vault/helpers/engines-display-data'; import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors'; +import sinon from 'sinon'; module('Integration | Component | ldap | Page::Configuration', function (hooks) { setupRenderingTest(hooks); @@ -22,37 +23,23 @@ module('Integration | Component | ldap | Page::Configuration', function (hooks) setupMirage(hooks); hooks.beforeEach(function () { - this.store = this.owner.lookup('service:store'); - - this.backend = createSecretsEngine(this.store); - this.breadcrumbs = generateBreadcrumbs(this.backend.id); - - this.store.pushPayload('ldap/config', { - modelName: 'ldap/config', - backend: 'ldap-test', - ...this.server.create('ldap-config'), - }); - this.config = this.store.peekRecord('ldap/config', 'ldap-test'); - + this.secretsEngine = createSecretsEngine(); + this.owner.lookup('service:secret-mount-path').update(this.secretsEngine.path); + this.breadcrumbs = generateBreadcrumbs(this.secretsEngine.id); + this.config = this.server.create('ldap-config'); this.model = { - backendModel: this.backend, + secretsEngine: this.secretsEngine, promptConfig: true, - configModel: this.config, + config: this.config, configError: null, - engineDisplayData: engineDisplayData(this.backend.type), + engineDisplayData: engineDisplayData(this.secretsEngine.type), }; - this.renderComponent = () => { - return render( - hbs``, - { - owner: this.engine, - } - ); - }; + this.renderComponent = () => + render(hbs``, { + owner: this.engine, + }); + setRunOptions({ rules: { // TODO: fix ConfirmAction rendered in toolbar not a list item @@ -63,7 +50,7 @@ module('Integration | Component | ldap | Page::Configuration', function (hooks) }); test('it should render tab page header', async function (assert) { - this.model.configModel = null; + this.model.config = null; await this.renderComponent(); @@ -76,8 +63,8 @@ module('Integration | Component | ldap | Page::Configuration', function (hooks) }); test('it should render config fetch error', async function (assert) { - this.model.configModel = null; - this.model.configError = { httpStatus: 403, message: 'Permission denied' }; + this.model.config = null; + this.model.configError = { status: 403, message: 'Permission denied' }; await this.renderComponent(); @@ -87,32 +74,33 @@ module('Integration | Component | ldap | Page::Configuration', function (hooks) test('it should render display fields', async function (assert) { await this.renderComponent(); - assert.dom(GENERAL.infoRowValue('Administrator Distinguished Name')).hasText(this.config.binddn); + assert.dom(GENERAL.infoRowValue('Administrator distinguished name')).hasText(this.config.binddn); assert.dom(GENERAL.infoRowValue('URL')).hasText(this.config.url); assert.dom(GENERAL.infoRowValue('Schema')).hasText(this.config.schema); - assert.dom(GENERAL.infoRowValue('Password Policy')).hasText(this.config.password_policy); + assert.dom(GENERAL.infoRowValue('Password policy')).hasText(this.config.password_policy); assert.dom(GENERAL.infoRowValue('Userdn')).hasText(this.config.userdn); assert.dom(GENERAL.infoRowValue('Userattr')).hasText(this.config.userattr); assert - .dom(GENERAL.infoRowValue('Connection Timeout')) + .dom(GENERAL.infoRowValue('Connection timeout')) .hasText(duration([this.config.connection_timeout])); - assert.dom(GENERAL.infoRowValue('Request Timeout')).hasText(duration([this.config.request_timeout])); - assert.dom(GENERAL.infoRowValue('CA Certificate')).hasText(this.config.certificate); + assert.dom(GENERAL.infoRowValue('Request timeout')).hasText(duration([this.config.request_timeout])); + assert.dom(GENERAL.infoRowValue('CA certificate')).hasText(this.config.certificate); assert.dom(GENERAL.infoRowValue('Start TLS')).includesText('No'); assert.dom(GENERAL.infoRowValue('Insecure TLS')).includesText('No'); - assert.dom(GENERAL.infoRowValue('Client TLS Certificate')).hasText(this.config.client_tls_cert); - assert.dom(GENERAL.infoRowValue('Client TLS Key')).hasText(this.config.client_tls_key); + assert.dom(GENERAL.infoRowValue('Client TLS certificate')).hasText(this.config.client_tls_cert); + assert.dom(GENERAL.infoRowValue('Client TLS key')).hasText(this.config.client_tls_key); }); test('it should rotate root password', async function (assert) { assert.expect(1); - this.server.post(`/${this.config.backend}/rotate-root`, () => { - assert.ok(true, 'Request made to rotate root password'); - }); + const rotateStub = sinon + .stub(this.owner.lookup('service:api').secrets, 'ldapRotateRootCredentials') + .resolves(); await this.renderComponent(); await click(GENERAL.confirmTrigger); await click(GENERAL.confirmButton); + assert.true(rotateStub.calledWith(this.secretsEngine.path), 'rotate root called with correct mount path'); }); }); diff --git a/ui/tests/integration/components/ldap/page/configure-test.js b/ui/tests/integration/components/ldap/page/configure-test.js index 668a426a8c..503965913f 100644 --- a/ui/tests/integration/components/ldap/page/configure-test.js +++ b/ui/tests/integration/components/ldap/page/configure-test.js @@ -6,25 +6,23 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { setupEngine } from 'ember-engines/test-support'; -import { setupMirage } from 'ember-cli-mirage/test-support'; import { render, click, fillIn } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; -import { Response } from 'miragejs'; import sinon from 'sinon'; import { generateBreadcrumbs } from 'vault/tests/helpers/ldap/ldap-helpers'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import LdapConfigForm from 'vault/forms/secrets/ldap/config'; const selectors = { radioCard: '[data-test-radio-card="OpenLDAP"]', save: '[data-test-config-save]', binddn: '[data-test-field="binddn"] input', - bindpass: '[data-test-field="bindpass"] input', + bindpass: '[data-test-input="bindpass"]', }; module('Integration | Component | ldap | Page::Configure', function (hooks) { setupRenderingTest(hooks); setupEngine(hooks, 'ldap'); - setupMirage(hooks); const fillAndSubmit = async (rotate) => { await click(selectors.radioCard); @@ -33,30 +31,30 @@ module('Integration | Component | ldap | Page::Configure', function (hooks) { await click(selectors.save); const buttonLabel = rotate === 'without' ? 'Save without rotating' : 'Save and rotate'; await click(GENERAL.button(buttonLabel)); + return { binddn: 'foo', bindpass: 'bar', schema: 'openldap', groupattr: 'cn', userattr: 'cn' }; }; hooks.beforeEach(function () { - this.store = this.owner.lookup('service:store'); - this.newModel = this.store.createRecord('ldap/config', { backend: 'ldap-new' }); + this.newForm = new LdapConfigForm({}, { isNew: true }); this.existingConfig = { schema: 'openldap', binddn: 'cn=vault,ou=Users,dc=hashicorp,dc=com', bindpass: 'foobar', }; - this.store.pushPayload('ldap/config', { - modelName: 'ldap/config', - backend: 'ldap-edit', - ...this.existingConfig, - }); - this.editModel = this.store.peekRecord('ldap/config', 'ldap-edit'); + this.editForm = new LdapConfigForm(this.existingConfig); this.breadcrumbs = generateBreadcrumbs('ldap', 'configure'); - this.model = { promptConfig: true, configModel: this.newModel }; // most of the tests use newModel but set this to editModel when needed - this.renderComponent = () => { - return render(hbs``, { + this.model = { promptConfig: true, form: this.newForm }; // most of the tests use newForm but set this to editForm when needed + + this.owner.lookup('service:secret-mount-path').update('ldap-new'); + this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo'); + const { secrets } = this.owner.lookup('service:api'); + this.configStub = sinon.stub(secrets, 'ldapConfigure').resolves(); + this.rotateStub = sinon.stub(secrets, 'ldapRotateRootCredentials').resolves(); + + this.renderComponent = () => + render(hbs``, { owner: this.engine, }); - }; - this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo'); }); test('it should render empty state when schema is not selected', async function (assert) { @@ -94,14 +92,13 @@ module('Integration | Component | ldap | Page::Configure', function (hooks) { test('it should save new configuration without rotating root password', async function (assert) { assert.expect(2); - this.server.post('/ldap-new/config', () => { - assert.ok(true, 'POST request made to save config'); - return new Response(204, {}); - }); - await this.renderComponent(); - await fillAndSubmit('without'); + const payload = await fillAndSubmit('without'); + assert.true( + this.configStub.calledWith('ldap-new', payload), + 'Config save called with correct mount path' + ); assert.ok( this.transitionStub.calledWith('vault.cluster.secrets.backend.ldap.configuration'), 'Transitions to configuration route on save success' @@ -111,18 +108,13 @@ module('Integration | Component | ldap | Page::Configure', function (hooks) { test('it should save new configuration and rotate root password', async function (assert) { assert.expect(3); - this.server.post('/ldap-new/config', () => { - assert.ok(true, 'POST request made to save config'); - return new Response(204, {}); - }); - this.server.post('/ldap-new/rotate-root', () => { - assert.ok(true, 'POST request made to rotate root password'); - return new Response(204, {}); - }); - await this.renderComponent(); - await fillAndSubmit('with'); - + const payload = await fillAndSubmit('with'); + assert.true( + this.configStub.calledWith('ldap-new', payload), + 'Config save called with correct mount path' + ); + assert.true(this.rotateStub.calledWith('ldap-new'), 'Rotate root called with correct mount path'); assert.ok( this.transitionStub.calledWith('vault.cluster.secrets.backend.ldap.configuration'), 'Transitions to configuration route on save success' @@ -130,7 +122,7 @@ module('Integration | Component | ldap | Page::Configure', function (hooks) { }); test('it should populate fields when editing form', async function (assert) { - this.model = { promptConfig: true, configModel: this.editModel }; + this.model = { promptConfig: true, form: this.editForm }; await this.renderComponent(); @@ -140,12 +132,6 @@ module('Integration | Component | ldap | Page::Configure', function (hooks) { await fillIn(selectors.binddn, 'foobar'); await click('[data-test-config-cancel]'); - assert.strictEqual( - this.model.configModel.binddn, - this.existingConfig.binddn, - 'Model is rolled back on cancel' - ); - assert.ok( this.transitionStub.calledWith('vault.cluster.secrets.backend.ldap.configuration'), 'Transitions to configuration route on save success' diff --git a/ui/tests/integration/components/ldap/page/libraries-test.js b/ui/tests/integration/components/ldap/page/libraries-test.js index f8e649b698..8070b9d3ad 100644 --- a/ui/tests/integration/components/ldap/page/libraries-test.js +++ b/ui/tests/integration/components/ldap/page/libraries-test.js @@ -41,7 +41,7 @@ module('Integration | Component | ldap | Page::Libraries', function (hooks) { return render( hbs``, diff --git a/ui/tests/integration/components/ldap/page/overview-test.js b/ui/tests/integration/components/ldap/page/overview-test.js index 70df76de3a..58c8b8b733 100644 --- a/ui/tests/integration/components/ldap/page/overview-test.js +++ b/ui/tests/integration/components/ldap/page/overview-test.js @@ -59,7 +59,7 @@ module('Integration | Component | ldap | Page::Overview', function (hooks) { return render( hbs`