variables = new HashMap<>();
+ variables.put("backend", properties.getBackend());
+ variables.put("key", String.format("%s/%s", this.credPath, properties.getRole()));
+ return variables;
+ }
+ };
+ }
+
+ @Override
+ public SecretBackendMetadata createMetadata(VaultLdapProperties backendDescriptor) {
+ return forLdap(backendDescriptor);
+ }
+
+ @Override
+ public boolean supports(VaultSecretBackendDescriptor backendDescriptor) {
+ return backendDescriptor instanceof VaultLdapProperties;
+ }
+
+ }
+
+}
diff --git a/spring-cloud-vault-config-ldap/src/main/java/org/springframework/cloud/vault/config/ldap/VaultLdapProperties.java b/spring-cloud-vault-config-ldap/src/main/java/org/springframework/cloud/vault/config/ldap/VaultLdapProperties.java
new file mode 100644
index 00000000..79e5d722
--- /dev/null
+++ b/spring-cloud-vault-config-ldap/src/main/java/org/springframework/cloud/vault/config/ldap/VaultLdapProperties.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2016-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.vault.config.ldap;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.vault.config.VaultSecretBackendDescriptor;
+import org.springframework.lang.Nullable;
+
+/**
+ * Configuration properties for Vault using the LDAP secret engine.
+ *
+ * @author Drew Mullen
+ * @since 5.0.1
+ */
+@ConfigurationProperties("spring.cloud.vault.ldap")
+public class VaultLdapProperties implements VaultSecretBackendDescriptor {
+
+ /**
+ * Enable LDAP secret engine usage.
+ */
+ private boolean enabled = false;
+
+ /**
+ * Role name for credentials.
+ */
+ @Nullable
+ private String role;
+
+ /**
+ * Enable static role usage.
+ */
+ private boolean staticRole = false;
+
+ /**
+ * LDAP secret engine backend path.
+ */
+ private String backend = "ldap";
+
+ /**
+ * Target property for the obtained username.
+ */
+ private String usernameProperty = "spring.ldap.username";
+
+ /**
+ * Target property for the obtained password.
+ */
+ private String passwordProperty = "spring.ldap.password";
+
+ @Override
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @Nullable
+ public String getRole() {
+ return this.role;
+ }
+
+ public void setRole(@Nullable String role) {
+ this.role = role;
+ }
+
+ public boolean isStaticRole() {
+ return this.staticRole;
+ }
+
+ public void setStaticRole(boolean staticRole) {
+ this.staticRole = staticRole;
+ }
+
+ @Override
+ public String getBackend() {
+ return this.backend;
+ }
+
+ public void setBackend(String backend) {
+ this.backend = backend;
+ }
+
+ public String getUsernameProperty() {
+ return this.usernameProperty;
+ }
+
+ public void setUsernameProperty(String usernameProperty) {
+ this.usernameProperty = usernameProperty;
+ }
+
+ public String getPasswordProperty() {
+ return this.passwordProperty;
+ }
+
+ public void setPasswordProperty(String passwordProperty) {
+ this.passwordProperty = passwordProperty;
+ }
+
+}
diff --git a/spring-cloud-vault-config-ldap/src/main/java/org/springframework/cloud/vault/config/ldap/package-info.java b/spring-cloud-vault-config-ldap/src/main/java/org/springframework/cloud/vault/config/ldap/package-info.java
new file mode 100644
index 00000000..b7648d5f
--- /dev/null
+++ b/spring-cloud-vault-config-ldap/src/main/java/org/springframework/cloud/vault/config/ldap/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Support classes for Vault LDAP secret engine integration. Allows Spring Cloud Vault to
+ * fetch LDAP credentials from HashiCorp Vault using either dynamic or static roles.
+ *
+ * Dynamic roles generate temporary credentials on-demand, while static roles manage the
+ * rotation of existing LDAP user credentials.
+ *
+ * Configuration is done via {@code spring.cloud.vault.ldap} properties.
+ *
+ * @see org.springframework.cloud.vault.config.ldap.VaultLdapProperties
+ * @see org.springframework.cloud.vault.config.ldap.VaultConfigLdapBootstrapConfiguration
+ */
+package org.springframework.cloud.vault.config.ldap;
diff --git a/spring-cloud-vault-config-ldap/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-vault-config-ldap/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000..8f572d32
--- /dev/null
+++ b/spring-cloud-vault-config-ldap/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.springframework.cloud.vault.config.ldap.VaultConfigLdapBootstrapConfiguration
diff --git a/spring-cloud-vault-config-ldap/src/test/java/org/springframework/cloud/vault/config/ldap/LdapSecretIntegrationTests.java b/spring-cloud-vault-config-ldap/src/test/java/org/springframework/cloud/vault/config/ldap/LdapSecretIntegrationTests.java
new file mode 100644
index 00000000..0a68dc83
--- /dev/null
+++ b/spring-cloud-vault-config-ldap/src/test/java/org/springframework/cloud/vault/config/ldap/LdapSecretIntegrationTests.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2016-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.vault.config.ldap;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import org.springframework.cloud.vault.config.VaultConfigOperations;
+import org.springframework.cloud.vault.config.VaultConfigTemplate;
+import org.springframework.cloud.vault.config.VaultProperties;
+import org.springframework.cloud.vault.config.ldap.VaultConfigLdapBootstrapConfiguration.LdapSecretBackendMetadataFactory;
+import org.springframework.cloud.vault.util.IntegrationTestSupport;
+import org.springframework.cloud.vault.util.Settings;
+import org.springframework.vault.core.VaultOperations;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration tests for {@link VaultConfigTemplate} using the LDAP secret engine with an
+ * in-memory UnboundID LDAP server.
+ *
+ * @author Drew Mullen
+ */
+public class LdapSecretIntegrationTests extends IntegrationTestSupport {
+
+ @RegisterExtension
+ public static final LdapServerExtension ldapServer = new LdapServerExtension();
+
+ private VaultProperties vaultProperties = Settings.createVaultProperties();
+
+ private VaultConfigOperations configOperations;
+
+ private VaultLdapProperties ldapProperties = new VaultLdapProperties();
+
+ /**
+ * Initialize the LDAP secret engine configuration with the in-memory LDAP server.
+ */
+ @BeforeEach
+ public void setUp() {
+ this.ldapProperties.setEnabled(true);
+ this.ldapProperties.setBackend("ldap");
+
+ VaultOperations vaultOperations = this.vaultRule.prepare().getVaultOperations();
+
+ if (!prepare().hasSecretBackend(this.ldapProperties.getBackend())) {
+ prepare().mountSecret(this.ldapProperties.getBackend());
+ }
+
+ Map ldapConfig = new HashMap<>();
+ ldapConfig.put("binddn", "uid=vault-admin,ou=people," + ldapServer.getBaseDn());
+ ldapConfig.put("bindpass", "vault-admin-password");
+ ldapConfig.put("url", ldapServer.getLdapUrl());
+ ldapConfig.put("userdn", "ou=people," + ldapServer.getBaseDn());
+
+ vaultOperations.write(String.format("%s/config", this.ldapProperties.getBackend()), ldapConfig);
+
+ this.configOperations = new VaultConfigTemplate(vaultOperations, this.vaultProperties);
+ }
+
+ /**
+ * Test for dynamic role credential retrieval.
+ */
+ @Test
+ public void shouldCreateDynamicCredentialsCorrectly() {
+ VaultOperations vaultOperations = this.vaultRule.prepare().getVaultOperations();
+
+ Map dynamicRoleConfig = new HashMap<>();
+ dynamicRoleConfig.put("creation_ldif",
+ "dn: cn={{.Username}},ou=people," + ldapServer.getBaseDn() + "\n" + "objectClass: person\n"
+ + "objectClass: top\n" + "cn: {{.Username}}\n" + "sn: {{.Username}}\n"
+ + "userPassword: {{.Password}}");
+ dynamicRoleConfig.put("deletion_ldif",
+ "dn: cn={{.Username}},ou=people," + ldapServer.getBaseDn() + "\nchangetype: delete");
+ dynamicRoleConfig.put("default_ttl", "1h");
+ dynamicRoleConfig.put("max_ttl", "24h");
+
+ vaultOperations.write(String.format("%s/role/dynamic-role", this.ldapProperties.getBackend()),
+ dynamicRoleConfig);
+
+ this.ldapProperties.setRole("dynamic-role");
+ this.ldapProperties.setStaticRole(false);
+
+ Map secretProperties = this.configOperations
+ .read(LdapSecretBackendMetadataFactory.forLdap(this.ldapProperties))
+ .getData();
+
+ assertThat(secretProperties).containsKeys("spring.ldap.username", "spring.ldap.password");
+ }
+
+ /**
+ * Test for static role credential retrieval.
+ */
+ @Test
+ public void shouldCreateStaticCredentialsCorrectly() {
+ VaultOperations vaultOperations = this.vaultRule.prepare().getVaultOperations();
+
+ Map staticRoleConfig = new HashMap<>();
+ staticRoleConfig.put("dn", "uid=static-user,ou=people," + ldapServer.getBaseDn());
+ staticRoleConfig.put("username", "static-user");
+ staticRoleConfig.put("rotation_period", "24h");
+
+ vaultOperations.write(String.format("%s/static-role/static-role", this.ldapProperties.getBackend()),
+ staticRoleConfig);
+
+ this.ldapProperties.setRole("static-role");
+ this.ldapProperties.setStaticRole(true);
+
+ Map secretProperties = this.configOperations
+ .read(LdapSecretBackendMetadataFactory.forLdap(this.ldapProperties))
+ .getData();
+
+ assertThat(secretProperties).containsKeys("spring.ldap.username", "spring.ldap.password");
+ }
+
+}
diff --git a/spring-cloud-vault-config-ldap/src/test/java/org/springframework/cloud/vault/config/ldap/LdapServerExtension.java b/spring-cloud-vault-config-ldap/src/test/java/org/springframework/cloud/vault/config/ldap/LdapServerExtension.java
new file mode 100644
index 00000000..ee87fbce
--- /dev/null
+++ b/spring-cloud-vault-config-ldap/src/test/java/org/springframework/cloud/vault/config/ldap/LdapServerExtension.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2016-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.vault.config.ldap;
+
+import com.unboundid.ldap.listener.InMemoryDirectoryServer;
+import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
+import com.unboundid.ldap.listener.InMemoryListenerConfig;
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+/**
+ * JUnit 5 extension for managing an in-memory UnboundID LDAP server for testing.
+ *
+ * @author Drew Mullen
+ */
+public class LdapServerExtension implements BeforeAllCallback, AfterAllCallback {
+
+ private InMemoryDirectoryServer ldapServer;
+
+ private int ldapPort = 10389;
+
+ private String baseDn = "dc=springframework,dc=org";
+
+ @Override
+ public void beforeAll(ExtensionContext context) throws Exception {
+ InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(this.baseDn);
+
+ config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", this.ldapPort));
+
+ this.ldapServer = new InMemoryDirectoryServer(config);
+ this.ldapServer.add("dn: " + this.baseDn, "objectClass: top", "objectClass: domain", "dc: springframework");
+ this.ldapServer.add("dn: ou=people," + this.baseDn, "objectClass: organizationalUnit", "ou: people");
+ this.ldapServer.add("dn: uid=static-user,ou=people," + this.baseDn, "objectClass: inetOrgPerson",
+ "objectClass: organizationalPerson", "objectClass: person", "objectClass: top", "cn: Static User",
+ "sn: User", "uid: static-user", "userPassword: initial-password");
+
+ this.ldapServer.add("dn: uid=vault-admin,ou=people," + this.baseDn, "objectClass: inetOrgPerson",
+ "objectClass: organizationalPerson", "objectClass: person", "objectClass: top", "cn: Vault Admin",
+ "sn: Admin", "uid: vault-admin", "userPassword: vault-admin-password");
+
+ this.ldapServer.startListening();
+ }
+
+ @Override
+ public void afterAll(ExtensionContext context) {
+ if (this.ldapServer != null) {
+ this.ldapServer.shutDown(true);
+ }
+ }
+
+ public int getLdapPort() {
+ return this.ldapPort;
+ }
+
+ public String getBaseDn() {
+ return this.baseDn;
+ }
+
+ public String getLdapUrl() {
+ return "ldap://localhost:" + this.ldapPort;
+ }
+
+ public InMemoryDirectoryServer getLdapServer() {
+ return this.ldapServer;
+ }
+
+}
diff --git a/spring-cloud-vault-config-ldap/src/test/java/org/springframework/cloud/vault/config/ldap/VaultConfigLdapBootstrapConfigurationTests.java b/spring-cloud-vault-config-ldap/src/test/java/org/springframework/cloud/vault/config/ldap/VaultConfigLdapBootstrapConfigurationTests.java
new file mode 100644
index 00000000..91c0deac
--- /dev/null
+++ b/spring-cloud-vault-config-ldap/src/test/java/org/springframework/cloud/vault/config/ldap/VaultConfigLdapBootstrapConfigurationTests.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2016-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.vault.config.ldap;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.cloud.vault.config.SecretBackendMetadata;
+import org.springframework.cloud.vault.config.ldap.VaultConfigLdapBootstrapConfiguration.LdapSecretBackendMetadataFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link VaultConfigLdapBootstrapConfiguration}.
+ *
+ * @author Drew Mullen
+ */
+public class VaultConfigLdapBootstrapConfigurationTests {
+
+ private LdapSecretBackendMetadataFactory factory = new LdapSecretBackendMetadataFactory();
+
+ private VaultLdapProperties properties = new VaultLdapProperties();
+
+ @Test
+ public void shouldCreateDynamicRoleMetadata() {
+
+ this.properties.setEnabled(true);
+ this.properties.setRole("my-role");
+ this.properties.setStaticRole(false);
+
+ SecretBackendMetadata metadata = this.factory.createMetadata(this.properties);
+
+ assertThat(metadata.getName()).isEqualTo("ldap with Role my-role");
+ assertThat(metadata.getPath()).isEqualTo("ldap/creds/my-role");
+ assertThat(metadata.getVariables()).containsEntry("backend", "ldap").containsEntry("key", "creds/my-role");
+ }
+
+ @Test
+ public void shouldCreateStaticRoleMetadata() {
+
+ this.properties.setEnabled(true);
+ this.properties.setRole("my-static-role");
+ this.properties.setStaticRole(true);
+
+ SecretBackendMetadata metadata = this.factory.createMetadata(this.properties);
+
+ assertThat(metadata.getName()).isEqualTo("ldap with Role my-static-role");
+ assertThat(metadata.getPath()).isEqualTo("ldap/static-cred/my-static-role");
+ assertThat(metadata.getVariables()).containsEntry("backend", "ldap")
+ .containsEntry("key", "static-cred/my-static-role");
+ }
+
+ @Test
+ public void shouldCreateMetadataWithCustomBackend() {
+
+ this.properties.setEnabled(true);
+ this.properties.setRole("my-role");
+ this.properties.setBackend("custom-ldap");
+
+ SecretBackendMetadata metadata = this.factory.createMetadata(this.properties);
+
+ assertThat(metadata.getName()).isEqualTo("custom-ldap with Role my-role");
+ assertThat(metadata.getPath()).isEqualTo("custom-ldap/creds/my-role");
+ assertThat(metadata.getVariables()).containsEntry("backend", "custom-ldap")
+ .containsEntry("key", "creds/my-role");
+ }
+
+ @Test
+ public void shouldTransformProperties() {
+
+ this.properties.setEnabled(true);
+ this.properties.setRole("my-role");
+
+ SecretBackendMetadata metadata = this.factory.createMetadata(this.properties);
+
+ Map input = new HashMap<>();
+ input.put("username", "test-user");
+ input.put("password", "test-pass");
+
+ Map transformed = metadata.getPropertyTransformer().transformProperties(input);
+
+ assertThat(transformed).containsEntry("spring.ldap.username", "test-user")
+ .containsEntry("spring.ldap.password", "test-pass");
+ }
+
+ @Test
+ public void shouldTransformPropertiesWithCustomPropertyNames() {
+
+ this.properties.setEnabled(true);
+ this.properties.setRole("my-role");
+ this.properties.setUsernameProperty("custom.username");
+ this.properties.setPasswordProperty("custom.password");
+
+ SecretBackendMetadata metadata = this.factory.createMetadata(this.properties);
+
+ Map input = new HashMap<>();
+ input.put("username", "test-user");
+ input.put("password", "test-pass");
+
+ Map transformed = metadata.getPropertyTransformer().transformProperties(input);
+
+ assertThat(transformed).containsEntry("custom.username", "test-user")
+ .containsEntry("custom.password", "test-pass");
+ }
+
+ @Test
+ public void shouldSupportVaultLdapProperties() {
+
+ VaultLdapProperties properties = new VaultLdapProperties();
+
+ assertThat(this.factory.supports(properties)).isTrue();
+ }
+
+}