Thread: LDAP lookup of connection parameters
This patch for libpq allows you to enter an LDAP URL in pg_service.conf. The URL will be queried and the resulting string(s) parsed for keyword = value connection options. The idea is to have connection information stored centrally on an LDAP server rather than on the client machine. On Windows the native library wldap32.dll is used, else OpenLDAP. If --enable_thread_safety has been given, -lldap_r is appended to PTHREAD_LIBS so that libpq will be linked against the tread safe library. There should probably also be a documentation patch for the --with-ldap option of ./configure, but I didn't write it because it also belongs to the "LDAP Auth" patch. I have added German translations for the new messages - how can I get translations into other languages? Yours, Laurenz Albe
Attachment
> This patch for libpq allows you to enter an LDAP URL in pg_service.conf. > The URL will be queried and the resulting string(s) parsed for > keyword = value connection options. > > The idea is to have connection information stored centrally on an LDAP > server rather than on the client machine. I forgot to mention that there was a discussion on Hackers about this: http://archives.postgresql.org/pgsql-hackers/2006-02/msg01182.php I have implemented a solution following the idea of http://archives.postgresql.org/pgsql-hackers/2006-02/msg01198.php because I thought that it was better to have something existing to talk about. Yours, Laurenz Albe
> This patch for libpq allows you to enter an LDAP URL in pg_service.conf. > The URL will be queried and the resulting string(s) parsed for > keyword = value connection options. > > The idea is to have connection information stored centrally on an LDAP > server rather than on the client machine. I forgot to mention that there was a brief discussion about this on Hackers: http://archives.postgresql.org/pgsql-hackers/2006-02/msg00828.php My implementation follows the idea from http://archives.postgresql.org/pgsql-hackers/2006-02/msg01198.php I thought it would be good to have some real code as a basis for further discussion whether this is a desirable feature or not. I felt somewhat encouraged because PostgreSQL already has a dependency on OpenLDAP since http://archives.postgresql.org/pgsql-patches/2005-12/msg00375.php and I do not have to introduce a new dependency. Yours, Laurenz Albe
Albe Laurenz wrote: > > This patch for libpq allows you to enter an LDAP URL in > pg_service.conf. > > The URL will be queried and the resulting string(s) parsed for > > keyword = value connection options. > > > > The idea is to have connection information stored centrally on an LDAP > > server rather than on the client machine. > > I forgot to mention that there was a brief discussion about this on > Hackers: > http://archives.postgresql.org/pgsql-hackers/2006-02/msg00828.php > > My implementation follows the idea from > http://archives.postgresql.org/pgsql-hackers/2006-02/msg01198.php > > I thought it would be good to have some real code as a basis for > further discussion whether this is a desirable feature or not. > > I felt somewhat encouraged because PostgreSQL already has a > dependency on OpenLDAP since > http://archives.postgresql.org/pgsql-patches/2005-12/msg00375.php > and I do not have to introduce a new dependency. Where are we on this? It allows pg_service.conf to query LDAP for connection strings. Is it a feature people want? -- Bruce Momjian http://candle.pha.pa.us EnterpriseDB http://www.enterprisedb.com + If your life is a hard drive, Christ can be your backup. +
I am confused why this patch requires libldap_r. Is there a need for threading? Should this be contingent on whether the threading flag was passed to configure? --------------------------------------------------------------------------- Albe Laurenz wrote: > This patch for libpq allows you to enter an LDAP URL in pg_service.conf. > The URL will be queried and the resulting string(s) parsed for > keyword = value connection options. > > The idea is to have connection information stored centrally on an LDAP > server rather than on the client machine. > > On Windows the native library wldap32.dll is used, else OpenLDAP. > If --enable_thread_safety has been given, -lldap_r is appended to > PTHREAD_LIBS so that libpq will be linked against the tread safe > library. > > There should probably also be a documentation patch for the --with-ldap > option of ./configure, but I didn't write it because it also belongs to > the > "LDAP Auth" patch. > > I have added German translations for the new messages - how can I get > translations into other languages? > > Yours, > Laurenz Albe Content-Description: ldap_service.patch [ Attachment, skipping... ] Content-Description: ldap_service_doc.patch [ Attachment, skipping... ] > > ---------------------------(end of broadcast)--------------------------- > TIP 6: explain analyze is your friend -- Bruce Momjian bruce@momjian.us EnterpriseDB http://www.enterprisedb.com + If your life is a hard drive, Christ can be your backup. +
Albe Laurenz wrote: > This patch for libpq allows you to enter an LDAP URL in pg_service.conf. > The URL will be queried and the resulting string(s) parsed for > keyword = value connection options. > > The idea is to have connection information stored centrally on an LDAP > server rather than on the client machine. > > On Windows the native library wldap32.dll is used, else OpenLDAP. > If --enable_thread_safety has been given, -lldap_r is appended to > PTHREAD_LIBS so that libpq will be linked against the tread safe > library. > > There should probably also be a documentation patch for the --with-ldap > option of ./configure, but I didn't write it because it also belongs to > the "LDAP Auth" patch. > > I have added German translations for the new messages - how can I get > translations into other languages? Translations are done later in the release process. I have heavily modified your patch to be clearer. Please review the attached version and test it to make sure it still works properly. Thanks. -- Bruce Momjian bruce@momjian.us EnterpriseDB http://www.enterprisedb.com + If your life is a hard drive, Christ can be your backup. + Index: configure.in =================================================================== RCS file: /cvsroot/pgsql/configure.in,v retrieving revision 1.469 diff -c -c -r1.469 configure.in *** configure.in 24 Jul 2006 16:32:44 -0000 1.469 --- configure.in 25 Jul 2006 21:44:20 -0000 *************** *** 1106,1111 **** --- 1106,1119 ---- 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" Index: doc/src/sgml/libpq.sgml =================================================================== RCS file: /cvsroot/pgsql/doc/src/sgml/libpq.sgml,v retrieving revision 1.213 diff -c -c -r1.213 libpq.sgml *** doc/src/sgml/libpq.sgml 4 Jul 2006 13:22:15 -0000 1.213 --- doc/src/sgml/libpq.sgml 25 Jul 2006 21:44:23 -0000 *************** *** 4126,4131 **** --- 4126,4197 ---- </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> Index: src/interfaces/libpq/Makefile =================================================================== RCS file: /cvsroot/pgsql/src/interfaces/libpq/Makefile,v retrieving revision 1.146 diff -c -c -r1.146 Makefile *** src/interfaces/libpq/Makefile 18 Jul 2006 22:18:08 -0000 1.146 --- src/interfaces/libpq/Makefile 25 Jul 2006 21:44:27 -0000 *************** *** 62,68 **** 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)) endif --- 62,68 ---- 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 -lwldap32, $(LIBS)) endif Index: src/interfaces/libpq/fe-connect.c =================================================================== RCS file: /cvsroot/pgsql/src/interfaces/libpq/fe-connect.c,v retrieving revision 1.333 diff -c -c -r1.333 fe-connect.c *** src/interfaces/libpq/fe-connect.c 7 Jun 2006 22:24:46 -0000 1.333 --- src/interfaces/libpq/fe-connect.c 25 Jul 2006 21:44:29 -0000 *************** *** 60,65 **** --- 60,78 ---- #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,2349 **** --- 2356,2762 ---- 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 componient 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"), url); + free(url); + return 3; + } + + 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"), url); + free(url); + return 3; + } + *p = '\0'; /* terminate hostname */ + dn = p + 1; + + /* port number? */ + if (p1 = strchr(dn, ':')) != 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"), url); + free(url); + return 3; + } + port = (int) lport; + } + /* Is there an 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"), url); + free(url); + return 3; + } + *p = '\0'; + attrs[0] = p + 1; + + /* Allow only one attribute */ + if (strchr(attrs[0], ',') != NULL) + { + printfPQExpBuffer(errorMessage, libpq_gettext( + "bad LDAP URL \"%s\": must have exactly one attribute\n"), url); + free(url); + return 3; + } + + /* 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"), url); + free(url); + return 3; + } + *p = '\0'; + scopestr = p + 1; + 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"), url); + free(url); + return 3; + } + + /* filter */ + if ((p = strchr(scopestr, '?')) == NULL || *(p + 1) == '\0' || *(p + 1) == '?') + { + printfPQExpBuffer(errorMessage, + libpq_gettext("bad LDAP URL \"%s\": no filter\n"), url); + free(url); + return 3; + } + *p = '\0'; + filter = p + 1; + if ((p = strchr(filter, '?')) != NULL) + *p = '\0'; + + /* 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; NULL != values[i]; ++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,2444 **** --- 2852,2877 ---- *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)
Bruce Momjian wrote: > Albe Laurenz wrote: >> This patch for libpq allows you to enter an LDAP URL in pg_service.conf. >> The URL will be queried and the resulting string(s) parsed for >> keyword = value connection options. > > I have heavily modified your patch to be clearer. Please review the > attached version and test it to make sure it still works properly. > Thanks. Most of your modifications are fine, but a quick look tells me that your modifications in the parsing of the LDAP URL have been too invasive, e.g.: - you look for the port number in the 'dn' and not in the 'hostname' - you check the validity of 'scopestr' and 'attrs[0]' before it is '\0'-terminated Would you prefer that I try to fix your fixes (and stick with your coding style) or do you want another go? Yours, Laurenz Albe
Albe Laurenz wrote: > Bruce Momjian wrote: > > Albe Laurenz wrote: > >> This patch for libpq allows you to enter an LDAP URL in > pg_service.conf. > >> The URL will be queried and the resulting string(s) parsed for > >> keyword = value connection options. > > > > I have heavily modified your patch to be clearer. Please review the > > attached version and test it to make sure it still works properly. > > Thanks. > > Most of your modifications are fine, but a quick look tells me that your > modifications in the parsing of the LDAP URL have been too invasive, > e.g.: > > - you look for the port number in the 'dn' and not in the 'hostname' > - you check the validity of 'scopestr' and 'attrs[0]' before it is > '\0'-terminated > > Would you prefer that I try to fix your fixes (and stick with your > coding style) > or do you want another go? Thanks for the review. Updated patch attached. Is that OK? -- Bruce Momjian bruce@momjian.us EnterpriseDB http://www.enterprisedb.com + If your life is a hard drive, Christ can be your backup. + Index: configure.in =================================================================== RCS file: /cvsroot/pgsql/configure.in,v retrieving revision 1.469 diff -c -c -r1.469 configure.in *** configure.in 24 Jul 2006 16:32:44 -0000 1.469 --- configure.in 26 Jul 2006 16:38:39 -0000 *************** *** 1106,1111 **** --- 1106,1119 ---- 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" Index: doc/src/sgml/libpq.sgml =================================================================== RCS file: /cvsroot/pgsql/doc/src/sgml/libpq.sgml,v retrieving revision 1.213 diff -c -c -r1.213 libpq.sgml *** doc/src/sgml/libpq.sgml 4 Jul 2006 13:22:15 -0000 1.213 --- doc/src/sgml/libpq.sgml 26 Jul 2006 16:38:41 -0000 *************** *** 4126,4131 **** --- 4126,4197 ---- </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> Index: src/interfaces/libpq/Makefile =================================================================== RCS file: /cvsroot/pgsql/src/interfaces/libpq/Makefile,v retrieving revision 1.146 diff -c -c -r1.146 Makefile *** src/interfaces/libpq/Makefile 18 Jul 2006 22:18:08 -0000 1.146 --- src/interfaces/libpq/Makefile 26 Jul 2006 16:38:49 -0000 *************** *** 62,68 **** 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)) endif --- 62,68 ---- 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 -lwldap32, $(LIBS)) endif Index: src/interfaces/libpq/fe-connect.c =================================================================== RCS file: /cvsroot/pgsql/src/interfaces/libpq/fe-connect.c,v retrieving revision 1.333 diff -c -c -r1.333 fe-connect.c *** src/interfaces/libpq/fe-connect.c 7 Jun 2006 22:24:46 -0000 1.333 --- src/interfaces/libpq/fe-connect.c 26 Jul 2006 16:38:58 -0000 *************** *** 60,65 **** --- 60,78 ---- #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,2349 **** --- 2356,2765 ---- 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 componient 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"), url); + 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"), url); + 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"), url); + 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"), url); + 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"), url); + 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"), url); + 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"), url); + 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"), url); + 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; NULL != values[i]; ++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,2444 **** --- 2855,2880 ---- *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)
Bruce Momjian wrote: >>>> This patch for libpq allows you to enter an LDAP URL in >>>> pg_service.conf. >>>> The URL will be queried and the resulting string(s) parsed for >>>> keyword = value connection options. >>> >>> I have heavily modified your patch to be clearer. Please >>> review the >>> attached version and test it to make sure it still works >>> properly. > > Thanks for the review. Updated patch attached. Is that OK? Mostly yes, and I must admit that the code has become more readable. There were two syntax errors and some minor odds and ends (partly my fault). I have slightly changed your patch and submit it again (diff the patches to see). Thanks for the effort, Laurenz Albe
Attachment
Patch applied. Thanks. --------------------------------------------------------------------------- Albe Laurenz wrote: > Bruce Momjian wrote: > >>>> This patch for libpq allows you to enter an LDAP URL in > >>>> pg_service.conf. > >>>> The URL will be queried and the resulting string(s) parsed for > >>>> keyword = value connection options. > >>> > >>> I have heavily modified your patch to be clearer. Please > >>> review the > >>> attached version and test it to make sure it still works > >>> properly. > > > > Thanks for the review. Updated patch attached. Is that OK? > > Mostly yes, and I must admit that the code has become more > readable. There were two syntax errors and some minor odds > and ends (partly my fault). > > I have slightly changed your patch and submit it again > (diff the patches to see). > > Thanks for the effort, > Laurenz Albe > Content-Description: ldap_v2.patch [ Attachment, skipping... ] -- Bruce Momjian bruce@momjian.us EnterpriseDB http://www.enterprisedb.com + If your life is a hard drive, Christ can be your backup. +