Drift in behavior between LdapAutoConfiguration and EmbeddedLdapAutoConfiguration.

LdapAutoConfiguration populates the base from properties in LdapContextSource and the EmbeddedLdapAutoConfiguration doesn't which changes how queries, etc.. switching between the two.

I don't mind submitting a PR but wanted confirmation of actual problem or expected behavior.

Tested on 2.4.0-M2

Thanks Mark

Comment From: snicoll

@mdiskin sorry, I am not sure I follow. The LdapContextSource is built from properties and so is the embedded auto-configuration as far as I can see.

Comment From: mdiskin

In the Non-Embedded Autocomplete the there is a property (single string). In the Embedded there is the ability to pass into unboundid an array of dn's but the corresponding LdapContextSource bean create doesn't set this base.

propertyMapper.from(properties.getBase()).to(source::setBase);

So if you are using the embedded for unit/integration testing and then Non-Embedded you have to add additional logic that fully qualifies each LdapTempate search e.g. add "dc=spring,dc=org".

I was playing with options in a forked version and the unboundid support for multiple DNs doesn't seem well supported given it only takes a single base so I wasn't sure whether to remove that array/functionality for the embed-only option. One thought would be for embedded to add another property 'base' (in addition to the dn array) ideally we could derive that from the other but that would not be very easy to determine.

Comment From: snicoll

We discussed this one and I can see the problem now. It's largely due to the fact the configuration between the embedded and the non embedded case are duplicated. Rather than creating a completely new LdapContextSource we should customise the one created by the regular auto-configuration (via a LdapContextSourceCustomizer perhaps?).

The fact that there is an .embedded subnamespace with competing configuration keys make it a bit more complex to decide what should be taken into account so perhaps we should refine the structure of those. We've flagged this for resolution in 2.2.x but we may move that if the changes are too involved.

Comment From: mdiskin

That would be awsome but sounds involved refactoring and not something for my first project commit. Let me know if I can help out maybe with testing/snapshot validation.

Additionally with this approach can you also look at the autoconfigure support for BaseLdapPathBeanPostProcessor and BaseLdapPathAware? I tried that to get around the above issue but again not something the embedded ldap supports out of the box.

Also, would be good to upgrade the unboundid versions to the 5.x at some point.

Comment From: philwebb

We want to rework EmbeddedLdapContextConfiguration, possibly removing it entirely. Although we do consider this a bug, such a fix would be a little risky for 2.3.x so we're going to look at it in 2.6.x.

Comment From: mdiskin

Makes sense. I can help test snapshot or milestone releases. Also, UnboundID 6.0.0 has since been released and may be good to include in 2.6.x changes

Comment From: mdiskin

Was hoping to get confirmation that will be in the 2.6 initial release (I'm working against the snapshot), but if not I'll start work on a stopgap measure as it's blocking some efforts internally.

Comment From: wilkinsona

An upgrade of UnboundID is unlikely at this time as Spring Security is still using 4.x. If you'd like to see an upgrade to 6.x, please open a Spring Security issue and we can take things from there.

Comment From: mdiskin

@wilkinsona I'll open that upgrade request (more of a nice to have) but the bugfix to align up the embedded and external ldap is the real blocker for me

Comment From: micheljung

UPDATE: This doesn't work, because base needs to be set before afterPropertiesSet() is called

~Workaround in Kotlin (FQCNs for clarification)~:

  @Autowired
  fun populateLdapContextSourceBaseProperty(
    ldapContextSource: org.springframework.ldap.core.support.LdapContextSource,
    ldapProperties: org.springframework.boot.autoconfigure.ldap.LdapProperties,
    inMemoryDirectoryServer: Optional<com.unboundid.ldap.listener.InMemoryDirectoryServer>,
  ) {
    if (!inMemoryDirectoryServer.isPresent) {
      return
    }

    if (ldapContextSource.baseLdapName != org.springframework.ldap.support.LdapUtils.emptyLdapName()) {
      logger.warn("Not setting 'base' property of LdapContextSource as it's already set: ${ldapContextSource.baseLdapName}")
      return
    }

    logger.debug("Setting 'base' property of LdapContextSource to '${ldapContextSource.baseLdapName}'" +
        " as a workaround for https://github.com/spring-projects/spring-boot/issues/23030")
    ldapContextSource.setBase(ldapProperties.base)
  }

Comment From: micheljung

This one works (Kotlin):

package com.example

import org.springframework.boot.autoconfigure.AutoConfigureBefore
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.ldap.LdapProperties
import org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration
import org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.DependsOn
import org.springframework.core.env.Environment
import org.springframework.ldap.core.ContextSource
import org.springframework.ldap.core.support.LdapContextSource

@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(EmbeddedLdapAutoConfiguration::class)
@EnableConfigurationProperties(LdapProperties::class, EmbeddedLdapProperties::class)
class EmbeddedLdapAutoConfiguration {

  // Workaround for https://github.com/spring-projects/spring-boot/issues/23030
  @Configuration(proxyBeanMethods = false)
  @ConditionalOnClass(ContextSource::class)
  internal class EmbeddedLdapContextConfiguration {
    @Bean
    @DependsOn("directoryServer")
    @ConditionalOnMissingBean
    fun ldapContextSource(
      environment: Environment,
      properties: LdapProperties,
      embeddedProperties: EmbeddedLdapProperties,
    ): LdapContextSource {
      val source = LdapContextSource()
      if (!embeddedProperties.credential.username.isNullOrEmpty() && !embeddedProperties.credential.password.isNullOrEmpty()) {
        source.userDn = embeddedProperties.credential.username
        source.password = embeddedProperties.credential.password
      }
      source.urls = properties.determineUrls(environment)
      source.setBase(properties.base)
      return source
    }
  }
}

spring.factories:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.EmbeddedLdapAutoConfiguration

Comment From: mbhave

We decided to fix this by populating the base for the embedded context. In the future, we might want to revisit this to consider the customizer option.