8160768: Add capability to custom resolve host/domain names within the default JNDI LDAP provider
authorrobm
Mon, 12 Nov 2018 08:33:59 -0800
changeset 52493 a609d549992a
parent 52492 c22997db2a22
child 52494 710e5a66a64e
8160768: Add capability to custom resolve host/domain names within the default JNDI LDAP provider Reviewed-by: alanb, dfuchs, chegar, mchung, vtewari
src/java.naming/share/classes/com/sun/jndi/ldap/DefaultLdapDnsProvider.java
src/java.naming/share/classes/com/sun/jndi/ldap/LdapCtxFactory.java
src/java.naming/share/classes/com/sun/jndi/ldap/LdapDnsProviderService.java
src/java.naming/share/classes/com/sun/jndi/ldap/ServiceLocator.java
src/java.naming/share/classes/javax/naming/directory/InitialDirContext.java
src/java.naming/share/classes/javax/naming/ldap/spi/LdapDnsProvider.java
src/java.naming/share/classes/javax/naming/ldap/spi/LdapDnsProviderResult.java
src/java.naming/share/classes/module-info.java
test/jdk/com/sun/jndi/ldap/LdapDnsProviderTest.java
test/jdk/com/sun/jndi/ldap/dnsprovider/TestDnsProvider.java
test/jdk/com/sun/security/auth/module/LdapLoginModule/CheckConfigs.policy
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.naming/share/classes/com/sun/jndi/ldap/DefaultLdapDnsProvider.java	Mon Nov 12 08:33:59 2018 -0800
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.jndi.ldap;
+
+import javax.naming.NamingException;
+import javax.naming.ldap.spi.LdapDnsProvider;
+import javax.naming.ldap.spi.LdapDnsProviderResult;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public class DefaultLdapDnsProvider {
+
+    public Optional<LdapDnsProviderResult> lookupEndpoints(String url,
+                                                           Map<?,?> env)
+            throws NamingException
+    {
+        if (url == null || env == null) {
+            throw new NullPointerException();
+        }
+
+        String domainName;
+        List<String> endpoints = new ArrayList<>();
+        LdapURL ldapUrl = new LdapURL(url);
+        String dn = ldapUrl.getDN();
+        String host = ldapUrl.getHost();
+        int port = ldapUrl.getPort();
+        String[] hostports;
+
+        // handle a URL with no hostport (ldap:/// or ldaps:///)
+        // locate the LDAP service using the URL's distinguished name
+        if (host == null
+                && port == -1
+                && dn != null
+                && (domainName = ServiceLocator.mapDnToDomainName(dn)) != null
+                && (hostports = ServiceLocator.getLdapService(domainName, env)) != null) {
+            // Generate new URLs that include the discovered hostports.
+            // Reuse the original URL scheme.
+            String scheme = ldapUrl.getScheme() + "://";
+            String query = ldapUrl.getQuery();
+            String urlSuffix = ldapUrl.getPath() + (query != null ? query : "");
+            for (String hostPort : hostports) {
+                // the hostports come from the DNS SRV records
+                // we assume the SRV record is scheme aware
+                endpoints.add(scheme + hostPort + urlSuffix);
+            }
+        } else {
+            // we don't have enough information to set the domain name
+            // correctly
+            domainName = "";
+            endpoints.add(url);
+        }
+
+        LdapDnsProviderResult res = new LdapDnsProviderResult(domainName, endpoints);
+        if (res.getEndpoints().size() == 0 && res.getDomainName().isEmpty()) {
+            return Optional.empty();
+        } else {
+            return Optional.of(res);
+        }
+    }
+
+}
--- a/src/java.naming/share/classes/com/sun/jndi/ldap/LdapCtxFactory.java	Mon Nov 12 16:40:25 2018 +0100
+++ b/src/java.naming/share/classes/com/sun/jndi/ldap/LdapCtxFactory.java	Mon Nov 12 08:33:59 2018 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1999, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -31,6 +31,7 @@
 
 import javax.naming.*;
 import javax.naming.directory.*;
+import javax.naming.ldap.spi.LdapDnsProviderResult;
 import javax.naming.spi.ObjectFactory;
 import javax.naming.spi.InitialContextFactory;
 import javax.naming.ldap.Control;
@@ -158,41 +159,73 @@
     }
 
     private static DirContext getUsingURL(String url, Hashtable<?,?> env)
-            throws NamingException {
-        DirContext ctx = null;
-        LdapURL ldapUrl = new LdapURL(url);
-        String dn = ldapUrl.getDN();
-        String host = ldapUrl.getHost();
-        int port = ldapUrl.getPort();
-        String[] hostports;
-        String domainName = null;
+            throws NamingException
+    {
+        try {
+            LdapDnsProviderResult r =
+                LdapDnsProviderService.getInstance().lookupEndpoints(url, env);
+            LdapCtx ctx;
+            NamingException lastException = null;
 
-        // handle a URL with no hostport (ldap:/// or ldaps:///)
-        // locate the LDAP service using the URL's distinguished name
-        if (host == null &&
-            port == -1 &&
-            dn != null &&
-            (domainName = ServiceLocator.mapDnToDomainName(dn)) != null &&
-            (hostports = ServiceLocator.getLdapService(domainName, env))
-                != null) {
-            // Generate new URLs that include the discovered hostports.
-            // Reuse the original URL scheme.
-            String scheme = ldapUrl.getScheme() + "://";
-            String[] newUrls = new String[hostports.length];
-            String query = ldapUrl.getQuery();
-            String urlSuffix = ldapUrl.getPath() + (query != null ? query : "");
-            for (int i = 0; i < hostports.length; i++) {
-                newUrls[i] = scheme + hostports[i] + urlSuffix;
+            /*
+             * Prior to this change we had been assuming that the url.getDN()
+             * should be converted to a domain name via
+             * ServiceLocator.mapDnToDomainName(url.getDN())
+             *
+             * However this is incorrect as we can't assume that the supplied
+             * url.getDN() is the same as the dns domain for the directory
+             * server.
+             *
+             * This means that we depend on the dnsProvider to return both
+             * the list of urls of individual hosts from which we attempt to
+             * create an LdapCtx from *AND* the domain name that they serve
+             *
+             * In order to do this the dnsProvider must return an
+             * {@link LdapDnsProviderResult}.
+             *
+             */
+            for (String u : r.getEndpoints()) {
+                try {
+                    ctx = getLdapCtxFromUrl(
+                            r.getDomainName(), url, new LdapURL(u), env);
+                    return ctx;
+                } catch (NamingException e) {
+                    // try the next element
+                    lastException = e;
+                }
             }
-            ctx = getUsingURLs(newUrls, env);
-            // Associate the derived domain name with the context
-            ((LdapCtx)ctx).setDomainName(domainName);
+
+            if (lastException != null) {
+                throw lastException;
+            }
 
-        } else {
-            ctx = new LdapCtx(dn, host, port, env, ldapUrl.useSsl());
-            // Record the URL that created the context
-            ((LdapCtx)ctx).setProviderUrl(url);
+            // lookupEndpoints returned an LdapDnsProviderResult with an empty
+            // list of endpoints
+            throw new NamingException("Could not resolve a valid ldap host");
+        } catch (NamingException e) {
+            // lookupEndpoints(url, env) may throw a NamingException, which
+            // there is no need to wrap.
+            throw e;
+        } catch (Exception e) {
+            NamingException ex = new NamingException();
+            ex.setRootCause(e);
+            throw ex;
         }
+    }
+
+    private static LdapCtx getLdapCtxFromUrl(String domain,
+                                             String url,
+                                             LdapURL u,
+                                             Hashtable<?,?> env)
+            throws NamingException
+    {
+        String dn = u.getDN();
+        String host = u.getHost();
+        int port = u.getPort();
+        LdapCtx ctx = new LdapCtx(dn, host, port, env, u.useSsl());
+        ctx.setDomainName(domain);
+        // Record the URL that created the context
+        ctx.setProviderUrl(url);
         return ctx;
     }
 
@@ -202,19 +235,17 @@
      * Not pretty, but potentially more informative than returning null.
      */
     private static DirContext getUsingURLs(String[] urls, Hashtable<?,?> env)
-            throws NamingException {
-        NamingException ne = null;
-        DirContext ctx = null;
-        for (int i = 0; i < urls.length; i++) {
+            throws NamingException
+    {
+        NamingException ex = null;
+        for (String u : urls) {
             try {
-                return getUsingURL(urls[i], env);
-            } catch (AuthenticationException e) {
-                throw e;
+                return getUsingURL(u, env);
             } catch (NamingException e) {
-                ne = e;
+                ex = e;
             }
         }
-        throw ne;
+        throw ex;
     }
 
     /**
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.naming/share/classes/com/sun/jndi/ldap/LdapDnsProviderService.java	Mon Nov 12 08:33:59 2018 -0800
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.jndi.ldap;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.*;
+import javax.naming.NamingException;
+import javax.naming.ldap.spi.LdapDnsProvider;
+import javax.naming.ldap.spi.LdapDnsProviderResult;
+import sun.security.util.SecurityConstants;
+
+/**
+ * The {@code LdapDnsProviderService} is responsible for creating and providing
+ * access to the registered {@code LdapDnsProvider}s. The {@link ServiceLoader}
+ * is used to find and register any implementations of {@link LdapDnsProvider}.
+ */
+final class LdapDnsProviderService {
+
+    private static volatile LdapDnsProviderService service;
+    private static final Object LOCK = new int[0];
+    private final ServiceLoader<LdapDnsProvider> providers;
+
+    /**
+     * Creates a new instance of LdapDnsProviderService
+     */
+    private LdapDnsProviderService() {
+        SecurityManager sm = System.getSecurityManager();
+        if (sm == null) {
+            providers = ServiceLoader.load(
+                    LdapDnsProvider.class,
+                    ClassLoader.getSystemClassLoader());
+        } else {
+            final PrivilegedAction<ServiceLoader<LdapDnsProvider>> pa =
+                    () -> ServiceLoader.load(
+                            LdapDnsProvider.class,
+                            ClassLoader.getSystemClassLoader());
+
+            providers = AccessController.doPrivileged(
+                pa,
+                null,
+                new RuntimePermission("ldapDnsProvider"),
+                SecurityConstants.GET_CLASSLOADER_PERMISSION);
+        }
+    }
+
+    /**
+     * Retrieve the singleton static instance of LdapDnsProviderService.
+     */
+    static LdapDnsProviderService getInstance() {
+        if (service != null) return service;
+        synchronized(LOCK) {
+            if (service != null) return service;
+            service = new LdapDnsProviderService();
+        }
+        return service;
+    }
+
+    /**
+     * Retrieve result from the first provider that successfully resolves
+     * the endpoints. If no results are found when calling installed
+     * subclasses of {@code LdapDnsProvider} then this method will fall back
+     * to the {@code DefaultLdapDnsProvider}.
+     *
+     * @throws NamingException if the {@code url} in not valid or an error
+     *                         occurred while performing the lookup.
+     */
+    LdapDnsProviderResult lookupEndpoints(String url, Hashtable<?,?> env)
+        throws NamingException
+    {
+        Iterator<LdapDnsProvider> iterator = providers.iterator();
+        Hashtable<?, ?> envCopy = new Hashtable<>(env);
+        LdapDnsProviderResult result = null;
+
+        while (result == null && iterator.hasNext()) {
+            result = iterator.next().lookupEndpoints(url, envCopy)
+                    .filter(r -> r.getEndpoints().size() > 0)
+                    .orElse(null);
+        }
+
+        if (result == null) {
+            return new DefaultLdapDnsProvider().lookupEndpoints(url, env)
+                .orElse(new LdapDnsProviderResult("", List.of()));
+        }
+        return result;
+    }
+
+}
--- a/src/java.naming/share/classes/com/sun/jndi/ldap/ServiceLocator.java	Mon Nov 12 16:40:25 2018 +0100
+++ b/src/java.naming/share/classes/com/sun/jndi/ldap/ServiceLocator.java	Mon Nov 12 08:33:59 2018 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2002, 2011, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2002, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,11 +25,7 @@
 
 package com.sun.jndi.ldap;
 
-import java.util.Arrays;
-import java.util.Hashtable;
-import java.util.Random;
-import java.util.StringTokenizer;
-import java.util.List;
+import java.util.*;
 
 import javax.naming.*;
 import javax.naming.directory.*;
@@ -113,6 +109,23 @@
      * @return An ordered list of hostports for the LDAP service or null if
      *         the service has not been located.
      */
+    static String[] getLdapService(String domainName, Map<?,?> environment) {
+        if (environment instanceof Hashtable) {
+            return getLdapService(domainName, (Hashtable)environment);
+        }
+        return getLdapService(domainName, new Hashtable<>(environment));
+    }
+
+    /**
+     * Locates the LDAP service for a given domain.
+     * Queries DNS for a list of LDAP Service Location Records (SRV) for a
+     * given domain name.
+     *
+     * @param domainName A string domain name.
+     * @param environment The possibly null environment of the context.
+     * @return An ordered list of hostports for the LDAP service or null if
+     *         the service has not been located.
+     */
     static String[] getLdapService(String domainName, Hashtable<?,?> environment) {
 
         if (domainName == null || domainName.length() == 0) {
--- a/src/java.naming/share/classes/javax/naming/directory/InitialDirContext.java	Mon Nov 12 16:40:25 2018 +0100
+++ b/src/java.naming/share/classes/javax/naming/directory/InitialDirContext.java	Mon Nov 12 08:33:59 2018 -0800
@@ -27,7 +27,6 @@
 package javax.naming.directory;
 
 import java.util.Hashtable;
-import javax.naming.spi.NamingManager;
 import javax.naming.*;
 
 /**
@@ -84,6 +83,36 @@
      * 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.naming/share/classes/javax/naming/ldap/spi/LdapDnsProvider.java	Mon Nov 12 08:33:59 2018 -0800
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package javax.naming.ldap.spi;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * 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, Map<?,?> env) throws NamingException;
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.naming/share/classes/javax/naming/ldap/spi/LdapDnsProviderResult.java	Mon Nov 12 08:33:59 2018 -0800
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package javax.naming.ldap.spi;
+
+import java.util.List;
+
+/**
+ * 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;
+    }
+
+}
--- a/src/java.naming/share/classes/module-info.java	Mon Nov 12 16:40:25 2018 +0100
+++ b/src/java.naming/share/classes/module-info.java	Mon Nov 12 08:33:59 2018 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -26,6 +26,10 @@
 /**
  * Defines the Java Naming and Directory Interface (JNDI) API.
  *
+ * @provides javax.naming.ldap.spi.LdapDnsProvider
+ *
+ * @uses javax.naming.ldap.spi.LdapDnsProvider
+ *
  * @moduleGraph
  * @since 9
  */
@@ -37,6 +41,7 @@
     exports javax.naming.event;
     exports javax.naming.ldap;
     exports javax.naming.spi;
+    exports javax.naming.ldap.spi;
 
     exports com.sun.jndi.toolkit.ctx to
         jdk.naming.dns;
@@ -46,6 +51,7 @@
 
     uses javax.naming.ldap.StartTlsResponse;
     uses javax.naming.spi.InitialContextFactory;
+    uses javax.naming.ldap.spi.LdapDnsProvider;
 
     provides java.security.Provider with
         sun.security.provider.certpath.ldap.JdkLDAP;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/com/sun/jndi/ldap/LdapDnsProviderTest.java	Mon Nov 12 08:33:59 2018 -0800
@@ -0,0 +1,231 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.Permission;
+import java.util.Hashtable;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.directory.SearchControls;
+
+/**
+ * @test
+ * @bug 8160768
+ * @summary ctx provider tests for ldap
+ * @modules java.naming/com.sun.jndi.ldap
+ * @compile dnsprovider/TestDnsProvider.java
+ * @run main/othervm LdapDnsProviderTest
+ * @run main/othervm LdapDnsProviderTest nosm
+ * @run main/othervm LdapDnsProviderTest smnodns
+ * @run main/othervm LdapDnsProviderTest smdns
+ * @run main/othervm LdapDnsProviderTest nosmbaddns
+ */
+
+class DNSSecurityManager extends SecurityManager {
+
+
+
+    /* run main/othervm LdapDnsProviderTest
+
+     * run main/othervm LdapDnsProviderTest nosm
+     * run main/othervm LdapDnsProviderTest smnodns
+     * run main/othervm LdapDnsProviderTest smdns
+     * run main/othervm LdapDnsProviderTest nosmbaddns
+     */
+
+    private boolean dnsProvider = false;
+
+    public void setAllowDnsProvider(boolean allow) {
+        dnsProvider = allow;
+    }
+
+    @Override
+    public void checkPermission(Permission p) {
+        if (p.getName().equals("ldapDnsProvider") && !dnsProvider) {
+            throw new SecurityException(p.getName());
+        }
+    }
+}
+
+class ProviderTest implements Callable<Boolean> {
+
+    private final String url;
+    private final String expected;
+    private final Hashtable<String, String> env = new Hashtable<>(11);
+
+    public ProviderTest(String url, String expected) {
+        this.url = url;
+        this.expected = expected;
+        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
+    }
+
+    boolean shutItDown(InitialContext ctx) {
+        try {
+            if (ctx != null) ctx.close();
+            return true;
+        } catch (NamingException ex) {
+            return false;
+        }
+    }
+
+    public Boolean call() {
+        boolean passed;
+        InitialContext ctx = null;
+
+        if (url != null) {
+            env.put(Context.PROVIDER_URL, url);
+        }
+
+        try {
+            ctx = new InitialDirContext(env);
+            SearchControls scl = new SearchControls();
+            scl.setSearchScope(SearchControls.SUBTREE_SCOPE);
+            ((InitialDirContext)ctx).search(
+                    "ou=People,o=Test", "(objectClass=*)", scl);
+            throw new RuntimeException("Search should not complete");
+        } catch (NamingException e) {
+            e.printStackTrace();
+            passed = e.toString().contains(expected);
+        } finally {
+            shutItDown(ctx);
+        }
+        return passed;
+    }
+}
+
+public class LdapDnsProviderTest {
+
+    private static final String TEST_CLASSES =
+            System.getProperty("test.classes", ".");
+
+    public static void writeFile(String content, File dstFile)
+        throws IOException
+    {
+        try (FileOutputStream dst = new FileOutputStream(dstFile)) {
+            byte[] buf = content.getBytes();
+            dst.write(buf, 0, buf.length);
+        }
+    }
+
+    public static void installServiceConfigurationFile(String content) {
+        String filename = "javax.naming.ldap.spi.LdapDnsProvider";
+
+        File dstDir = new File(TEST_CLASSES, "META-INF/services");
+        if (!dstDir.exists()) {
+            if (!dstDir.mkdirs()) {
+                throw new RuntimeException(
+                    "could not create META-INF/services directory " + dstDir);
+            }
+        }
+        File dstFile = new File(dstDir, filename);
+
+        try {
+            writeFile(content, dstFile);
+        } catch (IOException e) {
+            throw new RuntimeException("could not install " + dstFile, e);
+        }
+    }
+
+    public static void main(String[] args) throws Exception {
+        if (args.length > 0 && args[0].equals("nosm")) {
+            // no security manager, serviceloader
+            installServiceConfigurationFile("dnsprovider.TestDnsProvider");
+            runTest("ldap:///dc=example,dc=com", "yupyupyup:389");
+        } else if (args.length > 0 && args[0].equals("smnodns")) {
+            // security manager & serviceloader
+            installServiceConfigurationFile("dnsprovider.TestDnsProvider");
+            // install security manager
+            System.setSecurityManager(new DNSSecurityManager());
+            runTest("ldap:///dc=example,dc=com", "ServiceConfigurationError");
+        } else if (args.length > 0 && args[0].equals("smdns")) {
+            // security manager & serviceloader
+            DNSSecurityManager sm = new DNSSecurityManager();
+            installServiceConfigurationFile("dnsprovider.TestDnsProvider");
+            // install security manager
+            System.setSecurityManager(sm);
+            sm.setAllowDnsProvider(true);
+            runTest("ldap:///dc=example,dc=com", "yupyupyup:389");
+        } else if (args.length > 0 && args[0].equals("nosmbaddns")) {
+            // no security manager, no serviceloader
+            // DefaultLdapDnsProvider
+            installServiceConfigurationFile("dnsprovider.MissingDnsProvider");
+            // no SecurityManager
+            runTest("ldap:///dc=example,dc=com", "not found");
+        } else {
+            // no security manager, no serviceloader
+            // DefaultLdapDnsProvider
+            System.err.println("TEST_CLASSES:");
+            System.err.println(TEST_CLASSES);
+            File f = new File(
+                    TEST_CLASSES, "META-INF/services/javax.naming.ldap.spi.LdapDnsProvider");
+            if (f.exists()) {
+                f.delete();
+            }
+
+            // no SecurityManager
+            runTest("ldap:///dc=example,dc=com", "localhost:389");
+            runTest("ldap://localhost/dc=example,dc=com", "localhost:389");
+            runTest("ldap://localhost:111/dc=example,dc=com", "localhost:111");
+            runTest("ldaps://localhost:111/dc=example,dc=com", "localhost:111");
+            runTest("ldaps://localhost/dc=example,dc=com", "localhost:636");
+            runTest(null, "localhost:389");
+            runTest("", "ConfigurationException");
+        }
+    }
+
+    private static void runTest(String url, String expected) {
+        FutureTask<Boolean> future =
+            new FutureTask<>(
+                    new ProviderTest(url, expected));
+        new Thread(future).start();
+
+        System.err.println("Testing: " + url + ", " + expected);
+        while (!future.isDone()) {
+            try {
+                if (!future.get()) {
+                    System.err.println("Test failed");
+                    throw new RuntimeException(
+                            "Test failed, ProviderTest returned false");
+                }
+            } catch (Exception e) {
+                if (!e.toString().contains(expected)) {
+                    System.err.println("Test failed");
+                    throw new RuntimeException(
+                            "Test failed, unexpected result");
+                }
+            }
+        }
+        System.err.println("Test passed");
+    }
+
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/com/sun/jndi/ldap/dnsprovider/TestDnsProvider.java	Mon Nov 12 08:33:59 2018 -0800
@@ -0,0 +1,20 @@
+package dnsprovider;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import javax.naming.ldap.spi.LdapDnsProvider;
+import javax.naming.ldap.spi.LdapDnsProviderResult;
+
+public class TestDnsProvider extends LdapDnsProvider {
+    @Override
+    public Optional<LdapDnsProviderResult> lookupEndpoints(String url,
+                                                           Map<?, ?> env)
+    {
+        List<String> endpoints = new ArrayList<>();
+        endpoints.add("ldap://yupyupyup:389");
+        return Optional.of(
+                new LdapDnsProviderResult("test.com", endpoints));
+    }
+}
--- a/test/jdk/com/sun/security/auth/module/LdapLoginModule/CheckConfigs.policy	Mon Nov 12 16:40:25 2018 +0100
+++ b/test/jdk/com/sun/security/auth/module/LdapLoginModule/CheckConfigs.policy	Mon Nov 12 08:33:59 2018 -0800
@@ -6,4 +6,5 @@
     //permission java.net.SocketPermission "*:636", "connect";
     //permission javax.security.auth.AuthPermission "modifyPrincipals";
     permission java.lang.RuntimePermission "accessClassInPackage.com.sun.jndi.ldap";
+    permission java.lang.RuntimePermission "ldapDnsProvider";
 };