From 0ea0feb4c332564b18b424ab142ad1dac4e188a3 Mon Sep 17 00:00:00 2001
From: Jelte Fennema <jelte.fennema@microsoft.com>
Date: Tue, 27 Dec 2022 15:51:18 +0100
Subject: [PATCH v1] Support using all for the db user in pg_ident.conf

While pg_hba.conf has supported the "all" keyword since a very long
time, pg_ident.conf doesn't have this same functionality. This changes
permission checking in pg_ident.conf to handle "all" differently from
any other value in the database-username column. If "all" is specified
and the system-user matches the identifier, then the user is allowed to
authenticate no matter what user it tries to authenticate as.

This change makes it much easier to have a certain database
administrator peer or cert authentication, that allows connecting as
any user. Without this change you would need to add a line to
pg_ident.conf for every user that is in the database.
---
 doc/src/sgml/client-auth.sgml         |  5 ++-
 src/backend/libpq/hba.c               | 54 +++++++++++++++++----------
 src/backend/utils/adt/hbafuncs.c      |  4 +-
 src/include/libpq/hba.h               |  4 +-
 src/test/authentication/t/003_peer.pl | 21 +++++++++++
 5 files changed, 63 insertions(+), 25 deletions(-)

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index cc8c59206c..64c7713057 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -941,7 +941,10 @@ local   db1,db2,@demodbs  all                                   md5
    implying that they are equivalent.  The connection will be allowed if
    there is any map entry that pairs the user name obtained from the
    external authentication system with the database user name that the
-   user has requested to connect as.
+   user has requested to connect as. The value <literal>all</literal>
+   can be used as the <replaceable>database-username</replaceable> to specify
+   that all users are matched. Quoting <literal>all</literal> makes the keyword 
+   lose its special meaning.
   </para>
   <para>
    If the <replaceable>system-username</replaceable> field starts with a slash (<literal>/</literal>),
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 870b907697..6a0e7034f9 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -2792,7 +2792,7 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 	token = linitial(tokens);
 
 	/* Copy the ident user token */
-	parsedline->token = copy_auth_token(token);
+	parsedline->systemuser = copy_auth_token(token);
 
 	/* Get the PG rolename token */
 	field = lnext(tok_line->fields, field);
@@ -2800,13 +2800,13 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 	tokens = lfirst(field);
 	IDENT_MULTI_VALUE(tokens);
 	token = linitial(tokens);
-	parsedline->pg_role = pstrdup(token->string);
+	parsedline->dbuser = copy_auth_token(token);
 
 	/*
 	 * Now that the field validation is done, compile a regex from the user
 	 * token, if necessary.
 	 */
-	if (regcomp_auth_token(parsedline->token, file_name, line_num,
+	if (regcomp_auth_token(parsedline->systemuser, file_name, line_num,
 						   err_msg, elevel))
 	{
 		/* err_msg includes the error to report */
@@ -2835,7 +2835,7 @@ check_ident_usermap(IdentLine *identLine, const char *usermap_name,
 		return;
 
 	/* Match? */
-	if (token_has_regexp(identLine->token))
+	if (token_has_regexp(identLine->systemuser))
 	{
 		/*
 		 * Process the system username as a regular expression that returns
@@ -2847,7 +2847,7 @@ check_ident_usermap(IdentLine *identLine, const char *usermap_name,
 		char	   *ofs;
 		char	   *regexp_pgrole;
 
-		r = regexec_auth_token(ident_user, identLine->token, 2, matches);
+		r = regexec_auth_token(ident_user, identLine->systemuser, 2, matches);
 		if (r)
 		{
 			char		errstr[100];
@@ -2855,17 +2855,26 @@ check_ident_usermap(IdentLine *identLine, const char *usermap_name,
 			if (r != REG_NOMATCH)
 			{
 				/* REG_NOMATCH is not an error, everything else is */
-				pg_regerror(r, identLine->token->regex, errstr, sizeof(errstr));
+				pg_regerror(r, identLine->systemuser->regex, errstr, sizeof(errstr));
 				ereport(LOG,
 						(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
 						 errmsg("regular expression match for \"%s\" failed: %s",
-								identLine->token->string + 1, errstr)));
+								identLine->systemuser->string + 1, errstr)));
 				*error_p = true;
 			}
 			return;
 		}
 
-		if ((ofs = strstr(identLine->pg_role, "\\1")) != NULL)
+		/*
+		 * We can return early if the dbuser is the all keyword
+		 */
+		if (token_is_keyword(identLine->dbuser, "all"))
+		{
+			*found_p = true;
+			return;
+		}
+
+		if ((ofs = strstr(identLine->dbuser->string, "\\1")) != NULL)
 		{
 			int			offset;
 
@@ -2875,7 +2884,7 @@ check_ident_usermap(IdentLine *identLine, const char *usermap_name,
 				ereport(LOG,
 						(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
 						 errmsg("regular expression \"%s\" has no subexpressions as requested by backreference in \"%s\"",
-								identLine->token->string + 1, identLine->pg_role)));
+								identLine->systemuser->string + 1, identLine->dbuser->string)));
 				*error_p = true;
 				return;
 			}
@@ -2884,9 +2893,9 @@ check_ident_usermap(IdentLine *identLine, const char *usermap_name,
 			 * length: original length minus length of \1 plus length of match
 			 * plus null terminator
 			 */
-			regexp_pgrole = palloc0(strlen(identLine->pg_role) - 2 + (matches[1].rm_eo - matches[1].rm_so) + 1);
-			offset = ofs - identLine->pg_role;
-			memcpy(regexp_pgrole, identLine->pg_role, offset);
+			regexp_pgrole = palloc0(strlen(identLine->dbuser->string) - 2 + (matches[1].rm_eo - matches[1].rm_so) + 1);
+			offset = ofs - identLine->dbuser->string;
+			memcpy(regexp_pgrole, identLine->dbuser->string, offset);
 			memcpy(regexp_pgrole + offset,
 				   ident_user + matches[1].rm_so,
 				   matches[1].rm_eo - matches[1].rm_so);
@@ -2895,7 +2904,7 @@ check_ident_usermap(IdentLine *identLine, const char *usermap_name,
 		else
 		{
 			/* no substitution, so copy the match */
-			regexp_pgrole = pstrdup(identLine->pg_role);
+			regexp_pgrole = pstrdup(identLine->dbuser->string);
 		}
 
 		/*
@@ -2918,17 +2927,20 @@ check_ident_usermap(IdentLine *identLine, const char *usermap_name,
 	}
 	else
 	{
-		/* Not regular expression, so make complete match */
 		if (case_insensitive)
 		{
-			if (pg_strcasecmp(identLine->pg_role, pg_role) == 0 &&
-				pg_strcasecmp(identLine->token->string, ident_user) == 0)
+			if (
+				(token_is_keyword(identLine->dbuser, "all") ||
+				 pg_strcasecmp(identLine->dbuser->string, pg_role) == 0) &&
+				pg_strcasecmp(identLine->systemuser->string, ident_user) == 0)
 				*found_p = true;
 		}
 		else
 		{
-			if (strcmp(identLine->pg_role, pg_role) == 0 &&
-				strcmp(identLine->token->string, ident_user) == 0)
+			if (
+				(token_is_keyword(identLine->dbuser, "all") ||
+				 strcmp(identLine->dbuser->string, pg_role) == 0) &&
+				strcmp(identLine->systemuser->string, ident_user) == 0)
 				*found_p = true;
 		}
 	}
@@ -3073,7 +3085,8 @@ load_ident(void)
 		foreach(parsed_line_cell, new_parsed_lines)
 		{
 			newline = (IdentLine *) lfirst(parsed_line_cell);
-			free_auth_token(newline->token);
+			free_auth_token(newline->systemuser);
+			free_auth_token(newline->dbuser);
 		}
 		MemoryContextDelete(ident_context);
 		return false;
@@ -3085,7 +3098,8 @@ load_ident(void)
 		foreach(parsed_line_cell, parsed_ident_lines)
 		{
 			newline = (IdentLine *) lfirst(parsed_line_cell);
-			free_auth_token(newline->token);
+			free_auth_token(newline->systemuser);
+			free_auth_token(newline->dbuser);
 		}
 	}
 	if (parsed_ident_context != NULL)
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 633eda30d3..80a8b02b88 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -492,8 +492,8 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	if (ident != NULL)
 	{
 		values[index++] = CStringGetTextDatum(ident->usermap);
-		values[index++] = CStringGetTextDatum(ident->token->string);
-		values[index++] = CStringGetTextDatum(ident->pg_role);
+		values[index++] = CStringGetTextDatum(ident->systemuser->string);
+		values[index++] = CStringGetTextDatum(ident->dbuser->string);
 	}
 	else
 	{
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 90c51ad6fa..3214a27e66 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -142,8 +142,8 @@ typedef struct IdentLine
 	int			linenumber;
 
 	char	   *usermap;
-	char	   *pg_role;
-	AuthToken  *token;
+	AuthToken  *systemuser;
+	AuthToken  *dbuser;
 } IdentLine;
 
 /*
diff --git a/src/test/authentication/t/003_peer.pl b/src/test/authentication/t/003_peer.pl
index 26c34d05d3..f6d57f77f0 100644
--- a/src/test/authentication/t/003_peer.pl
+++ b/src/test/authentication/t/003_peer.pl
@@ -123,11 +123,32 @@ test_role($node, qq{testmapuser}, 'peer', 0, 'with user name map',
 	log_like =>
 	  [qr/connection authenticated: identity="$system_user" method=peer/]);
 
+# Tests with the "all" keyword
+reset_pg_ident($node, 'mypeermap', $system_user, 'all');
+
+# Success as the database role is the "all" keyword
+test_role($node, qq{testmapuser}, 'peer', 0, 'with user name map',
+	log_like =>
+	  [qr/connection authenticated: identity="$system_user" method=peer/]);
+
 # Test with regular expression in user name map.
 # Extract the last 3 characters from the system_user
 # or the entire system_user (if its length is <= -3).
 my $regex_test_string = substr($system_user, -3);
 
+# Success as the regular expression matches and database role is the "all"
+# keyword.
+reset_pg_ident($node, 'mypeermap', qq{/^.*$regex_test_string\$},
+	'all');
+test_role(
+	$node,
+	qq{testmapuser},
+	'peer',
+	0,
+	'with regular expression in user name map',
+	log_like =>
+	  [qr/connection authenticated: identity="$system_user" method=peer/]);
+
 # Success as the regular expression matches.
 reset_pg_ident($node, 'mypeermap', qq{/^.*$regex_test_string\$},
 	'testmapuser');
-- 
2.34.1

