From 4be9142998e76614146f88c825179264573bcf7c Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@enterprisedb.com>
Date: Fri, 16 Nov 2018 14:32:00 +1300
Subject: [PATCH] Add DNS SRV support for LDAP server discovery.

LDAP servers can be advertised on a network by registering DNS SRV
records for _ldap._tcp.<domain>.  The OpenLDAP command-line tools
know how to find servers via those records, if no server name is
provided by the user.  Teach PostgreSQL to follow the same convention
using non-standard extensions provided by OpenLDAP, where available.

Author: Thomas Munro
Reviewed-by:
Discussion: https://postgr.es/m/CAEepm=2hAnSfhdsd6vXsM6VZVN0br-FbAZ-O+Swk18S5HkCP=A@mail.gmail.com
---
 doc/src/sgml/client-auth.sgml |  19 ++++++
 src/backend/libpq/auth.c      | 113 +++++++++++++++++++++++++---------
 src/backend/libpq/hba.c       |   2 +
 3 files changed, 105 insertions(+), 29 deletions(-)

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index c2114021c3..f9e7416c79 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1671,6 +1671,16 @@ ldap[s]://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<rep
     <literal>ldapsearchattribute=uid</literal>.
    </para>
 
+   <para>
+     If <productname>PostgreSQL</productname> was compiled with OpenLDAP as
+     the LDAP client library, the <literal>ldapserver</literal> setting may be
+     omitted.  In that case, the hostname and port are looked up via DNS
+     service records.  The "SRV" records for the service
+     <literal>_ldap._tcp.domain</literal> are requested, where
+     <literal>domain</literal> is extracted from <literal>basedn</literal>.
+     This follows a convention used by OpenLDAP command-line tools.
+   </para>
+
    <para>
     Here is an example for a simple-bind LDAP configuration:
 <programlisting>
@@ -1716,6 +1726,15 @@ host ... ldap ldapserver=ldap.example.net ldapbasedn="dc=example, dc=net" ldapse
 </programlisting>
    </para>
 
+   <para>
+    Here is an example for a search+bind configuration that uses DNS SRV
+    discovery to find the hostname and port for the LDAP service using the
+    domain name <literal>example.net</literal>":
+<programlisting>
+host ... ldap ldapurl="ldap:///ou=people,dc=example,dc=net?cn"
+</programlisting>
+   </para>
+
    <tip>
     <para>
      Since LDAP often uses commas and spaces to separate the different
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 4f9d697d6d..cb62540c9f 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2356,37 +2356,81 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 		char	   *uris = NULL;
 
 		/*
-		 * We have a space-separated list of hostnames.  Convert it
-		 * to a space-separated list of URIs.
+		 * If the user provided no hostname, we can ask OpenLDAP to try to
+		 * find one by extracting a domain name from the base DN and then
+		 * using a DSN SRV record for _ldap._tcp.<domain>.  If one or more
+		 * such SRV records have been defined, we can get a hostname and
+		 * port.  The same convention is used by the OpenLDAP command line
+		 * tools.
 		 */
-		do
+		if (!hostnames || hostnames[0] == '\0')
 		{
-			char	   *hostname;
-			size_t		hostname_size;
-			char	   *new_uris;
-
-			/* Find the leading hostname. */
-			hostname_size = strcspn(hostnames, " ");
-			hostname = pnstrdup(hostnames, hostname_size);
-
-			/* Append a URI for this hostname. */
-			new_uris = psprintf("%s%s%s://%s:%d",
-								uris ? uris : "",
-								uris ? " " : "",
-								scheme,
-								hostname,
-								port->hba->ldapport);
-
-			pfree(hostname);
-			if (uris)
-				pfree(uris);
-			uris = new_uris;
-
-			/* Step over this hostname and any spaces. */
-			hostnames += hostname_size;
-			while (*hostnames == ' ')
-				++hostnames;
-		} while (*hostnames);
+			char	   *domain;
+			char	   *hostlist;
+			char	   *end;
+
+			/* ou=blah,dc=foo,dc=bar -> foo.bar */
+			if (ldap_dn2domain(port->hba->ldapbasedn, &domain))
+			{
+				ereport(LOG,
+						(errmsg("could not extract domain name from basedn")));
+				return STATUS_ERROR;
+			}
+			/* Look up host:port using DNS SRV for _ldap._tcp.foo.bar */
+			if (ldap_domain2hostlist(domain, &hostlist))
+			{
+				ereport(LOG,
+						(errmsg("could not look up a hostlist for %s",
+								domain)));
+				ldap_memfree(domain);
+				return STATUS_ERROR;
+			}
+			ldap_memfree(domain);
+			/*
+			 * OpenLDAP already ordered by weight and shuffled equal weight
+			 * servers, so we'll just take the first one.  The string is
+			 * of the format "host:port", separated by spaces.
+			 */
+			if ((end = strchr(hostlist, ' ')))
+				*end = '\0';
+			uris = psprintf("%s://%s", scheme, hostlist);
+			ldap_memfree(hostlist);
+		}
+		else
+		{
+			/*
+			 * We have a space-separated list of hostnames.  Convert it
+			 * to a space-separated list of URIs.
+			 */
+			do
+			{
+				char	   *hostname;
+				size_t		hostname_size;
+				char	   *new_uris;
+
+				/* Find the leading hostname. */
+				hostname_size = strcspn(hostnames, " ");
+				hostname = pnstrdup(hostnames, hostname_size);
+
+				/* Append a URI for this hostname. */
+				new_uris = psprintf("%s%s%s://%s:%d",
+									uris ? uris : "",
+									uris ? " " : "",
+									scheme,
+									hostname,
+									port->hba->ldapport);
+
+				pfree(hostname);
+				if (uris)
+					pfree(uris);
+				uris = new_uris;
+
+				/* Step over this hostname and any spaces. */
+				hostnames += hostname_size;
+				while (*hostnames == ' ')
+					++hostnames;
+			} while (*hostnames);
+		}
 
 		r = ldap_initialize(ldap, uris);
 		pfree(uris);
@@ -2536,12 +2580,23 @@ CheckLDAPAuth(Port *port)
 	int			r;
 	char	   *fulluser;
 
+#ifdef HAVE_LDAP_INITIALIZE
+	/* OpenLDAP allows empty hostname, if we have a basedn. */
+	if ((!port->hba->ldapserver || port->hba->ldapserver[0] == '\0') &&
+		(!port->hba->ldapbasedn || port->hba->ldapbasedn[0] == '\0'))
+	{
+		ereport(LOG,
+				(errmsg("LDAP server not specified, and no ldapbasedn")));
+		return STATUS_ERROR;
+	}
+#else
 	if (!port->hba->ldapserver || port->hba->ldapserver[0] == '\0')
 	{
 		ereport(LOG,
 				(errmsg("LDAP server not specified")));
 		return STATUS_ERROR;
 	}
+#endif
 
 	if (port->hba->ldapport == 0)
 	{
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 0129dd24d0..ab4cc99001 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1500,7 +1500,9 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
 	 */
 	if (parsedline->auth_method == uaLDAP)
 	{
+#ifndef HAVE_LDAP_INITIALIZE
 		MANDATORY_AUTH_ARG(parsedline->ldapserver, "ldapserver", "ldap");
+#endif
 
 		/*
 		 * LDAP can operate in two modes: either with a direct bind, using
-- 
2.19.1

