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}}
           
           
   {{else if (eq @attr.options.editType "stringArray")}}
     
       {{#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}}
     
           {{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 @@
+
+  
\ 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 @@
+
+  
+      <:body as |Body|>
+        
+          {{Body.data.account}} 
+          {{#if @showLibraryColumn}}
+            {{Body.data.library}} 
+          {{/if}}
+          
+            
+               
+           
+         
+      
+     
+  {{else}}
+     
+
+{{#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|}}
+    TLS Connection 
+  ;
+}
+
+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 
+   
+ 
+
+
+    
+      
+        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)}}
+      
+        Configure LDAP
+       
+    {{else}}
+      
+        Create library
+       
+    {{/if}}
+  
+ 
+
+{{#if @promptConfig}}
+  
+      
+        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"}}
+     
+   
+ 
+
+;
+}
+
+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
+           
+      
+     
+  
+
+    
+      
+        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}}
+      
+  <:toolbarActions>
+    {{#if @promptConfig}}
+      
+        Configure LDAP
+       
+    {{/if}}
+  
+ 
+
+{{#if @promptConfig}}
+  
+    
+      
+        {{or @roles.length "None"}}
+       
+     
+    
+      
+        {{or @libraries.length "None"}}
+       
+     
+  
+  
+    
+
+    
+      
+        
+          
+            Get credentials
+           
+        
+       
+    
+  
;
+  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"}}
+     
+   
+ 
+
+;
+}
+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
+     
+   
+ 
+
+
+    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")}}
+            
+              
+                {{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|}}
+    ;
+}
+
+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)}}
+      
+        Configure LDAP
+       
+    {{else}}
+      
+        Create role
+       
+    {{/if}}
+  
+ 
+
+{{#if @promptConfig}}
+  
+      
+        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 @@
+
+  
+     
+  
+    
+       
+   
+ 
+
+
+  
+    
+      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 @@
+
+          
+          
+        <: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`
+        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;
+}