Subject: Re: [PATCH] Support pg_ident mapping for LDAP

From: Jacob Champion <[email-masked]>
Date: Thu, 17 Feb 2022 19:15:57 +0000

On Fri, 2021-10-29 at 17:38 +0000, Jacob Champion wrote:
> v3 attached, which uses the above naming scheme and removes the stale
> TODO. Changes in since-v2.

v4 rebases over the recent TAP changes.

--Jacob
From e0f36725013610eade9bc83414c4d1f5adea17e2 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email-masked]>
Date: Mon, 30 Aug 2021 16:29:59 -0700
Subject: [PATCH v4] 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                   | 143 ++++++++++++++++++
 14 files changed, 297 insertions(+), 19 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index b2e02caefe..c2eca6521a 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 fc3ce6a53a..d073418a5d 100644
--- a/contrib/postgres_fdw/option.c
+++ b/contrib/postgres_fdw/option.c
@@ -316,10 +316,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 e0ab7cd555..2265d86575 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>
@@ -7924,6 +7946,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 7bb6e525a4..1ee562f42f 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -121,7 +121,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 efc53f3135..4723eb01c8 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2481,8 +2481,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
 
@@ -2534,6 +2536,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)
 	{
 		/*
@@ -2545,7 +2552,7 @@ CheckLDAPAuth(Port *port)
 		LDAPMessage *entry;
 		char	   *attributes[] = {LDAP_NO_ATTRS, NULL};
 		char	   *dn;
-		char	   *c;
+		const char *c;
 		int			count;
 
 		/*
@@ -2554,7 +2561,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 == '(' ||
@@ -2592,11 +2599,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,
@@ -2623,12 +2630,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,
@@ -2693,7 +2700,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);
@@ -2715,9 +2722,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 d84a40b726..7935a5dd29 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)
@@ -1717,8 +1735,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)
@@ -1935,6 +1954,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 735fed490b..6dc90f5ac1 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2232,6 +2232,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 dd3e5efba3..5ad4d58ec1 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 30d6b7b377..bf3f2124aa 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 26dbeaed97..509aef36da 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 4290553482..88b12873a4 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 094270cb5d..433dc3b87f 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -206,6 +206,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');
@@ -227,6 +298,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