]> granicus.if.org Git - postgresql/commitdiff
Allow ldaps when using ldap authentication
authorPeter Eisentraut <peter_e@gmx.net>
Wed, 3 Jan 2018 15:00:08 +0000 (10:00 -0500)
committerPeter Eisentraut <peter_e@gmx.net>
Wed, 3 Jan 2018 15:11:26 +0000 (10:11 -0500)
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://...

Add tests for both ways of requesting LDAPS and a test for the
pre-existing ldaptls=1.  Modify the 001_auth.pl test for "diagnostic
messages", which was previously relying on the server rejecting
ldaptls=1.

Author: Thomas Munro
Reviewed-By: Peter Eisentraut
Discussion: https://postgr.es/m/CAEepm=1s+pA-LZUjQ-9GQz0Z4rX_eK=DFXAF1nBQ+ROPimuOYQ@mail.gmail.com

configure
configure.in
doc/src/sgml/client-auth.sgml
src/backend/libpq/auth.c
src/backend/libpq/hba.c
src/include/libpq/hba.h
src/include/pg_config.h.in
src/test/ldap/t/001_auth.pl

index 82f332f545c29f5ec9da9ed86e6864849118c68a..d88863e50cf4bc72c400e804b5a91b31e1f8d1f6 100755 (executable)
--- a/configure
+++ b/configure
     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; }
index 4a20c9da965214a6d90f67457a9520bdd2a6ea30..4968b67bf929e47579c503316541220fdedac096 100644 (file)
@@ -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"
index c8a1bc79aa6ee04f04ceb700170a866e7eee4ad1..53832d08e295f0d87da35f7a086fa53cbbeab184 100644 (file)
@@ -1502,19 +1502,40 @@ 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 over SSL, supported by some LDAP server
+        implementations.  See also the <literal>ldaptls</literal> option for
+        an alternative.
+       </para>
+      </listitem>
+     </varlistentry>
      <varlistentry>
       <term><literal>ldaptls</literal></term>
       <listitem>
        <para>
-        Set to 1 to make the connection between PostgreSQL and the
-        LDAP server use TLS encryption. Note that this only encrypts
-        the traffic to the LDAP server &mdash; the connection to the client
-        will still be unencrypted unless SSL is used.
+        Set to 1 to make the connection between PostgreSQL and the LDAP server
+        use TLS encryption.  This uses the <literal>StartTLS</literal>
+        operation per RFC 4513.  See also the <literal>ldapscheme</literal>
+        option for an alternative.
        </para>
       </listitem>
      </varlistentry>
     </variablelist>
+   </para>
+
+   <para>
+    Note that using <literal>ldapscheme</literal> or
+    <literal>ldaptls</literal> only encrypts the traffic between the
+    PostgreSQL server and the LDAP server.  The connection between the
+    PostgreSQL server and the PostgreSQL client will still be unencrypted
+    unless SSL is used there as well.
+   </para>
 
+   <para>
     The following options are used in simple bind mode only:
     <variablelist>
      <varlistentry>
@@ -1536,7 +1557,9 @@ omicron         bryanh                  guest1
       </listitem>
      </varlistentry>
     </variablelist>
+   </para>
 
+   <para>
     The following options are used in search+bind mode only:
     <variablelist>
      <varlistentry>
@@ -1594,7 +1617,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>,
@@ -1608,16 +1631,19 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
         </para>
 
         <para>
-         For non-anonymous binds, <literal>ldapbinddn</literal>
-         and <literal>ldapbindpasswd</literal> must be specified as separate
-         options.
+         The URL scheme <literal>ldaps</literal> chooses the LDAPS method for
+         making LDAP connections over SSL, equivalent to using
+         <literal>ldapscheme=ldaps</literal>.  To use encrypted LDAP
+         connections using the <literal>StartTLS</literal> operation, use the
+         normal URL scheme <literal>ldap</literal> and specify the
+         <literal>ldaptls</literal> option in addition to
+         <literal>ldapurl</literal>.
         </para>
 
         <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.
+         For non-anonymous binds, <literal>ldapbinddn</literal>
+         and <literal>ldapbindpasswd</literal> must be specified as separate
+         options.
         </para>
 
         <para>
index 1d49ed784f69bbaab1bf7d26b98813427bd0ff36..3560edc33a0ad84bd410def6358b1edbb2d3026c 100644 (file)
@@ -2355,22 +2355,61 @@ 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);
+       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: %m")));
+
                return STATUS_ERROR;
        }
+#endif
+#endif
 
        if ((r = ldap_set_option(*ldap, LDAP_OPT_PROTOCOL_VERSION, &ldapversion)) != LDAP_SUCCESS)
        {
@@ -2493,7 +2532,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);
 
index f760d24886dae76f36b0ef824b8fe3bedf0b34af..aa20f266b8d936555e29298e85fa4c775b97b6d2 100644 (file)
@@ -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");
index e711bee8bffdae39c612628d418553032d10ace1..5f68f4c666134d682f47fea944d5d9450ac22044 100644 (file)
@@ -75,6 +75,7 @@ typedef struct HbaLine
        char       *pamservice;
        bool            pam_use_hostname;
        bool            ldaptls;
+       char       *ldapscheme;
        char       *ldapserver;
        int                     ldapport;
        char       *ldapbinddn;
index 0aa6be466653417f5e9a77636f4722af9fd8e612..27b136872111e731690c789456112a930663f9b0 100644 (file)
 /* 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
 
index 38760ece617e445add3b02ece9da4ea73f3c72a1..5508da459f3280f780025d04bb14e40f29d7cd46 100644 (file)
@@ -2,7 +2,7 @@ use strict;
 use warnings;
 use TestLib;
 use PostgresNode;
-use Test::More tests => 15;
+use Test::More tests => 19;
 
 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 "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;
+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,44 @@ 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');
+
+note "TLS";
+
+# request StartTLS with ldaptls=1
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapbasedn="$ldap_basedn" ldapsearchfilter="(uid=\$username)" ldaptls=1});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'StartTLS');
+
+# request LDAPS with ldapscheme=ldaps
+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');
+
+# request LDAPS with ldapurl=ldaps://...
+unlink($node->data_dir . '/pg_hba.conf');
+$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', 0, 'LDAPS with URL');
+
+# bad combination of LDAPS and StartTLS
 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)" ldaptls=1});
 $node->reload;
 
 $ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 2, 'any attempt fails due to unsupported TLS');
+test_access($node, 'test1', 2, 'bad combination of LDAPS and StartTLS');