diff --git a/configure b/configure index 20084080c5abe813b9150953ad57b92f77452a39..973c9ebf5540616e6d581c4a0137f5a09396c79d 100755 --- a/configure +++ b/configure @@ -17314,6 +17314,91 @@ _ACEOF fi +# this will link libpq against libldap_r +if test "$with_ldap" = yes ; then + if test "$PORTNAME" != "win32"; then + +echo "$as_me:$LINENO: checking for ldap_simple_bind in -lldap_r" >&5 +echo $ECHO_N "checking for ldap_simple_bind in -lldap_r... $ECHO_C" >&6 +if test "${ac_cv_lib_ldap_r_ldap_simple_bind+set}" = set; then + echo $ECHO_N "(cached) $ECHO_C" >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lldap_r $LIBS" +cat >conftest.$ac_ext <<_ACEOF +/* confdefs.h. */ +_ACEOF +cat confdefs.h >>conftest.$ac_ext +cat >>conftest.$ac_ext <<_ACEOF +/* end confdefs.h. */ + +/* Override any gcc2 internal prototype to avoid an error. */ +#ifdef __cplusplus +extern "C" +#endif +/* We use char because int might match the return type of a gcc2 + builtin and then its argument prototype would still apply. */ +char ldap_simple_bind (); +int +main () +{ +ldap_simple_bind (); + ; + return 0; +} +_ACEOF +rm -f conftest.$ac_objext conftest$ac_exeext +if { (eval echo "$as_me:$LINENO: \"$ac_link\"") >&5 + (eval $ac_link) 2>conftest.er1 + ac_status=$? + grep -v '^ *+' conftest.er1 >conftest.err + rm -f conftest.er1 + cat conftest.err >&5 + echo "$as_me:$LINENO: \$? = $ac_status" >&5 + (exit $ac_status); } && + { ac_try='test -z "$ac_c_werror_flag" + || test ! -s conftest.err' + { (eval echo "$as_me:$LINENO: \"$ac_try\"") >&5 + (eval $ac_try) 2>&5 + ac_status=$? + echo "$as_me:$LINENO: \$? = $ac_status" >&5 + (exit $ac_status); }; } && + { ac_try='test -s conftest$ac_exeext' + { (eval echo "$as_me:$LINENO: \"$ac_try\"") >&5 + (eval $ac_try) 2>&5 + ac_status=$? + echo "$as_me:$LINENO: \$? = $ac_status" >&5 + (exit $ac_status); }; }; then + ac_cv_lib_ldap_r_ldap_simple_bind=yes +else + echo "$as_me: failed program was:" >&5 +sed 's/^/| /' conftest.$ac_ext >&5 + +ac_cv_lib_ldap_r_ldap_simple_bind=no +fi +rm -f conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +echo "$as_me:$LINENO: result: $ac_cv_lib_ldap_r_ldap_simple_bind" >&5 +echo "${ECHO_T}$ac_cv_lib_ldap_r_ldap_simple_bind" >&6 +if test $ac_cv_lib_ldap_r_ldap_simple_bind = yes; then + cat >>confdefs.h <<_ACEOF +#define HAVE_LIBLDAP_R 1 +_ACEOF + + LIBS="-lldap_r $LIBS" + +else + { { echo "$as_me:$LINENO: error: library 'ldap_r' is required for LDAP" >&5 +echo "$as_me: error: library 'ldap_r' is required for LDAP" >&2;} + { (exit 1); exit 1; }; } +fi + + PTHREAD_LIBS="$PTHREAD_LIBS -lldap_r" + fi +fi + CFLAGS="$_CFLAGS" LIBS="$_LIBS" diff --git a/configure.in b/configure.in index fe3cde3f6a7ab36267fdb3cef10086aa35c0c511..7031f7d380aef75ad8c966a54219aa63a60eedb1 100644 --- a/configure.in +++ b/configure.in @@ -1,5 +1,5 @@ dnl Process this file with autoconf to produce a configure script. -dnl $PostgreSQL: pgsql/configure.in,v 1.469 2006/07/24 16:32:44 petere Exp $ +dnl $PostgreSQL: pgsql/configure.in,v 1.470 2006/07/27 13:20:24 momjian Exp $ dnl dnl Developers, please strive to achieve this order: dnl @@ -1106,6 +1106,14 @@ AC_CHECK_FUNCS([strerror_r getpwuid_r gethostbyname_r]) PGAC_FUNC_GETPWUID_R_5ARG PGAC_FUNC_STRERROR_R_INT +# this will link libpq against libldap_r +if test "$with_ldap" = yes ; then + if test "$PORTNAME" != "win32"; then + AC_CHECK_LIB(ldap_r, ldap_simple_bind, [], [AC_MSG_ERROR([library 'ldap_r' is required for LDAP])]) + PTHREAD_LIBS="$PTHREAD_LIBS -lldap_r" + fi +fi + CFLAGS="$_CFLAGS" LIBS="$_LIBS" diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 4d8b29de726ef6349c448d6901c173f7ddf467d2..7ffd15a0388d3d957eed028b434b905e5bd58c67 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -1,4 +1,4 @@ -<!-- $PostgreSQL: pgsql/doc/src/sgml/libpq.sgml,v 1.213 2006/07/04 13:22:15 momjian Exp $ --> +<!-- $PostgreSQL: pgsql/doc/src/sgml/libpq.sgml,v 1.214 2006/07/27 13:20:24 momjian Exp $ --> <chapter id="libpq"> <title><application>libpq</application> - C Library</title> @@ -4126,6 +4126,72 @@ installs too. The file's location can also be specified by the </sect1> +<sect1 id="libpq-ldap"> + <title>LDAP Lookup of Connection Parameters</title> + +<indexterm zone="libpq-ldap"> + <primary>LDAP connection parameter lookup</primary> +</indexterm> + +<para> +If <application>libpq</application> has been compiled with LDAP support (option +<literal><option>--with-ldap</option></literal> for <command>configure</command>) +it is possible to retrieve connection options like <literal>host</literal> +or <literal>dbname</literal> via LDAP from a central server. +The advantage is that if the connection parameters for a database change, +the connection information doesn't have to be updated on all client machines. +</para> + +<para> +LDAP connection parameter lookup uses the connection service file +<filename>pg_service.conf</filename> (see <xref linkend="libpq-pgservice">). +A line in a <filename>pg_service.conf</filename> stanza that starts with +<literal>ldap://</literal> will be recognized as an LDAP URL and an LDAP +query will be performed. The result must be a list of <literal>keyword = +value</literal> pairs which will be used to set connection options. +The URL must conform to RFC 1959 and be of the form +<synopsis> +ldap://[<replaceable>hostname</replaceable>[:<replaceable>port</replaceable>]]/<replaceable>search_base</replaceable>?<replaceable>attribute</replaceable>?<replaceable>search_scope</replaceable>?<replaceable>filter</replaceable> +</synopsis> +where <replaceable>hostname</replaceable> +defaults to <literal>localhost</literal> and +<replaceable>port</replaceable> defaults to 389. +</para> + +<para> +Processing of <filename>pg_service.conf</filename> is terminated after +a successful LDAP lookup, but is continued if the LDAP server cannot be +contacted. This is to provide a fallback with +further LDAP URL lines that point to different LDAP +servers, classical <literal>keyword = value</literal> pairs, or +default connection options. +If you would rather get an error message in this case, add a +syntactically incorrect line after the LDAP URL. +</para> + +<para> +A sample LDAP entry that has been created with the LDIF file +<synopsis> +version:1 +dn:cn=mydatabase,dc=mycompany,dc=com +changetype:add +objectclass:top +objectclass:groupOfUniqueNames +cn:mydatabase +uniqueMember:host=dbserver.mycompany.com +uniqueMember:port=5439 +uniqueMember:dbname=mydb +uniqueMember:user=mydb_user +uniqueMember:sslmode=require +</synopsis> +might be queried with the following LDAP URL: +<synopsis> +ldap://ldap.mycompany.com/dc=mycompany,dc=com?uniqueMember?one?(cn=mydatabase) +</synopsis> +</para> +</sect1> + + <sect1 id="libpq-ssl"> <title>SSL Support</title> diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 24218d12b70507866b1f8ca8796d1fe000144570..ea395c8834adfee6620b76e9a0659fad87756e0f 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -5,7 +5,7 @@ # Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # -# $PostgreSQL: pgsql/src/interfaces/libpq/Makefile,v 1.146 2006/07/18 22:18:08 momjian Exp $ +# $PostgreSQL: pgsql/src/interfaces/libpq/Makefile,v 1.147 2006/07/27 13:20:24 momjian Exp $ # #------------------------------------------------------------------------- @@ -62,7 +62,7 @@ else SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lssl -lsocket -lnsl -lresolv -lintl $(PTHREAD_LIBS), $(LIBS)) endif ifeq ($(PORTNAME), win32) -SHLIB_LINK += -lshfolder -lwsock32 -lws2_32 $(filter -leay32 -lssleay32 -lcomerr32 -lkrb5_32, $(LIBS)) +SHLIB_LINK += -lshfolder -lwsock32 -lws2_32 $(filter -leay32 -lssleay32 -lcomerr32 -lkrb5_32 -lwldap32, $(LIBS)) endif diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 74dfe3a9cf581a4a44d55d89eba9ef009bbb794f..00235fb0badc0950e106feee66270c8620c6a240 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/interfaces/libpq/fe-connect.c,v 1.333 2006/06/07 22:24:46 momjian Exp $ + * $PostgreSQL: pgsql/src/interfaces/libpq/fe-connect.c,v 1.334 2006/07/27 13:20:24 momjian Exp $ * *------------------------------------------------------------------------- */ @@ -60,6 +60,19 @@ #endif #endif +#ifdef USE_LDAP +#ifdef WIN32 +#include <winldap.h> +#else +/* OpenLDAP deprecates RFC 1823, but we want standard conformance */ +#define LDAP_DEPRECATED 1 +#include <ldap.h> +typedef struct timeval LDAP_TIMEVAL; +#endif +static int ldapServiceLookup(const char *purl, PQconninfoOption *options, + PQExpBuffer errorMessage); +#endif + #include "libpq/ip.h" #include "mb/pg_wchar.h" @@ -2343,7 +2356,410 @@ pqPacketSend(PGconn *conn, char pack_type, return STATUS_OK; } +#ifdef USE_LDAP + +#define LDAP_URL "ldap://" +#define LDAP_DEF_PORT 389 +#define PGLDAP_TIMEOUT 2 +#define ld_is_sp_tab(x) ((x) == ' ' || (x) == '\t') +#define ld_is_nl_cr(x) ((x) == '\r' || (x) == '\n') + + +/* + * ldapServiceLookup + * + * Search the LDAP URL passed as first argument, treat the result as a + * string of connection options that are parsed and added to the array of + * options passed as second argument. + * + * LDAP URLs must conform to RFC 1959 without escape sequences. + * ldap://host:port/dn?attributes?scope?filter?extensions + * + * Returns + * 0 if the lookup was successful, + * 1 if the connection to the LDAP server could be established but + * the search was unsuccessful, + * 2 if a connection could not be established, and + * 3 if a fatal error occurred. + * + * An error message is returned in the third argument for return codes 1 and 3. + */ +static int +ldapServiceLookup(const char *purl, PQconninfoOption *options, + PQExpBuffer errorMessage) +{ + int port = LDAP_DEF_PORT, scope, rc, msgid, size, state, oldstate, i; + bool found_keyword; + char *url, *hostname, *portstr, *endptr, *dn, *scopestr, *filter, + *result, *p, *p1 = NULL, *optname = NULL, *optval = NULL; + char *attrs[2] = {NULL, NULL}; + LDAP *ld = NULL; + LDAPMessage *res, *entry; + struct berval **values; + LDAP_TIMEVAL time = {PGLDAP_TIMEOUT, 0}; + + if ((url = strdup(purl)) == NULL) + { + printfPQExpBuffer(errorMessage, libpq_gettext("out of memory\n")); + return 3; + } + + /* + * Parse URL components, check for correctness. Basically, url has + * '\0' placed at component boundaries and variables are pointed + * at each component. + */ + + if (strncasecmp(url, LDAP_URL, strlen(LDAP_URL)) != 0) + { + printfPQExpBuffer(errorMessage, + libpq_gettext("bad LDAP URL \"%s\": scheme must be ldap://\n"), purl); + free(url); + return 3; + } + + /* hostname */ + hostname = url + strlen(LDAP_URL); + if (*hostname == '/') /* no hostname? */ + hostname = "localhost"; /* the default */ + + /* dn, "distinguished name" */ + p = strchr(url + strlen(LDAP_URL), '/'); + if (p == NULL || *(p + 1) == '\0' || *(p + 1) == '?') + { + printfPQExpBuffer(errorMessage, libpq_gettext( + "bad LDAP URL \"%s\": missing distinguished name\n"), purl); + free(url); + return 3; + } + *p = '\0'; /* terminate hostname */ + dn = p + 1; + + /* attribute */ + if ((p = strchr(dn, '?')) == NULL || *(p + 1) == '\0' || *(p + 1) == '?') + { + printfPQExpBuffer(errorMessage, libpq_gettext( + "bad LDAP URL \"%s\": must have exactly one attribute\n"), purl); + free(url); + return 3; + } + *p = '\0'; + attrs[0] = p + 1; + + /* scope */ + if ((p = strchr(attrs[0], '?')) == NULL || *(p + 1) == '\0' || *(p + 1) == '?') + { + printfPQExpBuffer(errorMessage, libpq_gettext( + "bad LDAP URL \"%s\": must have search scope (base/one/sub)\n"), purl); + free(url); + return 3; + } + *p = '\0'; + scopestr = p + 1; + + /* filter */ + if ((p = strchr(scopestr, '?')) == NULL || *(p + 1) == '\0' || *(p + 1) == '?') + { + printfPQExpBuffer(errorMessage, + libpq_gettext("bad LDAP URL \"%s\": no filter\n"), purl); + free(url); + return 3; + } + *p = '\0'; + filter = p + 1; + if ((p = strchr(filter, '?')) != NULL) + *p = '\0'; + + /* port number? */ + if ((p1 = strchr(hostname, ':')) != NULL) + { + long lport; + + *p1 = '\0'; + portstr = p1 + 1; + errno = 0; + lport = strtol(portstr, &endptr, 10); + if (*portstr == '\0' || *endptr != '\0' || errno || lport < 0 || lport > 65535) + { + printfPQExpBuffer(errorMessage, libpq_gettext( + "bad LDAP URL \"%s\": invalid port number\n"), purl); + free(url); + return 3; + } + port = (int) lport; + } + + /* Allow only one attribute */ + if (strchr(attrs[0], ',') != NULL) + { + printfPQExpBuffer(errorMessage, libpq_gettext( + "bad LDAP URL \"%s\": must have exactly one attribute\n"), purl); + free(url); + return 3; + } + + /* set scope */ + if (strcasecmp(scopestr, "base") == 0) + scope = LDAP_SCOPE_BASE; + else if (strcasecmp(scopestr, "one") == 0) + scope = LDAP_SCOPE_ONELEVEL; + else if (strcasecmp(scopestr, "sub") == 0) + scope = LDAP_SCOPE_SUBTREE; + else + { + printfPQExpBuffer(errorMessage, libpq_gettext( + "bad LDAP URL \"%s\": must have search scope (base/one/sub)\n"), purl); + free(url); + return 3; + } + + /* initialize LDAP structure */ + if ((ld = ldap_init(hostname, port)) == NULL) + { + printfPQExpBuffer(errorMessage, + libpq_gettext("error creating LDAP structure\n")); + free(url); + return 3; + } + + /* + * Initialize connection to the server. We do an explicit bind because + * we want to return 2 if the bind fails. + */ + if ((msgid = ldap_simple_bind(ld, NULL, NULL)) == -1) + { + /* error in ldap_simple_bind() */ + free(url); + ldap_unbind(ld); + return 2; + } + + /* wait some time for the connection to succeed */ + res = NULL; + if ((rc = ldap_result(ld, msgid, LDAP_MSG_ALL, &time, &res)) == -1 || + res == NULL) + { + if (res != NULL) + { + /* timeout */ + ldap_msgfree(res); + } + /* error in ldap_result() */ + free(url); + ldap_unbind(ld); + return 2; + } + ldap_msgfree(res); + + /* search */ + res = NULL; + if ((rc = ldap_search_st(ld, dn, scope, filter, attrs, 0, &time, &res)) + != LDAP_SUCCESS) + { + if (res != NULL) + ldap_msgfree(res); + printfPQExpBuffer(errorMessage, + libpq_gettext("lookup on LDAP server failed: %s\n"), + ldap_err2string(rc)); + ldap_unbind(ld); + free(url); + return 1; + } + + /* complain if there was not exactly one result */ + if ((rc = ldap_count_entries(ld, res)) != 1) + { + printfPQExpBuffer(errorMessage, + rc ? libpq_gettext("more than one entry found on LDAP lookup\n") + : libpq_gettext("no entry found on LDAP lookup\n")); + ldap_msgfree(res); + ldap_unbind(ld); + free(url); + return 1; + } + + /* get entry */ + if ((entry = ldap_first_entry(ld, res)) == NULL) + { + /* should never happen */ + printfPQExpBuffer(errorMessage, + libpq_gettext("no entry found on LDAP lookup\n")); + ldap_msgfree(res); + ldap_unbind(ld); + free(url); + return 1; + } + + /* get values */ + if ((values = ldap_get_values_len(ld, entry, attrs[0])) == NULL) + { + printfPQExpBuffer(errorMessage, + libpq_gettext("attribute has no values on LDAP lookup\n")); + ldap_msgfree(res); + ldap_unbind(ld); + free(url); + return 1; + } + + ldap_msgfree(res); + free(url); + + if (values[0] == NULL) + { + printfPQExpBuffer(errorMessage, + libpq_gettext("attribute has no values on LDAP lookup\n")); + ldap_value_free_len(values); + ldap_unbind(ld); + return 1; + } + + /* concatenate values to a single string */ + for (size = 0, i = 0; values[i] != NULL; ++i) + size += values[i]->bv_len + 1; + if ((result = malloc(size + 1)) == NULL) + { + printfPQExpBuffer(errorMessage, + libpq_gettext("out of memory\n")); + ldap_value_free_len(values); + ldap_unbind(ld); + return 3; + } + for (p = result, i = 0; values[i] != NULL; ++i) + { + strncpy(p, values[i]->bv_val, values[i]->bv_len); + p += values[i]->bv_len; + *(p++) = '\n'; + if (values[i + 1] == NULL) + *(p + 1) = '\0'; + } + + ldap_value_free_len(values); + ldap_unbind(ld); + + /* parse result string */ + oldstate = state = 0; + for (p = result; *p != '\0'; ++p) + { + switch (state) + { + case 0: /* between entries */ + if (!ld_is_sp_tab(*p) && !ld_is_nl_cr(*p)) + { + optname = p; + state = 1; + } + break; + case 1: /* in option name */ + if (ld_is_sp_tab(*p)) + { + *p = '\0'; + state = 2; + } + else if (ld_is_nl_cr(*p)) + { + printfPQExpBuffer(errorMessage, libpq_gettext( + "missing \"=\" after \"%s\" in connection info string\n"), + optname); + return 3; + } + else if (*p == '=') + { + *p = '\0'; + state = 3; + } + break; + case 2: /* after option name */ + if (*p == '=') + { + state = 3; + } + else if (!ld_is_sp_tab(*p)) + { + printfPQExpBuffer(errorMessage, libpq_gettext( + "missing \"=\" after \"%s\" in connection info string\n"), + optname); + return 3; + } + break; + case 3: /* before option value */ + if (*p == '\'') + { + optval = p + 1; + p1 = p + 1; + state = 5; + } + else if (ld_is_nl_cr(*p)) + { + optval = optname + strlen(optname); /* empty */ + state = 0; + } + else if (!ld_is_sp_tab(*p)) + { + optval = p; + state = 4; + } + break; + case 4: /* in unquoted option value */ + if (ld_is_sp_tab(*p) || ld_is_nl_cr(*p)) + { + *p = '\0'; + state = 0; + } + break; + case 5: /* in quoted option value */ + if (*p == '\'') + { + *p1 = '\0'; + state = 0; + } + else if (*p == '\\') + state = 6; + else + *(p1++) = *p; + break; + case 6: /* in quoted option value after escape */ + *(p1++) = *p; + state = 5; + break; + } + + if (state == 0 && oldstate != 0) + { + found_keyword = false; + for (i = 0; options[i].keyword; i++) + { + if (strcmp(options[i].keyword, optname) == 0) + { + if (options[i].val == NULL) + options[i].val = strdup(optval); + found_keyword = true; + break; + } + } + if (!found_keyword) + { + printfPQExpBuffer(errorMessage, + libpq_gettext("invalid connection option \"%s\"\n"), + optname); + return 1; + } + optname = NULL; + optval = NULL; + } + oldstate = state; + } + + if (state == 5 || state == 6) + { + printfPQExpBuffer(errorMessage, libpq_gettext( + "unterminated quoted string in connection info string\n")); + return 3; + } + + return 0; +} +#endif #define MAXBUFSIZE 256 @@ -2439,6 +2855,26 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage) *val; bool found_keyword; +#ifdef USE_LDAP + if (strncmp(line, "ldap", 4) == 0) + { + int rc = ldapServiceLookup(line, options, errorMessage); + /* if rc = 2, go on reading for fallback */ + switch (rc) + { + case 0: + fclose(f); + return 0; + case 1: + case 3: + fclose(f); + return 3; + case 2: + continue; + } + } +#endif + key = line; val = strchr(line, '='); if (val == NULL)