[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 <zofskeez@gmail.com>
This commit is contained in:
Vault Automation 2025-12-05 12:43:38 -05:00 committed by GitHub
parent cfc130b40b
commit c34e25fb76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 379 additions and 346 deletions

View file

@ -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<LdapConfigureRequest> {
constructor(...args: ConstructorParameters<typeof Form>) {
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.' }],
};
}

View file

@ -28,6 +28,8 @@ export interface FieldOptions {
labelDisabled?: string;
mapToBoolean?: string;
isOppositeValue?: boolean;
defaultSubText?: string;
defaultShown?: string;
}
export default class FormField {

View file

@ -4,13 +4,13 @@
}}
<LdapHeader
@model={{@model.backendModel}}
@model={{@model.secretsEngine}}
@promptConfig={{@model.promptConfig}}
@breadcrumbs={{@breadcrumbs}}
@configRoute="configuration"
>
<:toolbarActions>
{{#if @model.configModel}}
{{#if @model.config}}
<ConfirmAction
@buttonText="Rotate root"
class="toolbar-button"
@ -22,7 +22,7 @@
@isRunning={{this.rotateRoot.isRunning}}
/>
{{/if}}
{{#if @model.configModel}}
{{#if @model.config}}
<ToolbarLink @route="configure" data-test-secret-backend-configure>
Edit configuration
</ToolbarLink>
@ -30,16 +30,21 @@
</:toolbarActions>
</LdapHeader>
{{#if @model.configModel}}
{{#if @model.config}}
{{#each this.defaultFields as |field|}}
<InfoTableRow @label={{field.label}} @value={{field.value}} @formatTtl={{field.formatTtl}} @alwaysRender={{true}} />
<InfoTableRow
@label={{this.label field}}
@value={{get @model.config field}}
@formatTtl={{includes field (array "request_timeout" "connection_timeout")}}
@alwaysRender={{true}}
/>
{{/each}}
<h2 class="title is-4 has-top-margin-xl">TLS Connection</h2>
<hr class="is-marginless" />
{{#each this.connectionFields as |field|}}
<InfoTableRow @label={{field.label}} @value={{field.value}} @alwaysRender={{true}} />
<InfoTableRow @label={{this.label field}} @value={{get @model.config field}} @alwaysRender={{true}} />
{{/each}}
{{else if @model.configError}}
<Page::Error @error={{@model.configError}} />

View file

@ -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<Breadcrumb>;
}
interface Field {
label: string;
value: any; // eslint-disable-line @typescript-eslint/no-explicit-any
formatTtl?: boolean;
}
export default class LdapConfigurationPageComponent extends Component<Args> {
@service declare readonly flashMessages: FlashMessageService;
@service declare readonly api: ApiService;
@service declare readonly secretMountPath: SecretMountPath;
get defaultFields(): Array<Field> {
const model = this.args.model.configModel;
const keys = [
'binddn',
'url',
'schema',
'password_policy',
'userdn',
'userattr',
'connection_timeout',
'request_timeout',
];
return model.allFields.reduce<Array<Field>>((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<Field> {
const model = this.args.model.configModel;
const keys = ['certificate', 'starttls', 'insecure_tls', 'client_tls_cert', 'client_tls_key'];
return model.allFields.reduce<Array<Field>>((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)}`);
}
}
})
);
}

View file

@ -4,7 +4,7 @@
}}
<LdapHeader
@model={{@model.backendModel}}
@model={{@model.secretsEngine}}
@promptConfig={{@model.promptConfig}}
@breadcrumbs={{@breadcrumbs}}
@configRoute="configure"
@ -14,8 +14,8 @@
<Hds::Form::RadioCard::Group @name="schema options" as |RadioGroup|>
{{#each this.schemaOptions as |option|}}
<RadioGroup.RadioCard
@checked={{eq option.value @model.configModel.schema}}
{{on "change" (fn (mut @model.configModel.schema) option.value)}}
@checked={{eq option.value @model.form.data.schema}}
{{on "change" (fn (mut @model.form.data.schema) option.value)}}
data-test-radio-card={{option.title}}
as |Card|
>
@ -32,13 +32,9 @@
<h2 class="title is-4">Schema Options</h2>
<hr class="has-background-gray-200" />
{{#if @model.configModel.schema}}
{{#if @model.form.data.schema}}
<div class="has-top-margin-l">
<FormFieldGroups
@model={{@model.configModel}}
@groupName="formFieldGroups"
@modelValidations={{this.modelValidations}}
/>
<FormFieldGroups @model={{@model.form}} @groupName="formFieldGroups" @modelValidations={{this.modelValidations}} />
</div>
{{else}}
<EmptyState
@ -56,7 +52,7 @@
@text="Save"
data-test-config-save
type="submit"
disabled={{or this.save.isRunning (not @model.configModel.schema)}}
disabled={{or this.save.isRunning (not @model.form.data.schema)}}
/>
<Hds::Button
@text="Back"

View file

@ -9,15 +9,16 @@ import { action } from '@ember/object';
import { service } from '@ember/service';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
import errorMessage from 'vault/utils/error-message';
import type LdapConfigModel from 'vault/models/ldap/config';
import { Breadcrumb, ValidationMap } from 'vault/vault/app-types';
import type { LdapConfigureModel } from 'ldap/routes/configure';
import type { Breadcrumb, ValidationMap } from 'vault/vault/app-types';
import type FlashMessageService from 'vault/services/flash-messages';
import type RouterService from '@ember/routing/router-service';
import type ApiService from 'vault/services/api';
import type SecretMountPath from 'vault/services/secret-mount-path';
interface Args {
model: { configModel: LdapConfigModel };
model: LdapConfigureModel;
breadcrumbs: Array<Breadcrumb>;
}
interface SchemaOption {
@ -30,6 +31,8 @@ interface SchemaOption {
export default class LdapConfigurePageComponent extends Component<Args> {
@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<Args> {
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<Args> {
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);
})
);
}

View file

@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
}}
<LdapHeader @model={{@backendModel}} @promptConfig={{@promptConfig}} @breadcrumbs={{@breadcrumbs}}>
<LdapHeader @model={{@secretsEngine}} @promptConfig={{@promptConfig}} @breadcrumbs={{@breadcrumbs}}>
<:toolbarFilters>
{{#if (and (not @promptConfig) @libraries)}}
<FilterInput

View file

@ -11,15 +11,15 @@ import { getOwner } from '@ember/owner';
import errorMessage from 'vault/utils/error-message';
import type LdapLibraryModel from 'vault/models/ldap/library';
import type SecretEngineModel from 'vault/models/secret-engine';
import type FlashMessageService from 'vault/services/flash-messages';
import type { Breadcrumb, EngineOwner } from 'vault/vault/app-types';
import type RouterService from '@ember/routing/router-service';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
interface Args {
libraries: Array<LdapLibraryModel>;
promptConfig: boolean;
backendModel: SecretEngineModel;
secretsEngine: SecretsEngineResource;
breadcrumbs: Array<Breadcrumb>;
}

View file

@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
}}
<LdapHeader @model={{@backendModel}} @promptConfig={{@promptConfig}} @breadcrumbs={{@breadcrumbs}} />
<LdapHeader @model={{@secretsEngine}} @promptConfig={{@promptConfig}} @breadcrumbs={{@breadcrumbs}} />
{{#if @promptConfig}}
<ConfigCta />

View file

@ -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<LdapRoleModel>;
promptConfig: boolean;
backendModel: SecretEngineModel;
secretsEngine: SecretsEngineResource;
breadcrumbs: Array<Breadcrumb>;
}
@ -72,7 +72,7 @@ export default class LdapLibrariesPageComponent extends Component<Args> {
}
fetchLibraries = restartableTask(async () => {
const backend = this.args.backendModel.id;
const backend = this.args.secretsEngine.id;
const allLibraries: Array<LdapLibraryModel> = [];
try {

View file

@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
}}
<LdapHeader @model={{@backendModel}} @promptConfig={{@promptConfig}} @breadcrumbs={{@breadcrumbs}}>
<LdapHeader @model={{@secretsEngine}} @promptConfig={{@promptConfig}} @breadcrumbs={{@breadcrumbs}}>
<:toolbarFilters>
{{#if (and (not @promptConfig) @roles.meta.total)}}
<FilterInput

View file

@ -11,16 +11,16 @@ import errorMessage from 'vault/utils/error-message';
import { tracked } from '@glimmer/tracking';
import type LdapRoleModel from 'vault/models/ldap/role';
import type SecretEngineModel from 'vault/models/secret-engine';
import type FlashMessageService from 'vault/services/flash-messages';
import type { Breadcrumb, EngineOwner } from 'vault/vault/app-types';
import type RouterService from '@ember/routing/router-service';
import type PaginationService from 'vault/services/pagination';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
interface Args {
roles: Array<LdapRoleModel>;
promptConfig: boolean;
backendModel: SecretEngineModel;
secretsEngine: SecretsEngineResource;
breadcrumbs: Array<Breadcrumb>;
pageFilter: string;
}

View file

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

View file

@ -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<LdapApplicationRoute>;
export default class LdapApplicationRoute extends Route {
@service declare readonly api: ApiService;
async model(params: Record<string, unknown>, 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,
};
}
}

View file

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

View file

@ -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<Breadcrumb>;
}
interface RouteModel {
backendModel: SecretEngineModel;
promptConfig: boolean;
configModel: LdapConfigModel;
engineDisplayData: SecretsEngineResource;
}
export type LdapConfigureModel = ModelFrom<LdapConfigureRoute>;
@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' },
];
}

View file

@ -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<LdapLibraryModel>;
}
@ -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' },
];
}

View file

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

View file

@ -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<Breadcrumb>;
}
interface RouteModel {
backendModel: SecretEngineModel;
secretsEngine: SecretsEngineResource;
promptConfig: boolean;
roles: Array<LdapRoleModel>;
libraries: Array<LdapLibraryModel>;
librariesStatus: Array<LdapLibraryAccountStatus>;
}
@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 },
];
}
}

View file

@ -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<LdapRoleModel>;
}
@ -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' },
];
}

View file

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

View file

@ -6,6 +6,6 @@
<Page::Libraries
@libraries={{this.model.libraries}}
@promptConfig={{this.model.promptConfig}}
@backendModel={{this.model.backendModel}}
@secretsEngine={{this.model.secretsEngine}}
@breadcrumbs={{this.breadcrumbs}}
/>

View file

@ -6,6 +6,6 @@
<Page::Libraries
@libraries={{this.model.libraries}}
@promptConfig={{false}}
@backendModel={{this.model.backendModel}}
@secretsEngine={{this.model.secretsEngine}}
@breadcrumbs={{this.breadcrumbs}}
/>

View file

@ -5,7 +5,7 @@
<Page::Overview
@promptConfig={{this.model.promptConfig}}
@backendModel={{this.model.backendModel}}
@secretsEngine={{this.model.secretsEngine}}
@roles={{this.model.roles}}
@breadcrumbs={{this.breadcrumbs}}
/>

View file

@ -6,7 +6,7 @@
<Page::Roles
@roles={{this.model.roles}}
@promptConfig={{this.model.promptConfig}}
@backendModel={{this.model.backendModel}}
@secretsEngine={{this.model.secretsEngine}}
@breadcrumbs={{this.breadcrumbs}}
@pageFilter={{this.pageFilter}}
/>

View file

@ -7,9 +7,9 @@
<Page::Roles
@roles={{this.model.roles}}
@promptConfig={{false}}
@backendModel={{this.model.backendModel}}
@secretsEngine={{this.model.secretsEngine}}
@breadcrumbs={{this.breadcrumbs}}
@pageFilter={{this.pageFilter}}
@currentRouteParams={{array this.model.backendModel.id roleType path_to_role}}
@currentRouteParams={{array this.model.secretsEngine.id roleType path_to_role}}
/>
{{/let}}

View file

@ -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) => {

View file

@ -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`<Page::Configuration
@model={{this.model}}
@breadcrumbs={{this.breadcrumbs}}
/>`,
{
owner: this.engine,
}
);
};
this.renderComponent = () =>
render(hbs`<Page::Configuration @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`, {
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');
});
});

View file

@ -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`<Page::Configure @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`, {
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`<Page::Configure @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`, {
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'

View file

@ -41,7 +41,7 @@ module('Integration | Component | ldap | Page::Libraries', function (hooks) {
return render(
hbs`<Page::Libraries
@promptConfig={{this.promptConfig}}
@backendModel={{this.backend}}
@secretsEngine={{this.backend}}
@libraries={{this.libraries}}
@breadcrumbs={{this.breadcrumbs}}
/>`,

View file

@ -59,7 +59,7 @@ module('Integration | Component | ldap | Page::Overview', function (hooks) {
return render(
hbs`<Page::Overview
@promptConfig={{this.promptConfig}}
@backendModel={{this.backendModel}}
@secretsEngine={{this.backendModel}}
@roles={{this.roles}}
@libraries={{this.libraries}}
@librariesStatus={{(array)}}

View file

@ -49,7 +49,7 @@ module('Integration | Component | ldap | Page::Roles', function (hooks) {
return render(
hbs`<Page::Roles
@promptConfig={{this.promptConfig}}
@backendModel={{this.backend}}
@secretsEngine={{this.backend}}
@roles={{this.roles}}
@breadcrumbs={{this.breadcrumbs}}
@pageFilter={{this.pageFilter}}