Index: doc/src/sgml/client-auth.sgml
===================================================================
RCS file: /cvsroot/pgsql/doc/src/sgml/client-auth.sgml,v
retrieving revision 1.124
diff -c -r1.124 client-auth.sgml
*** doc/src/sgml/client-auth.sgml	1 Oct 2009 01:58:57 -0000	1.124
--- doc/src/sgml/client-auth.sgml	12 Dec 2009 21:29:17 -0000
***************
*** 1202,1208 ****
     </para>
  
     <para>
!     The server will bind to the distinguished name constructed as
      <replaceable>prefix</> <replaceable>username</> <replaceable>suffix</>.
      Typically, the <replaceable>prefix</> parameter is used to specify
      <literal>cn=</>, or <replaceable>DOMAIN</><literal>\</> in an Active
--- 1202,1209 ----
     </para>
  
     <para>
!     LDAP authentication can operate in two modes. In the first mode,
!     the server will bind to the distinguished name constructed as
      <replaceable>prefix</> <replaceable>username</> <replaceable>suffix</>.
      Typically, the <replaceable>prefix</> parameter is used to specify
      <literal>cn=</>, or <replaceable>DOMAIN</><literal>\</> in an Active
***************
*** 1211,1216 ****
--- 1212,1234 ----
     </para>
  
     <para>
+     In the second mode, the server first binds to the LDAP directory with
+     a fixed username and password, specified with <replaceable>ldapbinduser</>
+     and <replaceable>ldapbinddn</>, and performs a search for the user trying
+     to log in to the database. If no user and password is configured, an
+     anonymous bind will be attempted to the directory. The search will be
+     performed over the subtree at <replaceable>ldapbasedn</>, and will try to
+     do an exact match of the attribute specified in
+     <replaceable>ldapsearchattribute</>. If no attribute is specified, the
+     <literal>uid</> attribute will be used. Once the user has been found in
+     this search, the server disconnects and re-binds to the directory as
+     this user, using the password specified by the client, to verify that the
+     login is correct. This method allows for significantly more flexibility
+     in where the user objects are located in the directory, but will cause
+     two separate connections to the LDAP server to be made.
+    </para>
+ 
+    <para>
      The following configuration options are supported for LDAP:
      <variablelist>
       <varlistentry>
***************
*** 1222,1231 ****
        </listitem>
       </varlistentry>
       <varlistentry>
        <term><literal>ldapprefix</literal></term>
        <listitem>
         <para>
!         String to prepend to the username when forming the DN to bind as.
         </para>
        </listitem>
       </varlistentry>
--- 1240,1270 ----
        </listitem>
       </varlistentry>
       <varlistentry>
+       <term><literal>ldapport</literal></term>
+       <listitem>
+        <para>
+         Port number on LDAP server to connect to. If no port is specified,
+         the default port in the LDAP library will be used.
+        </para>
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term><literal>ldaptls</literal></term>
+       <listitem>
+        <para>
+         Set to <literal>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.
+        </para>
+       </listitem>
+      </varlistentry>
+      <varlistentry>
        <term><literal>ldapprefix</literal></term>
        <listitem>
         <para>
!         String to prepend to the username when forming the DN to bind as,
!         when doing simple bind authentication.
         </para>
        </listitem>
       </varlistentry>
***************
*** 1233,1262 ****
        <term><literal>ldapsuffix</literal></term>
        <listitem>
         <para>
!         String to append to the username when forming the DN to bind as.
         </para>
        </listitem>
       </varlistentry>
       <varlistentry>
!       <term><literal>ldapport</literal></term>
        <listitem>
         <para>
!         Port number on LDAP server to connect to. If no port is specified,
!         the default port in the LDAP library will be used.
         </para>
        </listitem>
       </varlistentry>
       <varlistentry>
!       <term><literal>ldaptls</literal></term>
        <listitem>
         <para>
!         Set to <literal>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.
         </para>
        </listitem>
       </varlistentry>
      </variablelist>
     </para>
  
--- 1272,1318 ----
        <term><literal>ldapsuffix</literal></term>
        <listitem>
         <para>
!         String to append to the username when forming the DN to bind as,
!         when doing simple bind authentication.
         </para>
        </listitem>
       </varlistentry>
       <varlistentry>
!       <term><literal>ldapbasedn</literal></term>
        <listitem>
         <para>
!         DN to root the search for the user in, when doing search+bind
!         authentication.
         </para>
        </listitem>
       </varlistentry>
       <varlistentry>
!       <term><literal>ldapbinddn</literal></term>
        <listitem>
         <para>
!         DN of user to bind to the directory with to perform the search when
!         doing search+bind authentication.
         </para>
        </listitem>
       </varlistentry>
+      <varlistentry>
+       <term><literal>ldapbindpasswd</literal></term>
+       <listitem>
+        <para>
+         Password for user to bind to the directory with to perform the search
+         when doing search+bind authentication.
+        </para>
+       </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>ldapsearchattribute</literal></term>
+        <listitem>
+         <para>
+          Attribute to match against the username in the search when doing
+          search+bind authentication.
+         </para>
+        </listitem>
+       </varlistentry>
      </variablelist>
     </para>
  
Index: src/backend/libpq/auth.c
===================================================================
RCS file: /cvsroot/pgsql/src/backend/libpq/auth.c,v
retrieving revision 1.187
diff -c -r1.187 auth.c
*** src/backend/libpq/auth.c	16 Oct 2009 22:08:36 -0000	1.187
--- src/backend/libpq/auth.c	12 Dec 2009 21:29:17 -0000
***************
*** 2096,2135 ****
   */
  #ifdef USE_LDAP
  
  static int
! CheckLDAPAuth(Port *port)
  {
- 	char	   *passwd;
- 	LDAP	   *ldap;
- 	int			r;
  	int			ldapversion = LDAP_VERSION3;
! 	char		fulluser[NAMEDATALEN + 256 + 1];
! 
! 	if (!port->hba->ldapserver || port->hba->ldapserver[0] == '\0')
! 	{
! 		ereport(LOG,
! 				(errmsg("LDAP server not specified")));
! 		return STATUS_ERROR;
! 	}
! 
! 	if (port->hba->ldapport == 0)
! 		port->hba->ldapport = LDAP_PORT;
! 
! 	sendAuthRequest(port, AUTH_REQ_PASSWORD);
! 
! 	passwd = recv_password_packet(port);
! 	if (passwd == NULL)
! 		return STATUS_EOF;		/* client wouldn't send password */
! 
! 	if (strlen(passwd) == 0)
! 	{
! 		ereport(LOG,
! 				(errmsg("empty password returned by client")));
! 		return STATUS_ERROR;
! 	}
  
! 	ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport);
! 	if (!ldap)
  	{
  #ifndef WIN32
  		ereport(LOG,
--- 2096,2113 ----
   */
  #ifdef USE_LDAP
  
+ /*
+  * Initialize a connection to the LDAP server, including setting up
+  * TLS if requested.
+  */
  static int
! InitializeLDAPConnection(Port *port, LDAP **ldap)
  {
  	int			ldapversion = LDAP_VERSION3;
! 	int			r;
  
! 	*ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport);
! 	if (!*ldap)
  	{
  #ifndef WIN32
  		ereport(LOG,
***************
*** 2143,2151 ****
  		return STATUS_ERROR;
  	}
  
! 	if ((r = ldap_set_option(ldap, LDAP_OPT_PROTOCOL_VERSION, &ldapversion)) != LDAP_SUCCESS)
  	{
! 		ldap_unbind(ldap);
  		ereport(LOG,
  		  (errmsg("could not set LDAP protocol version: error code %d", r)));
  		return STATUS_ERROR;
--- 2121,2129 ----
  		return STATUS_ERROR;
  	}
  
! 	if ((r = ldap_set_option(*ldap, LDAP_OPT_PROTOCOL_VERSION, &ldapversion)) != LDAP_SUCCESS)
  	{
! 		ldap_unbind(*ldap);
  		ereport(LOG,
  		  (errmsg("could not set LDAP protocol version: error code %d", r)));
  		return STATUS_ERROR;
***************
*** 2154,2160 ****
  	if (port->hba->ldaptls)
  	{
  #ifndef WIN32
! 		if ((r = ldap_start_tls_s(ldap, NULL, NULL)) != LDAP_SUCCESS)
  #else
  		static __ldap_start_tls_sA _ldap_start_tls_sA = NULL;
  
--- 2132,2138 ----
  	if (port->hba->ldaptls)
  	{
  #ifndef WIN32
! 		if ((r = ldap_start_tls_s(*ldap, NULL, NULL)) != LDAP_SUCCESS)
  #else
  		static __ldap_start_tls_sA _ldap_start_tls_sA = NULL;
  
***************
*** 2174,2180 ****
  				 * should never happen since we import other files from
  				 * wldap32, but check anyway
  				 */
! 				ldap_unbind(ldap);
  				ereport(LOG,
  						(errmsg("could not load wldap32.dll")));
  				return STATUS_ERROR;
--- 2152,2158 ----
  				 * should never happen since we import other files from
  				 * wldap32, but check anyway
  				 */
! 				ldap_unbind(*ldap);
  				ereport(LOG,
  						(errmsg("could not load wldap32.dll")));
  				return STATUS_ERROR;
***************
*** 2182,2188 ****
  			_ldap_start_tls_sA = (__ldap_start_tls_sA) GetProcAddress(ldaphandle, "ldap_start_tls_sA");
  			if (_ldap_start_tls_sA == NULL)
  			{
! 				ldap_unbind(ldap);
  				ereport(LOG,
  						(errmsg("could not load function _ldap_start_tls_sA in wldap32.dll"),
  						 errdetail("LDAP over SSL is not supported on this platform.")));
--- 2160,2166 ----
  			_ldap_start_tls_sA = (__ldap_start_tls_sA) GetProcAddress(ldaphandle, "ldap_start_tls_sA");
  			if (_ldap_start_tls_sA == NULL)
  			{
! 				ldap_unbind(*ldap);
  				ereport(LOG,
  						(errmsg("could not load function _ldap_start_tls_sA in wldap32.dll"),
  						 errdetail("LDAP over SSL is not supported on this platform.")));
***************
*** 2195,2215 ****
  			 * per process and is automatically cleaned up on process exit.
  			 */
  		}
! 		if ((r = _ldap_start_tls_sA(ldap, NULL, NULL, NULL, NULL)) != LDAP_SUCCESS)
  #endif
  		{
! 			ldap_unbind(ldap);
  			ereport(LOG,
  			 (errmsg("could not start LDAP TLS session: error code %d", r)));
  			return STATUS_ERROR;
  		}
  	}
  
! 	snprintf(fulluser, sizeof(fulluser), "%s%s%s",
! 			 port->hba->ldapprefix ? port->hba->ldapprefix : "",
! 			 port->user_name,
! 			 port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
! 	fulluser[sizeof(fulluser) - 1] = '\0';
  
  	r = ldap_simple_bind_s(ldap, fulluser, passwd);
  	ldap_unbind(ldap);
--- 2173,2374 ----
  			 * per process and is automatically cleaned up on process exit.
  			 */
  		}
! 		if ((r = _ldap_start_tls_sA(*ldap, NULL, NULL, NULL, NULL)) != LDAP_SUCCESS)
  #endif
  		{
! 			ldap_unbind(*ldap);
  			ereport(LOG,
  			 (errmsg("could not start LDAP TLS session: error code %d", r)));
  			return STATUS_ERROR;
  		}
  	}
  
! 	return STATUS_OK;
! }
! 
! /*
!  * Perform LDAP authentication
!  */
! static int
! CheckLDAPAuth(Port *port)
! {
! 	char	   *passwd;
! 	LDAP	   *ldap;
! 	int			r;
! 	char	   *fulluser;
! 
! 	if (!port->hba->ldapserver || port->hba->ldapserver[0] == '\0')
! 	{
! 		ereport(LOG,
! 				(errmsg("LDAP server not specified")));
! 		return STATUS_ERROR;
! 	}
! 
! 	if (port->hba->ldapport == 0)
! 		port->hba->ldapport = LDAP_PORT;
! 
! 	sendAuthRequest(port, AUTH_REQ_PASSWORD);
! 
! 	passwd = recv_password_packet(port);
! 	if (passwd == NULL)
! 		return STATUS_EOF;		/* client wouldn't send password */
! 
! 	if (strlen(passwd) == 0)
! 	{
! 		ereport(LOG,
! 				(errmsg("empty password returned by client")));
! 		return STATUS_ERROR;
! 	}
! 
! 	if (InitializeLDAPConnection(port, &ldap) == STATUS_ERROR)
! 		/* Error message already sent */
! 		return STATUS_ERROR;
! 
! 	if (port->hba->ldapbasedn)
! 	{
! 		/*
! 		 * First perform an LDAP search to find the DN for the user we are trying to log
! 		 * in as.
! 		 */
! 		char		   *filter;
! 		LDAPMessage	   *search_message;
! 		LDAPMessage	   *entry;
! 		char		   *attributes[2];
! 		char		   *dn;
! 		char		   *c;
! 
! 		/*
! 		 * Disallow any characters that we would otherwise need to escape, since they
! 		 * aren't really reasonable in a username anyway. Allowing them would make it
! 		 * possible to inject any kind of custom filters in the LDAP filter.
! 		 */
! 		for (c = port->user_name; *c; c++)
! 		{
! 			if (*c == '*' ||
! 				*c == '(' ||
! 				*c == ')' ||
! 				*c == '\\' ||
! 				*c == '/')
! 			{
! 				ereport(LOG,
! 						(errmsg("invalid character in username for LDAP authentication")));
! 				return STATUS_ERROR;
! 			}
! 		}
! 
! 		/*
! 		 * Bind with a pre-defined username/password (if available) for searching. If
! 		 * none is specified, this turns into an anonymous bind.
! 		 */
! 		r = ldap_simple_bind_s(ldap,
! 							   port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
! 							   port->hba->ldapbindpasswd ? port->hba->ldapbindpasswd : "");
! 		if (r != LDAP_SUCCESS)
! 		{
! 			ereport(LOG,
! 					(errmsg("could not perform initial LDAP bind for ldapbinddn \"%s\" on server \"%s\": error code %d",
! 							port->hba->ldapbinddn, port->hba->ldapserver, r)));
! 			return STATUS_ERROR;
! 		}
! 
! 		/* Fetch just one attribute, else *all* attributes are returned */
! 		attributes[0] = port->hba->ldapsearchattribute ? port->hba->ldapsearchattribute : "uid";
! 		attributes[1] = NULL;
! 
! 		filter = palloc(strlen(attributes[0])+strlen(port->user_name)+4);
! 		sprintf(filter, "(%s=%s)",
! 				 attributes[0],
! 				 port->user_name);
! 
! 		r = ldap_search_s(ldap,
! 						  port->hba->ldapbasedn,
! 						  LDAP_SCOPE_SUBTREE,
! 						  filter,
! 						  attributes,
! 						  0,
! 						  &search_message);
! 
! 		if (r != LDAP_SUCCESS)
! 		{
! 			ereport(LOG,
! 					(errmsg("could not search LDAP for filter \"%s\" on server \"%s\": error code %d",
! 							filter, port->hba->ldapserver, r)));
! 			pfree(filter);
! 			return STATUS_ERROR;
! 		}
! 
! 		if (ldap_count_entries(ldap, search_message) != 1)
! 		{
! 			if (ldap_count_entries(ldap, search_message) == 0)
! 				ereport(LOG,
! 						(errmsg("LDAP search failed for filter \"%s\" on server \"%s\": no such user",
! 								filter, port->hba->ldapserver)));
! 			else
! 				ereport(LOG,
! 						(errmsg("LDAP search failed for filter \"%s\" on server \"%s\": user is not unique (%d matches)",
! 								filter, port->hba->ldapserver, ldap_count_entries(ldap, search_message))));
! 
! 			pfree(filter);
! 			ldap_msgfree(search_message);
! 			return STATUS_ERROR;
! 		}
! 
! 		entry = ldap_first_entry(ldap, search_message);
! 		dn = ldap_get_dn(ldap, entry);
! 		if (dn == NULL)
! 		{
! 			int error;
! 			(void)ldap_get_option(ldap, LDAP_OPT_ERROR_NUMBER, &error);
! 			ereport(LOG,
! 					(errmsg("could not get dn for the first entry matching \"%s\" on server \"%s\": %s",
! 							filter, port->hba->ldapserver, ldap_err2string(error))));
! 			pfree(filter);
! 			ldap_msgfree(search_message);
! 			return STATUS_ERROR;
! 		}
! 		fulluser = pstrdup(dn);
! 
! 		pfree(filter);
! 		ldap_memfree(dn);
! 		ldap_msgfree(search_message);
! 
! 		/* Unbind and disconnect from the LDAP server */
! 		r = ldap_unbind_s(ldap);
! 		if (r != LDAP_SUCCESS)
! 		{
! 			int error;
! 			(void)ldap_get_option(ldap, LDAP_OPT_ERROR_NUMBER, &error);
! 			ereport(LOG,
! 					(errmsg("could not unbind after searching for user \"%s\" on server \"%s\": %s",
! 							fulluser, port->hba->ldapserver, ldap_err2string(error))));
! 			pfree(fulluser);
! 			return STATUS_ERROR;
! 		}
! 
! 		/*
! 		 * Need to re-initialize the LDAP connection, so that we can bind
! 		 * to it with a different username.
! 		 */
! 		if (InitializeLDAPConnection(port, &ldap) == STATUS_ERROR)
! 		{
! 			pfree(fulluser);
! 
! 			/* Error message already sent */
! 			return STATUS_ERROR;
! 		}
! 	}
! 	else
! 	{
! 		fulluser = palloc((port->hba->ldapprefix ? strlen(port->hba->ldapprefix) : 0) +
! 						  strlen(port->user_name) +
! 						  (port->hba->ldapsuffix ? strlen(port->hba->ldapsuffix) : 0) +
! 						  1);
! 
! 		sprintf(fulluser, "%s%s%s",
! 				 port->hba->ldapprefix ? port->hba->ldapprefix : "",
! 				 port->user_name,
! 				 port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
! 	}
  
  	r = ldap_simple_bind_s(ldap, fulluser, passwd);
  	ldap_unbind(ldap);
***************
*** 2219,2227 ****
--- 2378,2389 ----
  		ereport(LOG,
  				(errmsg("LDAP login failed for user \"%s\" on server \"%s\": error code %d",
  						fulluser, port->hba->ldapserver, r)));
+ 		pfree(fulluser);
  		return STATUS_ERROR;
  	}
  
+ 	pfree(fulluser);
+ 
  	return STATUS_OK;
  }
  #endif   /* USE_LDAP */
Index: src/backend/libpq/hba.c
===================================================================
RCS file: /cvsroot/pgsql/src/backend/libpq/hba.c,v
retrieving revision 1.192
diff -c -r1.192 hba.c
*** src/backend/libpq/hba.c	3 Oct 2009 20:04:39 -0000	1.192
--- src/backend/libpq/hba.c	12 Dec 2009 21:29:17 -0000
***************
*** 1103,1108 ****
--- 1103,1128 ----
  					return false;
  				}
  			}
+ 			else if (strcmp(token, "ldapbinddn") == 0)
+ 			{
+ 				REQUIRE_AUTH_OPTION(uaLDAP, "ldapbinddn", "ldap");
+ 				parsedline->ldapbinddn = pstrdup(c);
+ 			}
+ 			else if (strcmp(token, "ldapbindpasswd") == 0)
+ 			{
+ 				REQUIRE_AUTH_OPTION(uaLDAP, "ldapbindpasswd", "ldap");
+ 				parsedline->ldapbindpasswd = pstrdup(c);
+ 			}
+ 			else if (strcmp(token, "ldapsearchattribute") == 0)
+ 			{
+ 				REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
+ 				parsedline->ldapsearchattribute = pstrdup(c);
+ 			}
+ 			else if (strcmp(token, "ldapbasedn") == 0)
+ 			{
+ 				REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
+ 				parsedline->ldapbasedn = pstrdup(c);
+ 			}
  			else if (strcmp(token, "ldapprefix") == 0)
  			{
  				REQUIRE_AUTH_OPTION(uaLDAP, "ldapprefix", "ldap");
***************
*** 1156,1161 ****
--- 1176,1212 ----
  	if (parsedline->auth_method == uaLDAP)
  	{
  		MANDATORY_AUTH_ARG(parsedline->ldapserver, "ldapserver", "ldap");
+ 
+ 		/*
+ 		 * LDAP can operate in two modes: either with a direct bind, using
+ 		 * ldapprefix and ldapsuffix, or using a search+bind,
+ 		 * using ldapbasedn, ldapbinddn, ldapbindpasswd and ldapsearchattribute.
+ 		 * Disallow mixing these parameters.
+ 		 */
+ 		if (parsedline->ldapprefix || parsedline->ldapsuffix)
+ 		{
+ 			if (parsedline->ldapbasedn ||
+ 				parsedline->ldapbinddn ||
+ 				parsedline->ldapbindpasswd ||
+ 				parsedline->ldapsearchattribute)
+ 			{
+ 				ereport(LOG,
+ 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
+ 						 errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd or ldapsearchattribute together with ldapprefix"),
+ 						 errcontext("line %d of configuration file \"%s\"",
+ 									line_num, HbaFileName)));
+ 				return false;
+ 			}
+ 		}
+ 		else if (!parsedline->ldapbasedn)
+ 		{
+ 			ereport(LOG,
+ 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+ 					 errmsg("authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\" or \"ldapsuffix\" to be set"),
+ 					 errcontext("line %d of configuration file \"%s\"",
+ 								line_num, HbaFileName)));
+ 			return false;
+ 		}
  	}
  
  	/*
Index: src/include/libpq/hba.h
===================================================================
RCS file: /cvsroot/pgsql/src/include/libpq/hba.h,v
retrieving revision 1.59
diff -c -r1.59 hba.h
*** src/include/libpq/hba.h	1 Oct 2009 01:58:58 -0000	1.59
--- src/include/libpq/hba.h	12 Dec 2009 21:29:17 -0000
***************
*** 61,66 ****
--- 61,70 ----
  	bool		ldaptls;
  	char	   *ldapserver;
  	int			ldapport;
+ 	char	   *ldapbinddn;
+ 	char	   *ldapbindpasswd;
+ 	char	   *ldapsearchattribute;
+ 	char	   *ldapbasedn;
  	char	   *ldapprefix;
  	char	   *ldapsuffix;
  	bool		clientcert;
