From 4166e0133649c3c66c37af36bc79efb836a238d1 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@enterprisedb.com>
Date: Tue, 25 Sep 2018 13:47:34 +1200
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:
---
 doc/src/sgml/client-auth.sgml | 18 +++++++++++
 src/backend/libpq/auth.c      | 58 +++++++++++++++++++++++++++++++++--
 src/backend/libpq/hba.c       |  2 ++
 3 files changed, 76 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index c2114021c3..2116e636be 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1671,6 +1671,15 @@ 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>.
+   </para>
+
    <para>
     Here is an example for a simple-bind LDAP configuration:
 <programlisting>
@@ -1716,6 +1725,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 8517565535..203f08adac 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2354,8 +2354,51 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 	{
 		char	   *uri;
 
-		uri = psprintf("%s://%s:%d", scheme, port->hba->ldapserver,
-					   port->hba->ldapport);
+		/*
+		 * 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.
+		 */
+		if (!port->hba->ldapserver || port->hba->ldapserver[0] == '\0')
+		{
+			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';
+			uri = psprintf("%s://%s", scheme, hostlist);
+			ldap_memfree(hostlist);
+		}
+		else
+		{
+			uri = psprintf("%s://%s:%d", scheme, port->hba->ldapserver,
+						   port->hba->ldapport);
+		}
 		r = ldap_initialize(ldap, uri);
 		pfree(uri);
 		if (r != LDAP_SUCCESS)
@@ -2504,12 +2547,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 1a65ec87bd..8cd09a49b5 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.17.0

