From 0d5556ccd2f5b28f4e31a6093b5a610b6a5baf3a Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@enterprisedb.com>
Date: Fri, 3 Nov 2017 23:17:48 +1300
Subject: [PATCH] Allow ldaps when using ldap authentication.

While ldaptls=1 provides an RFC 4513 conforming way to do LDAP authentication
with TLS encryption, there was an earlier de facto standard way to do LDAP
over SSL called LDAPS.  Even though it's not enshrined in a standard, it's
still widely used and sometimes required by organizations' network policies.
There seems to be no reason not to support it when available in the client
library.  Therefore, add support when using OpenLDAP 2.4+ or Windows.  It can
be configured with ldapscheme=ldaps or ldapurl=ldaps://...

Author: Thomas Munro
Discussion: https://postgr.es/m/CAEepm=1s+pA-LZUjQ-9GQz0Z4rX_eK=DFXAF1nBQ+ROPimuOYQ@mail.gmail.com
---
 configure                     | 11 ++++++++
 configure.in                  |  1 +
 doc/src/sgml/client-auth.sgml | 18 ++++++++++---
 src/backend/libpq/auth.c      | 60 ++++++++++++++++++++++++++++++++++++++-----
 src/backend/libpq/hba.c       | 16 +++++++++++-
 src/include/libpq/hba.h       |  1 +
 src/include/pg_config.h.in    |  3 +++
 src/test/ldap/t/001_auth.pl   | 41 ++++++++++++++++++++++++++---
 8 files changed, 135 insertions(+), 16 deletions(-)

diff --git a/configure b/configure
index b8995ad547..fe27084168 100755
--- a/configure
+++ b/configure
@@ -10419,6 +10419,17 @@ fi
     else
       LDAP_LIBS_FE="-lldap $EXTRA_LDAP_LIBS"
     fi
+    for ac_func in ldap_initialize
+do :
+  ac_fn_c_check_func "$LINENO" "ldap_initialize" "ac_cv_func_ldap_initialize"
+if test "x$ac_cv_func_ldap_initialize" = xyes; then :
+  cat >>confdefs.h <<_ACEOF
+#define HAVE_LDAP_INITIALIZE 1
+_ACEOF
+
+fi
+done
+
   else
     { $as_echo "$as_me:${as_lineno-$LINENO}: checking for ldap_bind in -lwldap32" >&5
 $as_echo_n "checking for ldap_bind in -lwldap32... " >&6; }
diff --git a/configure.in b/configure.in
index 0e5aef37b4..24eddf7e25 100644
--- a/configure.in
+++ b/configure.in
@@ -1106,6 +1106,7 @@ if test "$with_ldap" = yes ; then
     else
       LDAP_LIBS_FE="-lldap $EXTRA_LDAP_LIBS"
     fi
+    AC_CHECK_FUNCS([ldap_initialize])
   else
     AC_CHECK_LIB(wldap32, ldap_bind, [], [AC_MSG_ERROR([library 'wldap32' is required for LDAP])])
     LDAP_LIBS_FE="-lwldap32"
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 99921ba079..3212c58fba 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1502,6 +1502,16 @@ omicron         bryanh                  guest1
        </para>
       </listitem>
      </varlistentry>
+     <varlistentry>
+      <term><literal>ldapscheme</literal></term>
+      <listitem>
+       <para>
+        Set to <literal>ldaps</literal> to use LDAPS.  This is a non-standard
+        way of using LDAP with SSL supported by some popular LDAP server
+        implementation.
+       </para>
+      </listitem>
+     </varlistentry>
      <varlistentry>
       <term><literal>ldaptls</literal></term>
       <listitem>
@@ -1594,7 +1604,7 @@ omicron         bryanh                  guest1
          An RFC 4516 LDAP URL.  This is an alternative way to write some of the
          other LDAP options in a more compact and standard form.  The format is
 <synopsis>
-ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>][?[<replaceable>filter</replaceable>]]]]
+ldap[s]://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>][?[<replaceable>filter</replaceable>]]]]
 </synopsis>
          <replaceable>scope</replaceable> must be one
          of <literal>base</literal>, <literal>one</literal>, <literal>sub</literal>,
@@ -1615,9 +1625,9 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
 
         <para>
          To use encrypted LDAP connections, the <literal>ldaptls</literal>
-         option has to be used in addition to <literal>ldapurl</literal>.
-         The <literal>ldaps</literal> URL scheme (direct SSL connection) is not
-         supported.
+         option should be used in addition to <literal>ldapurl</literal>.  A
+         non-standard alternative is to use
+         <literal>ldapscheme=ldaps</literal>.
         </para>
 
         <para>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 6c915a7289..51d448198a 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2320,22 +2320,62 @@ static int errdetail_for_ldap(LDAP *ldap);
 static int
 InitializeLDAPConnection(Port *port, LDAP **ldap)
 {
+	const char *scheme;
 	int			ldapversion = LDAP_VERSION3;
 	int			r;
 
-	*ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport);
+	/* Use a minimal URI to initialize an LDAP connection. */
+	scheme = port->hba->ldapscheme;
+	if (scheme == NULL)
+		scheme = "ldap";
+#ifdef WIN32
+	*ldap = ldap_sslinit(port->hba->ldapserver,
+						 port->hba->ldapport,
+						 strcmp(scheme, "ldaps") == 0);
 	if (!*ldap)
 	{
-#ifndef WIN32
-		ereport(LOG,
-				(errmsg("could not initialize LDAP: %m")));
-#else
 		ereport(LOG,
 				(errmsg("could not initialize LDAP: error code %d",
 						(int) LdapGetLastError())));
-#endif
+
+		return STATUS_ERROR;
+	}
+#else
+#ifdef HAVE_LDAP_INITIALIZE
+	{
+		char	   *uri;
+
+		uri = psprintf("%s://%s:%d", scheme, port->hba->ldapserver,
+					   port->hba->ldapport);
+		r = ldap_initialize(ldap, uri);
+		pfree(uri);
+		if (r != LDAP_SUCCESS)
+		{
+			ereport(LOG,
+					(errmsg("could not initialize LDAP: %s",
+							ldap_err2string(r))));
+
+			return STATUS_ERROR;
+		}
+	}
+#else
+	if (strcmp(scheme, "ldaps") == 0)
+	{
+		ereport(LOG,
+				(errmsg("ldaps not supported with this LDAP library")));
+
 		return STATUS_ERROR;
 	}
+	*ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport);
+	if (!*ldap)
+	{
+		ereport(LOG,
+				(errmsg("could not initialize LDAP")));
+
+		return STATUS_ERROR;
+	}
+#endif
+#endif
 
 	if ((r = ldap_set_option(*ldap, LDAP_OPT_PROTOCOL_VERSION, &ldapversion)) != LDAP_SUCCESS)
 	{
@@ -2458,7 +2498,13 @@ CheckLDAPAuth(Port *port)
 	}
 
 	if (port->hba->ldapport == 0)
-		port->hba->ldapport = LDAP_PORT;
+	{
+		if (port->hba->ldapscheme != NULL &&
+			strcmp(port->hba->ldapscheme, "ldaps") == 0)
+			port->hba->ldapport = LDAPS_PORT;
+		else
+			port->hba->ldapport = LDAP_PORT;
+	}
 
 	sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
 
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index ca78a7e0ba..8942a7fa4f 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1728,7 +1728,8 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 			return false;
 		}
 
-		if (strcmp(urldata->lud_scheme, "ldap") != 0)
+		if (strcmp(urldata->lud_scheme, "ldap") != 0 &&
+			strcmp(urldata->lud_scheme, "ldaps") != 0)
 		{
 			ereport(elevel,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
@@ -1739,6 +1740,8 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 			return false;
 		}
 
+		if (urldata->lud_scheme)
+			hbaline->ldapscheme = pstrdup(urldata->lud_scheme);
 		if (urldata->lud_host)
 			hbaline->ldapserver = pstrdup(urldata->lud_host);
 		hbaline->ldapport = urldata->lud_port;
@@ -1766,6 +1769,17 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 		else
 			hbaline->ldaptls = false;
 	}
+	else if (strcmp(name, "ldapscheme") == 0)
+	{
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapscheme", "ldap");
+		if (strcmp(val, "ldap") != 0 && strcmp(val, "ldaps") != 0)
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("invalid ldapscheme value: \"%s\"", val),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+		hbaline->ldapscheme = pstrdup(val);
+	}
 	else if (strcmp(name, "ldapserver") == 0)
 	{
 		REQUIRE_AUTH_OPTION(uaLDAP, "ldapserver", "ldap");
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index e711bee8bf..5f68f4c666 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -75,6 +75,7 @@ typedef struct HbaLine
 	char	   *pamservice;
 	bool		pam_use_hostname;
 	bool		ldaptls;
+	char	   *ldapscheme;
 	char	   *ldapserver;
 	int			ldapport;
 	char	   *ldapbinddn;
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index cfdcc5ac62..fcc3f8f2a8 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -307,6 +307,9 @@
 /* Define to 1 if you have the <ldap.h> header file. */
 #undef HAVE_LDAP_H
 
+/* Define to 1 if you have the `ldap_initialize' function. */
+#undef HAVE_LDAP_INITIALIZE
+
 /* Define to 1 if you have the `crypto' library (-lcrypto). */
 #undef HAVE_LIBCRYPTO
 
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 38760ece61..3b06ec1a6a 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -2,7 +2,7 @@ use strict;
 use warnings;
 use TestLib;
 use PostgresNode;
-use Test::More tests => 15;
+use Test::More tests => 17;
 
 my ($slapd, $ldap_bin_dir, $ldap_schema_dir);
 
@@ -33,13 +33,16 @@ elsif ($^O eq 'freebsd')
 $ENV{PATH} = "$ldap_bin_dir:$ENV{PATH}" if $ldap_bin_dir;
 
 my $ldap_datadir = "${TestLib::tmp_check}/openldap-data";
+my $slapd_certs = "${TestLib::tmp_check}/slapd-certs";
 my $slapd_conf = "${TestLib::tmp_check}/slapd.conf";
 my $slapd_pidfile = "${TestLib::tmp_check}/slapd.pid";
 my $slapd_logfile = "${TestLib::tmp_check}/slapd.log";
 my $ldap_conf = "${TestLib::tmp_check}/ldap.conf";
 my $ldap_server = 'localhost';
 my $ldap_port = int(rand() * 16384) + 49152;
+my $ldaps_port = $ldap_port + 1;
 my $ldap_url = "ldap://$ldap_server:$ldap_port";
+my $ldaps_url = "ldaps://$ldap_server:$ldaps_port";
 my $ldap_basedn = 'dc=example,dc=net';
 my $ldap_rootdn = 'cn=Manager,dc=example,dc=net';
 my $ldap_rootpw = 'secret';
@@ -63,13 +66,27 @@ access to *
 database ldif
 directory $ldap_datadir
 
+TLSCACertificateFile $slapd_certs/ca.crt
+TLSCertificateFile $slapd_certs/server.crt
+TLSCertificateKeyFile $slapd_certs/server.key
+
 suffix "dc=example,dc=net"
 rootdn "$ldap_rootdn"
 rootpw $ldap_rootpw});
 
+# don't bother to check the server's cert (though perhaps we should)
+append_to_file($ldap_conf,
+qq{TLS_REQCERT never
+});
+
 mkdir $ldap_datadir or die;
+mkdir $slapd_certs or die;
 
-system_or_bail $slapd, '-f', $slapd_conf, '-h', $ldap_url;
+system_or_bail "openssl", "req", "-new", "-nodes", "-keyout", "$slapd_certs/ca.key", "-x509", "-out", "$slapd_certs/ca.crt", "-subj", "/cn=CA";
+system_or_bail "openssl", "req", "-new", "-nodes", "-keyout", "$slapd_certs/server.key", "-out", "$slapd_certs/server.csr", "-subj", "/cn=server";
+system_or_bail "openssl", "x509", "-req", "-in", "$slapd_certs/server.csr", "-CA", "$slapd_certs/ca.crt", "-CAkey", "$slapd_certs/ca.key", "-CAcreateserial", "-out", "$slapd_certs/server.crt";
+
+system_or_bail $slapd, '-f', $slapd_conf, '-h', "$ldap_url $ldaps_url";
 
 END
 {
@@ -81,6 +98,7 @@ chmod 0600, $ldap_pwfile or die;
 
 $ENV{'LDAPURI'} = $ldap_url;
 $ENV{'LDAPBINDDN'} = $ldap_rootdn;
+$ENV{'LDAPCONF'} = $ldap_conf;
 
 note "loading LDAP data";
 
@@ -178,9 +196,24 @@ test_access($node, 'test1', 0, 'combined LDAP URL and search filter');
 
 note "diagnostic message";
 
+# note bad ldapprefix with a question mark that triggers a diagnostic message
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="?uid=" ldapsuffix=""});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 2, 'any attempt fails due to bad search pattern');
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapscheme=ldaps ldapport=$ldaps_port ldapbasedn="$ldap_basedn" ldapsearchfilter="(uid=\$username)"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'LDAPS');
+
 unlink($node->data_dir . '/pg_hba.conf');
-$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net" ldaptls=1});
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapurl="$ldaps_url/$ldap_basedn??sub?(uid=\$username)"});
 $node->reload;
 
 $ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 2, 'any attempt fails due to unsupported TLS');
+test_access($node, 'test1', 0, 'LDAPS with URL');
-- 
2.14.1

