diff --git a/ui/docs/fetch-secrets-engine-config.md b/ui/docs/fetch-secrets-engine-config.md
new file mode 100644
index 0000000000..3bdca22932
--- /dev/null
+++ b/ui/docs/fetch-secrets-engine-config.md
@@ -0,0 +1,92 @@
+# Fetch Secrets Engine Configuration Decorator
+
+The `fetch-secrets-engine-config` decorator is available in the core addon and can be used on a route that needs to be aware of the configuration details of a secrets engine prior to model hook execution. This is useful for conditionally displaying a call to action for the user to complete the configuration.
+
+## API
+
+The decorator accepts a single argument with the name of the Ember Data model to be fetched.
+
+- **modelName** [string] - name of the Ember Data model to fetch which is passed to the `queryRecord` method.
+
+With the provided model name, the decorator fetches the record using the store `queryRecord` method in the `beforeModel` route hook. Several properties are set on the route class based on the status of the request:
+
+- **configModel** [Model | null] - set on success with resolved Ember Data model.
+
+- **configError** [AdapterError | null] - set if the request errors with any status other than 404.
+
+- **promptConfig** [boolean] - set to `true` if the request returns a 404, otherwise set to `false`. This is for convenience since checking for `(!this.configModel && !this.configError)` would result in the same value.
+
+## Usage
+
+### Configure route
+
+```js
+@withConfig('foo/config')
+export default class FooConfigureRoute extends Route {
+ @service store;
+ @service secretMountPath;
+
+ model() {
+ const backend = this.secretMountPath.currentPath;
+ return this.configModel || this.store.createRecord('foo/config', { backend });
+ }
+}
+```
+
+In the scenario of creating/updating the configuration, the model is used to populate the form if available, otherwise the form is presented in an empty state. Fetch errors are not a concern, nor is prompting the user to configure so only the `configModel` property is used.
+
+### Configuration route
+
+```js
+@withConfig('foo/config')
+export default class FooConfigurationRoute extends Route {
+ @service store;
+ @service secretMountPath;
+
+ model() {
+ // the error could also be thrown to display the error template
+ // in this example a component is used to display the error
+ return {
+ configModel: this.configModel,
+ configError: this.configError,
+ };
+ }
+}
+```
+
+For configuration routes, the model and error properties may be used to determine what should be displayed to the user:
+
+`configuration.hbs`
+
+```hbs
+{{#if @configModel}}
+ {{#each @configModel.fields as |field|}}
+
+ {{/each}}
+{{else if @configError}}
+
+{{else}}
+
+{{/if}}
+```
+
+### Other routes (overview etc.)
+
+This is the most basic usage where a route only needs to be aware of whether or not to show the config prompt:
+
+```js
+@withConfig('foo/config')
+export default class FooOverviewRoute extends Route {
+ @service store;
+ @service secretMountPath;
+
+ model() {
+ const backend = this.secretMountPath.currentPath;
+ return hash({
+ promptConfig: this.promptConfig,
+ roles: this.store.query('foo/role', { backend }).catch(() => []),
+ libraries: this.store.query('foo/library', { backend }).catch(() => []),
+ });
+ }
+}
+```
diff --git a/ui/lib/core/addon/components/filter-input.hbs b/ui/lib/core/addon/components/filter-input.hbs
new file mode 100644
index 0000000000..5b00156a3d
--- /dev/null
+++ b/ui/lib/core/addon/components/filter-input.hbs
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/ui/lib/core/addon/components/filter-input.ts b/ui/lib/core/addon/components/filter-input.ts
new file mode 100644
index 0000000000..57df994000
--- /dev/null
+++ b/ui/lib/core/addon/components/filter-input.ts
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { debounce } from '@ember/runloop';
+
+import type { HTMLElementEvent } from 'vault/forms';
+
+interface Args {
+ placeholder?: string; // defaults to Type to filter results
+ wait?: number; // defaults to 200
+ onInput(value: string): void;
+}
+
+export default class FilterInputComponent extends Component
{
+ get placeholder() {
+ return this.args.placeholder || 'Type to filter results';
+ }
+
+ @action onInput(event: HTMLElementEvent) {
+ const callback = () => {
+ this.args.onInput(event.target.value);
+ };
+ const wait = this.args.wait || 200;
+ // ts complains when trying to pass object of optional args to callback as 3rd arg to debounce
+ debounce(this, callback, wait);
+ }
+}
diff --git a/ui/lib/core/addon/components/form-field.hbs b/ui/lib/core/addon/components/form-field.hbs
index 144edd9021..7c9e4b5af0 100644
--- a/ui/lib/core/addon/components/form-field.hbs
+++ b/ui/lib/core/addon/components/form-field.hbs
@@ -57,14 +57,6 @@
{{/each}}
- {{#if this.validationError}}
-
{{else if (eq @attr.options.editType "stringArray")}}
- {{#if this.validationError}}
-
- {{/if}}
{{else if (or (eq @attr.type "number") (eq @attr.type "string"))}}
{{#if (eq @attr.options.editType "textarea")}}
@@ -224,9 +212,6 @@
oninput={{this.onChangeWithEvent}}
class="textarea {{if this.validationError 'has-error-border'}}"
>
- {{#if this.validationError}}
-
- {{/if}}
{{else if (eq @attr.options.editType "password")}}
{{#if @attr.options.allowReset}}
+ {{! TODO: explore removing in favor of new model validations pattern since it is only used on the namespace model }}
{{#if @attr.options.validationAttr}}
{{#if (and (get @model this.valuePath) (not (get @model @attr.options.validationAttr)))}}
{{/if}}
{{/if}}
- {{#if this.validationError}}
-
- {{/if}}
- {{#if this.validationWarning}}
-
- {{/if}}
{{/if}}
{{else if (or (eq @attr.type "boolean") (eq @attr.options.editType "boolean"))}}
@@ -347,8 +323,27 @@
@value={{if (get @model this.valuePath) (stringify (get @model this.valuePath)) this.emptyData}}
@valueUpdated={{fn this.codemirrorUpdated false}}
@helpText={{@attr.options.helpText}}
+ @example={{@attr.options.example}}
/>
{{else if (eq @attr.options.editType "yield")}}
{{yield}}
{{/if}}
+ {{#if this.validationError}}
+
+ {{/if}}
+ {{#if this.validationWarning}}
+
+ {{/if}}
\ No newline at end of file
diff --git a/ui/lib/core/addon/components/json-editor.hbs b/ui/lib/core/addon/components/json-editor.hbs
index 1aca88bf9b..c7e42ca51a 100644
--- a/ui/lib/core/addon/components/json-editor.hbs
+++ b/ui/lib/core/addon/components/json-editor.hbs
@@ -15,6 +15,18 @@
{{yield}}
+ {{#if @example}}
+
+ Restore example
+
+
+ {{/if}}
{{@cardTitle}}
diff --git a/ui/lib/core/addon/components/page/breadcrumbs.hbs b/ui/lib/core/addon/components/page/breadcrumbs.hbs
index d466215500..8766b8b866 100644
--- a/ui/lib/core/addon/components/page/breadcrumbs.hbs
+++ b/ui/lib/core/addon/components/page/breadcrumbs.hbs
@@ -9,14 +9,16 @@
/
{{#if breadcrumb.linkExternal}}
- {{breadcrumb.label}}
+
+ {{breadcrumb.label}}
+
{{else if breadcrumb.route}}
{{#if breadcrumb.model}}
-
+
{{breadcrumb.label}}
{{else}}
-
+
{{breadcrumb.label}}
{{/if}}
diff --git a/ui/lib/core/addon/components/secrets-engine-mount-config.hbs b/ui/lib/core/addon/components/secrets-engine-mount-config.hbs
new file mode 100644
index 0000000000..a2c94f405c
--- /dev/null
+++ b/ui/lib/core/addon/components/secrets-engine-mount-config.hbs
@@ -0,0 +1,17 @@
+
+
+ {{#if this.showConfig}}
+ {{#each this.fields as |field|}}
+
+ {{/each}}
+ {{! block for additional fields that may be engine specific }}
+ {{yield}}
+ {{/if}}
+
\ No newline at end of file
diff --git a/ui/lib/core/addon/components/secrets-engine-mount-config.ts b/ui/lib/core/addon/components/secrets-engine-mount-config.ts
new file mode 100644
index 0000000000..7cf538030b
--- /dev/null
+++ b/ui/lib/core/addon/components/secrets-engine-mount-config.ts
@@ -0,0 +1,29 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+
+import type SecretEngineModel from 'vault/models/secret-engine';
+
+interface Args {
+ model: SecretEngineModel;
+}
+interface Field {
+ label: string;
+ value: string | boolean;
+}
+
+export default class SecretsEngineMountConfigComponent extends Component {
+ @tracked showConfig = false;
+
+ get fields(): Array {
+ const { model } = this.args;
+ return [
+ { label: 'Secret Engine Type', value: model.engineType },
+ { label: 'Path', value: model.path },
+ { label: 'Accessor', value: model.accessor },
+ { label: 'Local', value: model.local },
+ { label: 'Seal Wrap', value: model.sealWrap },
+ { label: 'Default Lease TTL', value: model.config.defaultLeaseTtl },
+ { label: 'Max Lease TTL', value: model.config.maxLeaseTtl },
+ ];
+ }
+}
diff --git a/ui/lib/kubernetes/addon/decorators/fetch-config.js b/ui/lib/core/addon/decorators/fetch-secrets-engine-config.ts
similarity index 54%
rename from ui/lib/kubernetes/addon/decorators/fetch-config.js
rename to ui/lib/core/addon/decorators/fetch-secrets-engine-config.ts
index 887e77360c..c01a0df7eb 100644
--- a/ui/lib/kubernetes/addon/decorators/fetch-config.js
+++ b/ui/lib/core/addon/decorators/fetch-secrets-engine-config.ts
@@ -5,35 +5,48 @@
import Route from '@ember/routing/route';
+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 Model from '@ember-data/model';
+import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports
+
/**
- * the overview, configure, configuration and roles routes all need to be aware of the config for the engine
+ * for use in routes that need to be aware of the config for a secrets engine
* if the user has not configured they are prompted to do so in each of the routes
* decorate the necessary routes to perform the check in the beforeModel hook since that may change what is returned for the model
*/
-export function withConfig() {
- return function decorator(SuperClass) {
+interface BaseRoute extends Route {
+ store: Store;
+ secretMountPath: SecretMountPath;
+}
+
+export function withConfig(modelName: string) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return function BaseRoute>(SuperClass: RouteClass) {
if (!Object.prototype.isPrototypeOf.call(Route, SuperClass)) {
// eslint-disable-next-line
console.error(
- 'withConfig decorator must be used on an instance of ember Route class. Decorator not applied to returned class'
+ 'withConfig decorator must be used on an instance of Ember Route class. Decorator not applied to returned class'
);
return SuperClass;
}
- return class FetchConfig extends SuperClass {
- configModel = null;
- configError = null;
+
+ return class FetchSecretsEngineConfig extends SuperClass {
+ configModel: Model | null = null;
+ configError: AdapterError | null = null;
promptConfig = false;
- async beforeModel() {
- super.beforeModel(...arguments);
+ async beforeModel(transition: Transition) {
+ super.beforeModel(transition);
- const backend = this.secretMountPath.get();
+ const backend = this.secretMountPath.currentPath;
// check the store for record first
- this.configModel = this.store.peekRecord('kubernetes/config', backend);
+ this.configModel = this.store.peekRecord(modelName, backend);
if (!this.configModel) {
return this.store
- .queryRecord('kubernetes/config', { backend })
+ .queryRecord(modelName, { backend })
.then((record) => {
this.configModel = record;
this.promptConfig = false;
diff --git a/ui/app/helpers/jsonify.js b/ui/lib/core/addon/helpers/jsonify.js
similarity index 100%
rename from ui/app/helpers/jsonify.js
rename to ui/lib/core/addon/helpers/jsonify.js
diff --git a/ui/lib/core/addon/modifiers/code-mirror.js b/ui/lib/core/addon/modifiers/code-mirror.js
index 7de623fc01..7aa2e86427 100644
--- a/ui/lib/core/addon/modifiers/code-mirror.js
+++ b/ui/lib/core/addon/modifiers/code-mirror.js
@@ -68,5 +68,9 @@ export default class CodeMirrorModifier extends Modifier {
editor.on('focus', bind(this, this._onFocus, namedArgs));
this._editor = editor;
+
+ if (namedArgs.onSetup) {
+ namedArgs.onSetup(editor);
+ }
}
}
diff --git a/ui/lib/core/app/components/filter-input.js b/ui/lib/core/app/components/filter-input.js
new file mode 100644
index 0000000000..99d5822bb6
--- /dev/null
+++ b/ui/lib/core/app/components/filter-input.js
@@ -0,0 +1,6 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+export { default } from 'core/components/filter-input';
diff --git a/ui/lib/core/app/components/secrets-engine-mount-config.js b/ui/lib/core/app/components/secrets-engine-mount-config.js
new file mode 100644
index 0000000000..bdc315a98a
--- /dev/null
+++ b/ui/lib/core/app/components/secrets-engine-mount-config.js
@@ -0,0 +1,6 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+export { default } from 'core/components/secrets-engine-mount-config';
diff --git a/ui/lib/core/app/helpers/jsonify.js b/ui/lib/core/app/helpers/jsonify.js
new file mode 100644
index 0000000000..c71705f803
--- /dev/null
+++ b/ui/lib/core/app/helpers/jsonify.js
@@ -0,0 +1,6 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+export { default, jsonify } from 'core/helpers/jsonify';
diff --git a/ui/lib/core/app/helpers/stringify.js b/ui/lib/core/app/helpers/stringify.js
index a0957233a7..c3464b67a9 100644
--- a/ui/lib/core/app/helpers/stringify.js
+++ b/ui/lib/core/app/helpers/stringify.js
@@ -3,4 +3,4 @@
* SPDX-License-Identifier: MPL-2.0
*/
-export { default } from 'core/helpers/stringify';
+export { default, stringify } from 'core/helpers/stringify';
diff --git a/ui/lib/kubernetes/addon/routes/configuration.js b/ui/lib/kubernetes/addon/routes/configuration.js
index dfc3c444a4..37ed7576ac 100644
--- a/ui/lib/kubernetes/addon/routes/configuration.js
+++ b/ui/lib/kubernetes/addon/routes/configuration.js
@@ -5,9 +5,9 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
-import { withConfig } from '../decorators/fetch-config';
+import { withConfig } from 'core/decorators/fetch-secrets-engine-config';
-@withConfig()
+@withConfig('kubernetes/config')
export default class KubernetesConfigureRoute extends Route {
@service store;
@service secretMountPath;
diff --git a/ui/lib/kubernetes/addon/routes/configure.js b/ui/lib/kubernetes/addon/routes/configure.js
index f35a2ce6d3..7e47900b03 100644
--- a/ui/lib/kubernetes/addon/routes/configure.js
+++ b/ui/lib/kubernetes/addon/routes/configure.js
@@ -5,15 +5,15 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
-import { withConfig } from '../decorators/fetch-config';
+import { withConfig } from 'core/decorators/fetch-secrets-engine-config';
-@withConfig()
+@withConfig('kubernetes/config')
export default class KubernetesConfigureRoute extends Route {
@service store;
@service secretMountPath;
async model() {
- const backend = this.secretMountPath.get();
+ const backend = this.secretMountPath.currentPath;
return this.configModel || this.store.createRecord('kubernetes/config', { backend });
}
diff --git a/ui/lib/kubernetes/addon/routes/overview.js b/ui/lib/kubernetes/addon/routes/overview.js
index 266c8e9fab..c6bcbffd07 100644
--- a/ui/lib/kubernetes/addon/routes/overview.js
+++ b/ui/lib/kubernetes/addon/routes/overview.js
@@ -5,16 +5,16 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
-import { withConfig } from 'kubernetes/decorators/fetch-config';
+import { withConfig } from 'core/decorators/fetch-secrets-engine-config';
import { hash } from 'rsvp';
-@withConfig()
+@withConfig('kubernetes/config')
export default class KubernetesOverviewRoute extends Route {
@service store;
@service secretMountPath;
async model() {
- const backend = this.secretMountPath.get();
+ const backend = this.secretMountPath.currentPath;
return hash({
promptConfig: this.promptConfig,
backend: this.modelFor('application'),
diff --git a/ui/lib/kubernetes/addon/routes/roles/create.js b/ui/lib/kubernetes/addon/routes/roles/create.js
index eba3a96650..fe0efa71e4 100644
--- a/ui/lib/kubernetes/addon/routes/roles/create.js
+++ b/ui/lib/kubernetes/addon/routes/roles/create.js
@@ -11,7 +11,7 @@ export default class KubernetesRolesCreateRoute extends Route {
@service secretMountPath;
model() {
- const backend = this.secretMountPath.get();
+ const backend = this.secretMountPath.currentPath;
return this.store.createRecord('kubernetes/role', { backend });
}
diff --git a/ui/lib/kubernetes/addon/routes/roles/index.js b/ui/lib/kubernetes/addon/routes/roles/index.js
index 357436322a..1b4b903988 100644
--- a/ui/lib/kubernetes/addon/routes/roles/index.js
+++ b/ui/lib/kubernetes/addon/routes/roles/index.js
@@ -5,10 +5,10 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
-import { withConfig } from 'kubernetes/decorators/fetch-config';
+import { withConfig } from 'core/decorators/fetch-secrets-engine-config';
import { hash } from 'rsvp';
-@withConfig()
+@withConfig('kubernetes/config')
export default class KubernetesRolesRoute extends Route {
@service store;
@service secretMountPath;
@@ -17,7 +17,7 @@ export default class KubernetesRolesRoute extends Route {
// filter roles based on pageFilter value
const { pageFilter } = transition.to.queryParams;
const roles = this.store
- .query('kubernetes/role', { backend: this.secretMountPath.get() })
+ .query('kubernetes/role', { backend: this.secretMountPath.currentPath })
.then((models) =>
pageFilter
? models.filter((model) => model.name.toLowerCase().includes(pageFilter.toLowerCase()))
diff --git a/ui/lib/kubernetes/addon/routes/roles/role/credentials.js b/ui/lib/kubernetes/addon/routes/roles/role/credentials.js
index d0558d4222..312f9d086c 100644
--- a/ui/lib/kubernetes/addon/routes/roles/role/credentials.js
+++ b/ui/lib/kubernetes/addon/routes/roles/role/credentials.js
@@ -11,7 +11,7 @@ export default class KubernetesRoleCredentialsRoute extends Route {
model() {
return {
roleName: this.paramsFor('roles.role').name,
- backend: this.secretMountPath.get(),
+ backend: this.secretMountPath.currentPath,
};
}
diff --git a/ui/lib/kubernetes/addon/routes/roles/role/details.js b/ui/lib/kubernetes/addon/routes/roles/role/details.js
index 36ce26ccb0..fdc86d5894 100644
--- a/ui/lib/kubernetes/addon/routes/roles/role/details.js
+++ b/ui/lib/kubernetes/addon/routes/roles/role/details.js
@@ -11,7 +11,7 @@ export default class KubernetesRoleDetailsRoute extends Route {
@service secretMountPath;
model() {
- const backend = this.secretMountPath.get();
+ const backend = this.secretMountPath.currentPath;
const { name } = this.paramsFor('roles.role');
return this.store.queryRecord('kubernetes/role', { backend, name });
}
diff --git a/ui/lib/kubernetes/addon/routes/roles/role/edit.js b/ui/lib/kubernetes/addon/routes/roles/role/edit.js
index e917c46e72..324d891d5c 100644
--- a/ui/lib/kubernetes/addon/routes/roles/role/edit.js
+++ b/ui/lib/kubernetes/addon/routes/roles/role/edit.js
@@ -11,7 +11,7 @@ export default class KubernetesRoleEditRoute extends Route {
@service secretMountPath;
model() {
- const backend = this.secretMountPath.get();
+ const backend = this.secretMountPath.currentPath;
const { name } = this.paramsFor('roles.role');
return this.store.queryRecord('kubernetes/role', { backend, name });
}
diff --git a/ui/lib/ldap/addon/components/accounts-checked-out.hbs b/ui/lib/ldap/addon/components/accounts-checked-out.hbs
new file mode 100644
index 0000000000..a9b50eb055
--- /dev/null
+++ b/ui/lib/ldap/addon/components/accounts-checked-out.hbs
@@ -0,0 +1,75 @@
+
+
+
+ {{#if this.filteredAccounts}}
+
+ <:body as |Body|>
+
+ {{Body.data.account}}
+ {{#if @showLibraryColumn}}
+ {{Body.data.library}}
+ {{/if}}
+
+
+
+ Check-in
+
+
+
+
+
+ {{else}}
+
+ {{/if}}
+
+
+{{#if this.selectedStatus}}
+
+
+
+ This action will check-in account
+ {{this.selectedStatus.account}}
+ back to the library. Do you want to proceed?
+
+
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/accounts-checked-out.ts b/ui/lib/ldap/addon/components/accounts-checked-out.ts
new file mode 100644
index 0000000000..0d4d880e4d
--- /dev/null
+++ b/ui/lib/ldap/addon/components/accounts-checked-out.ts
@@ -0,0 +1,72 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { inject as service } from '@ember/service';
+import { task } from 'ember-concurrency';
+import { waitFor } from '@ember/test-waiters';
+import errorMessage from 'vault/utils/error-message';
+
+import type FlashMessageService from 'vault/services/flash-messages';
+import type RouterService from '@ember/routing/router-service';
+import type AuthService from 'vault/services/auth';
+import type LdapLibraryModel from 'vault/models/ldap/library';
+import type { LdapLibraryAccountStatus } from 'vault/adapters/ldap/library';
+
+interface Args {
+ libraries: Array;
+ statuses: Array;
+ showLibraryColumn: boolean;
+}
+
+export default class LdapAccountsCheckedOutComponent extends Component {
+ @service declare readonly flashMessages: FlashMessageService;
+ @service declare readonly router: RouterService;
+ @service declare readonly auth: AuthService;
+
+ @tracked selectedStatus: LdapLibraryAccountStatus | undefined;
+
+ get columns() {
+ const columns = [{ label: 'Account' }, { label: 'Action' }];
+ if (this.args.showLibraryColumn) {
+ columns.splice(1, 0, { label: 'Library' });
+ }
+ return columns;
+ }
+
+ get filteredAccounts() {
+ // filter status to only show checked out accounts associated to the current user
+ // if disable_check_in_enforcement is true on the library set then all checked out accounts are displayed
+ return this.args.statuses.filter((status) => {
+ const authEntityId = this.auth.authData?.entity_id;
+ const isRoot = !status.borrower_entity_id && !authEntityId; // root user will not have an entity id and it won't be populated on status
+ const isEntity = status.borrower_entity_id === authEntityId;
+ const library = this.findLibrary(status.library);
+ const enforcementDisabled = library.disable_check_in_enforcement === 'Disabled';
+
+ return !status.available && (enforcementDisabled || isEntity || isRoot);
+ });
+ }
+
+ disableCheckIn = (name: string) => {
+ return !this.findLibrary(name).canCheckIn;
+ };
+
+ findLibrary(name: string): LdapLibraryModel {
+ return this.args.libraries.find((library) => library.name === name) as LdapLibraryModel;
+ }
+
+ @task
+ @waitFor
+ *checkIn() {
+ const { library, account } = this.selectedStatus as LdapLibraryAccountStatus;
+ try {
+ const libraryModel = this.findLibrary(library);
+ yield libraryModel.checkInAccount(account);
+ this.flashMessages.success(`Successfully checked in the account ${account}.`);
+ // transitioning to the current route to trigger the model hook so we can fetch the updated status
+ this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries.library.details.accounts');
+ } catch (error) {
+ this.selectedStatus = undefined;
+ this.flashMessages.danger(`Error checking in the account ${account}. \n ${errorMessage(error)}`);
+ }
+ }
+}
diff --git a/ui/lib/ldap/addon/components/config-cta.hbs b/ui/lib/ldap/addon/components/config-cta.hbs
new file mode 100644
index 0000000000..ed4fb22e45
--- /dev/null
+++ b/ui/lib/ldap/addon/components/config-cta.hbs
@@ -0,0 +1,9 @@
+
+
+ Configure LDAP
+
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/page/configuration.hbs b/ui/lib/ldap/addon/components/page/configuration.hbs
new file mode 100644
index 0000000000..385cecb11f
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/configuration.hbs
@@ -0,0 +1,39 @@
+
+ <:toolbarActions>
+ {{#if @configModel}}
+
+ Rotate root
+
+ {{/if}}
+
+ {{if @configModel "Edit configuration" "Configure LDAP"}}
+
+
+
+
+{{#if @configModel}}
+ {{#each this.defaultFields as |field|}}
+
+ {{/each}}
+
+ TLS Connection
+
+
+ {{#each this.connectionFields as |field|}}
+
+ {{/each}}
+{{else if @configError}}
+
+{{else}}
+
+{{/if}}
+
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/page/configuration.ts b/ui/lib/ldap/addon/components/page/configuration.ts
new file mode 100644
index 0000000000..8d7a4b751b
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/configuration.ts
@@ -0,0 +1,82 @@
+import Component from '@glimmer/component';
+import { inject as 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 type SecretEngineModel from 'vault/models/secret-engine';
+import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports
+import type { Breadcrumb } from 'vault/vault/app-types';
+import type FlashMessageService from 'vault/services/flash-messages';
+
+interface Args {
+ configModel: LdapConfigModel;
+ configError: AdapterError;
+ backendModel: SecretEngineModel;
+ breadcrumbs: Array;
+}
+
+interface Field {
+ label: string;
+ value: any; // eslint-disable-line @typescript-eslint/no-explicit-any
+ formatTtl?: boolean;
+}
+
+export default class LdapConfigurationPageComponent extends Component {
+ @service declare readonly flashMessages: FlashMessageService;
+
+ get defaultFields(): Array {
+ const model = this.args.configModel;
+ const keys = [
+ 'binddn',
+ 'url',
+ 'schema',
+ 'password_policy',
+ 'userdn',
+ 'userattr',
+ 'connection_timeout',
+ 'request_timeout',
+ ];
+ return model.allFields.reduce>((filtered, field) => {
+ if (keys.includes(field.name)) {
+ const label =
+ {
+ schema: 'Schema',
+ password_policy: 'Password Policy',
+ }[field.name] || field.options.label;
+ filtered.splice(keys.indexOf(field.name), 0, {
+ label,
+ value: model[field.name as keyof typeof model],
+ formatTtl: field.name.includes('timeout'),
+ });
+ }
+ return filtered;
+ }, []);
+ }
+
+ get connectionFields(): Array {
+ const model = this.args.configModel;
+ const keys = ['certificate', 'starttls', 'insecure_tls', 'client_tls_cert', 'client_tls_key'];
+ return model.allFields.reduce>((filtered, field) => {
+ if (keys.includes(field.name)) {
+ filtered.splice(keys.indexOf(field.name), 0, {
+ label: field.options.label,
+ value: model[field.name as keyof typeof model],
+ });
+ }
+ return filtered;
+ }, []);
+ }
+
+ @task
+ @waitFor
+ *rotateRoot() {
+ try {
+ yield this.args.configModel.rotateRoot();
+ this.flashMessages.success('Root password successfully rotated.');
+ } catch (error) {
+ this.flashMessages.danger(`Error rotating root password \n ${errorMessage(error)}`);
+ }
+ }
+}
diff --git a/ui/lib/ldap/addon/components/page/configure.hbs b/ui/lib/ldap/addon/components/page/configure.hbs
new file mode 100644
index 0000000000..088c69b440
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/configure.hbs
@@ -0,0 +1,113 @@
+
+
+
+
+
+ Configure LDAP
+
+
+
+
+
+
+
+{{#if this.showRotatePrompt}}
+
+
+
+ It’s best practice to rotate the administrator (root) password immediately after the initial configuration of the
+ LDAP engine. The rotation will update the password both in Vault and your directory server. Once rotated,
+ only Vault knows the new root password.
+
+
+
+ Would you like to rotate your new credentials? You can also do this later.
+
+
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/page/configure.ts b/ui/lib/ldap/addon/components/page/configure.ts
new file mode 100644
index 0000000000..4f1415a01e
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/configure.ts
@@ -0,0 +1,113 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import { inject as 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 FlashMessageService from 'vault/services/flash-messages';
+import type RouterService from '@ember/routing/router-service';
+
+interface Args {
+ model: LdapConfigModel;
+ breadcrumbs: Array;
+}
+interface SchemaOption {
+ title: string;
+ icon: string;
+ description: string;
+ value: string;
+}
+
+export default class LdapConfigurePageComponent extends Component {
+ @service declare readonly flashMessages: FlashMessageService;
+ @service declare readonly router: RouterService;
+
+ @tracked showRotatePrompt = false;
+ @tracked modelValidations: ValidationMap | null = null;
+ @tracked invalidFormMessage = '';
+ @tracked error = '';
+
+ get schemaOptions(): Array {
+ return [
+ {
+ title: 'OpenLDAP',
+ icon: 'folder',
+ description:
+ 'OpenLDAP is one of the most popular open source directory service developed by the OpenLDAP Project.',
+ value: 'openldap',
+ },
+ {
+ title: 'AD',
+ icon: 'microsoft',
+ description:
+ 'Active Directory is a directory service developed by Microsoft for Windows domain networks.',
+ value: 'ad',
+ },
+ {
+ title: 'RACF',
+ icon: 'users',
+ description:
+ "For managing IBM's Resource Access Control Facility (RACF) security system, the generated passwords must be 8 characters or less.",
+ value: 'racf',
+ },
+ ];
+ }
+
+ leave(route: string) {
+ this.router.transitionTo(`vault.cluster.secrets.backend.ldap.${route}`);
+ }
+
+ validate() {
+ const { isValid, state, invalidFormMessage } = this.args.model.validate();
+ this.modelValidations = isValid ? null : state;
+ this.invalidFormMessage = isValid ? '' : invalidFormMessage;
+ return isValid;
+ }
+
+ async rotateRoot() {
+ try {
+ await this.args.model.rotateRoot();
+ } 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)}`);
+ }
+ }
+
+ @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.isNew && !this.showRotatePrompt;
+
+ if (isValid && !this.showRotatePrompt) {
+ try {
+ yield this.args.model.save();
+ // if save was triggered from confirm action in rotate password prompt we need to make an additional request
+ if (rotate) {
+ yield this.rotateRoot();
+ }
+ 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.');
+ }
+ }
+ }
+
+ @action
+ cancel() {
+ const { model } = this.args;
+ const transitionRoute = model.isNew ? 'overview' : 'configuration';
+ const cleanupMethod = model.isNew ? 'unloadRecord' : 'rollbackAttributes';
+ model[cleanupMethod]();
+ this.leave(transitionRoute);
+ }
+}
diff --git a/ui/lib/ldap/addon/components/page/libraries.hbs b/ui/lib/ldap/addon/components/page/libraries.hbs
new file mode 100644
index 0000000000..40553b1156
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/libraries.hbs
@@ -0,0 +1,91 @@
+
+ <:toolbarFilters>
+ {{#if (and (not @promptConfig) @libraries)}}
+
+ {{/if}}
+
+ <:toolbarActions>
+ {{#if @promptConfig}}
+
+ Configure LDAP
+
+ {{else}}
+
+ Create library
+
+ {{/if}}
+
+
+
+{{#if @promptConfig}}
+
+{{else if (not this.filteredLibraries)}}
+ {{#if this.filterValue}}
+
+ {{else}}
+
+
+ Create library
+
+
+ {{/if}}
+{{else}}
+
+ {{#each this.filteredLibraries as |library|}}
+
+
+
+ {{library.name}}
+
+
+ {{#if library.libraryPath.isLoading}}
+
+
+ loading
+
+
+ {{else}}
+
+
+ Edit
+
+
+
+
+ Details
+
+
+ {{#if library.canDelete}}
+
+
+
+ {{/if}}
+ {{/if}}
+
+
+ {{/each}}
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/page/libraries.ts b/ui/lib/ldap/addon/components/page/libraries.ts
new file mode 100644
index 0000000000..2e177234e0
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/libraries.ts
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+import { getOwner } from '@ember/application';
+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';
+
+interface Args {
+ libraries: Array;
+ promptConfig: boolean;
+ backendModel: SecretEngineModel;
+ breadcrumbs: Array;
+}
+
+export default class LdapLibrariesPageComponent extends Component {
+ @service declare readonly flashMessages: FlashMessageService;
+
+ @tracked filterValue = '';
+
+ get mountPoint(): string {
+ const owner = getOwner(this) as EngineOwner;
+ return owner.mountPoint;
+ }
+
+ get filteredLibraries() {
+ const { libraries } = this.args;
+ return this.filterValue
+ ? libraries.filter((library) => library.name.toLowerCase().includes(this.filterValue.toLowerCase()))
+ : libraries;
+ }
+
+ @action
+ async onDelete(model: LdapLibraryModel) {
+ try {
+ const message = `Successfully deleted library ${model.name}.`;
+ await model.destroyRecord();
+ this.args.libraries.removeObject(model);
+ this.flashMessages.success(message);
+ } catch (error) {
+ this.flashMessages.danger(`Error deleting library \n ${errorMessage(error)}`);
+ }
+ }
+}
diff --git a/ui/lib/ldap/addon/components/page/library/check-out.hbs b/ui/lib/ldap/addon/components/page/library/check-out.hbs
new file mode 100644
index 0000000000..407f44d2cd
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/library/check-out.hbs
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+ Check-out
+
+
+
+
+
+
+
+ Warning
+
+ You won’t be able to access these credentials later, so please copy them now.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{if @credentials.renewable "True" "False"}}
+
+
+
+
+
+
+
+ Done
+
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/page/library/create-and-edit.hbs b/ui/lib/ldap/addon/components/page/library/create-and-edit.hbs
new file mode 100644
index 0000000000..734f7c5bcb
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/library/create-and-edit.hbs
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+ {{if @model.isNew "Create Library" "Edit Library"}}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/page/library/create-and-edit.ts b/ui/lib/ldap/addon/components/page/library/create-and-edit.ts
new file mode 100644
index 0000000000..210e14646f
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/library/create-and-edit.ts
@@ -0,0 +1,55 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import { inject as service } from '@ember/service';
+import { task } from 'ember-concurrency';
+import { waitFor } from '@ember/test-waiters';
+import errorMessage from 'vault/utils/error-message';
+
+import type LdapLibraryModel from 'vault/models/ldap/library';
+import { Breadcrumb, ValidationMap } from 'vault/vault/app-types';
+import type FlashMessageService from 'vault/services/flash-messages';
+import type RouterService from '@ember/routing/router-service';
+
+interface Args {
+ model: LdapLibraryModel;
+ breadcrumbs: Array;
+}
+
+export default class LdapCreateAndEditLibraryPageComponent extends Component {
+ @service declare readonly flashMessages: FlashMessageService;
+ @service declare readonly router: RouterService;
+
+ @tracked modelValidations: ValidationMap | null = null;
+ @tracked invalidFormMessage = '';
+ @tracked error = '';
+
+ @task
+ @waitFor
+ *save(event: Event) {
+ event.preventDefault();
+
+ const { model } = this.args;
+ const { isValid, state, invalidFormMessage } = model.validate();
+
+ this.modelValidations = isValid ? null : state;
+ this.invalidFormMessage = isValid ? '' : invalidFormMessage;
+
+ if (isValid) {
+ try {
+ const action = model.isNew ? 'created' : 'updated';
+ yield model.save();
+ this.flashMessages.success(`Successfully ${action} the library ${model.name}.`);
+ this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries.library.details', model.name);
+ } catch (error) {
+ this.error = errorMessage(error, 'Error saving library. Please try again or contact support.');
+ }
+ }
+ }
+
+ @action
+ cancel() {
+ this.args.model.rollbackAttributes();
+ this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries');
+ }
+}
diff --git a/ui/lib/ldap/addon/components/page/library/details.hbs b/ui/lib/ldap/addon/components/page/library/details.hbs
new file mode 100644
index 0000000000..96950bb9c6
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/library/details.hbs
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+ {{@model.name}}
+
+
+
+
+
+
+
+ Accounts
+ Configuration
+
+
+
+
+
+
+ {{#if @model.canDelete}}
+
+ Delete library
+
+ {{#if @model.canEdit}}
+
+ {{/if}}
+ {{/if}}
+ {{#if @model.canEdit}}
+
+ Edit library
+
+ {{/if}}
+
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/page/library/details.ts b/ui/lib/ldap/addon/components/page/library/details.ts
new file mode 100644
index 0000000000..a9d1d29527
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/library/details.ts
@@ -0,0 +1,31 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { inject as service } from '@ember/service';
+import errorMessage from 'vault/utils/error-message';
+
+import type LdapLibraryModel from 'vault/models/ldap/library';
+import { Breadcrumb } from 'vault/vault/app-types';
+import type FlashMessageService from 'vault/services/flash-messages';
+import type RouterService from '@ember/routing/router-service';
+
+interface Args {
+ model: LdapLibraryModel;
+ breadcrumbs: Array;
+}
+
+export default class LdapLibraryDetailsPageComponent extends Component {
+ @service declare readonly flashMessages: FlashMessageService;
+ @service declare readonly router: RouterService;
+
+ @action
+ async delete() {
+ try {
+ await this.args.model.destroyRecord();
+ this.flashMessages.success('Library deleted successfully.');
+ this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries');
+ } catch (error) {
+ const message = errorMessage(error, 'Unable to delete library. Please try again or contact support.');
+ this.flashMessages.danger(message);
+ }
+ }
+}
diff --git a/ui/lib/ldap/addon/components/page/library/details/accounts.hbs b/ui/lib/ldap/addon/components/page/library/details/accounts.hbs
new file mode 100644
index 0000000000..150cf3e97e
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/library/details/accounts.hbs
@@ -0,0 +1,90 @@
+
+
+
+
All accounts
+ {{#if @library.canCheckOut}}
+
+ Check-out
+
+ {{/if}}
+
+
+ The accounts within this library
+
+
+
+ <:body as |Body|>
+
+ {{Body.data.account}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{this.cliCommand}}
+
+ Copy
+
+
+
+
+
+
+
+{{#if this.showCheckOutPrompt}}
+
+
+
+ Current generated credential’s time-to-live is set at
+ {{format-duration @library.ttl}}. You can set a different limit if you’d like:
+
+
+
+
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/page/library/details/accounts.ts b/ui/lib/ldap/addon/components/page/library/details/accounts.ts
new file mode 100644
index 0000000000..559adc9a70
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/library/details/accounts.ts
@@ -0,0 +1,37 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+
+import type FlashMessageService from 'vault/services/flash-messages';
+import type RouterService from '@ember/routing/router-service';
+import type LdapLibraryModel from 'vault/models/ldap/library';
+import type { LdapLibraryAccountStatus } from 'vault/vault/adapters/ldap/library';
+import { TtlEvent } from 'vault/vault/app-types';
+
+interface Args {
+ library: LdapLibraryModel;
+ statuses: Array;
+}
+
+export default class LdapLibraryDetailsAccountsPageComponent extends Component {
+ @service declare readonly flashMessages: FlashMessageService;
+ @service declare readonly router: RouterService;
+
+ @tracked showCheckOutPrompt = false;
+ @tracked checkOutTtl: string | null = null;
+
+ get cliCommand() {
+ return `vault lease renew ad/library/${this.args.library.name}/check-out/:lease_id`;
+ }
+ @action
+ setTtl(data: TtlEvent) {
+ this.checkOutTtl = data.timeString;
+ }
+ @action
+ checkOut() {
+ this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries.library.check-out', {
+ queryParams: { ttl: this.checkOutTtl },
+ });
+ }
+}
diff --git a/ui/lib/ldap/addon/components/page/library/details/configuration.hbs b/ui/lib/ldap/addon/components/page/library/details/configuration.hbs
new file mode 100644
index 0000000000..a0e70f35d7
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/library/details/configuration.hbs
@@ -0,0 +1,21 @@
+{{#each @model.displayFields as |field|}}
+ {{#let (get @model field.name) as |value|}}
+ {{#if (eq field.name "disable_check_in_enforcement")}}
+
+
+ {{value}}
+
+ {{else}}
+
+ {{/if}}
+ {{/let}}
+{{/each}}
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/page/overview.hbs b/ui/lib/ldap/addon/components/page/overview.hbs
new file mode 100644
index 0000000000..6505cde8e2
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/overview.hbs
@@ -0,0 +1,69 @@
+
+ <:toolbarActions>
+ {{#if @promptConfig}}
+
+ Configure LDAP
+
+ {{/if}}
+
+
+
+{{#if @promptConfig}}
+
+{{else}}
+
+
+
+ {{or @roles.length "None"}}
+
+
+
+
+ {{or @libraries.length "None"}}
+
+
+
+
+
+
+
+
+
+
+
+ Get credentials
+
+
+
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/page/overview.ts b/ui/lib/ldap/addon/components/page/overview.ts
new file mode 100644
index 0000000000..eb732a209f
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/overview.ts
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+
+import type LdapLibraryModel from 'vault/models/ldap/library';
+import type SecretEngineModel from 'vault/models/secret-engine';
+import type RouterService from '@ember/routing/router-service';
+import type { Breadcrumb } from 'vault/vault/app-types';
+import LdapRoleModel from 'vault/models/ldap/role';
+import { LdapLibraryAccountStatus } from 'vault/vault/adapters/ldap/library';
+
+interface Args {
+ roles: Array;
+ libraries: Array;
+ librariesStatus: Array;
+ promptConfig: boolean;
+ backendModel: SecretEngineModel;
+ breadcrumbs: Array;
+}
+
+export default class LdapLibrariesPageComponent extends Component {
+ @service declare readonly router: RouterService;
+
+ @tracked selectedRole: LdapRoleModel | undefined;
+
+ @action
+ selectRole([roleName]: Array) {
+ const model = this.args.roles.find((role) => role.name === roleName);
+ this.selectedRole = model;
+ }
+
+ @action
+ generateCredentials() {
+ const { type, name } = this.selectedRole as LdapRoleModel;
+ this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles.role.credentials', type, name);
+ }
+}
diff --git a/ui/lib/ldap/addon/components/page/role/create-and-edit.hbs b/ui/lib/ldap/addon/components/page/role/create-and-edit.hbs
new file mode 100644
index 0000000000..1a013afe73
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/role/create-and-edit.hbs
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+ {{if @model.isNew "Create Role" "Edit Role"}}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/page/role/create-and-edit.ts b/ui/lib/ldap/addon/components/page/role/create-and-edit.ts
new file mode 100644
index 0000000000..96ce354bae
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/role/create-and-edit.ts
@@ -0,0 +1,82 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import { inject as service } from '@ember/service';
+import { task } from 'ember-concurrency';
+import { waitFor } from '@ember/test-waiters';
+import errorMessage from 'vault/utils/error-message';
+
+import type LdapRoleModel from 'vault/models/ldap/role';
+import { Breadcrumb, ValidationMap } from 'vault/vault/app-types';
+import type FlashMessageService from 'vault/services/flash-messages';
+import type RouterService from '@ember/routing/router-service';
+
+interface Args {
+ model: LdapRoleModel;
+ breadcrumbs: Array;
+}
+interface RoleTypeOption {
+ title: string;
+ icon: string;
+ description: string;
+ value: string;
+}
+
+export default class LdapCreateAndEditRolePageComponent extends Component {
+ @service declare readonly flashMessages: FlashMessageService;
+ @service declare readonly router: RouterService;
+
+ @tracked modelValidations: ValidationMap | null = null;
+ @tracked invalidFormMessage = '';
+ @tracked error = '';
+
+ get roleTypeOptions(): Array {
+ return [
+ {
+ title: 'Static role',
+ icon: 'user',
+ description: 'Static roles map to existing users in an LDAP system.',
+ value: 'static',
+ },
+ {
+ title: 'Dynamic role',
+ icon: 'folder-users',
+ description: 'Dynamic roles allow Vault to create and delete a user in an LDAP system.',
+ value: 'dynamic',
+ },
+ ];
+ }
+
+ @task
+ @waitFor
+ *save(event: Event) {
+ event.preventDefault();
+
+ const { model } = this.args;
+ const { isValid, state, invalidFormMessage } = model.validate();
+
+ this.modelValidations = isValid ? null : state;
+ this.invalidFormMessage = isValid ? '' : invalidFormMessage;
+
+ if (isValid) {
+ try {
+ const action = model.isNew ? 'created' : 'updated';
+ yield model.save();
+ this.flashMessages.success(`Successfully ${action} the role ${model.name}`);
+ this.router.transitionTo(
+ 'vault.cluster.secrets.backend.ldap.roles.role.details',
+ model.type,
+ model.name
+ );
+ } catch (error) {
+ this.error = errorMessage(error, 'Error saving role. Please try again or contact support.');
+ }
+ }
+ }
+
+ @action
+ cancel() {
+ this.args.model.rollbackAttributes();
+ this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles');
+ }
+}
diff --git a/ui/lib/ldap/addon/components/page/role/credentials.hbs b/ui/lib/ldap/addon/components/page/role/credentials.hbs
new file mode 100644
index 0000000000..c03f3b988f
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/role/credentials.hbs
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+ Credentials
+
+
+
+
+
+
+{{#if (eq @credentials.type "dynamic")}}
+
+ Warning
+
+ You won’t be able to access these credentials later, so please copy them now.
+
+
+{{/if}}
+
+
+ {{#each this.fields as |field|}}
+ {{#let (get @credentials field.key) as |value|}}
+ {{#if field.hasBlock}}
+
+ {{#if (eq field.hasBlock "masked")}}
+
+ {{else if (eq field.hasBlock "check")}}
+
+
+
+ {{if value "True" "False"}}
+
+
+ {{/if}}
+
+ {{else}}
+
+ {{/if}}
+ {{/let}}
+ {{/each}}
+
+
+
+
+ Done
+
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/page/role/credentials.ts b/ui/lib/ldap/addon/components/page/role/credentials.ts
new file mode 100644
index 0000000000..3c9f7115f7
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/role/credentials.ts
@@ -0,0 +1,33 @@
+import Component from '@glimmer/component';
+
+import type {
+ LdapStaticRoleCredentials,
+ LdapDynamicRoleCredentials,
+} from 'ldap/routes/roles/role/credentials';
+import { Breadcrumb } from 'vault/vault/app-types';
+
+interface Args {
+ credentials: LdapStaticRoleCredentials | LdapDynamicRoleCredentials;
+ breadcrumbs: Array;
+}
+
+export default class LdapRoleCredentialsPageComponent extends Component {
+ staticFields = [
+ { label: 'Last Vault rotation', key: 'last_vault_rotation', formatDate: 'MMM d yyyy, h:mm:ss aaa' },
+ { label: 'Password', key: 'password', hasBlock: 'masked' },
+ { label: 'Username', key: 'username' },
+ { label: 'Rotation period', key: 'rotation_period', formatTtl: true },
+ { label: 'Time remaining', key: 'ttl', formatTtl: true },
+ ];
+ dynamicFields = [
+ { label: 'Distinguished Name', key: 'distinguished_names' },
+ { label: 'Username', key: 'username', hasBlock: 'masked' },
+ { label: 'Password', key: 'password', hasBlock: 'masked' },
+ { label: 'Lease ID', key: 'lease_id' },
+ { label: 'Lease duration', key: 'lease_duration', formatTtl: true },
+ { label: 'Lease renewable', key: 'renewable', hasBlock: 'check' },
+ ];
+ get fields() {
+ return this.args.credentials.type === 'dynamic' ? this.dynamicFields : this.staticFields;
+ }
+}
diff --git a/ui/lib/ldap/addon/components/page/role/details.hbs b/ui/lib/ldap/addon/components/page/role/details.hbs
new file mode 100644
index 0000000000..4fb0773f41
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/role/details.hbs
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+ {{@model.name}}
+
+
+
+
+
+
+ {{#if @model.canDelete}}
+
+ Delete role
+
+
+ {{/if}}
+ {{#if @model.canReadCreds}}
+
+ Get credentials
+
+ {{/if}}
+ {{#if @model.canRotateStaticCreds}}
+
+ Rotate credentials
+
+ {{/if}}
+ {{#if @model.canEdit}}
+
+ Edit role
+
+ {{/if}}
+
+
+
+{{#each @model.displayFields as |field|}}
+ {{#let (get @model field.name) as |value|}}
+
+ {{/let}}
+{{/each}}
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/page/role/details.ts b/ui/lib/ldap/addon/components/page/role/details.ts
new file mode 100644
index 0000000000..e802415aa8
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/role/details.ts
@@ -0,0 +1,44 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { inject as service } from '@ember/service';
+import errorMessage from 'vault/utils/error-message';
+import { task } from 'ember-concurrency';
+import { waitFor } from '@ember/test-waiters';
+
+import type LdapRoleModel from 'vault/models/ldap/role';
+import { Breadcrumb } from 'vault/vault/app-types';
+import type FlashMessageService from 'vault/services/flash-messages';
+import type RouterService from '@ember/routing/router-service';
+
+interface Args {
+ model: LdapRoleModel;
+ breadcrumbs: Array;
+}
+
+export default class LdapRoleDetailsPageComponent extends Component {
+ @service declare readonly flashMessages: FlashMessageService;
+ @service declare readonly router: RouterService;
+
+ @action
+ async delete() {
+ try {
+ await this.args.model.destroyRecord();
+ this.flashMessages.success('Role deleted successfully.');
+ this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles');
+ } catch (error) {
+ const message = errorMessage(error, 'Unable to delete role. Please try again or contact support.');
+ this.flashMessages.danger(message);
+ }
+ }
+
+ @task
+ @waitFor
+ *rotateCredentials() {
+ try {
+ yield this.args.model.rotateStaticPassword();
+ this.flashMessages.success('Credentials successfully rotated.');
+ } catch (error) {
+ this.flashMessages.danger(`Error rotating credentials \n ${errorMessage(error)}`);
+ }
+ }
+}
diff --git a/ui/lib/ldap/addon/components/page/roles.hbs b/ui/lib/ldap/addon/components/page/roles.hbs
new file mode 100644
index 0000000000..4d78d0b0b1
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/roles.hbs
@@ -0,0 +1,117 @@
+
+ <:toolbarFilters>
+ {{#if (and (not @promptConfig) @roles)}}
+
+ {{/if}}
+
+ <:toolbarActions>
+ {{#if @promptConfig}}
+
+ Configure LDAP
+
+ {{else}}
+
+ Create role
+
+ {{/if}}
+
+
+
+{{#if @promptConfig}}
+
+{{else if (not this.filteredRoles)}}
+ {{#if this.filterValue}}
+
+ {{else}}
+
+
+ Create role
+
+
+ {{/if}}
+{{else}}
+
+ {{#each this.filteredRoles as |role|}}
+
+
+
+ {{role.name}}
+
+
+
+ {{#if role.rolePath.isLoading}}
+
+
+ loading
+
+
+ {{else}}
+
+
+ Edit
+
+
+
+
+ Get credentials
+
+
+ {{#if role.canRotateStaticCreds}}
+
+
+
+ {{/if}}
+
+
+ Details
+
+
+ {{#if role.canDelete}}
+
+
+
+ {{/if}}
+ {{/if}}
+
+
+ {{/each}}
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/components/page/roles.ts b/ui/lib/ldap/addon/components/page/roles.ts
new file mode 100644
index 0000000000..4bf4fe2d0c
--- /dev/null
+++ b/ui/lib/ldap/addon/components/page/roles.ts
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+import { getOwner } from '@ember/application';
+import errorMessage from 'vault/utils/error-message';
+
+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';
+
+interface Args {
+ roles: Array;
+ promptConfig: boolean;
+ backendModel: SecretEngineModel;
+ breadcrumbs: Array;
+}
+
+export default class LdapRolesPageComponent extends Component {
+ @service declare readonly flashMessages: FlashMessageService;
+
+ @tracked filterValue = '';
+
+ get mountPoint(): string {
+ const owner = getOwner(this) as EngineOwner;
+ return owner.mountPoint;
+ }
+
+ get filteredRoles() {
+ const { roles } = this.args;
+ return this.filterValue
+ ? roles.filter((role) => role.name.toLowerCase().includes(this.filterValue.toLowerCase()))
+ : roles;
+ }
+
+ @action
+ async onRotate(model: LdapRoleModel) {
+ try {
+ const message = `Successfully rotated credentials for ${model.name}.`;
+ await model.rotateStaticPassword();
+ this.flashMessages.success(message);
+ } catch (error) {
+ this.flashMessages.danger(`Error rotating credentials \n ${errorMessage(error)}`);
+ }
+ }
+
+ @action
+ async onDelete(model: LdapRoleModel) {
+ try {
+ const message = `Successfully deleted role ${model.name}.`;
+ await model.destroyRecord();
+ this.args.roles.removeObject(model);
+ this.flashMessages.success(message);
+ } catch (error) {
+ this.flashMessages.danger(`Error deleting role \n ${errorMessage(error)}`);
+ }
+ }
+}
diff --git a/ui/lib/ldap/addon/components/tab-page-header.hbs b/ui/lib/ldap/addon/components/tab-page-header.hbs
new file mode 100644
index 0000000000..680a125739
--- /dev/null
+++ b/ui/lib/ldap/addon/components/tab-page-header.hbs
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+ {{@model.id}}
+
+
+
+
+
+
+
+ Overview
+ Roles
+ Libraries
+ Configuration
+
+
+
+
+
+
+ {{yield to="toolbarFilters"}}
+
+
+ {{yield to="toolbarActions"}}
+
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/engine.js b/ui/lib/ldap/addon/engine.js
new file mode 100644
index 0000000000..6f2153712c
--- /dev/null
+++ b/ui/lib/ldap/addon/engine.js
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Engine from 'ember-engines/engine';
+import loadInitializers from 'ember-load-initializers';
+import Resolver from 'ember-resolver';
+import config from './config/environment';
+
+const { modulePrefix } = config;
+
+export default class LdapEngine extends Engine {
+ modulePrefix = modulePrefix;
+ Resolver = Resolver;
+ dependencies = {
+ services: ['router', 'store', 'secret-mount-path', 'flash-messages', 'auth'],
+ externalRoutes: ['secrets'],
+ };
+}
+
+loadInitializers(LdapEngine, modulePrefix);
diff --git a/ui/lib/ldap/addon/routes.js b/ui/lib/ldap/addon/routes.js
new file mode 100644
index 0000000000..cecd2af313
--- /dev/null
+++ b/ui/lib/ldap/addon/routes.js
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import buildRoutes from 'ember-engines/routes';
+
+export default buildRoutes(function () {
+ this.route('overview');
+ this.route('roles', function () {
+ this.route('create');
+ this.route('role', { path: '/:type/:name' }, function () {
+ this.route('details');
+ this.route('edit');
+ this.route('credentials');
+ });
+ });
+ this.route('libraries', function () {
+ this.route('create');
+ this.route('library', { path: '/:name' }, function () {
+ this.route('details', function () {
+ this.route('accounts');
+ this.route('configuration');
+ });
+ this.route('edit');
+ this.route('check-out');
+ });
+ });
+ this.route('configure');
+ this.route('configuration');
+});
diff --git a/ui/lib/ldap/addon/routes/configuration.ts b/ui/lib/ldap/addon/routes/configuration.ts
new file mode 100644
index 0000000000..160f227b8d
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/configuration.ts
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as 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'; // eslint-disable-line ember/use-ember-data-rfc-395-imports
+
+interface LdapConfigurationRouteModel {
+ backendModel: SecretEngineModel;
+ configModel: LdapConfigModel;
+ configError: AdapterError;
+}
+interface LdapConfigurationController extends Controller {
+ breadcrumbs: Array;
+ model: LdapConfigurationRouteModel;
+}
+
+@withConfig('ldap/config')
+export default class LdapConfigurationRoute extends Route {
+ @service declare readonly store: Store;
+ @service declare readonly secretMountPath: SecretMountPath;
+
+ declare configModel: LdapConfigModel;
+ declare configError: AdapterError;
+
+ model() {
+ return {
+ backendModel: this.modelFor('application'),
+ configModel: this.configModel,
+ configError: this.configError,
+ };
+ }
+
+ setupController(
+ controller: LdapConfigurationController,
+ resolvedModel: LdapConfigurationRouteModel,
+ transition: Transition
+ ) {
+ super.setupController(controller, resolvedModel, transition);
+
+ controller.breadcrumbs = [
+ { label: 'secrets', route: 'secrets', linkExternal: true },
+ { label: resolvedModel.backendModel.id },
+ ];
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/configure.ts b/ui/lib/ldap/addon/routes/configure.ts
new file mode 100644
index 0000000000..0286ea687b
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/configure.ts
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as 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 Controller from '@ember/controller';
+import type { Breadcrumb } from 'vault/vault/app-types';
+
+interface LdapConfigureController extends Controller {
+ breadcrumbs: Array;
+}
+
+@withConfig('ldap/config')
+export default class LdapConfigureRoute extends Route {
+ @service declare readonly store: Store;
+ @service declare readonly secretMountPath: SecretMountPath;
+
+ declare configModel: LdapConfigModel;
+
+ model() {
+ const backend = this.secretMountPath.currentPath;
+ return this.configModel || this.store.createRecord('ldap/config', { backend });
+ }
+
+ setupController(
+ controller: LdapConfigureController,
+ resolvedModel: LdapConfigModel,
+ transition: Transition
+ ) {
+ super.setupController(controller, resolvedModel, transition);
+
+ controller.breadcrumbs = [
+ { label: 'Secrets', route: 'secrets', linkExternal: true },
+ { label: resolvedModel.backend, route: 'overview' },
+ { label: 'Configure' },
+ ];
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/error.ts b/ui/lib/ldap/addon/routes/error.ts
new file mode 100644
index 0000000000..c4e9e3ba66
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/error.ts
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+
+import type Transition from '@ember/routing/transition';
+import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports
+import type SecretEngineModel from 'vault/models/secret-engine';
+import type { Breadcrumb } from 'vault/vault/app-types';
+import type Controller from '@ember/controller';
+import type SecretMountPath from 'vault/services/secret-mount-path';
+
+interface LdapErrorController extends Controller {
+ breadcrumbs: Array;
+ backend: SecretEngineModel;
+}
+
+export default class LdapErrorRoute extends Route {
+ @service declare readonly secretMountPath: SecretMountPath;
+
+ setupController(controller: LdapErrorController, resolvedModel: AdapterError, transition: Transition) {
+ super.setupController(controller, resolvedModel, transition);
+ controller.breadcrumbs = [
+ { label: 'secrets', route: 'secrets', linkExternal: true },
+ { label: this.secretMountPath.currentPath, route: 'overview' },
+ ];
+ controller.backend = this.modelFor('application') as SecretEngineModel;
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/libraries/create.ts b/ui/lib/ldap/addon/routes/libraries/create.ts
new file mode 100644
index 0000000000..d8acfa534c
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/libraries/create.ts
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+
+import type Store from '@ember-data/store';
+import type SecretMountPath from 'vault/services/secret-mount-path';
+import type LdapLibraryModel from 'vault/models/ldap/library';
+import type Controller from '@ember/controller';
+import type Transition from '@ember/routing/transition';
+import type { Breadcrumb } from 'vault/vault/app-types';
+
+interface LdapLibrariesCreateController extends Controller {
+ breadcrumbs: Array;
+ model: LdapLibraryModel;
+}
+
+export default class LdapLibrariesCreateRoute extends Route {
+ @service declare readonly store: Store;
+ @service declare readonly secretMountPath: SecretMountPath;
+
+ model() {
+ const backend = this.secretMountPath.currentPath;
+ return this.store.createRecord('ldap/library', { backend });
+ }
+
+ setupController(
+ controller: LdapLibrariesCreateController,
+ resolvedModel: LdapLibraryModel,
+ transition: Transition
+ ) {
+ super.setupController(controller, resolvedModel, transition);
+
+ controller.breadcrumbs = [
+ { label: resolvedModel.backend, route: 'overview' },
+ { label: 'libraries', route: 'libraries' },
+ { label: 'create' },
+ ];
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/libraries/index.ts b/ui/lib/ldap/addon/routes/libraries/index.ts
new file mode 100644
index 0000000000..5303677912
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/libraries/index.ts
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as 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';
+
+interface LdapLibrariesRouteModel {
+ backendModel: SecretEngineModel;
+ promptConfig: boolean;
+ libraries: Array;
+}
+interface LdapLibrariesController extends Controller {
+ breadcrumbs: Array;
+ 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;
+ return hash({
+ backendModel,
+ promptConfig: this.promptConfig,
+ libraries: this.store.query('ldap/library', { backend: backendModel.id }),
+ });
+ }
+
+ setupController(
+ controller: LdapLibrariesController,
+ resolvedModel: LdapLibrariesRouteModel,
+ transition: Transition
+ ) {
+ super.setupController(controller, resolvedModel, transition);
+
+ controller.breadcrumbs = [
+ { label: 'secrets', route: 'secrets', linkExternal: true },
+ { label: resolvedModel.backendModel.id },
+ ];
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/libraries/library.ts b/ui/lib/ldap/addon/routes/libraries/library.ts
new file mode 100644
index 0000000000..85e0d1ac74
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/libraries/library.ts
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+
+import type Store from '@ember-data/store';
+import type SecretMountPath from 'vault/services/secret-mount-path';
+
+interface LdapLibraryRouteParams {
+ name: string;
+}
+
+export default class LdapLibraryRoute extends Route {
+ @service declare readonly store: Store;
+ @service declare readonly secretMountPath: SecretMountPath;
+
+ model(params: LdapLibraryRouteParams) {
+ const backend = this.secretMountPath.currentPath;
+ const { name } = params;
+ return this.store.queryRecord('ldap/library', { backend, name });
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/libraries/library/check-out.ts b/ui/lib/ldap/addon/routes/libraries/library/check-out.ts
new file mode 100644
index 0000000000..455a1a0553
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/libraries/library/check-out.ts
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+import errorMessage from 'vault/utils/error-message';
+
+import type FlashMessageService from 'vault/services/flash-messages';
+import type RouterService from '@ember/routing/router-service';
+import type LdapLibraryModel from 'vault/models/ldap/library';
+import type Controller from '@ember/controller';
+import type Transition from '@ember/routing/transition';
+import type { Breadcrumb } from 'vault/vault/app-types';
+import { LdapLibraryCheckOutCredentials } from 'vault/vault/adapters/ldap/library';
+import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports
+
+interface LdapLibraryCheckOutController extends Controller {
+ breadcrumbs: Array;
+ model: LdapLibraryCheckOutCredentials;
+}
+
+export default class LdapLibraryCheckOutRoute extends Route {
+ @service declare readonly flashMessages: FlashMessageService;
+ @service declare readonly router: RouterService;
+
+ accountsRoute = 'vault.cluster.secrets.backend.ldap.libraries.library.details.accounts';
+
+ beforeModel(transition: Transition) {
+ // transition must be from the details.accounts route to ensure it was initiated by the check-out action
+ if (transition.from?.name !== this.accountsRoute) {
+ this.router.replaceWith(this.accountsRoute);
+ }
+ }
+ model(_params: object, transition: Transition) {
+ const { ttl } = transition.to.queryParams;
+ const library = this.modelFor('libraries.library') as LdapLibraryModel;
+ return library.checkOutAccount(ttl);
+ }
+ setupController(
+ controller: LdapLibraryCheckOutController,
+ resolvedModel: LdapLibraryCheckOutCredentials,
+ transition: Transition
+ ) {
+ super.setupController(controller, resolvedModel, transition);
+
+ const library = this.modelFor('libraries.library') as LdapLibraryModel;
+ controller.breadcrumbs = [
+ { label: library.backend, route: 'overview' },
+ { label: 'libraries', route: 'libraries' },
+ { label: library.name, route: 'libraries.library' },
+ { label: 'check-out' },
+ ];
+ }
+
+ @action
+ error(error: AdapterError) {
+ // if check-out fails, return to library details route
+ const message = errorMessage(error, 'Error checking out account. Please try again or contact support.');
+ this.flashMessages.danger(message);
+ this.router.replaceWith(this.accountsRoute);
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/libraries/library/details.ts b/ui/lib/ldap/addon/routes/libraries/library/details.ts
new file mode 100644
index 0000000000..61b24d3b29
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/libraries/library/details.ts
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+
+import type LdapLibraryModel from 'vault/models/ldap/library';
+import type Controller from '@ember/controller';
+import type Transition from '@ember/routing/transition';
+import type { Breadcrumb } from 'vault/vault/app-types';
+
+interface LdapLibraryDetailsController extends Controller {
+ breadcrumbs: Array;
+ model: LdapLibraryModel;
+}
+
+export default class LdapLibraryDetailsRoute extends Route {
+ setupController(
+ controller: LdapLibraryDetailsController,
+ resolvedModel: LdapLibraryModel,
+ transition: Transition
+ ) {
+ super.setupController(controller, resolvedModel, transition);
+
+ controller.breadcrumbs = [
+ { label: resolvedModel.backend, route: 'overview' },
+ { label: 'libraries', route: 'libraries' },
+ { label: resolvedModel.name },
+ ];
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/libraries/library/details/accounts.ts b/ui/lib/ldap/addon/routes/libraries/library/details/accounts.ts
new file mode 100644
index 0000000000..129451470f
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/libraries/library/details/accounts.ts
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { hash } from 'rsvp';
+
+import type LdapLibraryModel from 'vault/models/ldap/library';
+
+export default class LdapLibraryRoute extends Route {
+ model() {
+ const model = this.modelFor('libraries.library') as LdapLibraryModel;
+ return hash({
+ library: model,
+ statuses: model.fetchStatus(),
+ });
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/libraries/library/details/index.ts b/ui/lib/ldap/addon/routes/libraries/library/details/index.ts
new file mode 100644
index 0000000000..a3a3c92585
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/libraries/library/details/index.ts
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+
+import type RouterService from '@ember/routing/router-service';
+
+export default class LdapLibraryRoute extends Route {
+ @service declare readonly router: RouterService;
+
+ redirect() {
+ this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries.library.details.accounts');
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/libraries/library/edit.ts b/ui/lib/ldap/addon/routes/libraries/library/edit.ts
new file mode 100644
index 0000000000..c64b45dd8d
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/libraries/library/edit.ts
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+
+import type LdapLibraryModel from 'vault/models/ldap/library';
+import type Controller from '@ember/controller';
+import type Transition from '@ember/routing/transition';
+import type { Breadcrumb } from 'vault/vault/app-types';
+
+interface LdapLibraryEditController extends Controller {
+ breadcrumbs: Array;
+ model: LdapLibraryModel;
+}
+
+export default class LdapLibraryEditRoute extends Route {
+ setupController(
+ controller: LdapLibraryEditController,
+ resolvedModel: LdapLibraryModel,
+ transition: Transition
+ ) {
+ super.setupController(controller, resolvedModel, transition);
+
+ controller.breadcrumbs = [
+ { label: resolvedModel.backend, route: 'overview' },
+ { label: 'libraries', route: 'libraries' },
+ { label: resolvedModel.name, route: 'libraries.library.details' },
+ { label: 'edit' },
+ ];
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/libraries/library/index.ts b/ui/lib/ldap/addon/routes/libraries/library/index.ts
new file mode 100644
index 0000000000..61dd0122d8
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/libraries/library/index.ts
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+
+import type RouterService from '@ember/routing/router-service';
+
+export default class LdapLibraryRoute extends Route {
+ @service declare readonly router: RouterService;
+
+ redirect() {
+ this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries.library.details');
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/overview.ts b/ui/lib/ldap/addon/routes/overview.ts
new file mode 100644
index 0000000000..cf774c453c
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/overview.ts
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as 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';
+
+interface LdapOverviewController extends Controller {
+ breadcrumbs: Array;
+}
+interface LdapOverviewRouteModel {
+ backendModel: SecretEngineModel;
+ promptConfig: boolean;
+ roles: Array;
+ libraries: Array;
+ librariesStatus: Array;
+}
+
+@withConfig('ldap/config')
+export default class LdapOverviewRoute extends Route {
+ @service declare readonly store: Store;
+ @service declare readonly secretMountPath: SecretMountPath;
+
+ declare promptConfig: boolean;
+
+ async fetchLibrariesStatus(libraries: Array): Promise> {
+ const allStatuses: Array = [];
+
+ for (const library of libraries) {
+ try {
+ const statuses = await library.fetchStatus();
+ allStatuses.push(...statuses);
+ } catch (error) {
+ // suppressing error
+ }
+ }
+ return allStatuses;
+ }
+
+ async fetchLibraries(backend: string) {
+ return this.store.query('ldap/library', { backend }).catch(() => []);
+ }
+
+ async model() {
+ const backend = this.secretMountPath.currentPath;
+ const libraries = await this.fetchLibraries(backend);
+ return hash({
+ promptConfig: this.promptConfig,
+ backendModel: this.modelFor('application'),
+ roles: this.store.query('ldap/role', { backend }).catch(() => []),
+ libraries,
+ librariesStatus: this.fetchLibrariesStatus(libraries as Array),
+ });
+ }
+
+ setupController(
+ controller: LdapOverviewController,
+ resolvedModel: LdapOverviewRouteModel,
+ transition: Transition
+ ) {
+ super.setupController(controller, resolvedModel, transition);
+
+ controller.breadcrumbs = [
+ { label: 'secrets', route: 'secrets', linkExternal: true },
+ { label: resolvedModel.backendModel.id },
+ ];
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/roles/create.ts b/ui/lib/ldap/addon/routes/roles/create.ts
new file mode 100644
index 0000000000..7026b66e77
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/roles/create.ts
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+
+import type Store from '@ember-data/store';
+import type SecretMountPath from 'vault/services/secret-mount-path';
+import type LdapRoleModel from 'vault/models/ldap/role';
+import type Controller from '@ember/controller';
+import type Transition from '@ember/routing/transition';
+import type { Breadcrumb } from 'vault/vault/app-types';
+
+interface LdapRolesCreateController extends Controller {
+ breadcrumbs: Array;
+ model: LdapRoleModel;
+}
+
+export default class LdapRolesCreateRoute extends Route {
+ @service declare readonly store: Store;
+ @service declare readonly secretMountPath: SecretMountPath;
+
+ model() {
+ const backend = this.secretMountPath.currentPath;
+ return this.store.createRecord('ldap/role', { backend });
+ }
+
+ setupController(
+ controller: LdapRolesCreateController,
+ resolvedModel: LdapRoleModel,
+ transition: Transition
+ ) {
+ super.setupController(controller, resolvedModel, transition);
+
+ controller.breadcrumbs = [
+ { label: resolvedModel.backend, route: 'overview' },
+ { label: 'roles', route: 'roles' },
+ { label: 'create' },
+ ];
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/roles/index.ts b/ui/lib/ldap/addon/routes/roles/index.ts
new file mode 100644
index 0000000000..6d4818a986
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/roles/index.ts
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as 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 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';
+
+interface LdapRolesRouteModel {
+ backendModel: SecretEngineModel;
+ promptConfig: boolean;
+ roles: Array;
+}
+interface LdapRolesController extends Controller {
+ breadcrumbs: Array;
+ model: LdapRolesRouteModel;
+}
+
+@withConfig('ldap/config')
+export default class LdapRolesRoute extends Route {
+ @service declare readonly store: Store;
+ @service declare readonly secretMountPath: SecretMountPath;
+
+ declare promptConfig: boolean;
+
+ model() {
+ const backendModel = this.modelFor('application') as SecretEngineModel;
+ return hash({
+ backendModel,
+ promptConfig: this.promptConfig,
+ roles: this.store.query(
+ 'ldap/role',
+ { backend: backendModel.id },
+ { adapterOptions: { showPartialError: true } }
+ ),
+ });
+ }
+
+ setupController(
+ controller: LdapRolesController,
+ resolvedModel: LdapRolesRouteModel,
+ transition: Transition
+ ) {
+ super.setupController(controller, resolvedModel, transition);
+
+ controller.breadcrumbs = [
+ { label: 'secrets', route: 'secrets', linkExternal: true },
+ { label: resolvedModel.backendModel.id },
+ ];
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/roles/role.ts b/ui/lib/ldap/addon/routes/roles/role.ts
new file mode 100644
index 0000000000..238cdf01f2
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/roles/role.ts
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+
+import type Store from '@ember-data/store';
+import type SecretMountPath from 'vault/services/secret-mount-path';
+
+interface LdapRoleRouteParams {
+ name: string;
+ type: string;
+}
+
+export default class LdapRoleRoute extends Route {
+ @service declare readonly store: Store;
+ @service declare readonly secretMountPath: SecretMountPath;
+
+ model(params: LdapRoleRouteParams) {
+ const backend = this.secretMountPath.currentPath;
+ const { name, type } = params;
+ return this.store.queryRecord('ldap/role', { backend, name, type });
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/roles/role/credentials.ts b/ui/lib/ldap/addon/routes/roles/role/credentials.ts
new file mode 100644
index 0000000000..f0c91641fa
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/roles/role/credentials.ts
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+
+import type Store from '@ember-data/store';
+import type LdapRoleModel from 'vault/models/ldap/role';
+import type Controller from '@ember/controller';
+import type Transition from '@ember/routing/transition';
+import type { Breadcrumb } from 'vault/vault/app-types';
+
+interface LdapRoleCredentialsController extends Controller {
+ breadcrumbs: Array;
+ model: LdapRoleModel;
+}
+export interface LdapStaticRoleCredentials {
+ dn: string;
+ last_vault_rotation: string;
+ password: string;
+ last_password: string;
+ rotation_period: number;
+ ttl: number;
+ username: string;
+ type: string;
+}
+export interface LdapDynamicRoleCredentials {
+ distinguished_names: Array;
+ password: string;
+ username: string;
+ lease_id: string;
+ lease_duration: string;
+ renewable: boolean;
+ type: string;
+}
+
+export default class LdapRoleCredentialsRoute extends Route {
+ @service declare readonly store: Store;
+
+ model() {
+ const role = this.modelFor('roles.role') as LdapRoleModel;
+ return role.fetchCredentials();
+ }
+ setupController(
+ controller: LdapRoleCredentialsController,
+ resolvedModel: LdapStaticRoleCredentials | LdapDynamicRoleCredentials,
+ transition: Transition
+ ) {
+ super.setupController(controller, resolvedModel, transition);
+
+ const role = this.modelFor('roles.role') as LdapRoleModel;
+ controller.breadcrumbs = [
+ { label: role.backend, route: 'overview' },
+ { label: 'roles', route: 'roles' },
+ { label: role.name, route: 'roles.role' },
+ { label: 'credentials' },
+ ];
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/roles/role/details.ts b/ui/lib/ldap/addon/routes/roles/role/details.ts
new file mode 100644
index 0000000000..278e3f053f
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/roles/role/details.ts
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+
+import type LdapRoleModel from 'vault/models/ldap/role';
+import type Controller from '@ember/controller';
+import type Transition from '@ember/routing/transition';
+import type { Breadcrumb } from 'vault/vault/app-types';
+
+interface LdapRoleDetailsController extends Controller {
+ breadcrumbs: Array;
+ model: LdapRoleModel;
+}
+
+export default class LdapRoleEditRoute extends Route {
+ setupController(
+ controller: LdapRoleDetailsController,
+ resolvedModel: LdapRoleModel,
+ transition: Transition
+ ) {
+ super.setupController(controller, resolvedModel, transition);
+
+ controller.breadcrumbs = [
+ { label: resolvedModel.backend, route: 'overview' },
+ { label: 'roles', route: 'roles' },
+ { label: resolvedModel.name },
+ ];
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/roles/role/edit.ts b/ui/lib/ldap/addon/routes/roles/role/edit.ts
new file mode 100644
index 0000000000..b4fff55ddf
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/roles/role/edit.ts
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+
+import type LdapRoleModel from 'vault/models/ldap/role';
+import type Controller from '@ember/controller';
+import type Transition from '@ember/routing/transition';
+import type { Breadcrumb } from 'vault/vault/app-types';
+
+interface LdapRoleEditController extends Controller {
+ breadcrumbs: Array;
+ model: LdapRoleModel;
+}
+
+export default class LdapRoleEditRoute extends Route {
+ setupController(controller: LdapRoleEditController, resolvedModel: LdapRoleModel, transition: Transition) {
+ super.setupController(controller, resolvedModel, transition);
+
+ controller.breadcrumbs = [
+ { label: resolvedModel.backend, route: 'overview' },
+ { label: 'roles', route: 'roles' },
+ { label: resolvedModel.name, route: 'roles.role' },
+ { label: 'edit' },
+ ];
+ }
+}
diff --git a/ui/lib/ldap/addon/routes/roles/role/index.ts b/ui/lib/ldap/addon/routes/roles/role/index.ts
new file mode 100644
index 0000000000..5133dc9066
--- /dev/null
+++ b/ui/lib/ldap/addon/routes/roles/role/index.ts
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+
+import type RouterService from '@ember/routing/router-service';
+
+export default class LdapRoleRoute extends Route {
+ @service declare readonly router: RouterService;
+
+ redirect() {
+ this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles.role.details');
+ }
+}
diff --git a/ui/lib/ldap/addon/templates/configuration.hbs b/ui/lib/ldap/addon/templates/configuration.hbs
new file mode 100644
index 0000000000..d79318f09d
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/configuration.hbs
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/configure.hbs b/ui/lib/ldap/addon/templates/configure.hbs
new file mode 100644
index 0000000000..ade9f95d59
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/configure.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/error.hbs b/ui/lib/ldap/addon/templates/error.hbs
new file mode 100644
index 0000000000..ec31dbdc45
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/error.hbs
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/libraries/create.hbs b/ui/lib/ldap/addon/templates/libraries/create.hbs
new file mode 100644
index 0000000000..6d6a9b60f1
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/libraries/create.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/libraries/index.hbs b/ui/lib/ldap/addon/templates/libraries/index.hbs
new file mode 100644
index 0000000000..6a7b8c357e
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/libraries/index.hbs
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/libraries/library/check-out.hbs b/ui/lib/ldap/addon/templates/libraries/library/check-out.hbs
new file mode 100644
index 0000000000..df9b3dd0ca
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/libraries/library/check-out.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/libraries/library/details.hbs b/ui/lib/ldap/addon/templates/libraries/library/details.hbs
new file mode 100644
index 0000000000..3facec177d
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/libraries/library/details.hbs
@@ -0,0 +1,3 @@
+
+
+{{outlet}}
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/libraries/library/details/accounts.hbs b/ui/lib/ldap/addon/templates/libraries/library/details/accounts.hbs
new file mode 100644
index 0000000000..f4a0737bac
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/libraries/library/details/accounts.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/libraries/library/details/configuration.hbs b/ui/lib/ldap/addon/templates/libraries/library/details/configuration.hbs
new file mode 100644
index 0000000000..5bb8dc5833
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/libraries/library/details/configuration.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/libraries/library/edit.hbs b/ui/lib/ldap/addon/templates/libraries/library/edit.hbs
new file mode 100644
index 0000000000..6d6a9b60f1
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/libraries/library/edit.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/overview.hbs b/ui/lib/ldap/addon/templates/overview.hbs
new file mode 100644
index 0000000000..78e8ad99f1
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/overview.hbs
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/roles/create.hbs b/ui/lib/ldap/addon/templates/roles/create.hbs
new file mode 100644
index 0000000000..a5770ab85a
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/roles/create.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/roles/index.hbs b/ui/lib/ldap/addon/templates/roles/index.hbs
new file mode 100644
index 0000000000..694d1818d8
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/roles/index.hbs
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/roles/role/credentials.hbs b/ui/lib/ldap/addon/templates/roles/role/credentials.hbs
new file mode 100644
index 0000000000..3918b7c16e
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/roles/role/credentials.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/roles/role/details.hbs b/ui/lib/ldap/addon/templates/roles/role/details.hbs
new file mode 100644
index 0000000000..e7ad643c75
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/roles/role/details.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/lib/ldap/addon/templates/roles/role/edit.hbs b/ui/lib/ldap/addon/templates/roles/role/edit.hbs
new file mode 100644
index 0000000000..a5770ab85a
--- /dev/null
+++ b/ui/lib/ldap/addon/templates/roles/role/edit.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/lib/ldap/config/environment.js b/ui/lib/ldap/config/environment.js
new file mode 100644
index 0000000000..4368f39225
--- /dev/null
+++ b/ui/lib/ldap/config/environment.js
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+/* eslint-env node */
+'use strict';
+
+module.exports = function (environment) {
+ const ENV = {
+ modulePrefix: 'ldap',
+ environment,
+ };
+
+ return ENV;
+};
diff --git a/ui/lib/ldap/index.js b/ui/lib/ldap/index.js
new file mode 100644
index 0000000000..cade4e0e02
--- /dev/null
+++ b/ui/lib/ldap/index.js
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+/* eslint-env node */
+/* eslint-disable n/no-extraneous-require */
+'use strict';
+
+const { buildEngine } = require('ember-engines/lib/engine-addon');
+
+module.exports = buildEngine({
+ name: 'ldap',
+
+ lazyLoading: Object.freeze({
+ enabled: false,
+ }),
+
+ isDevelopingAddon() {
+ return true;
+ },
+});
diff --git a/ui/lib/ldap/package.json b/ui/lib/ldap/package.json
new file mode 100644
index 0000000000..5288e352ef
--- /dev/null
+++ b/ui/lib/ldap/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "ldap",
+ "keywords": [
+ "ember-addon",
+ "ember-engine"
+ ],
+ "dependencies": {
+ "@hashicorp/design-system-components": "*",
+ "ember-cli-htmlbars": "*",
+ "ember-cli-babel": "*",
+ "ember-concurrency": "*",
+ "@ember/test-waiters": "*",
+ "ember-cli-typescript": "*",
+ "@types/ember": "latest",
+ "@types/ember-data": "latest",
+ "@types/ember-data__store": "latest",
+ "@types/ember__array": "latest",
+ "@types/ember__component": "latest",
+ "@types/ember__controller": "latest",
+ "@types/ember__engine": "latest",
+ "@types/ember__routing": "latest",
+ "@types/rsvp": "latest"
+ },
+ "ember-addon": {
+ "paths": [
+ "../core"
+ ]
+ }
+}
diff --git a/ui/mirage/factories/ldap-config.js b/ui/mirage/factories/ldap-config.js
new file mode 100644
index 0000000000..4b2db19895
--- /dev/null
+++ b/ui/mirage/factories/ldap-config.js
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { Factory } from 'ember-cli-mirage';
+
+export default Factory.extend({
+ backend: 'ldap-test',
+ binddn: 'cn=vault,ou=Users,dc=hashicorp,dc=com',
+ bindpass: 'pa$$w0rd',
+ url: 'ldaps://127.0.0.11',
+ password_policy: 'default',
+ schema: 'openldap',
+ starttls: false,
+ insecure_tls: false,
+ certificate:
+ '-----BEGIN CERTIFICATE-----\nMIIDNTCCAh2gApGgAwIBAgIULNEk+01LpkDeJujfsAgIULNEkAgIULNEckApGgAwIBAg+01LpkDeJuj\n-----END CERTIFICATE-----',
+ client_tls_cert:
+ '-----BEGIN CERTIFICATE-----\nMIIDNTCCAh2gApGgAwIBAgIULNEk+01LpkDeJujfsAgIULNEkAgIULNEckApGgAwIBAg+01LpkDeJuj\n-----END CERTIFICATE-----',
+ client_tls_key: '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=',
+ userdn: 'ou=Users,dc=hashicorp,dc=com',
+ userattr: 'cn',
+ upndomain: 'vault@hashicorp.com',
+ connection_timeout: 90,
+ request_timeout: 30,
+});
diff --git a/ui/mirage/factories/ldap-credential.js b/ui/mirage/factories/ldap-credential.js
new file mode 100644
index 0000000000..ce3d6dbd6a
--- /dev/null
+++ b/ui/mirage/factories/ldap-credential.js
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { Factory, trait } from 'ember-cli-mirage';
+
+export default Factory.extend({
+ // static props
+ static: trait({
+ last_vault_rotation: '2023-07-31T10:32:49.744033-06:00',
+ password: 'fQ428N5JVeB2MbINwBCIbPh2ffhkJP0jZT3SfopZO0xRmbOaKRa6bwtAw3d2m4DR',
+ username: 'foobar',
+ rotation_period: 86400,
+ ttl: 71365,
+ type: 'static',
+ }),
+
+ // dynamic props
+ dynamic: trait({
+ distinguished_names: [
+ 'cn=v_userpass-test_dynamic-role_mrx3r26XIj_1690836430,ou=users,dc=learn,dc=example',
+ ],
+ username: 'v_userpass-test_dynamic-role_mrx3r26XIj_1690836430',
+ password: 'YE2qe1vpiBtEvjCSr7BmI0NhSPPmrizngNYxa3lEebMFvAussxHf3PWfDVJPxXj1',
+ lease_id: 'ldap/creds/dynamic-role/SZN8HcuieCbdDobD7jTb6V9X',
+ lease_duration: 3600,
+ renewable: true,
+ type: 'dynamic',
+ }),
+});
diff --git a/ui/mirage/factories/ldap-library.js b/ui/mirage/factories/ldap-library.js
new file mode 100644
index 0000000000..dd22c92d2a
--- /dev/null
+++ b/ui/mirage/factories/ldap-library.js
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { Factory } from 'ember-cli-mirage';
+
+export default Factory.extend({
+ name: (i) => `library-${i}`,
+ service_account_names: () => ['fizz@example.com', 'buzz@example.com'],
+ ttl: '10h',
+ max_ttl: '20h',
+ disable_check_in_enforcement: false,
+});
diff --git a/ui/mirage/factories/ldap-role.js b/ui/mirage/factories/ldap-role.js
new file mode 100644
index 0000000000..e92a8a1968
--- /dev/null
+++ b/ui/mirage/factories/ldap-role.js
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { Factory, trait } from 'ember-cli-mirage';
+
+export default Factory.extend({
+ name: (i) => `role-${i}`,
+
+ // static props
+ static: trait({
+ dn: 'cn=hashicorp,ou=Users,dc=hashicorp,dc=com',
+ rotation_period: 10,
+ username: 'hashicorp',
+ type: 'static',
+ }),
+
+ // dynamic props
+ dynamic: trait({
+ creation_ldif: `dn: cn={{.Username}},ou=users,dc=learn,dc=example
+ objectClass: person
+ objectClass: top
+ cn: learn
+ sn: {{.Password | utf16le | base64}}
+ memberOf: cn=dev,ou=groups,dc=learn,dc=example
+ userPassword: {{.Password}}
+ `,
+ deletion_ldif: `dn: cn={{.Username}},ou=users,dc=learn,dc=example
+ changetype: delete
+ `,
+ rollback_ldif: `dn: cn={{.Username}},ou=users,dc=learn,dc=example
+ changetype: delete
+ `,
+ username_template: '{{.DisplayName}}_{{.RoleName}}',
+ default_ttl: 3600,
+ max_ttl: 86400,
+ type: 'dynamic',
+ }),
+});
diff --git a/ui/mirage/handlers/index.js b/ui/mirage/handlers/index.js
index 65377c3568..5da1be58be 100644
--- a/ui/mirage/handlers/index.js
+++ b/ui/mirage/handlers/index.js
@@ -14,5 +14,6 @@ import mfaLogin from './mfa-login';
import oidcConfig from './oidc-config';
import hcpLink from './hcp-link';
import kubernetes from './kubernetes';
+import ldap from './ldap';
-export { base, clients, db, kms, mfaConfig, mfaLogin, oidcConfig, hcpLink, kubernetes };
+export { base, clients, db, kms, mfaConfig, mfaLogin, oidcConfig, hcpLink, kubernetes, ldap };
diff --git a/ui/mirage/handlers/ldap.js b/ui/mirage/handlers/ldap.js
new file mode 100644
index 0000000000..afa56d921e
--- /dev/null
+++ b/ui/mirage/handlers/ldap.js
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { Response } from 'miragejs';
+
+export default function (server) {
+ const query = (req) => {
+ const { name, backend } = req.params;
+ return name ? { name } : { backend };
+ };
+ const getRecord = (schema, req, dbKey) => {
+ const record = schema.db[dbKey].findBy(query(req));
+ if (record) {
+ delete record.id;
+ delete record.name;
+ delete record.backend;
+ delete record.type;
+ return { data: record };
+ }
+ return new Response(404, {}, { errors: [] });
+ };
+ const createOrUpdateRecord = (schema, req, dbKey) => {
+ const data = JSON.parse(req.requestBody);
+ const dbCollection = schema.db[dbKey];
+ dbCollection.firstOrCreate(query(req), data);
+ dbCollection.update(query(req), data);
+ return new Response(204);
+ };
+ const listRecords = (schema, dbKey, query = {}) => {
+ const records = schema.db[dbKey].where(query);
+ return {
+ data: { keys: records.map((record) => record.name) },
+ };
+ };
+
+ // mount
+ server.post('/sys/mounts/:path', () => new Response(204));
+ server.get('/sys/internal/ui/mounts/:path', () => ({
+ data: {
+ accessor: 'ldap_ade94329',
+ type: 'ldap',
+ path: 'ldap-test/',
+ uuid: '35e9119d-5708-4b6b-58d2-f913e27f242d',
+ config: {},
+ },
+ }));
+ // config
+ server.post('/:backend/config', (schema, req) => createOrUpdateRecord(schema, req, 'ldapConfigs'));
+ server.get('/:backend/config', (schema, req) => getRecord(schema, req, 'ldapConfigs'));
+ // roles
+ server.post('/:backend/static-role/:name', (schema, req) => createOrUpdateRecord(schema, req, 'ldapRoles'));
+ server.post('/:backend/role/:name', (schema, req) => createOrUpdateRecord(schema, req, 'ldapRoles'));
+ server.get('/:backend/static-role/:name', (schema, req) => getRecord(schema, req, 'ldapRoles', 'static'));
+ server.get('/:backend/role/:name', (schema, req) => getRecord(schema, req, 'ldapRoles', 'dynamic'));
+ server.get('/:backend/static-role', (schema) => listRecords(schema, 'ldapRoles', { type: 'static' }));
+ server.get('/:backend/role', (schema) => listRecords(schema, 'ldapRoles', { type: 'dynamic' }));
+ // role credentials
+ server.get('/:backend/static-cred/:name', (schema) => ({
+ data: schema.db.ldapCredentials.firstOrCreate({ type: 'static' }),
+ }));
+ server.get('/:backend/creds/:name', (schema) => ({
+ data: schema.db.ldapCredentials.firstOrCreate({ type: 'dynamic' }),
+ }));
+ // libraries
+ server.post('/:backend/library/:name', (schema, req) => createOrUpdateRecord(schema, req, 'ldapLibraries'));
+ server.get('/:backend/library/:name', (schema, req) => getRecord(schema, req, 'ldapLibraries'));
+ server.get('/:backend/library', (schema) => listRecords(schema, 'ldapLibraries'));
+ server.get('/:backend/library/:name/status', () => ({
+ 'bob.johnson': { available: false, borrower_client_token: '8b80c305eb3a7dbd161ef98f10ea60a116ce0910' },
+ 'mary.smith': { available: true },
+ }));
+}
diff --git a/ui/mirage/scenarios/default.js b/ui/mirage/scenarios/default.js
index 72963c52f1..56a74a5405 100644
--- a/ui/mirage/scenarios/default.js
+++ b/ui/mirage/scenarios/default.js
@@ -5,13 +5,13 @@
import ENV from 'vault/config/environment';
const { handler } = ENV['ember-cli-mirage'];
-import kubernetesScenario from './kubernetes';
+import scenarios from './index';
export default function (server) {
server.create('clients/config');
server.create('feature', { feature_flags: ['SOME_FLAG', 'VAULT_CLOUD_ADMIN_NAMESPACE'] });
- if (handler === 'kubernetes') {
- kubernetesScenario(server);
+ if (handler in scenarios) {
+ scenarios[handler](server);
}
}
diff --git a/ui/mirage/scenarios/index.js b/ui/mirage/scenarios/index.js
new file mode 100644
index 0000000000..c7adfcd73b
--- /dev/null
+++ b/ui/mirage/scenarios/index.js
@@ -0,0 +1,4 @@
+import kubernetes from './kubernetes';
+import ldap from './ldap';
+
+export { kubernetes, ldap };
diff --git a/ui/mirage/scenarios/ldap.js b/ui/mirage/scenarios/ldap.js
new file mode 100644
index 0000000000..19b6b61f4c
--- /dev/null
+++ b/ui/mirage/scenarios/ldap.js
@@ -0,0 +1,11 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+export default function (server) {
+ server.create('ldap-config', { path: 'kubernetes' });
+ server.create('ldap-role', 'static', { name: 'static-role' });
+ server.create('ldap-role', 'dynamic', { name: 'dynamic-role' });
+ server.create('ldap-library', { name: 'test-library' });
+}
diff --git a/ui/package.json b/ui/package.json
index be7cad9e41..273b8016e9 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -240,6 +240,7 @@
"lib/keep-gitkeep",
"lib/kmip",
"lib/kubernetes",
+ "lib/ldap",
"lib/kv",
"lib/open-api-explorer",
"lib/pki",
diff --git a/ui/tests/acceptance/secrets/backend/database/secret-test.js b/ui/tests/acceptance/secrets/backend/database/secret-test.js
index 5276c9d020..ccf018459c 100644
--- a/ui/tests/acceptance/secrets/backend/database/secret-test.js
+++ b/ui/tests/acceptance/secrets/backend/database/secret-test.js
@@ -33,7 +33,7 @@ const newConnection = async (backend, plugin = 'mongodb-database-plugin') => {
const navToConnection = async (backend, connection) => {
await visit('/vault/secrets');
- await click(`[data-test-auth-backend-link="${backend}"]`);
+ await click(`[data-test-secrets-backend-link="${backend}"]`);
await click('[data-test-secret-list-tab="Connections"]');
await click(`[data-test-secret-link="${connection}"]`);
return;
@@ -454,7 +454,7 @@ module('Acceptance | secrets/database/*', function (hooks) {
// Check with restricted permissions
await authPage.login(token);
await click('[data-test-sidebar-nav-link="Secrets engines"]');
- assert.dom(`[data-test-auth-backend-link="${backend}"]`).exists('Shows backend on secret list page');
+ assert.dom(`[data-test-secrets-backend-link="${backend}"]`).exists('Shows backend on secret list page');
await navToConnection(backend, connection);
assert.strictEqual(
currentURL(),
diff --git a/ui/tests/acceptance/secrets/backend/engines-test.js b/ui/tests/acceptance/secrets/backend/engines-test.js
index ed976aad13..79b657b77c 100644
--- a/ui/tests/acceptance/secrets/backend/engines-test.js
+++ b/ui/tests/acceptance/secrets/backend/engines-test.js
@@ -65,7 +65,7 @@ module('Acceptance | secret-engine list view', function (hooks) {
await backendsPage.visit();
await settled();
- const rows = document.querySelectorAll('[data-test-auth-backend-link]');
+ const rows = document.querySelectorAll('[data-test-secrets-backend-link]');
const rowUnsupported = Array.from(rows).filter((row) => row.innerText.includes('nomad'));
const rowSupported = Array.from(rows).filter((row) => row.innerText.includes('cubbyhole'));
assert
@@ -93,7 +93,7 @@ module('Acceptance | secret-engine list view', function (hooks) {
await clickTrigger('#filter-by-engine-type');
await searchSelect.options.objectAt(0).click();
- const rows = document.querySelectorAll('[data-test-auth-backend-link]');
+ const rows = document.querySelectorAll('[data-test-secrets-backend-link]');
const rowsAws = Array.from(rows).filter((row) => row.innerText.includes('aws'));
assert.strictEqual(rows.length, rowsAws.length, 'all rows returned are aws');
@@ -101,12 +101,12 @@ module('Acceptance | secret-engine list view', function (hooks) {
await clickTrigger('#filter-by-engine-name');
const firstItemToSelect = searchSelect.options.objectAt(0).text;
await searchSelect.options.objectAt(0).click();
- const singleRow = document.querySelectorAll('[data-test-auth-backend-link]');
+ const singleRow = document.querySelectorAll('[data-test-secrets-backend-link]');
assert.strictEqual(singleRow.length, 1, 'returns only one row');
assert.dom(singleRow[0]).includesText(firstItemToSelect, 'shows the filtered by name engine');
// clear filter by engine name
await searchSelect.deleteButtons.objectAt(1).click();
- const rowsAgain = document.querySelectorAll('[data-test-auth-backend-link]');
+ const rowsAgain = document.querySelectorAll('[data-test-secrets-backend-link]');
assert.ok(rowsAgain.length > 1, 'filter has been removed');
// cleanup
diff --git a/ui/tests/acceptance/secrets/backend/kv/secret-test.js b/ui/tests/acceptance/secrets/backend/kv/secret-test.js
index fbf49af435..4fcc69ce53 100644
--- a/ui/tests/acceptance/secrets/backend/kv/secret-test.js
+++ b/ui/tests/acceptance/secrets/backend/kv/secret-test.js
@@ -712,7 +712,7 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) {
// on login users are directed to dashboard, so we would need to visit the vault secrets page to click on an engine
await visit('vault/secrets');
// test if metadata tab there with no read access message and no ability to edit.
- await click(`[data-test-auth-backend-link=${enginePath}]`);
+ await click(`[data-test-secrets-backend-link=${enginePath}]`);
assert
.dom('[data-test-get-credentials]')
.exists(
diff --git a/ui/tests/acceptance/secrets/backend/ldap/libraries-test.js b/ui/tests/acceptance/secrets/backend/ldap/libraries-test.js
new file mode 100644
index 0000000000..10bc31f23e
--- /dev/null
+++ b/ui/tests/acceptance/secrets/backend/ldap/libraries-test.js
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupApplicationTest } from 'ember-qunit';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import ldapMirageScenario from 'vault/mirage/scenarios/ldap';
+import ENV from 'vault/config/environment';
+import authPage from 'vault/tests/pages/auth';
+import { click } from '@ember/test-helpers';
+import { isURL, visitURL } from 'vault/tests/helpers/ldap';
+
+module('Acceptance | ldap | libraries', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ hooks.before(function () {
+ ENV['ember-cli-mirage'].handler = 'ldap';
+ });
+
+ hooks.beforeEach(async function () {
+ ldapMirageScenario(this.server);
+ await authPage.login();
+ return visitURL('libraries');
+ });
+
+ hooks.after(function () {
+ ENV['ember-cli-mirage'].handler = null;
+ });
+
+ test('it should transition to create library route on toolbar link click', async function (assert) {
+ await click('[data-test-toolbar-action="library"]');
+ assert.true(isURL('libraries/create'), 'Transitions to library create route on toolbar link click');
+ });
+
+ test('it should transition to library details route on list item click', async function (assert) {
+ await click('[data-test-list-item-link] a');
+ assert.true(
+ isURL('libraries/test-library/details/accounts'),
+ 'Transitions to library details accounts route on list item click'
+ );
+ });
+
+ test('it should transition to routes from list item action menu', async function (assert) {
+ assert.expect(2);
+
+ for (const action of ['edit', 'details']) {
+ await click('[data-test-popup-menu-trigger]');
+ await click(`[data-test-${action}]`);
+ const uri = action === 'details' ? 'details/accounts' : action;
+ assert.true(
+ isURL(`libraries/test-library/${uri}`),
+ `Transitions to ${action} route on list item action menu click`
+ );
+ await click('[data-test-breadcrumb="libraries"]');
+ }
+ });
+
+ test('it should transition to details routes from tab links', async function (assert) {
+ await click('[data-test-list-item-link] a');
+ await click('[data-test-tab="config"]');
+ assert.true(
+ isURL('libraries/test-library/details/configuration'),
+ 'Transitions to configuration route on tab click'
+ );
+
+ await click('[data-test-tab="accounts"]');
+ assert.true(
+ isURL('libraries/test-library/details/accounts'),
+ 'Transitions to accounts route on tab click'
+ );
+ });
+
+ test('it should transition to routes from library details toolbar links', async function (assert) {
+ await click('[data-test-list-item-link] a');
+ await click('[data-test-edit]');
+ assert.true(isURL('libraries/test-library/edit'), 'Transitions to credentials route from toolbar link');
+ });
+});
diff --git a/ui/tests/acceptance/secrets/backend/ldap/overview-test.js b/ui/tests/acceptance/secrets/backend/ldap/overview-test.js
new file mode 100644
index 0000000000..ba1a12ee22
--- /dev/null
+++ b/ui/tests/acceptance/secrets/backend/ldap/overview-test.js
@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupApplicationTest } from 'ember-qunit';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import ldapMirageScenario from 'vault/mirage/scenarios/ldap';
+import ENV from 'vault/config/environment';
+import authPage from 'vault/tests/pages/auth';
+import { click, fillIn, visit } from '@ember/test-helpers';
+import { selectChoose } from 'ember-power-select/test-support';
+import { isURL, visitURL } from 'vault/tests/helpers/ldap';
+
+module('Acceptance | ldap | overview', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ hooks.before(function () {
+ ENV['ember-cli-mirage'].handler = 'ldap';
+ });
+
+ hooks.beforeEach(async function () {
+ return authPage.login();
+ });
+
+ hooks.after(function () {
+ ENV['ember-cli-mirage'].handler = null;
+ });
+
+ test('it should transition to ldap overview on mount success', async function (assert) {
+ await visit('/vault/secrets');
+ await click('[data-test-enable-engine]');
+ await click('[data-test-mount-type="ldap"]');
+ await click('[data-test-mount-next]');
+ await fillIn('[data-test-input="path"]', 'ldap-test');
+ await click('[data-test-mount-submit]');
+ assert.true(isURL('overview'), 'Transitions to ldap overview route on mount success');
+ });
+
+ test('it should transition to routes on tab link click', async function (assert) {
+ assert.expect(4);
+
+ await visitURL('overview');
+
+ for (const tab of ['roles', 'libraries', 'config', 'overview']) {
+ await click(`[data-test-tab="${tab}"]`);
+ const route = tab === 'config' ? 'configuration' : tab;
+ assert.true(isURL(route), `Transitions to ${route} route on tab link click`);
+ }
+ });
+
+ test('it should transition to configuration route when engine is not configured', async function (assert) {
+ await visitURL('overview');
+ await click('[data-test-config-cta] a');
+ assert.true(isURL('configure'), 'Transitions to configure route on cta link click');
+
+ await click('[data-test-breadcrumb="ldap-test"]');
+ await click('[data-test-toolbar-action="config"]');
+ assert.true(isURL('configure'), 'Transitions to configure route on toolbar link click');
+ });
+ // including a test for the configuration route here since it is the only one needed
+ test('it should transition to configuration edit on toolbar link click', async function (assert) {
+ ldapMirageScenario(this.server);
+ await visitURL('overview');
+ await click('[data-test-tab="config"]');
+ await click('[data-test-toolbar-config-action]');
+ assert.true(isURL('configure'), 'Transitions to configure route on toolbar link click');
+ });
+
+ test('it should transition to create role route on card action link click', async function (assert) {
+ ldapMirageScenario(this.server);
+ await visitURL('overview');
+ await click('[data-test-overview-card="Roles"] a');
+ assert.true(isURL('roles/create'), 'Transitions to role create route on card action link click');
+ });
+
+ test('it should transition to create library route on card action link click', async function (assert) {
+ ldapMirageScenario(this.server);
+ await visitURL('overview');
+ await click('[data-test-overview-card="Libraries"] a');
+ assert.true(isURL('libraries/create'), 'Transitions to library create route on card action link click');
+ });
+
+ test('it should transition to role credentials route on generate credentials action', async function (assert) {
+ ldapMirageScenario(this.server);
+ await visitURL('overview');
+ await selectChoose('.search-select', 'static-role');
+ await click('[data-test-generate-credential-button]');
+ assert.true(
+ isURL('roles/static/static-role/credentials'),
+ 'Transitions to role credentials route on generate credentials action'
+ );
+
+ await click('[data-test-breadcrumb="ldap-test"]');
+ await selectChoose('.search-select', 'dynamic-role');
+ await click('[data-test-generate-credential-button]');
+ assert.true(
+ isURL('roles/dynamic/dynamic-role/credentials'),
+ 'Transitions to role credentials route on generate credentials action'
+ );
+ });
+});
diff --git a/ui/tests/acceptance/secrets/backend/ldap/roles-test.js b/ui/tests/acceptance/secrets/backend/ldap/roles-test.js
new file mode 100644
index 0000000000..ce0c659962
--- /dev/null
+++ b/ui/tests/acceptance/secrets/backend/ldap/roles-test.js
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupApplicationTest } from 'ember-qunit';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import ldapMirageScenario from 'vault/mirage/scenarios/ldap';
+import ENV from 'vault/config/environment';
+import authPage from 'vault/tests/pages/auth';
+import { click } from '@ember/test-helpers';
+import { isURL, visitURL } from 'vault/tests/helpers/ldap';
+
+module('Acceptance | ldap | roles', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ hooks.before(function () {
+ ENV['ember-cli-mirage'].handler = 'ldap';
+ });
+
+ hooks.beforeEach(async function () {
+ ldapMirageScenario(this.server);
+ await authPage.login();
+ return visitURL('roles');
+ });
+
+ hooks.after(function () {
+ ENV['ember-cli-mirage'].handler = null;
+ });
+
+ test('it should transition to create role route on toolbar link click', async function (assert) {
+ await click('[data-test-toolbar-action="role"]');
+ assert.true(isURL('roles/create'), 'Transitions to role create route on toolbar link click');
+ });
+
+ test('it should transition to role details route on list item click', async function (assert) {
+ await click('[data-test-list-item-link]:nth-of-type(1) a');
+ assert.true(
+ isURL('roles/dynamic/dynamic-role/details'),
+ 'Transitions to role details route on list item click'
+ );
+
+ await click('[data-test-breadcrumb="roles"]');
+ await click('[data-test-list-item-link]:nth-of-type(2) a');
+ assert.true(
+ isURL('roles/static/static-role/details'),
+ 'Transitions to role details route on list item click'
+ );
+ });
+
+ test('it should transition to routes from list item action menu', async function (assert) {
+ assert.expect(3);
+
+ for (const action of ['edit', 'get-creds', 'details']) {
+ await click('[data-test-popup-menu-trigger]');
+ await click(`[data-test-${action}]`);
+ const uri = action === 'get-creds' ? 'credentials' : action;
+ assert.true(
+ isURL(`roles/dynamic/dynamic-role/${uri}`),
+ `Transitions to ${uri} route on list item action menu click`
+ );
+ await click('[data-test-breadcrumb="roles"]');
+ }
+ });
+
+ test('it should transition to routes from role details toolbar links', async function (assert) {
+ await click('[data-test-list-item-link]:nth-of-type(1) a');
+ await click('[data-test-get-credentials]');
+ assert.true(
+ isURL('roles/dynamic/dynamic-role/credentials'),
+ 'Transitions to credentials route from toolbar link'
+ );
+
+ await click('[data-test-breadcrumb="dynamic-role"]');
+ await click('[data-test-edit]');
+ assert.true(isURL('roles/dynamic/dynamic-role/edit'), 'Transitions to edit route from toolbar link');
+ });
+});
diff --git a/ui/tests/acceptance/settings/mount-secret-backend-test.js b/ui/tests/acceptance/settings/mount-secret-backend-test.js
index af1f77006a..c7ca2fc6db 100644
--- a/ui/tests/acceptance/settings/mount-secret-backend-test.js
+++ b/ui/tests/acceptance/settings/mount-secret-backend-test.js
@@ -133,7 +133,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
await page.secretList();
await settled();
assert
- .dom(`[data-test-auth-backend-link=${path}]`)
+ .dom(`[data-test-secrets-backend-link=${path}]`)
.exists({ count: 1 }, 'renders only one instance of the engine');
});
diff --git a/ui/tests/helpers/ldap.js b/ui/tests/helpers/ldap.js
new file mode 100644
index 0000000000..9edcc86494
--- /dev/null
+++ b/ui/tests/helpers/ldap.js
@@ -0,0 +1,35 @@
+import { visit, currentURL } from '@ember/test-helpers';
+
+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');
+};
+
+export const generateBreadcrumbs = (backend, childRoute) => {
+ const breadcrumbs = [{ label: 'secrets', route: 'secrets', linkExternal: true }];
+ const root = { label: backend };
+ if (childRoute) {
+ root.route = 'overview';
+ breadcrumbs.push({ label: childRoute });
+ }
+ breadcrumbs.splice(1, 0, root);
+ return breadcrumbs;
+};
+
+const baseURL = '/vault/secrets/ldap-test/ldap/';
+const stripLeadingSlash = (uri) => (uri.charAt(0) === '/' ? uri.slice(1) : uri);
+
+export const isURL = (uri) => {
+ return currentURL() === `${baseURL}${stripLeadingSlash(uri)}`;
+};
+
+export const visitURL = (uri) => {
+ return visit(`${baseURL}${stripLeadingSlash(uri)}`);
+};
diff --git a/ui/tests/integration/components/filter-input-test.js b/ui/tests/integration/components/filter-input-test.js
new file mode 100644
index 0000000000..fddd655675
--- /dev/null
+++ b/ui/tests/integration/components/filter-input-test.js
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, fillIn } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+module('Integration | Component | filter-input', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it should render placeholder and send input event', async function (assert) {
+ assert.expect(2);
+
+ this.onInput = (value) => {
+ assert.strictEqual(value, 'foo', 'onInput event sent with value');
+ };
+
+ await render(hbs` `);
+
+ assert
+ .dom('[data-test-filter-input]')
+ .hasAttribute('placeholder', 'Filter roles', 'Placeholder set on input element');
+
+ await fillIn('[data-test-filter-input]', 'foo');
+ });
+});
diff --git a/ui/tests/integration/components/json-editor-test.js b/ui/tests/integration/components/json-editor-test.js
index 9ae8007c74..f38e17f8ae 100644
--- a/ui/tests/integration/components/json-editor-test.js
+++ b/ui/tests/integration/components/json-editor-test.js
@@ -6,7 +6,7 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { create } from 'ember-cli-page-object';
-import { render, fillIn, find, waitUntil } from '@ember/test-helpers';
+import { render, fillIn, find, waitUntil, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import jsonEditor from '../../pages/components/json-editor';
import sinon from 'sinon';
@@ -78,4 +78,26 @@ module('Integration | Component | json-editor', function (hooks) {
});
assert.dom('.CodeMirror-linenumber').doesNotExist('on readOnly does not show line numbers');
});
+
+ test('it should render example and restore it', async function (assert) {
+ this.value = null;
+ this.example = 'this is a test example';
+
+ await render(hbs`
+
+ `);
+
+ assert.dom('.CodeMirror-code').hasText(`1${this.example}`, 'Example renders when there is no value');
+ assert.dom('[data-test-restore-example]').isDisabled('Restore button disabled when showing example');
+ await fillIn('textarea', '');
+ await fillIn('textarea', 'adding a value should allow the example to be restored');
+ await click('[data-test-restore-example]');
+ assert.dom('.CodeMirror-code').hasText(`1${this.example}`, 'Example is restored');
+ assert.strictEqual(this.value, null, 'Value is cleared on restore example');
+ });
});
diff --git a/ui/tests/integration/components/ldap/accounts-checked-out-test.js b/ui/tests/integration/components/ldap/accounts-checked-out-test.js
new file mode 100644
index 0000000000..4f8e4512c4
--- /dev/null
+++ b/ui/tests/integration/components/ldap/accounts-checked-out-test.js
@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
+import sinon from 'sinon';
+
+module('Integration | Component | ldap | AccountsCheckedOut', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
+
+ this.store = this.owner.lookup('service:store');
+ this.authStub = sinon.stub(this.owner.lookup('service:auth'), 'authData');
+
+ this.store.pushPayload('ldap/library', {
+ modelName: 'ldap/library',
+ backend: 'ldap-test',
+ ...this.server.create('ldap-library', { name: 'test-library' }),
+ });
+ this.library = this.store.peekRecord('ldap/library', 'test-library');
+ this.statuses = [
+ {
+ account: 'foo.bar',
+ available: false,
+ library: 'test-library',
+ borrower_client_token: '123',
+ borrower_entity_id: '456',
+ },
+ { account: 'bar.baz', available: false, library: 'test-library' },
+ { account: 'checked.in', available: true, library: 'test-library' },
+ ];
+ this.renderComponent = () => {
+ return render(
+ hbs`
+
+
+ `,
+ {
+ owner: this.engine,
+ }
+ );
+ };
+ });
+
+ test('it should render empty state when no accounts are checked out', async function (assert) {
+ this.statuses = [
+ { account: 'foo', available: true, library: 'test-library' },
+ { account: 'bar', available: true, library: 'test-library' },
+ ];
+
+ await this.renderComponent();
+
+ assert
+ .dom('[data-test-empty-state-title]')
+ .hasText('No accounts checked out yet', 'Empty state title renders');
+ assert
+ .dom('[data-test-empty-state-message]')
+ .hasText('There is no account that is currently in use.', 'Empty state message renders');
+ });
+
+ test('it should filter accounts for root user', async function (assert) {
+ this.authStub.value({});
+
+ await this.renderComponent();
+
+ assert.dom('[data-test-checked-out-account]').exists({ count: 1 }, 'Correct number of accounts render');
+ assert
+ .dom('[data-test-checked-out-account="bar.baz"]')
+ .hasText('bar.baz', 'Account renders that was checked out by root user');
+ });
+
+ test('it should filter accounts for non root user', async function (assert) {
+ this.authStub.value({ entity_id: '456' });
+
+ await this.renderComponent();
+
+ assert.dom('[data-test-checked-out-account]').exists({ count: 1 }, 'Correct number of accounts render');
+ assert
+ .dom('[data-test-checked-out-account="foo.bar"]')
+ .hasText('foo.bar', 'Account renders that was checked out by non root user');
+ });
+
+ test('it should display all accounts when check-in enforcement is disabled on library', async function (assert) {
+ this.library.disable_check_in_enforcement = 'Disabled';
+
+ await this.renderComponent();
+
+ assert.dom('[data-test-checked-out-account]').exists({ count: 2 }, 'Correct number of accounts render');
+ assert
+ .dom('[data-test-checked-out-account="checked.in"]')
+ .doesNotExist('checked.in', 'Checked in accounts do not render');
+ });
+
+ test('it should display details in table', async function (assert) {
+ this.authStub.value({ entity_id: '456' });
+
+ await this.renderComponent();
+
+ assert.dom('[data-test-checked-out-account="foo.bar"]').hasText('foo.bar', 'Account renders');
+ assert.dom('[data-test-checked-out-library="foo.bar"]').doesNotExist('Library column is hidden');
+ assert
+ .dom('[data-test-checked-out-account-action="foo.bar"]')
+ .includesText('Check-in', 'Check-in action renders');
+
+ this.showLibraryColumn = true;
+ await this.renderComponent();
+
+ assert.dom('[data-test-checked-out-library="foo.bar"]').hasText('test-library', 'Library column renders');
+ });
+
+ test('it should check in account', async function (assert) {
+ assert.expect(2);
+
+ const transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
+ this.library.disable_check_in_enforcement = 'Disabled';
+
+ this.server.post('/ldap-test/library/test-library/check-in', (schema, req) => {
+ const json = JSON.parse(req.requestBody);
+ assert.deepEqual(
+ json.service_account_names,
+ ['foo.bar'],
+ 'Check-in request made with correct account names'
+ );
+ });
+
+ await this.renderComponent();
+
+ await click('[data-test-checked-out-account-action="foo.bar"]');
+ await click('[data-test-check-in-confirm]');
+
+ const didTransition = transitionStub.calledWith(
+ 'vault.cluster.secrets.backend.ldap.libraries.library.details.accounts'
+ );
+ assert.true(didTransition, 'Transitions to accounts route on check-in success');
+ });
+});
diff --git a/ui/tests/integration/components/ldap/config-cta-test.js b/ui/tests/integration/components/ldap/config-cta-test.js
new file mode 100644
index 0000000000..ff977603cd
--- /dev/null
+++ b/ui/tests/integration/components/ldap/config-cta-test.js
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+module('Integration | Component | ldap | ConfigCta', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ test('it should render message and action', async function (assert) {
+ await render(hbs` `, { owner: this.engine });
+ assert.dom('[data-test-empty-state-title]').hasText('LDAP not configured', 'Title renders');
+ assert
+ .dom('[data-test-empty-state-message]')
+ .hasText('Get started by setting up the connection with your existing LDAP system.', 'Message renders');
+ assert.dom('[data-test-config-cta] a').hasText('Configure LDAP', 'Action renders');
+ });
+});
diff --git a/ui/tests/integration/components/ldap/page/configuration-test.js b/ui/tests/integration/components/ldap/page/configuration-test.js
new file mode 100644
index 0000000000..5e75635edd
--- /dev/null
+++ b/ui/tests/integration/components/ldap/page/configuration-test.js
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+import { duration } from 'core/helpers/format-duration';
+import { createSecretsEngine, generateBreadcrumbs } from 'vault/tests/helpers/ldap';
+
+const selectors = {
+ rotateAction: '[data-test-toolbar-rotate-action] button',
+ confirmRotate: '[data-test-confirm-button]',
+ configAction: '[data-test-toolbar-config-action]',
+ configCta: '[data-test-config-cta]',
+ mountConfig: '[data-test-mount-config]',
+ pageError: '[data-test-page-error]',
+ fieldValue: (label) => `[data-test-value-div="${label}"]`,
+};
+
+module('Integration | Component | ldap | Page::Configuration', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ 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.renderComponent = () => {
+ return render(
+ hbs` `,
+ {
+ owner: this.engine,
+ }
+ );
+ };
+ });
+
+ test('it should render tab page header, config cta and mount config', async function (assert) {
+ this.config = null;
+
+ await this.renderComponent();
+
+ assert.dom('.title svg').hasClass('flight-icon-folder-users', 'LDAP icon renders in title');
+ assert.dom('.title').hasText('ldap-test', 'Mount path renders in title');
+ assert
+ .dom(selectors.rotateAction)
+ .doesNotExist('Rotate root action is hidden when engine is not configured');
+ assert.dom(selectors.configAction).hasText('Configure LDAP', 'Toolbar action has correct text');
+ assert.dom(selectors.configCta).exists('Config cta renders');
+ assert.dom(selectors.mountConfig).exists('Mount config renders');
+ });
+
+ test('it should render config fetch error', async function (assert) {
+ this.config = null;
+ this.error = { httpStatus: 403, message: 'Permission denied' };
+
+ await this.renderComponent();
+
+ assert.dom(selectors.pageError).exists('Config fetch error is rendered');
+ });
+
+ test('it should render display fields', async function (assert) {
+ await this.renderComponent();
+
+ assert.dom(selectors.fieldValue('Administrator Distinguished Name')).hasText(this.config.binddn);
+ assert.dom(selectors.fieldValue('URL')).hasText(this.config.url);
+ assert.dom(selectors.fieldValue('Schema')).hasText(this.config.schema);
+ assert.dom(selectors.fieldValue('Password Policy')).hasText(this.config.password_policy);
+ assert.dom(selectors.fieldValue('Userdn')).hasText(this.config.userdn);
+ assert.dom(selectors.fieldValue('Userattr')).hasText(this.config.userattr);
+ assert
+ .dom(selectors.fieldValue('Connection Timeout'))
+ .hasText(duration([this.config.connection_timeout]));
+ assert.dom(selectors.fieldValue('Request Timeout')).hasText(duration([this.config.request_timeout]));
+ assert.dom(selectors.fieldValue('CA Certificate')).hasText(this.config.certificate);
+ assert.dom(selectors.fieldValue('Start TLS')).includesText('No');
+ assert.dom(selectors.fieldValue('Insecure TLS')).includesText('No');
+ assert.dom(selectors.fieldValue('Client TLS Certificate')).hasText(this.config.client_tls_cert);
+ assert.dom(selectors.fieldValue('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');
+ });
+
+ await this.renderComponent();
+ await click(selectors.rotateAction);
+ await click(selectors.confirmRotate);
+ });
+});
diff --git a/ui/tests/integration/components/ldap/page/configure-test.js b/ui/tests/integration/components/ldap/page/configure-test.js
new file mode 100644
index 0000000000..7cdebf58a8
--- /dev/null
+++ b/ui/tests/integration/components/ldap/page/configure-test.js
@@ -0,0 +1,150 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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';
+
+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',
+};
+
+module('Integration | Component | ldap | Page::Configure', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ const fillAndSubmit = async (rotate) => {
+ await click(selectors.radioCard);
+ await fillIn(selectors.binddn, 'foo');
+ await fillIn(selectors.bindpass, 'bar');
+ await click(selectors.save);
+ await click(`[data-test-save-${rotate}-rotate]`);
+ };
+
+ hooks.beforeEach(function () {
+ this.store = this.owner.lookup('service:store');
+ this.newModel = this.store.createRecord('ldap/config', { backend: 'ldap-new' });
+ 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.breadcrumbs = generateBreadcrumbs('ldap', 'configure');
+ this.model = this.newModel; // most of the tests use newModel but set this to editModel when needed
+ this.renderComponent = () => {
+ return render(
+ hbs`
`,
+ {
+ owner: this.engine,
+ }
+ );
+ };
+ this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
+ });
+
+ test('it should render empty state when schema is not selected', async function (assert) {
+ await this.renderComponent();
+
+ assert.dom('[data-test-empty-state-title]').hasText('Choose an option', 'Empty state title renders');
+ assert
+ .dom('[data-test-empty-state-message]')
+ .hasText('Pick an option above to see available configuration options', 'Empty state title renders');
+ assert.dom(selectors.save).isDisabled('Save button is disabled when schema is not selected');
+
+ await click(selectors.radioCard);
+ assert
+ .dom('[data-test-component="empty-state"]')
+ .doesNotExist('Empty state is hidden when schema is selected');
+ });
+
+ test('it should render validation messages for invalid form', async function (assert) {
+ await this.renderComponent();
+
+ await click(selectors.radioCard);
+ await click(selectors.save);
+
+ assert
+ .dom('[data-test-field="binddn"] [data-test-inline-error-message]')
+ .hasText('Administrator distinguished name is required.', 'Validation message renders for binddn');
+ assert
+ .dom('[data-test-field="bindpass"] [data-test-inline-error-message]')
+ .hasText('Administrator password is required.', 'Validation message renders for bindpass');
+ assert
+ .dom('[data-test-invalid-form-message] p')
+ .hasText('There are 2 errors with this form.', 'Invalid form message renders');
+ });
+
+ 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');
+
+ assert.ok(
+ this.transitionStub.calledWith('vault.cluster.secrets.backend.ldap.configuration'),
+ 'Transitions to configuration route on save success'
+ );
+ });
+
+ 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');
+
+ assert.ok(
+ this.transitionStub.calledWith('vault.cluster.secrets.backend.ldap.configuration'),
+ 'Transitions to configuration route on save success'
+ );
+ });
+
+ test('it should populate fields when editing form', async function (assert) {
+ this.model = this.editModel;
+
+ await this.renderComponent();
+
+ assert.dom(selectors.radioCard).isChecked('Correct radio card is checked for schema value');
+ assert.dom(selectors.binddn).hasValue(this.existingConfig.binddn, 'binddn value renders');
+
+ await fillIn(selectors.binddn, 'foobar');
+ await click('[data-test-config-cancel]');
+
+ assert.strictEqual(this.model.binddn, this.existingConfig.binddn, 'Model is rolled back on cancel');
+ assert.ok(
+ this.transitionStub.calledWith('vault.cluster.secrets.backend.ldap.configuration'),
+ 'Transitions to configuration route on save success'
+ );
+ });
+});
diff --git a/ui/tests/integration/components/ldap/page/libraries-test.js b/ui/tests/integration/components/ldap/page/libraries-test.js
new file mode 100644
index 0000000000..1b8ee8abaf
--- /dev/null
+++ b/ui/tests/integration/components/ldap/page/libraries-test.js
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
+import { createSecretsEngine, generateBreadcrumbs } from 'vault/tests/helpers/ldap';
+
+module('Integration | Component | ldap | Page::Libraries', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
+
+ this.store = this.owner.lookup('service:store');
+ this.backend = createSecretsEngine(this.store);
+ this.breadcrumbs = generateBreadcrumbs(this.backend.id);
+
+ for (const name of ['foo', 'bar']) {
+ this.store.pushPayload('ldap/library', {
+ modelName: 'ldap/library',
+ backend: 'ldap-test',
+ ...this.server.create('ldap-library', { name }),
+ });
+ }
+ this.libraries = this.store.peekAll('ldap/library');
+ this.promptConfig = false;
+
+ this.renderComponent = () => {
+ return render(
+ hbs` `,
+ { owner: this.engine }
+ );
+ };
+ });
+
+ test('it should render tab page header and config cta', async function (assert) {
+ this.promptConfig = true;
+
+ await this.renderComponent();
+
+ assert.dom('.title svg').hasClass('flight-icon-folder-users', 'LDAP icon renders in title');
+ assert.dom('.title').hasText('ldap-test', 'Mount path renders in title');
+ assert
+ .dom('[data-test-toolbar-action="config"]')
+ .hasText('Configure LDAP', 'Correct toolbar action renders');
+ assert.dom('[data-test-config-cta]').exists('Config cta renders');
+ });
+
+ test('it should render create libraries cta', async function (assert) {
+ this.libraries = null;
+
+ await this.renderComponent();
+
+ assert
+ .dom('[data-test-toolbar-action="library"]')
+ .hasText('Create library', 'Toolbar action has correct text');
+ assert
+ .dom('[data-test-toolbar-action="library"] svg')
+ .hasClass('flight-icon-plus', 'Toolbar action has correct icon');
+ assert
+ .dom('[data-test-filter-input]')
+ .doesNotExist('Libraries filter input is hidden when libraries have not been created');
+ assert.dom('[data-test-empty-state-title]').hasText('No libraries created yet', 'Title renders');
+ assert
+ .dom('[data-test-empty-state-message]')
+ .hasText(
+ 'Use libraries to manage a set of highly privileged accounts that can be shared among a team.',
+ 'Message renders'
+ );
+ assert.dom('[data-test-empty-state-actions] a').hasText('Create library', 'Action renders');
+ });
+
+ test('it should render libraries list', async function (assert) {
+ await this.renderComponent();
+
+ assert.dom('[data-test-list-item-content] svg').hasClass('flight-icon-folder', 'List item icon renders');
+ assert.dom('[data-test-library]').hasText(this.libraries.firstObject.name, 'List item name renders');
+
+ await click('[data-test-popup-menu-trigger]');
+ assert.dom('[data-test-edit]').hasText('Edit', 'Edit link renders in menu');
+ assert.dom('[data-test-details]').hasText('Details', 'Details link renders in menu');
+ assert.dom('[data-test-delete]').hasText('Delete', 'Details link renders in menu');
+ });
+
+ test('it should filter libraries', async function (assert) {
+ await this.renderComponent();
+
+ await fillIn('[data-test-filter-input]', 'baz');
+ assert
+ .dom('[data-test-empty-state-title]')
+ .hasText('There are no libraries matching "baz"', 'Filter message renders');
+
+ await fillIn('[data-test-filter-input]', 'foo');
+ assert.dom('[data-test-list-item-content]').exists({ count: 1 }, 'List is filtered with correct results');
+
+ await fillIn('[data-test-filter-input]', '');
+ assert
+ .dom('[data-test-list-item-content]')
+ .exists({ count: 2 }, 'All libraries are displayed when filter is cleared');
+ });
+});
diff --git a/ui/tests/integration/components/ldap/page/library/check-out-test.js b/ui/tests/integration/components/ldap/page/library/check-out-test.js
new file mode 100644
index 0000000000..3dacda591e
--- /dev/null
+++ b/ui/tests/integration/components/ldap/page/library/check-out-test.js
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+import sinon from 'sinon';
+
+module('Integration | Component | ldap | Page::Library::CheckOut', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.creds = {
+ account: 'foo.bar',
+ password: 'password',
+ lease_id: 'ldap/library/test/check-out/123',
+ lease_duration: 86400,
+ renewable: true,
+ };
+ this.breadcrumbs = [
+ { label: 'ldap-test', route: 'overview' },
+ { label: 'libraries', route: 'libraries' },
+ { label: 'test-library', route: 'libraries.library' },
+ { label: 'check-out' },
+ ];
+
+ this.renderComponent = () => {
+ return render(
+ hbs` `,
+ { owner: this.engine }
+ );
+ };
+ });
+
+ test('it should render page title and breadcrumbs', async function (assert) {
+ await this.renderComponent();
+
+ assert.dom('[data-test-header-title]').hasText('Check-out', 'Page title renders');
+ assert
+ .dom('[data-test-breadcrumbs] li:nth-child(1)')
+ .containsText('ldap-test', 'Overview breadcrumb renders');
+ assert
+ .dom('[data-test-breadcrumbs] li:nth-child(2) a')
+ .containsText('libraries', 'Libraries breadcrumb renders');
+ assert
+ .dom('[data-test-breadcrumbs] li:nth-child(3)')
+ .containsText('test-library', 'Library breadcrumb renders');
+ assert
+ .dom('[data-test-breadcrumbs] li:nth-child(4)')
+ .containsText('check-out', 'Check-out breadcrumb renders');
+ });
+
+ test('it should render check out information and credentials', async function (assert) {
+ const transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
+
+ await this.renderComponent();
+
+ assert
+ .dom('[data-test-alert-description]')
+ .hasText(
+ 'You won’t be able to access these credentials later, so please copy them now.',
+ 'Warning alert renders'
+ );
+ assert.dom('[data-test-row-value="Account name"]').hasText('foo.bar', 'Account name renders');
+ await click('[data-test-button="toggle-masked"]');
+ assert.dom('[data-test-value-div="Password"] .masked-value').hasText('password', 'Password renders');
+ assert
+ .dom('[data-test-row-value="Lease ID"]')
+ .hasText('ldap/library/test/check-out/123', 'Lease ID renders');
+ assert
+ .dom('[data-test-value-div="Lease renewable"] svg')
+ .hasClass('flight-icon-check-circle', 'Lease renewable true icon renders');
+ assert
+ .dom('[data-test-value-div="Lease renewable"] svg')
+ .hasClass('has-text-success', 'Lease renewable true icon color renders');
+ assert.dom('[data-test-value-div="Lease renewable"] span').hasText('True', 'Lease renewable renders');
+
+ this.creds.renewable = false;
+ await this.renderComponent();
+ assert
+ .dom('[data-test-value-div="Lease renewable"] svg')
+ .hasClass('flight-icon-x-circle', 'Lease renewable false icon renders');
+ assert
+ .dom('[data-test-value-div="Lease renewable"] svg')
+ .hasClass('has-text-danger', 'Lease renewable false icon color renders');
+ assert.dom('[data-test-value-div="Lease renewable"] span').hasText('False', 'Lease renewable renders');
+
+ await click('[data-test-done]');
+ const didTransition = transitionStub.calledWith(
+ 'vault.cluster.secrets.backend.ldap.libraries.library.details.accounts'
+ );
+ assert.true(didTransition, 'Transitions to accounts route on done');
+ });
+});
diff --git a/ui/tests/integration/components/ldap/page/library/create-and-edit-test.js b/ui/tests/integration/components/ldap/page/library/create-and-edit-test.js
new file mode 100644
index 0000000000..eeef481a20
--- /dev/null
+++ b/ui/tests/integration/components/ldap/page/library/create-and-edit-test.js
@@ -0,0 +1,158 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 sinon from 'sinon';
+
+module('Integration | Component | ldap | Page::Library::CreateAndEdit', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ const router = this.owner.lookup('service:router');
+ const routerStub = sinon.stub(router, 'transitionTo');
+ this.transitionCalledWith = (routeName, name) => {
+ const route = `vault.cluster.secrets.backend.ldap.${routeName}`;
+ const args = name ? [route, name] : [route];
+ return routerStub.calledWith(...args);
+ };
+
+ this.store = this.owner.lookup('service:store');
+ this.newModel = this.store.createRecord('ldap/library', { backend: 'ldap-test' });
+
+ this.libraryData = this.server.create('ldap-library', { name: 'test-library' });
+ this.store.pushPayload('ldap/library', {
+ modelName: 'ldap/library',
+ backend: 'ldap-test',
+ ...this.libraryData,
+ });
+
+ this.breadcrumbs = [
+ { label: 'ldap', route: 'overview' },
+ { label: 'libraries', route: 'libraries' },
+ { label: 'create' },
+ ];
+
+ this.renderComponent = () => {
+ return render(
+ hbs` `,
+ { owner: this.engine }
+ );
+ };
+ });
+
+ test('it should populate form when editing', async function (assert) {
+ this.model = this.store.peekRecord('ldap/library', this.libraryData.name);
+
+ await this.renderComponent();
+
+ assert.dom('[data-test-input="name"]').hasValue(this.libraryData.name, 'Name renders');
+ [0, 1].forEach((index) => {
+ assert
+ .dom(`[data-test-string-list-input="${index}"]`)
+ .hasValue(this.libraryData.service_account_names[index], 'Service account renders');
+ });
+ assert.dom('[data-test-ttl-value="Default lease TTL"]').hasAnyValue('Default lease ttl renders');
+ assert.dom('[data-test-ttl-value="Max lease TTL"]').hasAnyValue('Max lease ttl renders');
+ const checkInValue = this.libraryData.disable_check_in_enforcement ? 'Disabled' : 'Enabled';
+ assert
+ .dom(`[data-test-input="disable_check_in_enforcement"] input#${checkInValue}`)
+ .isChecked('Correct radio is checked for check-in enforcement');
+ });
+
+ test('it should go back to list route and clean up model on cancel', async function (assert) {
+ this.model = this.store.peekRecord('ldap/library', this.libraryData.name);
+ const spy = sinon.spy(this.model, 'rollbackAttributes');
+
+ await this.renderComponent();
+ await click('[data-test-cancel]');
+
+ assert.ok(spy.calledOnce, 'Model is rolled back on cancel');
+ assert.ok(this.transitionCalledWith('libraries'), 'Transitions to libraries list route on cancel');
+ });
+
+ test('it should validate form fields', async function (assert) {
+ this.model = this.newModel;
+
+ await this.renderComponent();
+ await click('[data-test-save]');
+
+ assert
+ .dom('[data-test-field-validation="name"] p')
+ .hasText('Library name is required.', 'Name validation error renders');
+ assert
+ .dom('[data-test-field-validation="service_account_names"] p')
+ .hasText('At least one service account is required.', 'Service account name validation error renders');
+ assert
+ .dom('[data-test-invalid-form-message] p')
+ .hasText('There are 2 errors with this form.', 'Invalid form message renders');
+ });
+
+ test('it should create new library', async function (assert) {
+ assert.expect(2);
+
+ this.server.post('/ldap-test/library/new-library', (schema, req) => {
+ const data = JSON.parse(req.requestBody);
+ const expected = {
+ service_account_names: 'foo@bar.com,bar@baz.com',
+ ttl: '24h',
+ max_ttl: '24h',
+ disable_check_in_enforcement: true,
+ };
+ assert.deepEqual(data, expected, 'POST request made with correct properties when creating library');
+ });
+
+ this.model = this.newModel;
+
+ await this.renderComponent();
+
+ await fillIn('[data-test-input="name"]', 'new-library');
+ await fillIn('[data-test-string-list-input="0"]', 'foo@bar.com');
+ await click('[data-test-string-list-button="add"]');
+ await fillIn('[data-test-string-list-input="1"]', 'bar@baz.com');
+ await click('[data-test-string-list-button="add"]');
+ await click('[data-test-input="disable_check_in_enforcement"] input#Disabled');
+ await click('[data-test-save]');
+
+ assert.ok(
+ this.transitionCalledWith('libraries.library.details', 'new-library'),
+ 'Transitions to library details route on save success'
+ );
+ });
+
+ test('it should save edited library with correct properties', async function (assert) {
+ assert.expect(2);
+
+ this.server.post('/ldap-test/library/test-library', (schema, req) => {
+ const data = JSON.parse(req.requestBody);
+ const expected = {
+ service_account_names: this.libraryData.service_account_names[1],
+ ttl: this.libraryData.ttl,
+ max_ttl: this.libraryData.max_ttl,
+ disable_check_in_enforcement: true,
+ };
+ assert.deepEqual(expected, data, 'POST request made to save library with correct properties');
+ });
+
+ this.model = this.store.peekRecord('ldap/library', this.libraryData.name);
+
+ await this.renderComponent();
+
+ await click('[data-test-string-list-button="delete"]');
+ await click('[data-test-input="disable_check_in_enforcement"] input#Disabled');
+ await click('[data-test-save]');
+
+ assert.ok(
+ this.transitionCalledWith('libraries.library.details', 'test-library'),
+ 'Transitions to library details route on save success'
+ );
+ });
+});
diff --git a/ui/tests/integration/components/ldap/page/library/details-test.js b/ui/tests/integration/components/ldap/page/library/details-test.js
new file mode 100644
index 0000000000..c4573978d7
--- /dev/null
+++ b/ui/tests/integration/components/ldap/page/library/details-test.js
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+import sinon from 'sinon';
+
+module('Integration | Component | ldap | Page::Library::Details', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.server.post('/sys/capabilities-self', () => ({
+ data: {
+ capabilities: ['root'],
+ },
+ }));
+
+ this.store = this.owner.lookup('service:store');
+
+ this.store.pushPayload('ldap/library', {
+ modelName: 'ldap/library',
+ backend: 'ldap-test',
+ ...this.server.create('ldap-library', { name: 'test-library' }),
+ });
+ this.model = this.store.peekRecord('ldap/library', 'test-library');
+
+ this.breadcrumbs = [
+ { label: 'ldap-test', route: 'overview' },
+ { label: 'libraries', route: 'libraries' },
+ { label: 'test-library' },
+ ];
+ });
+
+ test('it should render page header, tabs and toolbar actions', async function (assert) {
+ assert.expect(10);
+
+ this.server.delete(`/${this.model.backend}/library/${this.model.name}`, () => {
+ assert.ok(true, 'Request made to delete library');
+ return;
+ });
+
+ await render(hbs` `, {
+ owner: this.engine,
+ });
+
+ assert.dom('[data-test-header-title]').hasText(this.model.name, 'Library name renders in header');
+ assert
+ .dom('[data-test-breadcrumbs] li:nth-child(1)')
+ .containsText(this.model.backend, 'Overview breadcrumb renders');
+ assert
+ .dom('[data-test-breadcrumbs] li:nth-child(2) a')
+ .containsText('libraries', 'Libraries breadcrumb renders');
+ assert
+ .dom('[data-test-breadcrumbs] li:nth-child(3)')
+ .containsText(this.model.name, 'Library breadcrumb renders');
+
+ assert.dom('[data-test-tab="accounts"]').hasText('Accounts', 'Accounts tab renders');
+ assert.dom('[data-test-tab="config"]').hasText('Configuration', 'Configuration tab renders');
+
+ assert.dom('[data-test-delete] button').hasText('Delete library', 'Delete action renders');
+ assert.dom('[data-test-edit]').hasText('Edit library', 'Edit action renders');
+
+ const transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
+ await click('[data-test-delete] button');
+ await click('[data-test-confirm-button]');
+ assert.ok(
+ transitionStub.calledWith('vault.cluster.secrets.backend.ldap.libraries'),
+ 'Transitions to libraries route on delete success'
+ );
+ });
+});
diff --git a/ui/tests/integration/components/ldap/page/library/details/accounts-test.js b/ui/tests/integration/components/ldap/page/library/details/accounts-test.js
new file mode 100644
index 0000000000..eb7670bc98
--- /dev/null
+++ b/ui/tests/integration/components/ldap/page/library/details/accounts-test.js
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
+import sinon from 'sinon';
+
+module('Integration | Component | ldap | Page::Library::Details::Accounts', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
+
+ this.store = this.owner.lookup('service:store');
+
+ this.store.pushPayload('ldap/library', {
+ modelName: 'ldap/library',
+ backend: 'ldap-test',
+ ...this.server.create('ldap-library', { name: 'test-library' }),
+ });
+ this.model = this.store.peekRecord('ldap/library', 'test-library');
+ this.statuses = [
+ {
+ account: 'foo.bar',
+ available: false,
+ library: 'test-library',
+ borrower_client_token: '123',
+ borrower_entity_id: '456',
+ },
+ { account: 'bar.baz', available: true, library: 'test-library' },
+ ];
+ this.renderComponent = () => {
+ return render(
+ hbs`
+
+
+ `,
+ {
+ owner: this.engine,
+ }
+ );
+ };
+ });
+
+ test('it should render account cards', async function (assert) {
+ const transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
+
+ await this.renderComponent();
+
+ assert.dom('[data-test-account-name="foo.bar"]').hasText('foo.bar', 'Account name renders');
+ assert
+ .dom('[data-test-account-status="foo.bar"]')
+ .hasText('Unavailable', 'Correct badge renders for checked out account');
+ assert
+ .dom('[data-test-account-status="bar.baz"]')
+ .hasText('Available', 'Correct badge renders for available account');
+
+ await click('[data-test-check-out]');
+ await fillIn('[data-test-ttl-value="TTL"]', 4);
+ await click('[data-test-check-out="save"]');
+
+ const didTransition = transitionStub.calledWith(
+ 'vault.cluster.secrets.backend.ldap.libraries.library.check-out',
+ { queryParams: { ttl: '4h' } }
+ );
+ assert.true(didTransition, 'Transitions to check out route on action click');
+
+ assert.dom('[data-test-checked-out-card]').exists('Accounts checked out card renders');
+
+ assert
+ .dom('[data-test-cli-command]')
+ .hasText('vault lease renew ad/library/test-library/check-out/:lease_id', 'Renew cli command renders');
+ assert.dom(`[data-test-cli-command-copy]`).exists('Renew cli command copy button renders');
+ });
+});
diff --git a/ui/tests/integration/components/ldap/page/library/details/configuration-test.js b/ui/tests/integration/components/ldap/page/library/details/configuration-test.js
new file mode 100644
index 0000000000..c703379b28
--- /dev/null
+++ b/ui/tests/integration/components/ldap/page/library/details/configuration-test.js
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+import { duration } from 'core/helpers/format-duration';
+
+module('Integration | Component | ldap | Page::Library::Details::Configuration', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.store = this.owner.lookup('service:store');
+
+ this.store.pushPayload('ldap/library', {
+ modelName: 'ldap/library',
+ backend: 'ldap-test',
+ ...this.server.create('ldap-library', { name: 'test-library' }),
+ });
+ this.model = this.store.peekRecord('ldap/library', 'test-library');
+ this.renderComponent = () => {
+ return render(hbs` `, {
+ owner: this.engine,
+ });
+ };
+ });
+
+ test('it should render configuration details', async function (assert) {
+ await this.renderComponent();
+
+ const fields = [
+ { label: 'Library name', key: 'name' },
+ { label: 'TTL', key: 'ttl' },
+ { label: 'Max TTL', key: 'max_ttl' },
+ { label: 'Check-in enforcement', key: 'disable_check_in_enforcement' },
+ ];
+ fields.forEach((field) => {
+ const { label, key } = field;
+ const value = label.includes('TTL') ? duration([this.model[key]]) : this.model[key];
+ const method = key === 'disable_check_in_enforcement' ? 'includesText' : 'hasText';
+
+ assert.dom(`[data-test-row-label="${label}"]`).hasText(label, `${label} info row label renders`);
+ assert.dom(`[data-test-value-div="${label}"]`)[method](value, `${label} info row label renders`);
+ });
+
+ assert
+ .dom('[data-test-check-in-icon]')
+ .hasClass('flight-icon-check-circle', 'Correct icon renders for enabled check in enforcement');
+ assert
+ .dom('[data-test-check-in-icon]')
+ .hasClass('icon-true', 'Correct class renders for enabled check in enforcement');
+
+ this.model.disable_check_in_enforcement = 'Disabled';
+ await this.renderComponent();
+
+ assert
+ .dom('[data-test-check-in-icon]')
+ .hasClass('flight-icon-x-square', 'Correct icon renders for disabled check in enforcement');
+ assert
+ .dom('[data-test-check-in-icon]')
+ .hasClass('icon-false', 'Correct class renders for disabled check in enforcement');
+ });
+});
diff --git a/ui/tests/integration/components/ldap/page/overview-test.js b/ui/tests/integration/components/ldap/page/overview-test.js
new file mode 100644
index 0000000000..c5771f8a5a
--- /dev/null
+++ b/ui/tests/integration/components/ldap/page/overview-test.js
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+import { createSecretsEngine, generateBreadcrumbs } from 'vault/tests/helpers/ldap';
+import sinon from 'sinon';
+
+module('Integration | Component | ldap | Page::Overview', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.store = this.owner.lookup('service:store');
+
+ this.backendModel = createSecretsEngine(this.store);
+ this.breadcrumbs = generateBreadcrumbs(this.backendModel.id);
+
+ const pushPayload = (type) => {
+ this.store.pushPayload(`ldap/${type}`, {
+ modelName: `ldap/${type}`,
+ backend: 'ldap-test',
+ ...this.server.create(`ldap-${type}`),
+ });
+ };
+
+ ['role', 'library'].forEach((type) => {
+ pushPayload(type);
+ if (type === 'role') {
+ pushPayload(type);
+ }
+ const key = type === 'role' ? 'roles' : 'libraries';
+ this[key] = this.store.peekAll(`ldap/${type}`);
+ });
+
+ this.renderComponent = () => {
+ return render(
+ hbs` `,
+ {
+ owner: this.engine,
+ }
+ );
+ };
+ });
+
+ test('it should render tab page header and config cta', async function (assert) {
+ this.promptConfig = true;
+
+ await this.renderComponent();
+
+ assert.dom('.title svg').hasClass('flight-icon-folder-users', 'LDAP icon renders in title');
+ assert.dom('.title').hasText('ldap-test', 'Mount path renders in title');
+ assert.dom('[data-test-toolbar-action="config"]').hasText('Configure LDAP', 'Toolbar action renders');
+ assert.dom('[data-test-config-cta]').exists('Config cta renders');
+ });
+
+ test('it should render overview cards', async function (assert) {
+ const transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
+
+ await this.renderComponent();
+
+ assert.dom('[data-test-roles-count]').hasText('2', 'Roles card renders with correct count');
+ assert.dom('[data-test-libraries-count]').hasText('1', 'Libraries card renders with correct count');
+ assert
+ .dom('[data-test-overview-card-container="Accounts checked-out"]')
+ .exists('Accounts checked-out card renders');
+
+ await click('[data-test-component="search-select"] .ember-power-select-trigger');
+ await click('.ember-power-select-option');
+ await click('[data-test-generate-credential-button]');
+
+ const didTransition = transitionStub.calledWith(
+ 'vault.cluster.secrets.backend.ldap.roles.role.credentials',
+ this.roles[0].type,
+ this.roles[0].name
+ );
+ assert.true(didTransition, 'Transitions to credentials route when generating credentials');
+ });
+});
diff --git a/ui/tests/integration/components/ldap/page/role/create-and-edit-test.js b/ui/tests/integration/components/ldap/page/role/create-and-edit-test.js
new file mode 100644
index 0000000000..0df2016283
--- /dev/null
+++ b/ui/tests/integration/components/ldap/page/role/create-and-edit-test.js
@@ -0,0 +1,197 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 sinon from 'sinon';
+
+module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ const router = this.owner.lookup('service:router');
+ const routerStub = sinon.stub(router, 'transitionTo');
+ this.transitionCalledWith = (routeName, name) => {
+ const route = `vault.cluster.secrets.backend.ldap.${routeName}`;
+ const args = name ? [route, name] : [route];
+ return routerStub.calledWith(...args);
+ };
+
+ this.store = this.owner.lookup('service:store');
+ this.newModel = this.store.createRecord('ldap/role', { backend: 'ldap-test' });
+
+ ['static', 'dynamic'].forEach((type) => {
+ this[`${type}RoleData`] = this.server.create('ldap-role', type, { name: `${type}-role` });
+ this.store.pushPayload('ldap/role', {
+ modelName: 'ldap/role',
+ backend: 'ldap-test',
+ type,
+ ...this[`${type}RoleData`],
+ });
+ });
+
+ this.breadcrumbs = [
+ { label: 'ldap', route: 'overview' },
+ { label: 'roles', route: 'roles' },
+ { label: 'create' },
+ ];
+
+ this.renderComponent = () => {
+ return render(
+ hbs` `,
+ { owner: this.engine }
+ );
+ };
+ });
+
+ test('it should display different form fields based on type', async function (assert) {
+ assert.expect(12);
+
+ this.model = this.newModel;
+ await this.renderComponent();
+
+ assert.dom('[data-test-radio-card="static"]').isChecked('Static role type selected by default');
+
+ const checkFields = (fields) => {
+ fields.forEach((field) => {
+ assert
+ .dom(`[data-test-field="${field}"]`)
+ .exists(`${field} field renders when static type is selected`);
+ });
+ };
+
+ checkFields(['name', 'dn', 'username', 'rotation_period']);
+ await click('[data-test-radio-card="dynamic"]');
+ checkFields([
+ 'name',
+ 'default_ttl',
+ 'max_ttl',
+ 'username_template',
+ 'creation_ldif',
+ 'deletion_ldif',
+ 'rollback_ldif',
+ ]);
+ });
+
+ test('it should populate form and disable type cards when editing', async function (assert) {
+ assert.expect(12);
+
+ const checkFields = (fields, element = 'input:last-child') => {
+ fields.forEach((field) => {
+ const isLdif = field.includes('ldif');
+ const method = isLdif ? 'includesText' : 'hasValue';
+ const value = isLdif ? 'dn: cn={{.Username}},ou=users,dc=learn,dc=example' : this.model[field];
+ assert.dom(`[data-test-field="${field}"] ${element}`)[method](value, `${field} field value renders`);
+ });
+ };
+ const checkTtl = (fields) => {
+ fields.forEach((field) => {
+ assert
+ .dom(`[data-test-field="${field}"] [data-test-ttl-inputs] input`)
+ .hasAnyValue(`${field} field ttl value renders`);
+ });
+ };
+
+ this.model = this.store.peekRecord('ldap/role', 'static-role');
+ await this.renderComponent();
+ assert.dom('[data-test-radio-card="static"]').isDisabled('Type selection is disabled when editing');
+ checkFields(['name', 'dn', 'username']);
+ checkTtl(['rotation_period']);
+
+ this.model = this.store.peekRecord('ldap/role', 'dynamic-role');
+ await this.renderComponent();
+ checkFields(['name', 'username_template']);
+ checkTtl(['default_ttl', 'max_ttl']);
+ checkFields(['creation_ldif', 'deletion_ldif', 'rollback_ldif'], '.CodeMirror-code');
+ });
+
+ test('it should go back to list route and clean up model on cancel', async function (assert) {
+ this.model = this.store.peekRecord('ldap/role', 'static-role');
+ const spy = sinon.spy(this.model, 'rollbackAttributes');
+
+ await this.renderComponent();
+ await click('[data-test-cancel]');
+
+ assert.ok(spy.calledOnce, 'Model is rolled back on cancel');
+ assert.ok(this.transitionCalledWith('roles'), 'Transitions to roles list route on cancel');
+ });
+
+ test('it should validate form fields', async function (assert) {
+ const renderAndAssert = async (fields) => {
+ await this.renderComponent();
+ await click('[data-test-save]');
+
+ fields.forEach((field) => {
+ assert
+ .dom(`[data-test-field="${field}"] [data-test-inline-error-message]`)
+ .exists('Validation message renders');
+ });
+
+ assert
+ .dom('[data-test-invalid-form-message]')
+ .hasText(`There are ${fields.length} errors with this form.`);
+ };
+
+ this.model = this.newModel;
+ await renderAndAssert(['name', 'username', 'rotation_period']);
+
+ await click('[data-test-radio-card="dynamic"]');
+ await renderAndAssert(['name', 'creation_ldif', 'deletion_ldif']);
+ });
+
+ test('it should create new role', async function (assert) {
+ assert.expect(2);
+
+ this.server.post('/ldap-test/static-role/test-role', (schema, req) => {
+ const data = JSON.parse(req.requestBody);
+ const expected = { dn: 'foo', username: 'bar', rotation_period: '5s' };
+ assert.deepEqual(data, expected, 'POST request made with correct properties when creating role');
+ });
+
+ this.model = this.newModel;
+ await this.renderComponent();
+
+ await fillIn('[data-test-input="name"]', 'test-role');
+ await fillIn('[data-test-input="dn"]', 'foo');
+ await fillIn('[data-test-input="username"]', 'bar');
+ await fillIn('[data-test-ttl-value="Rotation period"]', 5);
+ await click('[data-test-save]');
+
+ assert.ok(
+ this.transitionCalledWith('roles.role.details', 'static', 'test-role'),
+ 'Transitions to role details route on save success'
+ );
+ });
+
+ test('it should save edited role with correct properties', async function (assert) {
+ assert.expect(2);
+
+ this.server.post('/ldap-test/static-role/:name', (schema, req) => {
+ const data = JSON.parse(req.requestBody);
+ const expected = { dn: 'foo', username: 'bar', rotation_period: '30s' };
+ assert.deepEqual(expected, data, 'POST request made to save role with correct properties');
+ });
+
+ this.model = this.store.peekRecord('ldap/role', 'static-role');
+ await this.renderComponent();
+
+ await fillIn('[data-test-input="name"]', 'test-role');
+ await fillIn('[data-test-input="dn"]', 'foo');
+ await fillIn('[data-test-input="username"]', 'bar');
+ await fillIn('[data-test-ttl-value="Rotation period"]', 30);
+ await click('[data-test-save]');
+
+ assert.ok(
+ this.transitionCalledWith('roles.role.details', 'static', 'test-role'),
+ 'Transitions to role details route on save success'
+ );
+ });
+});
diff --git a/ui/tests/integration/components/ldap/page/role/credentials-test.js b/ui/tests/integration/components/ldap/page/role/credentials-test.js
new file mode 100644
index 0000000000..779793b75b
--- /dev/null
+++ b/ui/tests/integration/components/ldap/page/role/credentials-test.js
@@ -0,0 +1,131 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+import sinon from 'sinon';
+import { duration } from 'core/helpers/format-duration';
+import { dateFormat } from 'core/helpers/date-format';
+
+module('Integration | Component | ldap | Page::Role::Credentials', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.breadcrumbs = [
+ { label: 'ldap-test', route: 'overview' },
+ { label: 'roles', route: 'roles' },
+ { label: 'test-role', route: 'roles.role' },
+ { label: 'credentials' },
+ ];
+ this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
+ });
+
+ test('it should render page title and breadcrumbs', async function (assert) {
+ this.creds = [];
+ await render(
+ hbs` `,
+ { owner: this.engine }
+ );
+
+ assert.dom('[data-test-header-title]').hasText('Credentials', 'Page title renders');
+ assert
+ .dom('[data-test-breadcrumbs] li:nth-child(1)')
+ .containsText('ldap-test', 'Overview breadcrumb renders');
+ assert.dom('[data-test-breadcrumbs] li:nth-child(2) a').containsText('roles', 'Roles breadcrumb renders');
+ assert
+ .dom('[data-test-breadcrumbs] li:nth-child(3)')
+ .containsText('test-role', 'Role breadcrumb renders');
+ assert
+ .dom('[data-test-breadcrumbs] li:nth-child(4)')
+ .containsText('credentials', 'Credentials breadcrumb renders');
+ });
+
+ test('it should render fields for static role', async function (assert) {
+ const fields = [
+ {
+ label: 'Last Vault rotation',
+ value: () => dateFormat([this.creds.last_vault_rotation, 'MMM d yyyy, h:mm:ss aaa'], {}),
+ },
+ { label: 'Password', key: 'password', isMasked: true },
+ { label: 'Username', key: 'username' },
+ { label: 'Rotation period', value: () => duration([this.creds.rotation_period]) },
+ { label: 'Time remaining', value: () => duration([this.creds.ttl]) },
+ ];
+ this.creds = this.server.create('ldap-credential', 'static');
+
+ await render(
+ hbs` `,
+ { owner: this.engine }
+ );
+
+ for (const field of fields) {
+ assert
+ .dom(`[data-test-row-label="${field.label}"]`)
+ .hasText(field.label, `${field.label} label renders`);
+
+ if (field.isMasked) {
+ await click(`[data-test-value-div="${field.label}"] [data-test-button="toggle-masked"]`);
+ }
+
+ const value = field.value ? field.value() : this.creds[field.key];
+ assert.dom(`[data-test-value-div="${field.label}"]`).hasText(value, `${field.label} value renders`);
+ }
+
+ await click('[data-test-done]');
+ assert.true(
+ this.transitionStub.calledOnceWith('vault.cluster.secrets.backend.ldap.roles.role.details'),
+ 'Transitions to correct route on done'
+ );
+ });
+
+ test('it should render fields for dynamic role', async function (assert) {
+ const fields = [
+ { label: 'Distinguished Name', value: () => this.creds.distinguished_names.join(', ') },
+ { label: 'Username', key: 'username', isMasked: true },
+ { label: 'Password', key: 'password', isMasked: true },
+ { label: 'Lease ID', key: 'lease_id' },
+ { label: 'Lease duration', value: () => duration([this.creds.lease_duration]) },
+ { label: 'Lease renewable', value: () => (this.creds.renewable ? 'True' : 'False') },
+ ];
+ this.creds = this.server.create('ldap-credential', 'dynamic');
+
+ await render(
+ hbs` `,
+ { owner: this.engine }
+ );
+
+ assert
+ .dom('[data-test-alert-description]')
+ .hasText(
+ 'You won’t be able to access these credentials later, so please copy them now.',
+ 'Alert renders for dynamic roles'
+ );
+
+ for (const field of fields) {
+ assert
+ .dom(`[data-test-row-label="${field.label}"]`)
+ .hasText(field.label, `${field.label} label renders`);
+
+ if (field.isMasked) {
+ await click(`[data-test-value-div="${field.label}"] [data-test-button="toggle-masked"]`);
+ }
+
+ const value = field.value ? field.value() : this.creds[field.key];
+ assert.dom(`[data-test-value-div="${field.label}"]`).hasText(value, `${field.label} value renders`);
+ }
+
+ await click('[data-test-done]');
+ assert.true(
+ this.transitionStub.calledOnceWith('vault.cluster.secrets.backend.ldap.roles.role.details'),
+ 'Transitions to correct route on done'
+ );
+ });
+});
diff --git a/ui/tests/integration/components/ldap/page/role/details-test.js b/ui/tests/integration/components/ldap/page/role/details-test.js
new file mode 100644
index 0000000000..6a0ad36cf3
--- /dev/null
+++ b/ui/tests/integration/components/ldap/page/role/details-test.js
@@ -0,0 +1,121 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+import sinon from 'sinon';
+import { duration } from 'core/helpers/format-duration';
+
+module('Integration | Component | ldap | Page::Role::Details', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.server.post('/sys/capabilities-self', () => ({
+ data: {
+ capabilities: ['root'],
+ },
+ }));
+ this.renderComponent = (type) => {
+ const data = this.server.create('ldap-role', type);
+ const store = this.owner.lookup('service:store');
+ store.pushPayload('ldap/role', {
+ modelName: 'ldap/role',
+ backend: 'ldap-test',
+ type,
+ ...data,
+ });
+ this.model = store.peekRecord('ldap/role', data.name);
+ this.breadcrumbs = [
+ { label: this.model.backend, route: 'overview' },
+ { label: 'roles', route: 'roles' },
+ { label: this.model.name },
+ ];
+ return render(hbs` `, {
+ owner: this.engine,
+ });
+ };
+ });
+
+ test('it should render header with role name and breadcrumbs', async function (assert) {
+ await this.renderComponent('static');
+ assert.dom('[data-test-header-title]').hasText(this.model.name, 'Role name renders in header');
+ assert
+ .dom('[data-test-breadcrumbs] li:nth-child(1)')
+ .containsText(this.model.backend, 'Overview breadcrumb renders');
+ assert.dom('[data-test-breadcrumbs] li:nth-child(2) a').containsText('roles', 'Roles breadcrumb renders');
+ assert
+ .dom('[data-test-breadcrumbs] li:nth-child(3)')
+ .containsText(this.model.name, 'Role breadcrumb renders');
+ });
+
+ test('it should render toolbar actions', async function (assert) {
+ assert.expect(7);
+
+ await this.renderComponent('static');
+
+ assert.dom('[data-test-delete] button').hasText('Delete role', 'Delete action renders');
+ assert.dom('[data-test-get-credentials]').hasText('Get credentials', 'Get credentials action renders');
+ assert.dom('[data-test-rotate-credentials]').exists('Rotate credentials action renders for static role');
+ assert.dom('[data-test-edit]').hasText('Edit role', 'Edit action renders');
+
+ await this.renderComponent('dynamic');
+ // defined after render so this.model is defined
+ this.server.delete(`/${this.model.backend}/role/${this.model.name}`, () => {
+ assert.ok(true, 'Request made to delete role');
+ return;
+ });
+ const transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
+
+ assert
+ .dom('[data-test-rotate-credentials]')
+ .doesNotExist('Rotate credentials action is hidden for dynamic role');
+
+ await click('[data-test-delete] button');
+ await click('[data-test-confirm-button]');
+ assert.ok(
+ transitionStub.calledWith('vault.cluster.secrets.backend.ldap.roles'),
+ 'Transitions to roles route on delete success'
+ );
+ });
+
+ test('it should render details fields', async function (assert) {
+ assert.expect(26);
+
+ const fields = [
+ { label: 'Role name', key: 'name' },
+ { label: 'Role type', key: 'type' },
+ { label: 'Distinguished name', key: 'dn', type: 'static' },
+ { label: 'Username', key: 'username', type: 'static' },
+ { label: 'Rotation period', key: 'rotation_period', type: 'static' },
+ { label: 'TTL', key: 'default_ttl', type: 'dynamic' },
+ { label: 'Max TTL', key: 'max_ttl', type: 'dynamic' },
+ { label: 'Username template', key: 'username_template', type: 'dynamic' },
+ { label: 'Creation LDIF', key: 'creation_ldif', type: 'dynamic' },
+ { label: 'Deletion LDIF', key: 'deletion_ldif', type: 'dynamic' },
+ { label: 'Rollback LDIF', key: 'rollback_ldif', type: 'dynamic' },
+ ];
+
+ for (const type of ['static', 'dynamic']) {
+ await this.renderComponent(type);
+
+ const typeFields = fields.filter((field) => !field.type || field.type === type);
+ typeFields.forEach((field) => {
+ assert
+ .dom(`[data-test-row-label="${field.label}"]`)
+ .hasText(field.label, `${field.label} label renders`);
+ const modelValue = this.model[field.key];
+ const isDuration = ['TTL', 'Max TTL', 'Rotation period'].includes(field.label);
+ const value = isDuration ? duration([modelValue]) : modelValue;
+ assert.dom(`[data-test-row-value="${field.label}"]`).hasText(value, `${field.label} value renders`);
+ });
+ }
+ });
+});
diff --git a/ui/tests/integration/components/ldap/page/roles-test.js b/ui/tests/integration/components/ldap/page/roles-test.js
new file mode 100644
index 0000000000..402458f66b
--- /dev/null
+++ b/ui/tests/integration/components/ldap/page/roles-test.js
@@ -0,0 +1,127 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
+import { createSecretsEngine, generateBreadcrumbs } from 'vault/tests/helpers/ldap';
+
+module('Integration | Component | ldap | Page::Roles', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
+
+ this.store = this.owner.lookup('service:store');
+ this.backend = createSecretsEngine(this.store);
+ this.breadcrumbs = generateBreadcrumbs(this.backend.id);
+
+ for (const type of ['static', 'dynamic']) {
+ this.store.pushPayload('ldap/role', {
+ modelName: 'ldap/role',
+ backend: 'ldap-test',
+ type,
+ ...this.server.create('ldap-role', type, { name: `${type}-test` }),
+ });
+ }
+ this.backend = this.store.peekRecord('secret-engine', 'ldap-test');
+ this.roles = this.store.peekAll('ldap/role');
+ this.promptConfig = false;
+
+ this.renderComponent = () => {
+ return render(
+ hbs` `,
+ { owner: this.engine }
+ );
+ };
+ });
+
+ test('it should render tab page header and config cta', async function (assert) {
+ this.promptConfig = true;
+
+ await this.renderComponent();
+
+ assert.dom('.title svg').hasClass('flight-icon-folder-users', 'LDAP icon renders in title');
+ assert.dom('.title').hasText('ldap-test', 'Mount path renders in title');
+ assert
+ .dom('[data-test-toolbar-action="config"]')
+ .hasText('Configure LDAP', 'Correct toolbar action renders');
+ assert.dom('[data-test-config-cta]').exists('Config cta renders');
+ });
+
+ test('it should render create roles cta', async function (assert) {
+ this.roles = null;
+
+ await this.renderComponent();
+
+ assert.dom('[data-test-toolbar-action="role"]').hasText('Create role', 'Toolbar action has correct text');
+ assert
+ .dom('[data-test-toolbar-action="role"] svg')
+ .hasClass('flight-icon-plus', 'Toolbar action has correct icon');
+ assert
+ .dom('[data-test-filter-input]')
+ .doesNotExist('Roles filter input is hidden when roles have not been created');
+ assert.dom('[data-test-empty-state-title]').hasText('No roles created yet', 'Title renders');
+ assert
+ .dom('[data-test-empty-state-message]')
+ .hasText(
+ 'Roles in Vault will allow you to manage LDAP credentials. Create a role to get started.',
+ 'Message renders'
+ );
+ assert.dom('[data-test-empty-state-actions] a').hasText('Create role', 'Action renders');
+ });
+
+ test('it should render roles list', async function (assert) {
+ await this.renderComponent();
+
+ assert.dom('[data-test-list-item-content] svg').hasClass('flight-icon-user', 'List item icon renders');
+ assert
+ .dom('[data-test-role="static-test"]')
+ .hasText(this.roles.firstObject.name, 'List item name renders');
+ assert
+ .dom('[data-test-role-type-badge="static-test"]')
+ .hasText(this.roles.firstObject.type, 'List item type badge renders');
+
+ await click('[data-test-popup-menu-trigger]');
+ assert.dom('[data-test-edit]').hasText('Edit', 'Edit link renders in menu');
+ assert.dom('[data-test-get-creds]').hasText('Get credentials', 'Get credentials link renders in menu');
+ assert
+ .dom('[data-test-rotate-creds]')
+ .hasText('Rotate credentials', 'Rotate credentials link renders in menu');
+ assert.dom('[data-test-details]').hasText('Details', 'Details link renders in menu');
+ assert.dom('[data-test-delete]').hasText('Delete', 'Details link renders in menu');
+
+ await click('[data-test-popup-menu-trigger]:last-of-type');
+ assert.dom('[data-test-rotate-creds]').doesNotExist('Rotate credentials link is hidden for dynamic type');
+ });
+
+ test('it should filter roles', async function (assert) {
+ await this.renderComponent();
+
+ await fillIn('[data-test-filter-input]', 'foo');
+ assert
+ .dom('[data-test-empty-state-title]')
+ .hasText('There are no roles matching "foo"', 'Filter message renders');
+
+ await fillIn('[data-test-filter-input]', 'static');
+ assert.dom('[data-test-list-item-content]').exists({ count: 1 }, 'List is filtered with correct results');
+
+ await fillIn('[data-test-filter-input]', '');
+ assert
+ .dom('[data-test-list-item-content]')
+ .exists({ count: 2 }, 'All roles are displayed when filter is cleared');
+ });
+});
diff --git a/ui/tests/integration/components/ldap/tab-page-header-test.js b/ui/tests/integration/components/ldap/tab-page-header-test.js
new file mode 100644
index 0000000000..ead50b4d91
--- /dev/null
+++ b/ui/tests/integration/components/ldap/tab-page-header-test.js
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+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 } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+module('Integration | Component | ldap | TabPageHeader', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'ldap');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.store = this.owner.lookup('service:store');
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'ldap_64e858b1',
+ path: 'ldap-test/',
+ type: 'ldap',
+ },
+ });
+ this.model = this.store.peekRecord('secret-engine', 'ldap-test');
+ this.mount = this.model.path.slice(0, -1);
+ this.breadcrumbs = [{ label: 'secrets', route: 'secrets', linkExternal: true }, { label: this.mount }];
+ });
+
+ test('it should render breadcrumbs', async function (assert) {
+ await render(hbs` `, {
+ owner: this.engine,
+ });
+ assert.dom('[data-test-breadcrumbs] li:nth-child(1) a').hasText('secrets', 'Secrets breadcrumb renders');
+
+ assert
+ .dom('[data-test-breadcrumbs] li:nth-child(2)')
+ .containsText(this.mount, 'Mount path breadcrumb renders');
+ });
+
+ test('it should render title', async function (assert) {
+ await render(hbs` `, {
+ owner: this.engine,
+ });
+ assert
+ .dom('[data-test-header-title] svg')
+ .hasClass('flight-icon-folder-users', 'Correct icon renders in title');
+ assert.dom('[data-test-header-title]').hasText(this.mount, 'Mount path renders in title');
+ });
+
+ test('it should render tabs', async function (assert) {
+ await render(hbs` `, {
+ owner: this.engine,
+ });
+ assert.dom('[data-test-tab="overview"]').hasText('Overview', 'Overview tab renders');
+ assert.dom('[data-test-tab="roles"]').hasText('Roles', 'Roles tab renders');
+ assert.dom('[data-test-tab="libraries"]').hasText('Libraries', 'Libraries tab renders');
+ assert.dom('[data-test-tab="config"]').hasText('Configuration', 'Configuration tab renders');
+ });
+
+ test('it should yield toolbar blocks', async function (assert) {
+ await render(
+ hbs`
+
+ <:toolbarFilters>
+ Toolbar filters
+
+ <:toolbarActions>
+ Toolbar actions
+
+
+ `,
+ { owner: this.engine }
+ );
+
+ assert
+ .dom('.toolbar-filters [data-test-filters]')
+ .hasText('Toolbar filters', 'Block is yielded for toolbar filters');
+ assert
+ .dom('.toolbar-actions [data-test-actions]')
+ .hasText('Toolbar actions', 'Block is yielded for toolbar actions');
+ });
+});
diff --git a/ui/tests/integration/components/secrets-engine-mount-config-test.js b/ui/tests/integration/components/secrets-engine-mount-config-test.js
new file mode 100644
index 0000000000..b8a9309471
--- /dev/null
+++ b/ui/tests/integration/components/secrets-engine-mount-config-test.js
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, click } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+const selectors = {
+ toggle: '[data-test-mount-config-toggle]',
+ field: '[data-test-mount-config-field]',
+ rowValue: (label) => `[data-test-value-div="${label}"]`,
+};
+
+module('Integration | Component | secrets-engine-mount-config', function (hooks) {
+ setupRenderingTest(hooks);
+
+ hooks.beforeEach(function () {
+ const store = this.owner.lookup('service:store');
+ store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ path: 'ldap-test/',
+ type: 'ldap',
+ accessor: 'ldap_7e838627',
+ local: false,
+ seal_wrap: true,
+ config: {
+ id: 'foo',
+ default_lease_ttl: 0,
+ max_lease_ttl: 10000,
+ },
+ },
+ });
+ this.model = store.peekRecord('secret-engine', 'ldap-test');
+ });
+
+ test('it should toggle config fields visibility', async function (assert) {
+ await render(hbs` `);
+
+ assert
+ .dom(selectors.toggle)
+ .hasText('Show mount configuration', 'Correct toggle copy renders when closed');
+ assert.dom(selectors.field).doesNotExist('Mount config fields are hidden');
+
+ await click(selectors.toggle);
+
+ assert.dom(selectors.toggle).hasText('Hide mount configuration', 'Correct toggle copy renders when open');
+ assert.dom(selectors.field).exists('Mount config fields are visible');
+ });
+
+ test('it should render correct config fields', async function (assert) {
+ await render(hbs` `);
+ await click(selectors.toggle);
+
+ assert
+ .dom(selectors.rowValue('Secret Engine Type'))
+ .hasText(this.model.engineType, 'Secret engine type renders');
+ assert.dom(selectors.rowValue('Path')).hasText(this.model.path, 'Path renders');
+ assert.dom(selectors.rowValue('Accessor')).hasText(this.model.accessor, 'Accessor renders');
+ assert.dom(selectors.rowValue('Local')).includesText('No', 'Local renders');
+ assert.dom(selectors.rowValue('Seal Wrap')).includesText('Yes', 'Seal wrap renders');
+ assert.dom(selectors.rowValue('Default Lease TTL')).includesText('0', 'Default Lease TTL renders');
+ assert.dom(selectors.rowValue('Max Lease TTL')).includesText('10000', 'Max Lease TTL renders');
+ });
+
+ test('it should yield block for additional fields', async function (assert) {
+ await render(hbs`
+
+ It Yields!
+
+ `);
+
+ await click(selectors.toggle);
+ assert.dom('[data-test-yield]').hasText('It Yields!', 'Component yields block for additional fields');
+ });
+});
diff --git a/ui/tests/pages/secrets/backends.js b/ui/tests/pages/secrets/backends.js
index 135e638d18..c19d8005bf 100644
--- a/ui/tests/pages/secrets/backends.js
+++ b/ui/tests/pages/secrets/backends.js
@@ -9,7 +9,7 @@ import uiPanel from 'vault/tests/pages/components/console/ui-panel';
export default create({
consoleToggle: clickable('[data-test-console-toggle]'),
visit: visitable('/vault/secrets'),
- rows: collection('[data-test-auth-backend-link]', {
+ rows: collection('[data-test-secrets-backend-link]', {
path: text('[data-test-secret-path]'),
menu: clickable('[data-test-popup-menu-trigger]'),
}),
diff --git a/ui/tests/unit/adapters/kubernetes/config-test.js b/ui/tests/unit/adapters/kubernetes/config-test.js
index edc73f7667..7ccf6dd88b 100644
--- a/ui/tests/unit/adapters/kubernetes/config-test.js
+++ b/ui/tests/unit/adapters/kubernetes/config-test.js
@@ -58,4 +58,14 @@ module('Unit | Adapter | kubernetes/config', function (hooks) {
const record = this.store.peekRecord('kubernetes/config', 'kubernetes-test');
await record.destroyRecord();
});
+
+ test('it should check the config vars endpoint', async function (assert) {
+ assert.expect(1);
+
+ this.server.get('/kubernetes-test/check', () => {
+ assert.ok('GET request made to config vars check endpoint');
+ });
+
+ await this.store.adapterFor('kubernetes/config').checkConfigVars('kubernetes-test');
+ });
});
diff --git a/ui/tests/unit/adapters/ldap/config-test.js b/ui/tests/unit/adapters/ldap/config-test.js
new file mode 100644
index 0000000000..cb320c0d1c
--- /dev/null
+++ b/ui/tests/unit/adapters/ldap/config-test.js
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+
+module('Unit | Adapter | ldap/config', function (hooks) {
+ setupTest(hooks);
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.store = this.owner.lookup('service:store');
+ this.store.unloadAll('ldap/config');
+ });
+
+ test('it should make request to correct endpoint when querying record', async function (assert) {
+ assert.expect(1);
+ this.server.get('/ldap-test/config', () => {
+ assert.ok('GET request made to correct endpoint when querying record');
+ });
+ await this.store.queryRecord('ldap/config', { backend: 'ldap-test' });
+ });
+
+ test('it should make request to correct endpoint when creating new record', async function (assert) {
+ assert.expect(1);
+ this.server.post('/ldap-test/config', () => {
+ assert.ok('POST request made to correct endpoint when creating new record');
+ });
+ const record = this.store.createRecord('ldap/config', { backend: 'ldap-test' });
+ await record.save();
+ });
+
+ test('it should make request to correct endpoint when updating record', async function (assert) {
+ assert.expect(1);
+ this.server.post('/ldap-test/config', () => {
+ assert.ok('POST request made to correct endpoint when updating record');
+ });
+ this.store.pushPayload('ldap/config', {
+ modelName: 'ldap/config',
+ backend: 'ldap-test',
+ });
+ const record = this.store.peekRecord('ldap/config', 'ldap-test');
+ await record.save();
+ });
+
+ test('it should make request to correct endpoint when deleting record', async function (assert) {
+ assert.expect(1);
+ this.server.delete('/ldap-test/config', () => {
+ assert.ok('DELETE request made to correct endpoint when deleting record');
+ });
+ this.store.pushPayload('ldap/config', {
+ modelName: 'ldap/config',
+ backend: 'ldap-test',
+ });
+ const record = this.store.peekRecord('ldap/config', 'ldap-test');
+ await record.destroyRecord();
+ });
+});
diff --git a/ui/tests/unit/adapters/ldap/library-test.js b/ui/tests/unit/adapters/ldap/library-test.js
new file mode 100644
index 0000000000..bebff25108
--- /dev/null
+++ b/ui/tests/unit/adapters/ldap/library-test.js
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+
+module('Unit | Adapter | ldap/library', function (hooks) {
+ setupTest(hooks);
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.store = this.owner.lookup('service:store');
+ this.adapter = this.store.adapterFor('ldap/library');
+ });
+
+ test('it should make request to correct endpoint when listing records', async function (assert) {
+ assert.expect(1);
+
+ this.server.get('/ldap-test/library', (schema, req) => {
+ assert.ok(req.queryParams.list, 'GET request made to correct endpoint when listing records');
+ return { data: { keys: ['test-library'] } };
+ });
+
+ await this.store.query('ldap/library', { backend: 'ldap-test' });
+ });
+
+ test('it should make request to correct endpoint when querying record', async function (assert) {
+ assert.expect(1);
+
+ this.server.get('/ldap-test/library/test-library', () => {
+ assert.ok('GET request made to correct endpoint when querying record');
+ });
+
+ await this.store.queryRecord('ldap/library', { backend: 'ldap-test', name: 'test-library' });
+ });
+
+ test('it should make request to correct endpoint when creating new record', async function (assert) {
+ assert.expect(1);
+
+ this.server.post('/ldap-test/library/test-library', () => {
+ assert.ok('POST request made to correct endpoint when creating new record');
+ });
+
+ await this.store.createRecord('ldap/library', { backend: 'ldap-test', name: 'test-library' }).save();
+ });
+
+ test('it should make request to correct endpoint when updating record', async function (assert) {
+ assert.expect(1);
+
+ this.server.post('/ldap-test/library/test-library', () => {
+ assert.ok('POST request made to correct endpoint when updating record');
+ });
+
+ this.store.pushPayload('ldap/library', {
+ modelName: 'ldap/library',
+ backend: 'ldap-test',
+ name: 'test-library',
+ });
+
+ await this.store.peekRecord('ldap/library', 'test-library').save();
+ });
+
+ test('it should make request to correct endpoint when deleting record', async function (assert) {
+ assert.expect(1);
+
+ this.server.delete('/ldap-test/library/test-library', () => {
+ assert.ok('DELETE request made to correct endpoint when deleting record');
+ });
+
+ this.store.pushPayload('ldap/library', {
+ modelName: 'ldap/library',
+ backend: 'ldap-test',
+ name: 'test-library',
+ });
+
+ await this.store.peekRecord('ldap/library', 'test-library').destroyRecord();
+ });
+
+ test('it should make request to correct endpoint when fetching check-out status', async function (assert) {
+ assert.expect(1);
+
+ this.server.get('/ldap-test/library/test-library/status', () => {
+ assert.ok('GET request made to correct endpoint when fetching check-out status');
+ });
+
+ await this.adapter.fetchStatus('ldap-test', 'test-library');
+ });
+
+ test('it should make request to correct endpoint when checking out library', async function (assert) {
+ assert.expect(1);
+
+ this.server.post('/ldap-test/library/test-library/check-out', (schema, req) => {
+ const json = JSON.parse(req.requestBody);
+ assert.strictEqual(json.ttl, '1h', 'POST request made to correct endpoint when checking out library');
+ return {
+ data: { password: 'test', service_account_name: 'foo@bar.com' },
+ };
+ });
+
+ await this.adapter.checkOutAccount('ldap-test', 'test-library', '1h');
+ });
+
+ test('it should make request to correct endpoint when checking in service accounts', async function (assert) {
+ assert.expect(1);
+
+ this.server.post('/ldap-test/library/test-library/check-in', (schema, req) => {
+ const json = JSON.parse(req.requestBody);
+ assert.deepEqual(
+ json.service_account_names,
+ ['foo@bar.com'],
+ 'POST request made to correct endpoint when checking in service accounts'
+ );
+ return {
+ data: {
+ 'check-ins': ['foo@bar.com'],
+ },
+ };
+ });
+
+ await this.adapter.checkInAccount('ldap-test', 'test-library', ['foo@bar.com']);
+ });
+});
diff --git a/ui/tests/unit/adapters/ldap/role-test.js b/ui/tests/unit/adapters/ldap/role-test.js
new file mode 100644
index 0000000000..4c073094e2
--- /dev/null
+++ b/ui/tests/unit/adapters/ldap/role-test.js
@@ -0,0 +1,227 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { Response } from 'miragejs';
+import sinon from 'sinon';
+
+module('Unit | Adapter | ldap/role', function (hooks) {
+ setupTest(hooks);
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.store = this.owner.lookup('service:store');
+ this.adapter = this.store.adapterFor('ldap/role');
+ this.path = 'role';
+ });
+
+ test('it should make request to correct endpoints when listing records', async function (assert) {
+ assert.expect(6);
+
+ const assertRequest = (schema, req) => {
+ assert.ok(req.queryParams.list, 'list query param sent when listing roles');
+ const name = req.params.path === 'static-role' ? 'static-test' : 'dynamic-test';
+ return { data: { keys: [name] } };
+ };
+
+ this.server.get('/ldap-test/static-role', assertRequest);
+ this.server.get('/ldap-test/role', assertRequest);
+
+ this.models = await this.store.query('ldap/role', { backend: 'ldap-test' });
+
+ const model = this.models.firstObject;
+ assert.strictEqual(this.models.length, 2, 'Returns responses from both endpoints');
+ assert.strictEqual(model.backend, 'ldap-test', 'Backend value is set on records returned from query');
+ // sorted alphabetically by name so dynamic should be first
+ assert.strictEqual(model.type, 'dynamic', 'Type value is set on records returned from query');
+ assert.strictEqual(model.name, 'dynamic-test', 'Name value is set on records returned from query');
+ });
+
+ test('it should conditionally trigger info level flash message for single endpoint error from query', async function (assert) {
+ const flashMessages = this.owner.lookup('service:flashMessages');
+ const flashSpy = sinon.spy(flashMessages, 'info');
+
+ this.server.get('/ldap-test/static-role', () => {
+ return new Response(403, {}, { errors: ['permission denied'] });
+ });
+ this.server.get('/ldap-test/role', () => ({ data: { keys: ['dynamic-test'] } }));
+
+ await this.store.query('ldap/role', { backend: 'ldap-test' });
+ await this.store.query(
+ 'ldap/role',
+ { backend: 'ldap-test' },
+ { adapterOptions: { showPartialError: true } }
+ );
+
+ assert.true(
+ flashSpy.calledOnceWith('Error fetching roles from /v1/ldap-test/static-role: permission denied'),
+ 'Partial error info only displays when adapter option is passed'
+ );
+ });
+
+ test('it should throw error for query when requests to both endpoints fail', async function (assert) {
+ assert.expect(1);
+
+ this.server.get('/ldap-test/:path', (schema, req) => {
+ const errors = {
+ 'static-role': ['permission denied'],
+ role: ['server error'],
+ }[req.params.path];
+ return new Response(req.params.path === 'static-role' ? 403 : 500, {}, { errors });
+ });
+
+ try {
+ await this.store.query('ldap/role', { backend: 'ldap-test' });
+ } catch (error) {
+ assert.deepEqual(
+ error,
+ {
+ message: 'Error fetching roles:',
+ errors: ['/v1/ldap-test/static-role: permission denied', '/v1/ldap-test/role: server error'],
+ },
+ 'Error is thrown with correct payload from query'
+ );
+ }
+ });
+
+ test('it should make request to correct endpoints when querying record', async function (assert) {
+ assert.expect(5);
+
+ this.server.get('/ldap-test/:path/test-role', (schema, req) => {
+ assert.strictEqual(
+ req.params.path,
+ this.path,
+ 'GET request made to correct endpoint when querying record'
+ );
+ });
+
+ for (const type of ['dynamic', 'static']) {
+ this.model = await this.store.queryRecord('ldap/role', {
+ backend: 'ldap-test',
+ type,
+ name: 'test-role',
+ });
+ this.path = 'static-role';
+ }
+
+ assert.strictEqual(
+ this.model.backend,
+ 'ldap-test',
+ 'Backend value is set on records returned from query'
+ );
+ assert.strictEqual(this.model.type, 'static', 'Type value is set on records returned from query');
+ assert.strictEqual(this.model.name, 'test-role', 'Name value is set on records returned from query');
+ });
+
+ test('it should make request to correct endpoints when creating new record', async function (assert) {
+ assert.expect(2);
+
+ this.server.post('/ldap-test/:path/test-role', (schema, req) => {
+ assert.strictEqual(
+ req.params.path,
+ this.path,
+ 'POST request made to correct endpoint when creating new record'
+ );
+ });
+
+ const getModel = (type) => {
+ return this.store.createRecord('ldap/role', {
+ backend: 'ldap-test',
+ name: 'test-role',
+ type,
+ });
+ };
+
+ for (const type of ['dynamic', 'static']) {
+ const model = getModel(type);
+ await model.save();
+ this.path = 'static-role';
+ }
+ });
+
+ test('it should make request to correct endpoints when updating record', async function (assert) {
+ assert.expect(2);
+
+ this.server.post('/ldap-test/:path/test-role', (schema, req) => {
+ assert.strictEqual(
+ req.params.path,
+ this.path,
+ 'POST request made to correct endpoint when updating record'
+ );
+ });
+
+ this.store.pushPayload('ldap/role', {
+ modelName: 'ldap/role',
+ backend: 'ldap-test',
+ name: 'test-role',
+ });
+ const record = this.store.peekRecord('ldap/role', 'test-role');
+
+ for (const type of ['dynamic', 'static']) {
+ record.type = type;
+ await record.save();
+ this.path = 'static-role';
+ }
+ });
+
+ test('it should make request to correct endpoints when deleting record', async function (assert) {
+ assert.expect(2);
+
+ this.server.delete('/ldap-test/:path/test-role', (schema, req) => {
+ assert.strictEqual(
+ req.params.path,
+ this.path,
+ 'DELETE request made to correct endpoint when deleting record'
+ );
+ });
+
+ const getModel = () => {
+ this.store.pushPayload('ldap/role', {
+ modelName: 'ldap/role',
+ backend: 'ldap-test',
+ name: 'test-role',
+ });
+ return this.store.peekRecord('ldap/role', 'test-role');
+ };
+
+ for (const type of ['dynamic', 'static']) {
+ const record = getModel();
+ record.type = type;
+ await record.destroyRecord();
+ this.path = 'static-role';
+ }
+ });
+
+ test('it should make request to correct endpoints when fetching credentials', async function (assert) {
+ assert.expect(2);
+
+ this.path = 'creds';
+
+ this.server.get('/ldap-test/:path/test-role', (schema, req) => {
+ assert.strictEqual(
+ req.params.path,
+ this.path,
+ 'GET request made to correct endpoint when fetching credentials'
+ );
+ });
+
+ for (const type of ['dynamic', 'static']) {
+ await this.adapter.fetchCredentials('ldap-test', type, 'test-role');
+ this.path = 'static-cred';
+ }
+ });
+
+ test('it should make request to correct endpoint when rotating static role password', async function (assert) {
+ assert.expect(1);
+
+ this.server.post('/ldap-test/rotate-role/test-role', () => {
+ assert.ok('GET request made to correct endpoint when rotating static role password');
+ });
+
+ await this.adapter.rotateStaticPassword('ldap-test', 'test-role');
+ });
+});
diff --git a/ui/tests/unit/decorators/fetch-secrets-engine-config-test.js b/ui/tests/unit/decorators/fetch-secrets-engine-config-test.js
new file mode 100644
index 0000000000..b9a3cc9aeb
--- /dev/null
+++ b/ui/tests/unit/decorators/fetch-secrets-engine-config-test.js
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+import sinon from 'sinon';
+import Route from '@ember/routing/route';
+import { withConfig } from 'core/decorators/fetch-secrets-engine-config';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { inject as service } from '@ember/service';
+import { Response } from 'miragejs';
+
+module('Unit | Decorators | fetch-secrets-engine-config', function (hooks) {
+ setupTest(hooks);
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.spy = sinon.spy(console, 'error');
+ this.store = this.owner.lookup('service:store');
+ this.backend = 'test-path';
+ this.owner.lookup('service:secretMountPath').update(this.backend);
+
+ this.createClass = () => {
+ @withConfig('ldap/config')
+ class Foo extends Route {
+ @service store;
+ @service secretMountPath;
+ }
+ // service injection will fail if class is not instantiated with an owner
+ return new Foo(this.owner);
+ };
+ });
+ hooks.afterEach(function () {
+ this.spy.restore();
+ });
+
+ test('it should warn when applying decorator to class that does not extend Route', function (assert) {
+ @withConfig()
+ class Foo {} // eslint-disable-line
+ const message =
+ 'withConfig decorator must be used on an instance of Ember Route class. Decorator not applied to returned class';
+ assert.ok(this.spy.calledWith(message), 'Error is printed to console');
+ });
+
+ test('it should return cached record from store if it exists', async function (assert) {
+ this.store.pushPayload('ldap/config', {
+ modelName: 'ldap/config',
+ backend: this.backend,
+ });
+ const peekSpy = sinon.spy(this.store, 'peekRecord');
+ const route = this.createClass();
+
+ await route.beforeModel();
+ assert.true(peekSpy.calledWith('ldap/config', this.backend), 'peekRecord called for config model');
+ assert.strictEqual(route.configModel.backend, this.backend, 'config model set on class');
+ assert.strictEqual(route.configError, null, 'error is unset when model is found');
+ assert.false(route.promptConfig, 'promptConfig is false when model is found');
+ });
+
+ test('it should fetch record when not in the store', async function (assert) {
+ assert.expect(4);
+
+ this.server.get('/test-path/config', () => {
+ assert.ok(true, 'fetch request is made');
+ return {};
+ });
+
+ const route = this.createClass();
+ await route.beforeModel();
+
+ assert.strictEqual(route.configModel.backend, this.backend, 'config model set on class');
+ assert.strictEqual(route.configError, null, 'error is unset when model is found');
+ assert.false(route.promptConfig, 'promptConfig is false when model is found');
+ });
+
+ test('it should set prompt value when fetch returns a 404', async function (assert) {
+ assert.expect(4);
+
+ this.server.get('/test-path/config', () => {
+ assert.ok(true, 'fetch request is made');
+ return new Response(404, {}, { errors: [] });
+ });
+
+ const route = this.createClass();
+ await route.beforeModel();
+
+ assert.strictEqual(route.configModel, null, 'config is not set when error is returned');
+ assert.strictEqual(route.configError, null, 'error is unset when 404 is returned');
+ assert.true(route.promptConfig, 'promptConfig is true when 404 is returned');
+ });
+
+ test('it should set error value when fetch returns error other than 404', async function (assert) {
+ assert.expect(4);
+
+ const error = { errors: ['Permission denied'] };
+ this.server.get('/test-path/config', () => {
+ assert.ok(true, 'fetch request is made');
+ return new Response(403, {}, error);
+ });
+
+ const route = this.createClass();
+ await route.beforeModel();
+
+ assert.strictEqual(route.configModel, null, 'config is not set when error is returned');
+ assert.deepEqual(
+ route.configError.errors,
+ error.errors,
+ 'error is set when error other than 404 is returned'
+ );
+ assert.false(route.promptConfig, 'promptConfig is false when error other than 404 is returned');
+ });
+});
diff --git a/ui/tests/unit/machines/secrets-machine-test.js b/ui/tests/unit/machines/secrets-machine-test.js
index 5bf2358a7a..a41bcad6ff 100644
--- a/ui/tests/unit/machines/secrets-machine-test.js
+++ b/ui/tests/unit/machines/secrets-machine-test.js
@@ -386,75 +386,6 @@ module('Unit | Machine | secrets-machine', function () {
],
},
},
- {
- currentState: 'enable',
- event: 'CONTINUE',
- params: 'ad',
- expectedResults: {
- value: 'list',
- actions: [
- { type: 'render', level: 'step', component: 'wizard/secrets-list' },
- { type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
- ],
- },
- },
- {
- currentState: 'list',
- event: 'CONTINUE',
- params: 'ad',
- expectedResults: {
- value: 'display',
- actions: [
- { component: 'wizard/secrets-display', level: 'step', type: 'render' },
- { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' },
- ],
- },
- },
- {
- currentState: 'display',
- event: 'RESET',
- params: 'ad',
- expectedResults: {
- value: 'idle',
- actions: [
- {
- params: ['vault.cluster.settings.mount-secret-backend'],
- type: 'routeTransition',
- },
- {
- component: 'wizard/mounts-wizard',
- level: 'feature',
- type: 'render',
- },
- {
- component: 'wizard/secrets-idle',
- level: 'step',
- type: 'render',
- },
- ],
- },
- },
- {
- currentState: 'display',
- event: 'DONE',
- params: 'ad',
- expectedResults: {
- value: 'complete',
- actions: ['completeFeature'],
- },
- },
- {
- currentState: 'display',
- event: 'ERROR',
- params: 'ad',
- expectedResults: {
- value: 'error',
- actions: [
- { component: 'wizard/tutorial-error', level: 'step', type: 'render' },
- { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' },
- ],
- },
- },
{
currentState: 'enable',
event: 'CONTINUE',
diff --git a/ui/tests/unit/serializers/ldap/library-test.js b/ui/tests/unit/serializers/ldap/library-test.js
new file mode 100644
index 0000000000..66dcdbdfb6
--- /dev/null
+++ b/ui/tests/unit/serializers/ldap/library-test.js
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import { setupTest } from 'vault/tests/helpers';
+
+module('Unit | Serializer | ldap/library', function (hooks) {
+ setupTest(hooks);
+
+ hooks.beforeEach(function () {
+ this.store = this.owner.lookup('service:store');
+ });
+
+ test('it should normalize and serialize disable_check_in_enforcement value', async function (assert) {
+ assert.expect(4);
+
+ const model = this.store.createRecord('ldap/library', {
+ backend: 'ldap-test',
+ name: 'test-library',
+ });
+ const cases = [
+ { value: false, transformed: 'Enabled' },
+ { value: true, transformed: 'Disabled' },
+ ];
+
+ cases.forEach(({ value, transformed }) => {
+ const normalized = this.store.normalize('ldap/library', { disable_check_in_enforcement: value });
+ assert.strictEqual(
+ normalized.data.attributes.disable_check_in_enforcement,
+ transformed,
+ `Normalizes ${value} value to ${transformed}`
+ );
+ model.disable_check_in_enforcement = transformed;
+ const { disable_check_in_enforcement } = model.serialize();
+ assert.strictEqual(disable_check_in_enforcement, value, `Serializes ${transformed} value to ${value}`);
+ });
+ });
+});
diff --git a/ui/tests/unit/serializers/ldap/role-test.js b/ui/tests/unit/serializers/ldap/role-test.js
new file mode 100644
index 0000000000..7fad0d3272
--- /dev/null
+++ b/ui/tests/unit/serializers/ldap/role-test.js
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import { setupTest } from 'vault/tests/helpers';
+
+module('Unit | Serializer | ldap/role', function (hooks) {
+ setupTest(hooks);
+
+ hooks.beforeEach(function () {
+ const store = this.owner.lookup('service:store');
+ this.model = store.createRecord('ldap/role', {
+ backend: 'ldap',
+ name: 'test-role',
+ dn: 'cn=hashicorp,ou=Users,dc=hashicorp,dc=com',
+ rotation_period: '24h',
+ username: 'hashicorp',
+ creation_ldif: 'foo',
+ deletion_ldif: 'bar',
+ rollback_ldif: 'baz',
+ username_template: 'default',
+ default_ttl: '1h',
+ max_ttl: '24h',
+ });
+ });
+
+ test('it should serialize attributes based on type', async function (assert) {
+ assert.expect(11);
+
+ const serializeAndAssert = (type) => {
+ this.model.type = type;
+ const payload = this.model.serialize();
+ // intentionally not using fieldsForType from model to detect any drift
+ const fieldsForType = {
+ static: ['username', 'dn', 'rotation_period'],
+ dynamic: [
+ 'default_ttl',
+ 'max_ttl',
+ 'username_template',
+ 'creation_ldif',
+ 'deletion_ldif',
+ 'rollback_ldif',
+ ],
+ }[type];
+
+ assert.strictEqual(
+ Object.keys(payload).length,
+ fieldsForType.length,
+ `Correct number of keys exist in serialized payload for ${type} role type`
+ );
+ Object.keys(payload).forEach((key) => {
+ assert.true(
+ fieldsForType.includes(key),
+ `${key} property exists in serialized payload for ${type} role type`
+ );
+ });
+ };
+
+ serializeAndAssert('static');
+ serializeAndAssert('dynamic');
+ });
+});
diff --git a/ui/tsconfig.json b/ui/tsconfig.json
index f86f81d510..1bd966c208 100644
--- a/ui/tsconfig.json
+++ b/ui/tsconfig.json
@@ -43,6 +43,8 @@
"kmip/*": ["lib/kmip/addon/*"],
"kmip/test-support": ["lib/kmip/addon-test-support"],
"kmip/test-support/*": ["lib/kmip/addon-test-support/*"],
+ "ldap": ["lib/ldap/addon"],
+ "ldap/*": ["lib/ldap/addon/*"],
"kv": ["lib/kv/addon"],
"kv/*": ["lib/kv/addon/*"],
"kv/test-support": ["lib/kv/addon-test-support"],
@@ -77,6 +79,7 @@
"lib/core/**/*",
"lib/css/**/*",
"lib/kmip/**/*",
+ "lib/ldap/**/*",
"lib/open-api-explorer/**/*",
"lib/pki/**/*",
"lib/replication/**/*",
diff --git a/ui/types/ember-data/types/registries/adapter.d.ts b/ui/types/ember-data/types/registries/adapter.d.ts
index 6c0a082f39..ce8f738dfa 100644
--- a/ui/types/ember-data/types/registries/adapter.d.ts
+++ b/ui/types/ember-data/types/registries/adapter.d.ts
@@ -8,6 +8,8 @@ import Adapter from 'ember-data/adapter';
import ModelRegistry from 'ember-data/types/registries/model';
import PkiIssuerAdapter from 'vault/adapters/pki/issuer';
import PkiTidyAdapter from 'vault/adapters/pki/tidy';
+import LdapRoleAdapter from 'vault/adapters/ldap/role';
+import LdapLibraryAdapter from 'vault/adapters/ldap/library';
import KvDataAdapter from 'vault/adapters/kv/data';
import KvMetadataAdapter from 'vault/adapters/kv/metadata';
@@ -15,6 +17,8 @@ import KvMetadataAdapter from 'vault/adapters/kv/metadata';
* Catch-all for ember-data.
*/
export default interface AdapterRegistry {
+ 'ldap/library': LdapLibraryAdapter;
+ 'ldap/role': LdapRoleAdapter;
'pki/issuer': PkiIssuerAdapter;
'pki/tidy': PkiTidyAdapter;
'kv/data': KvDataAdapterAdapter;
@@ -22,3 +26,7 @@ export default interface AdapterRegistry {
application: Application;
[key: keyof ModelRegistry]: Adapter;
}
+
+export default interface AdapterError extends Error {
+ httpStatus: number;
+}
diff --git a/ui/types/vault/adapters/ldap/library.d.ts b/ui/types/vault/adapters/ldap/library.d.ts
new file mode 100644
index 0000000000..0eb5c9ab74
--- /dev/null
+++ b/ui/types/vault/adapters/ldap/library.d.ts
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Store from '@ember-data/store';
+import { AdapterRegistry } from 'ember-data/adapter';
+
+export interface LdapLibraryAccountStatus {
+ account: string;
+ available: boolean;
+ library: string;
+ borrower_client_token?: string;
+ borrower_entity_id?: string;
+}
+
+export interface LdapLibraryCheckOutCredentials {
+ account: string;
+ password: string;
+ lease_id: string;
+ lease_duration: number;
+ renewable: boolean;
+}
+
+export default interface LdapLibraryAdapter extends AdapterRegistry {
+ fetchCheckOutStatus(backend: string, name: string): Promise>;
+ checkOutAccount(backend: string, name: string, ttl?: string): Promise;
+ checkInAccount(backend: string, name: string, service_account_names: Array): Promise;
+}
diff --git a/ui/types/vault/adapters/ldap/role.d.ts b/ui/types/vault/adapters/ldap/role.d.ts
new file mode 100644
index 0000000000..81c01bf3c6
--- /dev/null
+++ b/ui/types/vault/adapters/ldap/role.d.ts
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Store from '@ember-data/store';
+import { AdapterRegistry } from 'ember-data/adapter';
+
+export default interface LdapRoleAdapter extends AdapterRegistry {
+ fetchCredentials(backend: string, type: string, name: string);
+ rotateStaticPassword(backend: string, name: string);
+}
diff --git a/ui/types/vault/app-types.ts b/ui/types/vault/app-types.ts
index 5dc9970698..28d1476f97 100644
--- a/ui/types/vault/app-types.ts
+++ b/ui/types/vault/app-types.ts
@@ -2,6 +2,8 @@
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
+import type EmberDataModel from '@ember-data/model';
+import type Owner from '@ember/owner';
// Type that comes back from expandAttributeMeta
export interface FormField {
@@ -41,6 +43,23 @@ export interface ModelValidations {
invalidFormMessage: string;
}
+export interface Model extends Omit {
+ // override isNew which is a computed prop and ts will complain since it sees it as a function
+ isNew: boolean;
+}
+
+export interface WithFormFieldsModel extends Model {
+ formFields: Array;
+ formFieldGroups: FormFieldGroups;
+ allFields: Array;
+}
+
+export interface WithValidationsModel extends Model {
+ validate(): ModelValidations;
+}
+
+export interface WithFormFieldsAndValidationsModel extends WithFormFieldsModel, WithValidationsModel {}
+
export interface Breadcrumb {
label: string;
route?: string;
@@ -54,6 +73,16 @@ export interface TtlEvent {
goSafeTimeString: string;
}
+export interface Breadcrumb {
+ label: string;
+ route?: string;
+ linkExternal?: boolean;
+}
+
+export interface EngineOwner extends Owner {
+ mountPoint: string;
+}
+
// Generic interfaces
export interface StringMap {
[key: string]: string;
diff --git a/ui/types/vault/models/ldap/config.d.ts b/ui/types/vault/models/ldap/config.d.ts
new file mode 100644
index 0000000000..15ec81d212
--- /dev/null
+++ b/ui/types/vault/models/ldap/config.d.ts
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+import type { WithFormFieldsAndValidationsModel } from 'vault/app-types';
+
+export default interface LdapConfigModel extends WithFormFieldsAndValidationsModel {
+ backend: string;
+ binddn: string;
+ bindpass: string;
+ url: string;
+ schema: string;
+ password_policy: string;
+ starttls: boolean;
+ insecure_tls: boolean;
+ certificate: string;
+ client_tls_cert: string;
+ client_tls_key: string;
+ userdn: string;
+ userattr: string;
+ upndomain: string;
+ connection_timeout: number;
+ request_timeout: number;
+ rotateRoot(): Promise;
+}
diff --git a/ui/types/vault/models/ldap/library.d.ts b/ui/types/vault/models/ldap/library.d.ts
new file mode 100644
index 0000000000..cde7808a4b
--- /dev/null
+++ b/ui/types/vault/models/ldap/library.d.ts
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+import type { WithFormFieldsAndValidationsModel } from 'vault/app-types';
+import type { FormField } from 'vault/app-types';
+import CapabilitiesModel from '../capabilities';
+import type {
+ LdapLibraryAccountStatus,
+ LdapLibraryCheckOutCredentials,
+} from 'vault/vault/adapters/ldap/library';
+
+export default interface LdapLibraryModel extends WithFormFieldsAndValidationsModel {
+ backend: string;
+ name: string;
+ service_account_names: string;
+ default_ttl: number;
+ max_ttl: number;
+ disable_check_in_enforcement: string;
+ get displayFields(): Array;
+ libraryPath: CapabilitiesModel;
+ statusPath: CapabilitiesModel;
+ checkOutPath: CapabilitiesModel;
+ checkInPath: CapabilitiesModel;
+ get canCreate(): boolean;
+ get canDelete(): boolean;
+ get canEdit(): boolean;
+ get canRead(): boolean;
+ get canList(): boolean;
+ get canReadStatus(): boolean;
+ get canCheckOut(): boolean;
+ get canCheckIn(): boolean;
+ fetchStatus(): Promise>;
+ checkOutAccount(ttl?: string): Promise;
+ checkInAccount(account: string): Promise;
+}
diff --git a/ui/types/vault/models/ldap/role.d.ts b/ui/types/vault/models/ldap/role.d.ts
new file mode 100644
index 0000000000..eac9b4d3a4
--- /dev/null
+++ b/ui/types/vault/models/ldap/role.d.ts
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+import type { WithFormFieldsAndValidationsModel } from 'vault/app-types';
+import type { FormField } from 'vault/app-types';
+import CapabilitiesModel from '../capabilities';
+import { LdapDynamicRoleCredentials, LdapStaticRoleCredentials } from 'ldap/routes/roles/role/credentials';
+export default interface LdapRoleModel extends WithFormFieldsAndValidationsModel {
+ type: string;
+ backend: string;
+ name: string;
+ dn: string;
+ username: string;
+ rotation_period: string;
+ default_ttl: string;
+ max_ttl: string;
+ username_template: string;
+ creation_ldif: string;
+ rollback_ldif: string;
+ get isStatic(): string;
+ get isDynamic(): string;
+ get fieldsForType(): Array;
+ get displayFields(): Array;
+ get roleUri(): string;
+ get credsUri(): string;
+ rolePath: CapabilitiesModel;
+ credsPath: CapabilitiesModel;
+ staticRotateCredsPath: CapabilitiesModel;
+ get canCreate(): boolean;
+ get canDelete(): boolean;
+ get canEdit(): boolean;
+ get canRead(): boolean;
+ get canList(): boolean;
+ get canReadCreds(): boolean;
+ get canRotateStaticCreds(): boolean;
+ fetchCredentials(): Promise;
+ rotateStaticPassword(): Promise;
+}
diff --git a/ui/types/vault/models/mount-config.d.ts b/ui/types/vault/models/mount-config.d.ts
new file mode 100644
index 0000000000..99ae9976d9
--- /dev/null
+++ b/ui/types/vault/models/mount-config.d.ts
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Model from '@ember-data/model';
+
+export default class MountConfigModel extends Model {
+ defaultLeaseTtl: string;
+ maxLeaseTtl: string;
+ auditNonHmacRequestKeys: string;
+ auditNonHmacResponseKeys: string;
+ listingVisibility: string;
+ passthroughRequestHeaders: string;
+ allowedResponseHeaders: string;
+ tokenType: string;
+ allowedManagedKeys: string;
+}
diff --git a/ui/types/vault/models/secret-engine.d.ts b/ui/types/vault/models/secret-engine.d.ts
new file mode 100644
index 0000000000..554c8b78ea
--- /dev/null
+++ b/ui/types/vault/models/secret-engine.d.ts
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Model from '@ember-data/model';
+
+import type { ModelValidations, FormField, FormFieldGroups } from 'vault/app-types';
+import type MountConfigModel from 'vault/models/mount-config';
+
+export default class SecretEngineModel extends Model {
+ path: string;
+ type: string;
+ description: string;
+ config: MountConfigModel;
+ local: boolean;
+ sealWrap: boolean;
+ externalEntropyAccess: boolean;
+ version: number;
+ privateKey: string;
+ publicKey: string;
+ generateSigningKey: boolean;
+ lease: string;
+ leaseMax: string;
+ accessor: string;
+ maxVersions: number;
+ casRequired: boolean;
+ deleteVersionAfter: string;
+ get modelTypeForKV(): string;
+ get isV2KV(): boolean;
+ get attrs(): Array;
+ get fieldGroups(): FormFieldGroups;
+ get icon(): string;
+ get engineType(): string;
+ get shouldIncludeInList(): boolean;
+ get isSupportedBackend(): boolean;
+ get backendLink(): string;
+ get accessor(): string;
+ get localDisplay(): string;
+ get formFields(): Array;
+ get formFieldGroups(): FormFieldGroups;
+ saveCA(options: object): Promise;
+ saveZeroAddressConfig(): Promise;
+ validate(): ModelValidations;
+ // need to override isNew which is a computed prop and ts will complain since it sees it as a function
+ isNew: boolean;
+}
diff --git a/ui/types/vault/services/auth.d.ts b/ui/types/vault/services/auth.d.ts
new file mode 100644
index 0000000000..6833a58596
--- /dev/null
+++ b/ui/types/vault/services/auth.d.ts
@@ -0,0 +1,12 @@
+// temporary interface for auth service until it can be updated to ts
+// add properties as needed
+
+import Service from '@ember/service';
+
+export interface AuthData {
+ entity_id: string;
+}
+
+export default class AuthService extends Service {
+ authData: AuthData;
+}