Re: LDAP lookup of connection parameters - Mailing list pgsql-patches

From Bruce Momjian
Subject Re: LDAP lookup of connection parameters
Date
Msg-id 200607261646.k6QGk2L09408@momjian.us
Whole thread Raw
In response to Re: LDAP lookup of connection parameters  ("Albe Laurenz" <all@adv.magwien.gv.at>)
List pgsql-patches
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)

pgsql-patches by date:

Previous
From: Tom Lane
Date:
Subject: Re: [HACKERS] Resurrecting per-page cleaner for btree
Next
From: Greg Stark
Date:
Subject: Re: [HACKERS] Resurrecting per-page cleaner for btree