From ad2ff251fd708647712677306d50b789560a5d16 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] Add ldaps support for ldap authentication.

While ldaptls=1 provides a standards-based way to do LDAP authentication with
StartTLS 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 a
common request since popular LDAP servers support it and some users prefer it.

Author: Thomas Munro
---
 doc/src/sgml/client-auth.sgml | 18 ++++++++++++++----
 src/backend/libpq/auth.c      | 21 ++++++++++++++++++---
 src/backend/libpq/hba.c       | 15 ++++++++++++++-
 src/include/libpq/hba.h       |  1 +
 src/test/ldap/t/001_auth.pl   | 39 +++++++++++++++++++++++++++++++++++----
 5 files changed, 82 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 99921ba079..612a857ea7 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>ldapschema=ldaps</literal>.
         </para>
 
         <para>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index ab74fd8dfd..a2d32bc2ed 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2320,11 +2320,20 @@ static int errdetail_for_ldap(LDAP *ldap);
 static int
 InitializeLDAPConnection(Port *port, LDAP **ldap)
 {
+	const char *scheme;
+	char	   *uri;
 	int			ldapversion = LDAP_VERSION3;
 	int			r;
 
-	*ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport);
-	if (!*ldap)
+	/* Use a minimal URI to initialize an LDAP connection. */
+	scheme = port->hba->ldapscheme;
+	if (scheme == NULL)
+		scheme = "ldap";
+	uri = psprintf("%s://%s:%d", scheme, port->hba->ldapserver,
+				   port->hba->ldapport);
+	r = ldap_initialize(ldap, uri);
+	pfree(uri);
+	if (r != LDAP_SUCCESS)
 	{
 #ifndef WIN32
 		ereport(LOG,
@@ -2458,7 +2467,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 b2c487a8e8..61ff70f3a9 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,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 			return false;
 		}
 
+		hbaline->ldapscheme = pstrdup(urldata->lud_scheme);
 		hbaline->ldapserver = pstrdup(urldata->lud_host);
 		hbaline->ldapport = urldata->lud_port;
 		hbaline->ldapbasedn = pstrdup(urldata->lud_dn);
@@ -1764,6 +1766,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/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 38760ece61..85d0079b17 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,26 @@ 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});
 
+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
 {
@@ -178,9 +194,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

