JDK-8192975 : Add an SPI to allow custom DNS resolution of LDAP endpoints
  • Type: CSR
  • Component: core-libs
  • Sub-Component: javax.naming
  • Priority: P4
  • Status: Closed
  • Resolution: Approved
  • Fix Versions: 12
  • Submitted: 2017-12-04
  • Updated: 2018-11-16
  • Resolved: 2018-11-07
Related Reports
CSR :  
Description
Summary
-------

Define a DNS provider service interface which can be used by
com.sun.jndi.LdapCtx to resolve the physical LDAP servers for a given domain.
A library or application can provide an implementation of this service in
order to route ldap requests to individual servers which don't resolve
correctly via traditional SRV lookups.


Problem
-------

The current resolution system does not offer enough flexibility to take certain
LDAP DNS configurations into account.

For example _ldap._tcp cannot be expected to point to an LDAPS endpoint,
RFC 2782 doesn't discuss the use of SSL, and there are various approaches to
its configuration. This means applications may need to tailor their server
resolution on a per domain basis.


Solution
-------

Add a new service (com.sun.jndi.ldap.LdapDnsProviderService) which can load
implementations of a new abstract class (javax.naming.spi.ldap.LdapDnsProvider)
via the ServiceLoader if the application has the (new) "ldapDnsProvider"
RuntimePermission or if no SecurityManager is installed. If no LdapDnsProvider
implementation is provided (or the provided implementations fail to resolve
an endpoint), a default provider which mimics current behaviour is used.


Specification
-------

This change is in the java.naming module. The javax.naming module will add an export for javax.naming.spi.ldap.

src/java.naming/share/classes/javax/naming/directory/InitialDirContext.java

[changing]

```
    /**
     * Constructs an initial DirContext using the supplied environment.
     * Environment properties are discussed in the
     * {@code javax.naming.InitialContext} class description.
     *
+    * <p> If the {@code java.naming.provider.url} property of the supplied
+    * environment consists of a URL (or a list of URLs) using the ldap
+    * protocol the resulting {@link javax.naming.ldap.LdapContext} will use
+    * an LDAP server resolved by the configured {@link
+    * javax.naming.ldap.spi.LdapDnsProvider LdapDnsProviders}:
+    * <ol>
+    * <li>If this is the first {@code InitialDirContext} created with a
+    *     {@code java.naming.provider.url} using the ldap protocol then the
+    *     {@linkplain java.util.ServiceLoader ServiceLoader} mechanism is
+    *     used to locate {@linkplain javax.naming.ldap.spi.LdapDnsProvider
+    *     LdapDnsProvider} implementations using the system class loader.
+    *     The order that providers are located is implementation specific
+    *     and an implementation is free to cache the located providers.
+    * <li>The {@code lookupEndpoints} method of each provider, if instantiated,
+    *     is invoked once with a combination of each of the URLs in the the
+    *     {@code java.naming.provider.url} property and the environment until
+    *     a provider returns non-empty or all providers have been exhausted.
+    *     If none of the
+    *     {@linkplain javax.naming.ldap.spi.LdapDnsProvider LdapDnsProviders}
+    *     return a non-empty
+    *     {@linkplain javax.naming.ldap.spi.LdapDnsProviderResult result} then
+    *     the implementation will make a best-effort attempt to determine an
+    *     endpoint. A
+    *     {@linkplain java.util.ServiceConfigurationError ServiceConfigurationError},
+    *     {@code Error} or {@code RuntimeException} thrown when loading or
+    *     calling an {@linkplain javax.naming.ldap.spi.LdapDnsProvider
+    *     LdapDnsProvider}, if encountered, will be propagated to the calling
+    *     thread.
+    * </ol>
+    *
     * <p> This constructor will not modify {@code environment}
     * or save a reference to it, but may save a clone.
     * Caller should not modify mutable keys and values in
     * {@code environment} after it has been passed to the constructor.
     *
     * @param environment
     *          environment used to create the initial DirContext.
     *          Null indicates an empty environment.
     *
     * @throws  NamingException if a naming exception is encountered
     */
    public InitialDirContext(Hashtable<?,?> environment)
        throws NamingException
    {
        super(environment);
    }
```

src/java.naming/share/classes/javax/naming/spi/ldap/LdapDnsProvider.java

[adding]
```
/**
 * Service-provider class for DNS lookups when performing LDAP operations.
 *
 * <p> An LDAP DNS provider is a concrete subclass of this class that
 * has a zero-argument constructor. LDAP DNS providers are located using the
 * ServiceLoader facility, as specified by
 * {@linkplain javax.naming.directory.InitialDirContext InitialDirectContext}.
 *
 * The
 * {@link java.util.ServiceLoader ServiceLoader} is used to create and register
 * implementations of {@code LdapDnsProvider}.
 *
 * <p> An LDAP DNS provider can be used in environments where the default
 * DNS resolution mechanism is not sufficient to accurately pinpoint the
 * correct LDAP servers needed to perform LDAP operations. For example, in an
 * environment containing a mix of {@code ldap} and {@code ldaps} servers
 * you may want the {@linkplain javax.naming.ldap.LdapContext LdapContext}
 * to query {@code ldaps} servers only.
 *
 * @since 12
 */
public abstract class LdapDnsProvider {

    // The {@code RuntimePermission("ldapDnsProvider")} is
    // necessary to subclass and instantiate the {@code LdapDnsProvider} class.
    private static final RuntimePermission DNSPROVIDER_PERMISSION =
            new RuntimePermission("ldapDnsProvider");

    /**
     * Creates a new instance of {@code LdapDnsProvider}.
     *
     * @throws SecurityException if a security manager is present and its
     *                           {@code checkPermission} method doesn't allow
     *                           the {@code RuntimePermission("ldapDnsProvider")}.
     */
    protected LdapDnsProvider() {
        this(checkPermission());
    }

    private LdapDnsProvider(Void unused) {
        // nothing to do.
    }

    private static Void checkPermission() {
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(DNSPROVIDER_PERMISSION);
        }
        return null;
    }

    /**
     * Lookup the endpoints and domain name for the given {@link Context}
     * {@link Context#PROVIDER_URL provider URL} and environment. The resolved
     * endpoints and domain name are returned as an
     * {@link LdapDnsProviderResult}.
     *
     * <p> An endpoint is a {@code String} representation of an LDAP URL which
     * points to an LDAP server to be used for LDAP operations. The syntax of
     * an LDAP URL is defined by <a href="http://www.ietf.org/rfc/rfc2255.txt">
     * <i>RFC&nbsp;2255: The LDAP URL Format</i></a>.
     *
     * @param url   The {@link Context} {@link Context#PROVIDER_URL provider URL}
     * @param env   The {@link Context} environment.
     *
     * @return  an {@link LdapDnsProviderResult} or empty {@code Optional}
     *          if the lookup fails.
     *
     * @throws NamingException      if the {@code url} is not valid or an error
     *                              occurred while performing the lookup.
     * @throws NullPointerException if either {@code url} or {@code env} are
     *                              {@code null}.
     */
    public abstract Optional<LdapDnsProviderResult> lookupEndpoints(
            String url, Hashtable<?,?> env) throws NamingException;

}

```

src/java.naming/share/classes/javax/naming/spi/ldap/LdapDnsProviderResult.java

[adding]
```
/**
 * The result of a DNS lookup for an LDAP URL.
 *
 * <p> This class is used by an {@link LdapDnsProvider} to return the result
 * of a DNS lookup for a given LDAP URL. The result consists of a domain name
 * and its associated ldap server endpoints.
 *
 * <p> A {@code null} {@code domainName} is equivalent to and represented
 * by an empty string.
 *
 * @since 12
 */
public final class LdapDnsProviderResult {

    private final String domainName;
    private final List<String> endpoints;

    /**
     * Construct an LdapDnsProviderResult consisting of a resolved domain name
     * and the ldap server endpoints that serve the domain.
     *
     * @param domainName    the resolved domain name; can be null.
     * @param endpoints     the possibly empty list of resolved ldap server
     *                      endpoints
     *
     * @throws NullPointerException   if {@code endpoints} contains {@code null}
     *                                elements.
     * @throws ClassCastException     if {@code endpoints} contains non-
     *                                {@code String} elements.
     */
    public LdapDnsProviderResult(String domainName, List<String> endpoints) {
        this.domainName = (domainName == null) ? "" : domainName;
        this.endpoints = List.copyOf(endpoints);
    }

    /**
     * Returns the domain name resolved from the ldap URL. This method returns
     * the empty string if the {@code LdapDnsProviderResult} is created with a
     * null domain name.
     *
     * @return  the resolved domain name
     */
    public String getDomainName() {
        return domainName;
    }

    /**
     * Returns the possibly empty list of individual server endpoints resolved
     * from the ldap URL.
     *
     * @return  a possibly empty unmodifiable {@link List} containing the
     *          resolved ldap server endpoints
     */
    public List<String> getEndpoints() {
        return endpoints;
    }

}

```

Comments
Re-approved updated request.
07-11-2018

So Daniel suggested that we move the lookupEndpoints signature to use Map<?, ?> instead of Hashtable. (this can be accommodated with a pretty innocuous change in ServiceLocator) http://cr.openjdk.java.net/~robm/8160768/webrev.09/ Adding the new webrev for completeness.
06-11-2018

Moving to Approved.
25-10-2018

Apologies [~darcy]. Should work now.
17-10-2018

[~robm], I get a 404 on the .08 version of the webrev, but the .07 URL from last December resolves.
16-10-2018

Yes, the data structure used to represent a Context environment in JNDI is Hashtable. There is little point in converting it for use with this API alone. Also, Mandy & Alan have looked at the module changes. Updated webrev: http://cr.openjdk.java.net/~robm/8160768/webrev.08/
16-10-2018

Marking this request as pended to reflect discussions about fixVerions and API usage. Please refinalize after the module-usage review is complete and the API updates are made.
13-12-2017

Rob, thanks for the explanation on LdapDnsProviderResult. Please have Mandy or Alan review the module-usage aspects of this change. Looking over javax.naming, although the package was added in 1.3, it often uses collection classes like Hashtable that predate the Collections framework added in 1.2. In particular, "Hashtable" is used rather than "Map." I assume lookupEndpoints uses Hastable for consistency with the existing part of the API. Is it intentional that LdapDnsProviderResult uses List rather than Enumeration?
13-12-2017

The intention of the abstract class is to allow for a permission check similar to the pattern used by System.LoggerFinder. See: http://cr.openjdk.java.net/~robm/8160768/webrev.07/ http://mail.openjdk.java.net/pipermail/core-libs-dev/2017-December/050342.html
12-12-2017

Is there a reason LdapDnsProviderResult an abstract class as opposed to an interface? I'd prefer to see this change targeted to JDK 11 rather than 10 unless there is a strong reason to get it into 10.
11-12-2017

looks good to me.
08-12-2017

Note: some discussion may be required as to the location of the new classes (LdapDnsProvider.java & LdapDnsProviderResult.java) Currently there exist two directories: javax/naming/spi javax/naming/ldap The former contains SPI classes related to the naming interface in general and is not specific to LDAP. The latter contains classes specific to LDAP but unrelated to the SPI. I thought it made sense to add an LDAP specific folder to the "spi" folder for these classes.
04-12-2017