diff --git a/configure b/configure index 82f332f545..d88863e50c 100755 --- a/configure +++ b/configure @@ -10424,6 +10424,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 4a20c9da96..4968b67bf9 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 c8a1bc79aa..53832d08e2 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -1502,19 +1502,40 @@ omicron bryanh guest1 + + ldapscheme + + + Set to ldaps to use LDAPS. This is a non-standard + way of using LDAP over SSL, supported by some LDAP server + implementations. See also the ldaptls option for + an alternative. + + + ldaptls - 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 — 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 StartTLS + operation per RFC 4513. See also the ldapscheme + option for an alternative. + + + Note that using ldapscheme or + ldaptls 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. + + + The following options are used in simple bind mode only: @@ -1536,7 +1557,9 @@ omicron bryanh guest1 + + The following options are used in search+bind mode only: @@ -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 -ldap://host[:port]/basedn[?[attribute][?[scope][?[filter]]]] +ldap[s]://host[:port]/basedn[?[attribute][?[scope][?[filter]]]] scope must be one of base, one, sub, @@ -1608,16 +1631,19 @@ ldap://host[:port]/ - For non-anonymous binds, ldapbinddn - and ldapbindpasswd must be specified as separate - options. + The URL scheme ldaps chooses the LDAPS method for + making LDAP connections over SSL, equivalent to using + ldapscheme=ldaps. To use encrypted LDAP + connections using the StartTLS operation, use the + normal URL scheme ldap and specify the + ldaptls option in addition to + ldapurl. - To use encrypted LDAP connections, the ldaptls - option has to be used in addition to ldapurl. - The ldaps URL scheme (direct SSL connection) is not - supported. + For non-anonymous binds, ldapbinddn + and ldapbindpasswd must be specified as separate + options. diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 1d49ed784f..3560edc33a 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -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); diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index f760d24886..aa20f266b8 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 0aa6be4666..27b1368721 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -310,6 +310,9 @@ /* Define to 1 if you have the 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..5508da459f 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 => 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}); -mkdir $ldap_datadir or die; +# don't bother to check the server's cert (though perhaps we should) +append_to_file($ldap_conf, +qq{TLS_REQCERT never +}); -system_or_bail $slapd, '-f', $slapd_conf, '-h', $ldap_url; +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 $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=",dc=example,dc=net" ldaptls=1}); +$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 unsupported TLS'); +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 ldapurl="$ldaps_url/$ldap_basedn??sub?(uid=\$username)" ldaptls=1}); +$node->reload; + +$ENV{"PGPASSWORD"} = 'secret1'; +test_access($node, 'test1', 2, 'bad combination of LDAPS and StartTLS');