mirror of
https://github.com/hashicorp/vault.git
synced 2026-06-04 14:25:35 -04:00
* 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:
parent
cfc130b40b
commit
c34e25fb76
32 changed files with 379 additions and 346 deletions
70
ui/app/forms/secrets/ldap/config.ts
Normal file
70
ui/app/forms/secrets/ldap/config.ts
Normal 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.' }],
|
||||
};
|
||||
}
|
||||
|
|
@ -28,6 +28,8 @@ export interface FieldOptions {
|
|||
labelDisabled?: string;
|
||||
mapToBoolean?: string;
|
||||
isOppositeValue?: boolean;
|
||||
defaultSubText?: string;
|
||||
defaultShown?: string;
|
||||
}
|
||||
|
||||
export default class FormField {
|
||||
|
|
|
|||
|
|
@ -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}} />
|
||||
|
|
|
|||
|
|
@ -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)}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
48
ui/lib/ldap/addon/routes/application.ts
Normal file
48
ui/lib/ldap/addon/routes/application.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
/>
|
||||
|
|
@ -6,6 +6,6 @@
|
|||
<Page::Libraries
|
||||
@libraries={{this.model.libraries}}
|
||||
@promptConfig={{false}}
|
||||
@backendModel={{this.model.backendModel}}
|
||||
@secretsEngine={{this.model.secretsEngine}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
|
|
@ -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}}
|
||||
/>
|
||||
|
|
@ -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}}
|
||||
/>
|
||||
|
|
@ -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}}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
/>`,
|
||||
|
|
|
|||
|
|
@ -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)}}
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
|
|||
Loading…
Reference in a new issue