On Tue, 2021-09-28 at 18:15 +0000, Jacob Champion wrote:
>          | authn             authz
> ---------+-----------------------------------
>   envvar | PGAUTHUSER        PGUSER
> conninfo | authuser          user
> frontend | conn->pgauthuser  conn->pguser
>  backend | port->auth_user   port->user_name

v3 attached, which uses the above naming scheme and removes the stale
TODO. Changes in since-v2.

--Jacob
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out 
b/contrib/postgres_fdw/expected/postgres_fdw.out
index 838ca83b55..cf74aeac41 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -188,7 +188,7 @@ ALTER USER MAPPING FOR public SERVER testserver1
 ALTER USER MAPPING FOR public SERVER testserver1
        OPTIONS (ADD sslmode 'require');
 ERROR:  invalid option "sslmode"
-HINT:  Valid options in this context are: user, password, sslpassword, 
ldapuser, password_required, sslcert, sslkey
+HINT:  Valid options in this context are: user, password, sslpassword, 
authuser, password_required, sslcert, sslkey
 -- But we can add valid ones fine
 ALTER USER MAPPING FOR public SERVER testserver1
        OPTIONS (ADD sslpassword 'dummy');
diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c
index c734467f01..310e30598c 100644
--- a/contrib/postgres_fdw/option.c
+++ b/contrib/postgres_fdw/option.c
@@ -315,11 +315,11 @@ InitPgFdwOptions(void)
                popt->keyword = lopt->keyword;
 
                /*
-                * "user", "ldapuser", and any secret options are allowed only 
on user
+                * "user", "authuser", and any secret options are allowed only 
on user
                 * mappings.  Everything else is a server option.
                 */
                if (strcmp(lopt->keyword, "user") == 0 ||
-                       strcmp(lopt->keyword, "ldapuser") == 0 ||
+                       strcmp(lopt->keyword, "authuser") == 0 ||
                        strchr(lopt->dispchar, '*'))
                        popt->optcontext = UserMappingRelationId;
                else
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 8a0ec715e4..9e62d36fb4 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1455,8 +1455,8 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
-     <varlistentry id="libpq-connect-ldapuser" xreflabel="ldapuser">
-      <term><literal>ldapuser</literal></term>
+     <varlistentry id="libpq-connect-authuser" xreflabel="authuser">
+      <term><literal>authuser</literal></term>
       <listitem>
        <para>
         When connecting to servers that use LDAP authentication, this option
@@ -7943,10 +7943,10 @@ myEventProc(PGEventId evtId, void *evtInfo, void 
*passThrough)
     <listitem>
      <para>
       <indexterm>
-       <primary><envar>PGLDAPUSER</envar></primary>
+       <primary><envar>PGAUTHUSER</envar></primary>
       </indexterm>
-      <envar>PGLDAPUSER</envar> behaves the same as the
-      <xref linkend="libpq-connect-ldapuser"/> connection parameter.
+      <envar>PGAUTHUSER</envar> behaves the same as the
+      <xref linkend="libpq-connect-authuser"/> connection parameter.
      </para>
     </listitem>
 
diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml
index 1165550955..4286dcfc47 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -120,7 +120,7 @@
     <itemizedlist spacing="compact">
      <listitem>
       <para>
-       <literal>user</literal>, <literal>ldapuser</literal>,
+       <literal>user</literal>, <literal>authuser</literal>,
        <literal>password</literal> and <literal>sslpassword</literal> (specify 
these
        in a user mapping, instead, or use a service file)
       </para>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index e2010fa402..5205bc1a53 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2534,8 +2534,8 @@ CheckLDAPAuth(Port *port)
                return STATUS_ERROR;
        }
 
-       /* If a PGLDAPUSER was not provided, default to PGUSER. */
-       ldapuser = port->ldapuser;
+       /* If a PGAUTHUSER was not provided, default to PGUSER. */
+       ldapuser = port->auth_user;
        if (!ldapuser || !ldapuser[0])
                ldapuser = port->user_name;
 
diff --git a/src/backend/postmaster/postmaster.c 
b/src/backend/postmaster/postmaster.c
index bc7e727dc5..e34d6f5726 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2208,8 +2208,8 @@ retry1:
                                port->database_name = pstrdup(valptr);
                        else if (strcmp(nameptr, "user") == 0)
                                port->user_name = pstrdup(valptr);
-                       else if (strcmp(nameptr, "ldapuser") == 0)
-                               port->ldapuser = pstrdup(valptr);
+                       else if (strcmp(nameptr, "authuser") == 0)
+                               port->auth_user = pstrdup(valptr);
                        else if (strcmp(nameptr, "options") == 0)
                                port->cmdline_options = pstrdup(valptr);
                        else if (strcmp(nameptr, "replication") == 0)
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 26f764aae9..ae9332cd45 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -147,6 +147,12 @@ typedef struct Port
        char       *cmdline_options;
        List       *guc_options;
 
+       /*
+        * The username to use during authentication, if different from 
user_name,
+        * or else NULL. Currently only supported for the LDAP auth method.
+        */
+       const char *auth_user;
+
        /*
         * The startup packet application name, only used here for the 
"connection
         * authorized" log message. We shouldn't use this post-startup, instead
@@ -203,11 +209,6 @@ typedef struct Port
        void       *gss;
 #endif
 
-       /*
-        * LDAP structures.
-        */
-       const char *ldapuser;
-
        /*
         * SSL structures.
         */
diff --git a/src/interfaces/libpq/fe-connect.c 
b/src/interfaces/libpq/fe-connect.c
index 6c7a3a94cb..75d0d7d4ec 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -344,9 +344,9 @@ static const internalPQconninfoOption PQconninfoOptions[] = 
{
                "Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 
15 */
        offsetof(struct pg_conn, target_session_attrs)},
 
-       {"ldapuser", "PGLDAPUSER", NULL, NULL,
-               "LDAP-User", "", 20,
-       offsetof(struct pg_conn, pgldapuser)},
+       {"authuser", "PGAUTHUSER", NULL, NULL,
+               "Auth-User", "", 20,
+       offsetof(struct pg_conn, pgauthuser)},
 
        /* Terminating entry --- MUST BE LAST */
        {NULL, NULL, NULL, NULL,
@@ -1440,8 +1440,6 @@ connectOptions2(PGconn *conn)
                        goto oom_error;
        }
 
-       /* TODO: unset pgldapuser if it's the same as pguser, for 
compatibility? */
-
        /*
         * Only if we get this far is it appropriate to try to connect. (We 
need a
         * state flag, rather than just the boolean result of this function, in
diff --git a/src/interfaces/libpq/fe-protocol3.c 
b/src/interfaces/libpq/fe-protocol3.c
index 42e4e535ea..308fdcdf6f 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -2220,8 +2220,8 @@ build_startup_packet(const PGconn *conn, char *packet,
                ADD_STARTUP_OPTION("replication", conn->replication);
        if (conn->pgoptions && conn->pgoptions[0])
                ADD_STARTUP_OPTION("options", conn->pgoptions);
-       if (conn->pgldapuser && conn->pgldapuser[0])
-               ADD_STARTUP_OPTION("ldapuser", conn->pgldapuser);
+       if (conn->pgauthuser && conn->pgauthuser[0])
+               ADD_STARTUP_OPTION("authuser", conn->pgauthuser);
        if (conn->send_appname)
        {
                /* Use appname if present, otherwise use fallback */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index ea8e23da08..288e93f367 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -390,7 +390,7 @@ struct pg_conn
        char       *krbsrvname;         /* Kerberos service name */
        char       *gsslib;                     /* What GSS library to use 
("gssapi" or
                                                                 * "sspi") */
-       char       *pgldapuser;         /* LDAP username, if not pguser */
+       char       *pgauthuser;         /* LDAP username, if not pguser */
        char       *ssl_min_protocol_version;   /* minimum TLS protocol version 
*/
        char       *ssl_max_protocol_version;   /* maximum TLS protocol version 
*/
        char       *target_session_attrs;       /* desired session properties */
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 4a771e1b79..963d4a97ce 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -246,15 +246,15 @@ test_access(
                qr/no match in usermap "mymap" for user "test2" authenticated 
as "test2"/,
        ]);
 
-$ENV{"PGLDAPUSER"} = 'test2';
+$ENV{"PGAUTHUSER"} = 'test2';
 test_access(
        $node, 'test1', 0,
-       'succeeds with different PGLDAPUSER',
+       'succeeds with different PGAUTHUSER',
        log_like => [
                qr/connection authenticated: 
identity="uid=test2,dc=example,dc=net" method=ldap/,
                qr/connection authorized: user=test1/,
        ]);
-delete $ENV{"PGLDAPUSER"};
+delete $ENV{"PGAUTHUSER"};
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -339,15 +339,15 @@ test_access(
                qr/no match in usermap "mymap" for user "test2" authenticated 
as "test2"/,
        ]);
 
-$ENV{"PGLDAPUSER"} = 'test2';
+$ENV{"PGAUTHUSER"} = 'test2';
 test_access(
        $node, 'test1', 0,
-       'succeeds with different PGLDAPUSER',
+       'succeeds with different PGAUTHUSER',
        log_like => [
                qr/connection authenticated: 
identity="uid=test2,dc=example,dc=net" method=ldap/,
                qr/connection authorized: user=test1/,
        ]);
-delete $ENV{"PGLDAPUSER"};
+delete $ENV{"PGAUTHUSER"};
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
From e2d1c987a363e486e4e4457d928ca393dbbc6eb3 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchamp...@vmware.com>
Date: Mon, 30 Aug 2021 16:29:59 -0700
Subject: [PATCH v3] Allow user name mapping with LDAP

Enable the `map` HBA option for the ldap auth method. To make effective
use of this from the client side, the authuser connection option (and a
corresponding environment variable, PGAUTHUSER) has been added; it
defaults to the PGUSER.

For more advanced mapping, the ldap_map_dn HBA option can be set to use
the full user Distinguished Name during user mapping. (This parallels
the include_realm=1 and clientname=DN options.)
---
 .../postgres_fdw/expected/postgres_fdw.out    |   2 +-
 contrib/postgres_fdw/option.c                 |   8 +-
 doc/src/sgml/client-auth.sgml                 |  45 +++++-
 doc/src/sgml/libpq.sgml                       |  32 ++++
 doc/src/sgml/postgres-fdw.sgml                |   3 +-
 src/backend/libpq/auth.c                      |  38 +++--
 src/backend/libpq/hba.c                       |  29 +++-
 src/backend/postmaster/postmaster.c           |   2 +
 src/include/libpq/hba.h                       |   1 +
 src/include/libpq/libpq-be.h                  |   6 +
 src/interfaces/libpq/fe-connect.c             |   4 +
 src/interfaces/libpq/fe-protocol3.c           |   2 +
 src/interfaces/libpq/libpq-int.h              |   1 +
 src/test/ldap/t/001_auth.pl                   | 145 +++++++++++++++++-
 14 files changed, 298 insertions(+), 20 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index fd141a0fa5..cf74aeac41 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -188,7 +188,7 @@ ALTER USER MAPPING FOR public SERVER testserver1
 ALTER USER MAPPING FOR public SERVER testserver1
 	OPTIONS (ADD sslmode 'require');
 ERROR:  invalid option "sslmode"
-HINT:  Valid options in this context are: user, password, sslpassword, password_required, sslcert, sslkey
+HINT:  Valid options in this context are: user, password, sslpassword, authuser, password_required, sslcert, sslkey
 -- But we can add valid ones fine
 ALTER USER MAPPING FOR public SERVER testserver1
 	OPTIONS (ADD sslpassword 'dummy');
diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c
index 48c7417e6e..310e30598c 100644
--- a/contrib/postgres_fdw/option.c
+++ b/contrib/postgres_fdw/option.c
@@ -315,10 +315,12 @@ InitPgFdwOptions(void)
 		popt->keyword = lopt->keyword;
 
 		/*
-		 * "user" and any secret options are allowed only on user mappings.
-		 * Everything else is a server option.
+		 * "user", "authuser", and any secret options are allowed only on user
+		 * mappings.  Everything else is a server option.
 		 */
-		if (strcmp(lopt->keyword, "user") == 0 || strchr(lopt->dispchar, '*'))
+		if (strcmp(lopt->keyword, "user") == 0 ||
+			strcmp(lopt->keyword, "authuser") == 0 ||
+			strchr(lopt->dispchar, '*'))
 			popt->optcontext = UserMappingRelationId;
 		else
 			popt->optcontext = ForeignServerRelationId;
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 02f0489112..d902eb9d01 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1624,10 +1624,11 @@ omicron         bryanh                  guest1
     This authentication method operates similarly to
     <literal>password</literal> except that it uses LDAP
     as the password verification method. LDAP is used only to validate
-    the user name/password pairs. Therefore the user must already
-    exist in the database before LDAP can be used for
-    authentication.
-   </para>
+    the user name/password pairs. Therefore database users must already
+    exist in the database before LDAP can be used for authentication. User name
+    mapping can be used to allow the LDAP user name to be different from the
+    database user name.
+</para>
 
    <para>
     LDAP authentication can operate in two modes. In the first mode,
@@ -1703,6 +1704,42 @@ omicron         bryanh                  guest1
        </para>
       </listitem>
      </varlistentry>
+     <varlistentry>
+      <term><literal>map</literal></term>
+      <listitem>
+       <para>
+        Allows for mapping between LDAP and database user names. See
+        <xref linkend="auth-username-maps"/> for details.
+       </para>
+      </listitem>
+     </varlistentry>
+     <varlistentry>
+      <term><literal>ldap_map_dn</literal></term>
+      <listitem>
+       <para>
+        Set to 1 to use the user's full Distinguished Name (e.g.
+        <literal>uid=someuser,dc=example,dc=com</literal>) during user name
+        mapping. When set to 0 (the default), only the bare username (in this
+        example, <literal>someuser</literal>) will be mapped. This option may
+        only be used in conjunction with the <literal>map</literal> option.
+       </para>
+       <note>
+        <para>
+         When using regular expression matching in combination with this option,
+         care should be taken to correctly anchor the regular expression in
+         order to avoid false positives. For example, the anchored expression
+         <literal>/,dc=example,dc=com$</literal> will match only DNs underneath
+         the <literal>example.com</literal> subtree, whereas the unanchored
+         <literal>/dc=example,dc=com</literal> could match
+         <literal>uid=mal,dc=example,dc=community,dc=invalid</literal>
+         or any number of DNs that are not rooted at
+         <literal>example.com</literal>. (Whether these DNs exist in the LDAP
+         tree to be abused is dependent on the LDAP deployment and outside the
+         scope of this documentation.)
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
 
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index b449c834a9..9e62d36fb4 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1455,6 +1455,28 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-authuser" xreflabel="authuser">
+      <term><literal>authuser</literal></term>
+      <listitem>
+       <para>
+        When connecting to servers that use LDAP authentication, this option
+        sets the username that should be used to bind to the LDAP server.
+        By default, the <productname>PostgreSQL</productname> user name
+        (<xref linkend="libpq-connect-user"/>) is used to bind.
+       </para>
+
+       <note>
+        <para>
+         For this option to be useful, the server must be appropriately
+         configured with a user name map (<xref linkend="auth-username-maps"/>).
+         Server versions prior to 15 always use the
+         <productname>PostgreSQL</productname> user name to bind and cannot make
+         use of this option.
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-gssencmode" xreflabel="gssencmode">
       <term><literal>gssencmode</literal></term>
       <listitem>
@@ -7918,6 +7940,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGAUTHUSER</envar></primary>
+      </indexterm>
+      <envar>PGAUTHUSER</envar> behaves the same as the
+      <xref linkend="libpq-connect-authuser"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml
index 41c952fbe3..4286dcfc47 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -120,7 +120,8 @@
     <itemizedlist spacing="compact">
      <listitem>
       <para>
-       <literal>user</literal>, <literal>password</literal> and <literal>sslpassword</literal> (specify these
+       <literal>user</literal>, <literal>authuser</literal>,
+       <literal>password</literal> and <literal>sslpassword</literal> (specify these
        in a user mapping, instead, or use a service file)
       </para>
      </listitem>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index a317aef1c9..5205bc1a53 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2479,8 +2479,10 @@ CheckLDAPAuth(Port *port)
 	char	   *passwd;
 	LDAP	   *ldap;
 	int			r;
+	const char *ldapuser;
 	char	   *fulluser;
 	const char *server_name;
+	const char *map_name;
 
 #ifdef HAVE_LDAP_INITIALIZE
 
@@ -2532,6 +2534,11 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* If a PGAUTHUSER was not provided, default to PGUSER. */
+	ldapuser = port->auth_user;
+	if (!ldapuser || !ldapuser[0])
+		ldapuser = port->user_name;
+
 	if (port->hba->ldapbasedn)
 	{
 		/*
@@ -2543,7 +2550,7 @@ CheckLDAPAuth(Port *port)
 		LDAPMessage *entry;
 		char	   *attributes[] = {LDAP_NO_ATTRS, NULL};
 		char	   *dn;
-		char	   *c;
+		const char *c;
 		int			count;
 
 		/*
@@ -2552,7 +2559,7 @@ CheckLDAPAuth(Port *port)
 		 * them would make it possible to inject any kind of custom filters in
 		 * the LDAP filter.
 		 */
-		for (c = port->user_name; *c; c++)
+		for (c = ldapuser; *c; c++)
 		{
 			if (*c == '*' ||
 				*c == '(' ||
@@ -2590,11 +2597,11 @@ CheckLDAPAuth(Port *port)
 
 		/* Build a custom filter or a single attribute filter? */
 		if (port->hba->ldapsearchfilter)
-			filter = FormatSearchFilter(port->hba->ldapsearchfilter, port->user_name);
+			filter = FormatSearchFilter(port->hba->ldapsearchfilter, ldapuser);
 		else if (port->hba->ldapsearchattribute)
-			filter = psprintf("(%s=%s)", port->hba->ldapsearchattribute, port->user_name);
+			filter = psprintf("(%s=%s)", port->hba->ldapsearchattribute, ldapuser);
 		else
-			filter = psprintf("(uid=%s)", port->user_name);
+			filter = psprintf("(uid=%s)", ldapuser);
 
 		r = ldap_search_s(ldap,
 						  port->hba->ldapbasedn,
@@ -2621,12 +2628,12 @@ CheckLDAPAuth(Port *port)
 		{
 			if (count == 0)
 				ereport(LOG,
-						(errmsg("LDAP user \"%s\" does not exist", port->user_name),
+						(errmsg("LDAP user \"%s\" does not exist", ldapuser),
 						 errdetail("LDAP search for filter \"%s\" on server \"%s\" returned no entries.",
 								   filter, server_name)));
 			else
 				ereport(LOG,
-						(errmsg("LDAP user \"%s\" is not unique", port->user_name),
+						(errmsg("LDAP user \"%s\" is not unique", ldapuser),
 						 errdetail_plural("LDAP search for filter \"%s\" on server \"%s\" returned %d entry.",
 										  "LDAP search for filter \"%s\" on server \"%s\" returned %d entries.",
 										  count,
@@ -2691,7 +2698,7 @@ CheckLDAPAuth(Port *port)
 	else
 		fulluser = psprintf("%s%s%s",
 							port->hba->ldapprefix ? port->hba->ldapprefix : "",
-							port->user_name,
+							ldapuser,
 							port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
 
 	r = ldap_simple_bind_s(ldap, fulluser, passwd);
@@ -2713,9 +2720,22 @@ CheckLDAPAuth(Port *port)
 
 	ldap_unbind(ldap);
 	pfree(passwd);
+
+	/*
+	 * Check the usermap for authorization. If ldap_map_dn is set and the admin
+	 * has set up an explicit map, we use the full distinguised name during the
+	 * mapping.  Otherwise we just check the bare LDAP username.
+	 */
+	map_name = ldapuser;
+	if (port->hba->usermap && port->hba->usermap[0] && port->hba->ldap_map_dn)
+		map_name = fulluser;
+
+	r = check_usermap(port->hba->usermap, port->user_name, map_name,
+					  pg_krb_caseins_users);
+
 	pfree(fulluser);
 
-	return STATUS_OK;
+	return r;
 }
 
 /*
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 3be8778d21..da9e1ae509 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1596,6 +1596,24 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
 			*err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
 			return NULL;
 		}
+
+		/*
+		 * Setting ldap_map_dn=1 tells the server to use the full DN when
+		 * mapping users, but it doesn't do anything if there's no usermap
+		 * defined. To prevent confusion, don't allow this to be set without a
+		 * map.
+		 */
+		if (parsedline->ldap_map_dn
+			&& !(parsedline->usermap && parsedline->usermap[0]))
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("authentication option \"ldap_map_dn\" requires argument \"map\" to be set"),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = "authentication option \"ldap_map_dn\" requires argument \"map\" to be set";
+			return NULL;
+		}
 	}
 
 	if (parsedline->auth_method == uaRADIUS)
@@ -1713,8 +1731,9 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 			hbaline->auth_method != uaPeer &&
 			hbaline->auth_method != uaGSS &&
 			hbaline->auth_method != uaSSPI &&
+			hbaline->auth_method != uaLDAP &&
 			hbaline->auth_method != uaCert)
-			INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, and cert"));
+			INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, ldap, and cert"));
 		hbaline->usermap = pstrdup(val);
 	}
 	else if (strcmp(name, "clientcert") == 0)
@@ -1931,6 +1950,14 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 		REQUIRE_AUTH_OPTION(uaLDAP, "ldapsuffix", "ldap");
 		hbaline->ldapsuffix = pstrdup(val);
 	}
+	else if (strcmp(name, "ldap_map_dn") == 0)
+	{
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldap_map_dn", "ldap");
+		if (strcmp(val, "1") == 0)
+			hbaline->ldap_map_dn = true;
+		else
+			hbaline->ldap_map_dn = false;
+	}
 	else if (strcmp(name, "krb_realm") == 0)
 	{
 		if (hbaline->auth_method != uaGSS &&
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index e2a76ba055..e34d6f5726 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2208,6 +2208,8 @@ retry1:
 				port->database_name = pstrdup(valptr);
 			else if (strcmp(nameptr, "user") == 0)
 				port->user_name = pstrdup(valptr);
+			else if (strcmp(nameptr, "authuser") == 0)
+				port->auth_user = pstrdup(valptr);
 			else if (strcmp(nameptr, "options") == 0)
 				port->cmdline_options = pstrdup(valptr);
 			else if (strcmp(nameptr, "replication") == 0)
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8d9f3821b1..8729056f09 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -106,6 +106,7 @@ typedef struct HbaLine
 	int			ldapscope;
 	char	   *ldapprefix;
 	char	   *ldapsuffix;
+	bool		ldap_map_dn;
 	ClientCertMode clientcert;
 	ClientCertName clientcertname;
 	char	   *krb_realm;
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 02015efe13..ae9332cd45 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -147,6 +147,12 @@ typedef struct Port
 	char	   *cmdline_options;
 	List	   *guc_options;
 
+	/*
+	 * The username to use during authentication, if different from user_name,
+	 * or else NULL. Currently only supported for the LDAP auth method.
+	 */
+	const char *auth_user;
+
 	/*
 	 * The startup packet application name, only used here for the "connection
 	 * authorized" log message. We shouldn't use this post-startup, instead
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index b288d346f9..75d0d7d4ec 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -344,6 +344,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */
 	offsetof(struct pg_conn, target_session_attrs)},
 
+	{"authuser", "PGAUTHUSER", NULL, NULL,
+		"Auth-User", "", 20,
+	offsetof(struct pg_conn, pgauthuser)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 9ab3bf1fcb..308fdcdf6f 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -2220,6 +2220,8 @@ build_startup_packet(const PGconn *conn, char *packet,
 		ADD_STARTUP_OPTION("replication", conn->replication);
 	if (conn->pgoptions && conn->pgoptions[0])
 		ADD_STARTUP_OPTION("options", conn->pgoptions);
+	if (conn->pgauthuser && conn->pgauthuser[0])
+		ADD_STARTUP_OPTION("authuser", conn->pgauthuser);
 	if (conn->send_appname)
 	{
 		/* Use appname if present, otherwise use fallback */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 490458adef..288e93f367 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -390,6 +390,7 @@ struct pg_conn
 	char	   *krbsrvname;		/* Kerberos service name */
 	char	   *gsslib;			/* What GSS library to use ("gssapi" or
 								 * "sspi") */
+	char	   *pgauthuser;		/* LDAP username, if not pguser */
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 5a9a009832..963d4a97ce 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -9,7 +9,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 28;
+	plan tests => 60;
 }
 else
 {
@@ -210,6 +210,77 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+note "ident mapping";
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net" map=mymap}
+);
+$node->restart;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access(
+	$node, 'test1', 2,
+	'fails without mapping',
+	log_like => [
+		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+		qr/no match in usermap "mymap" for user "test1"/,
+	]);
+
+$node->append_conf('pg_ident.conf', qq{mymap  /^  test1});
+$node->restart;
+
+test_access(
+	$node, 'test1', 0,
+	'succeeds with mapping',
+	log_like => [
+		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	]);
+
+$ENV{"PGPASSWORD"} = 'secret2';
+test_access(
+	$node, 'test2', 2,
+	'fails with unmapped role',
+	log_like => [
+		qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/,
+		qr/no match in usermap "mymap" for user "test2" authenticated as "test2"/,
+	]);
+
+$ENV{"PGAUTHUSER"} = 'test2';
+test_access(
+	$node, 'test1', 0,
+	'succeeds with different PGAUTHUSER',
+	log_like => [
+		qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/,
+		qr/connection authorized: user=test1/,
+	]);
+delete $ENV{"PGAUTHUSER"};
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net" map=mymap ldap_map_dn=1}
+);
+unlink($node->data_dir . '/pg_ident.conf');
+$node->append_conf('pg_ident.conf', qq{mymap  "/,dc=example,dc=net\$"  test1});
+$node->restart;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access(
+	$node, 'test1', 0,
+	'succeeds with full DN mapping',
+	log_like => [
+		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	]);
+
+$ENV{"PGPASSWORD"} = 'secret2';
+test_access(
+	$node, 'test2', 2,
+	'fails with unmapped role for full DN mapping',
+	log_like => [
+		qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/,
+		qr/no match in usermap "mymap" for user "test2" authenticated as "uid=test2,dc=example,dc=net"/,
+	]);
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
@@ -231,6 +302,78 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+note "search+bind ident mapping";
+
+unlink($node->data_dir . '/pg_ident.conf');
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapbasedn="$ldap_basedn" map=mymap}
+);
+$node->restart;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access(
+	$node, 'test1', 2,
+	'fails without mapping',
+	log_like => [
+		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+		qr/no match in usermap "mymap" for user "test1" authenticated as "test1"/,
+	]);
+
+$node->append_conf('pg_ident.conf', qq{mymap  /^  test1});
+$node->restart;
+
+test_access(
+	$node, 'test1', 0,
+	'succeeds with mapping',
+	log_like => [
+		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	]);
+
+$ENV{"PGPASSWORD"} = 'secret2';
+test_access(
+	$node, 'test2', 2,
+	'fails with unmapped role',
+	log_like => [
+		qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/,
+		qr/no match in usermap "mymap" for user "test2" authenticated as "test2"/,
+	]);
+
+$ENV{"PGAUTHUSER"} = 'test2';
+test_access(
+	$node, 'test1', 0,
+	'succeeds with different PGAUTHUSER',
+	log_like => [
+		qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/,
+		qr/connection authorized: user=test1/,
+	]);
+delete $ENV{"PGAUTHUSER"};
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapbasedn="$ldap_basedn" map=mymap ldap_map_dn=1}
+);
+unlink($node->data_dir . '/pg_ident.conf');
+$node->append_conf('pg_ident.conf', qq{mymap  "/,dc=example,dc=net\$"  test1});
+$node->restart;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access(
+	$node, 'test1', 0,
+	'succeeds with full DN mapping',
+	log_like => [
+		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	]);
+
+$ENV{"PGPASSWORD"} = 'secret2';
+test_access(
+	$node, 'test2', 2,
+	'fails with unmapped role for full DN mapping',
+	log_like => [
+		qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/,
+		qr/no match in usermap "mymap" for user "test2" authenticated as "uid=test2,dc=example,dc=net"/,
+	]);
+
 note "multiple servers";
 
 unlink($node->data_dir . '/pg_hba.conf');
-- 
2.25.1

Reply via email to