From e7194dc73ad65488233981652da57f87d3b9357a Mon Sep 17 00:00:00 2001
From: Thomas Munro <munro@ip9.org>
Date: Thu, 13 Jul 2017 19:23:06 +1200
Subject: [PATCH] Allow custom search filters to be configured for LDAP auth.

Before, only filters of the form "(<ldapsearchattribute>=<user>)"
could be used to search an LDAP server.  Allow filters to be built
from prefix + user + suffix so that extra restrictions can be placed
on the set of LDAP entries that can be used to log into PostgreSQL.
---
 doc/src/sgml/client-auth.sgml | 30 +++++++++++++++++++++
 src/backend/libpq/auth.c      | 11 +++++---
 src/backend/libpq/hba.c       | 61 ++++++++++++++++++++++++++++++++++++++++---
 src/include/libpq/hba.h       |  2 ++
 4 files changed, 97 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 819db811b2..825c89ebe3 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1508,6 +1508,25 @@ omicron         bryanh                  guest1
        </listitem>
       </varlistentry>
       <varlistentry>
+       <term><literal>ldapsearchprefix</literal></term>
+       <listitem>
+        <para>
+         The prefix of a search filter used to search for the user name
+         when doing search+bind authentication.  This allows for more general
+         search filters than <literal>ldapsearchattribute</literal>.
+        </para>
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term><literal>ldapsearchsuffix</literal></term>
+       <listitem>
+        <para>
+         The suffix of a search filter used to search for the user name
+         when doing search+bind authentication.
+        </para>
+       </listitem>
+      </varlistentry>
+      <varlistentry>
        <term><literal>ldapurl</literal></term>
        <listitem>
         <para>
@@ -1550,6 +1569,17 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
    </para>
 
    <para>
+    When using search+bind mode, the search can be performed either with a
+    single attribute specified with <literal>ldapsearchattribute</literal>,
+    or with a custom search filter specified with
+    <literal>ldapsearchprefix</literal> and <literal>ldapsearchsuffix</literal>.
+    Specifying <literal>ldapsearchattribute=foo</literal> is equivalent
+    to specifying <literal>ldapsearchprefix="(foo=" ldapsearchsuffix=")"</literal>.
+    If none of these options are specified explicitly, the default is
+    <literal>ldapsearchattribute=uid</literal>.
+   </para>
+
+   <para>
     Here is an example for a simple-bind LDAP configuration:
 <programlisting>
 host ... ldap ldapserver=ldap.example.net ldapprefix="cn=" ldapsuffix=", dc=example, dc=net"
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index dd7de7c3a4..c3c9008c8c 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2471,9 +2471,14 @@ CheckLDAPAuth(Port *port)
 		attributes[0] = port->hba->ldapsearchattribute ? port->hba->ldapsearchattribute : "uid";
 		attributes[1] = NULL;
 
-		filter = psprintf("(%s=%s)",
-						  attributes[0],
-						  port->user_name);
+		/* Build an entirely custom filter or a single attribute filter? */
+		if (port->hba->ldapsearchprefix != NULL)
+				filter = psprintf("%s%s%s",
+								  port->hba->ldapsearchprefix,
+								  port->user_name,
+								  port->hba->ldapsearchsuffix);
+		else
+				filter = psprintf("(%s=%s)", attributes[0], port->user_name);
 
 		r = ldap_search_s(ldap,
 						  port->hba->ldapbasedn,
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 42afead9fd..92844edb18 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1505,7 +1505,8 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
 		/*
 		 * 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.
+		 * ldapbasedn, ldapbinddn, ldapbindpasswd and either
+		 * ldapsearchattribute or ldapsearchprefix and ldapsearchsuffix.
 		 * Disallow mixing these parameters.
 		 */
 		if (parsedline->ldapprefix || parsedline->ldapsuffix)
@@ -1513,14 +1514,16 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
 			if (parsedline->ldapbasedn ||
 				parsedline->ldapbinddn ||
 				parsedline->ldapbindpasswd ||
-				parsedline->ldapsearchattribute)
+				parsedline->ldapsearchattribute ||
+				parsedline->ldapsearchprefix ||
+				parsedline->ldapsearchsuffix)
 			{
 				ereport(elevel,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
-						 errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix"),
+						 errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchprefix, ldapsearchsuffix or ldapurl together with ldapprefix"),
 						 errcontext("line %d of configuration file \"%s\"",
 									line_num, HbaFileName)));
-				*err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix";
+				*err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchprefix or ldapsearchsuffix or ldapurl together with ldapprefix";
 				return NULL;
 			}
 		}
@@ -1534,6 +1537,36 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
 			*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
 			return NULL;
 		}
+
+		/*
+		 * When using search+bind, you can either user a simple attribute
+		 * defaulting to "uid" or a fully custom search filter.  You can't
+		 * do both.
+		 */
+		if (parsedline->ldapsearchattribute &&
+			(parsedline->ldapsearchprefix || parsedline->ldapsearchsuffix))
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("cannot use ldapsearchattribute together with ldapsearchprefix and ldapsearchsuffix"),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = "cannot use ldapsearchattribute together with ldapsearchprefix and ldapsearchsuffix";
+			return NULL;
+		}
+
+		/* If using a fully custom filter, you need to supply both ends. */
+		if ((parsedline->ldapsearchprefix != NULL) !=
+			(parsedline->ldapsearchsuffix != NULL))
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("ldapsearchprefix and ldapsearchsuffix must be specified together"),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = "ldapsearchprefix and ldapsearchsuffix must be specified together";
+			return NULL;
+		}
 	}
 
 	if (parsedline->auth_method == uaRADIUS)
@@ -1788,6 +1821,16 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 		REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
 		hbaline->ldapsearchattribute = pstrdup(val);
 	}
+	else if (strcmp(name, "ldapsearchprefix") == 0)
+	{
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchprefix", "ldap");
+		hbaline->ldapsearchprefix = pstrdup(val);
+	}
+	else if (strcmp(name, "ldapsearchsuffix") == 0)
+	{
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchsuffix", "ldap");
+		hbaline->ldapsearchsuffix = pstrdup(val);
+	}
 	else if (strcmp(name, "ldapbasedn") == 0)
 	{
 		REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
@@ -2266,6 +2309,16 @@ gethba_options(HbaLine *hba)
 				CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
 											 hba->ldapsearchattribute));
 
+		if (hba->ldapsearchprefix)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapsearchprefix=%s",
+											 hba->ldapsearchprefix));
+
+		if (hba->ldapsearchsuffix)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapsearchsuffix=%s",
+											 hba->ldapsearchsuffix));
+
 		if (hba->ldapscope)
 			options[noptions++] =
 				CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 07d92d4f9f..c14322ffe6 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -80,6 +80,8 @@ typedef struct HbaLine
 	char	   *ldapbinddn;
 	char	   *ldapbindpasswd;
 	char	   *ldapsearchattribute;
+	char	   *ldapsearchprefix;
+	char	   *ldapsearchsuffix;
 	char	   *ldapbasedn;
 	int			ldapscope;
 	char	   *ldapprefix;
-- 
2.13.2

